为了账号安全,请及时绑定邮箱和手机立即绑定

Flutter集成旧项目并重构帖子详情页

最近一直在做公司新项目的Flutter工作,主要负责部分Flutter页面的编写以及与原生Android的桥接。主要的集成工作由于人员紧张,交给平台组同学来做 。 公司平台组提供了一整套的集成工具链, 开发工具, MVVM结构等一系列轮子,开箱即用。时间长了, 只停留在使用层面,很少深究,还是需要自己多看看。

这次为旧项目集成Flutter, 并使用Flutter重写帖子详情页。 来体会官方提供的, 混合模式的搭建以及开发。

本次需要重写的旧原生页面为:

device-2021-03-31-171416.png

重写之后的Flutter页面为:

详情页.png

好了, 话不多说了, 让我们开始吧。

1、旧项目集成Flutter

1.1 Flutter混合开发模式

Flutter混合开发模式一般有两种方式:

1、将原生项目作为Flutter项目的子项目, Flutter默认户创建Android和iOS的工程目录, 可以在该目录下进行原生客户端开发;

Flutter Application 项目结构

2、创建Flutter Module 作为依赖项,添加到现有的原生项目中。

第二种方式相对第一种方式更解耦, 尤其是针对现有项目改造成本更小。

1.2 Flutter Module的创建方式

使用 As 创建 Flutter Module

在 As 中选择 File->New->New Flutter Project,选择 Flutter Module 创建 Flutter Module 子项目,如下:

创建Flutter Module

1.3 添加Flutter的两种方式

将Flutter添加到原生工程中, 有两种方式

  1. 以aar的方式集成到现有Android项目中
  2. 以 Flutet module 的方式集成到现有 Android 项目中

在日常的开发过程中, 都是以第二种方式, 将Flutter Module集成到现有Android项目中,进行混合编译,之后便可以使用Flutter 的热更新。

在Jenkins自动化打包时,采用第一种方式, 先将Flutter工程打成aar产物, 结合生成 的aar产物进行编译Android apk文件。

以 Flutet module 的方式集成到现有 Android 项目中:

在 setting.gradle 文件中配置 flutter module 如下:

include ':app', ':easeui'


// 以下是新增
setBinding(new Binding([gradle: this]))
evaluate(new File(
        settingsDir,
        '../flutter_bbs/.android/include_flutter.groovy'
))

然后在 build.gradle 文件中添加 flutter module 的依赖,如下:

dependencies {
  implementation project(':flutter')
}

build完成后, 项目已经变成了原生项目和Flutter项目的混合编译, 此时的项目结构已经变为混合编译的项目结构:
混合编译的项目结构

1.4 添加单个页面

此时实现原生界面到Flutter界面的跳转

修改Flutter入口文件

import 'package:flutter/material.dart';
import 'package:flutter_bbs/post_deatil/view/post_deatil_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PostDetailPage(),
    );
  }
}

此时展示Flutter版的社区详情页

import 'package:flutter/material.dart';

class PostDetailPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return PostDetailState();
  }
}

class PostDetailState extends State {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("帖子详情"),
      ),
      body: Center(
        child: Text(
          "帖子详情",
          style: TextStyle(fontSize: 20, color: Colors.blueAccent),
        ),
      ),
    );
  }
}

在原生工程中创建一个 Activity 继承 FlutterActivity 并在 AndroidManifest.xml 文件中声明:

class MomentDetailActivity : FlutterActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}
        <activity
            android:name=".flutter.MomentDetailActivity"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
        </activity>

如何启动这个Activity那?

startActivity(new Intent(getActivity(), MomentDetailActivity.class));

实现效果为:

帖子详情.png

2、重写帖子详情页

此次使用Flutter重写的页面为帖子详情页
device-2021-03-31-171416.png

可以看出, 整个页面可以用一个ListView搞定, ListView包含多种类型。帖子详情, 分割线, 评论, 评论空态等

2.1 集成Bmob Flutter 仓库

由于原项目使用的是Bmob云提供数据服务, 所以在Flutter项目中也需要集成Bmob仓库,实现数据访问, 接入地址

在Flutter工程的pubspec.yaml文件中增加依赖

dependencies:
  data_plugin: ^0.0.16

在终端输入以下命令进行安装:

 flutter packages get

在runApp中进行一下初始化操作:

/**
 * 非加密方式初始化
 */
Bmob.init("https://api2.bmob.cn", "appId", "apiKey");

2.2 原生工程页面向Flutter页面传递帖子Id

原生工程中,将跳转Flutter页面的方式改为:

            val intent = Intent(context, MomentDetailActivity::class.java)
            intent.action = Intent.ACTION_RUN
            intent.putExtra(
                "route",
                "moment?noteId = ${note.objectId}"
            )
            context?.startActivity(intent)

Flutter工程中, 接收传递过来的参数:

    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        routes: {
          Routes.MOMENT: (BuildContext context) => PostDetailPage(null),
        },
        onGenerateRoute: (settings) {
          Uri uri = Uri.parse(settings.name);
          Map<String, String> params = uri.queryParameters;
          return MaterialPageRoute(
              builder: (context) => PostDetailPage(params));
        });

此时Flutter的帖子详情页可以拿到了帖子的Id

屏幕快照 2021-03-30 下午2.37.11.png

2.3 Flutter 根据帖子Id获取帖子信息

2.3.1 数据拉取

新建网络信息类

import 'package:data_plugin/bmob/bmob_query.dart';
import 'package:data_plugin/utils/dialog_util.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bbs/post_deatil/model/bean/note.dart';

class NetWorkRepo {
  static Note getNoteInfo(BuildContext context, String noteId) {
    BmobQuery<Note> query = BmobQuery();
    query
        .queryObject(noteId)
        .then((value) => {showSuccess(context, value.toString())});
  }
}

在PostDetailPage 初始化时进行拉取

class PostDetailState extends State<PostDetailPage> {
  String _noteId;

  @override
  void initState() {
    super.initState();
    _noteId = widget._map["noteId"] as String;
    _initData();
  }

  void _initData() { // 拉取帖子信息
    NetWorkRepo.getNoteInfo(context, _noteId);
  }
  
  ...
  }

拉取结果为

数据拉取.png

2.3.2 Json解析

对Json数据 进行反序列化为bean实体。

这里使用Json2Dart插件(个人认为json_serializable库比较难使用, 坑也比较多)

image

image

生成的代码为:


import 'package:data_plugin/bmob/table/bmob_object.dart';

class Note extends BmobObject {
  String content;
  String createdAt;
  String objectId;
  int replaycount;
  String title;
  int top;
  String typeid;
  String updatedAt;
  String userid;
  int zancount;

  Note.fromJsonMap(Map<String, dynamic> map)
      : content = map["content"],
        createdAt = map["createdAt"],
        objectId = map["objectId"],
        replaycount = map["replaycount"],
        title = map["title"],
        top = map["top"],
        typeid = map["typeid"],
        updatedAt = map["updatedAt"],
        userid = map["userid"],
        zancount = map["zancount"];

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['content'] = content;
    data['createdAt'] = createdAt;
    data['objectId'] = objectId;
    data['replaycount'] = replaycount;
    data['title'] = title;
    data['top'] = top;
    data['typeid'] = typeid;
    data['updatedAt'] = updatedAt;
    data['userid'] = userid;
    data['zancount'] = zancount;
    return data;
  }

  @override
  Map getParams() {
    toJson();
  }

  @override
  String toString() {
    return 'Note{content: $content, createdAt: $createdAt, objectId: $objectId, replaycount: $replaycount, title: $title, top: $top, typeid: $typeid, updatedAt: $updatedAt, userid: $userid, zancount: $zancount}';
  }
}


此时已将Json数据转换为了Bean实体:

bean实体.png

2.3.3 UI展示

接下来将帖子实体展示在UI上

修改post_deatil_page.dart, 整个页面只显示一个ListView, Item根据数据类型决定

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("帖子详情"),
        ),
        body: ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            return _buildListViewCell(
                items[index]); //根据数据去构造不同的widget填充到ListView中
          },
        ));
  }

  Widget _buildListViewCell(Object object) {
    if (object is Note) {  // 如果数据类型是帖子类型
      return MomentDetailWidget(object); // 返回帖子详细信息Widget
    }
  }

帖子的详细信息MomentDetailWidget

import 'package:flutter/material.dart';
import 'package:flutter_bbs/post_deatil/model/bean/note.dart';

class MomentDetailWidget extends StatelessWidget {
  final Note note;

  MomentDetailWidget(this.note);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(left: 20, top: 10, bottom: 10, right: 20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          _buildHeaderWidget(),
          _buildContentWidget(),
          _buildIconWidget(),
          _buildReplayWidget(),
        ],
      ),
    );
  }

  Widget _buildHeaderWidget() {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Container(
          margin: EdgeInsets.only(right: 10),
          child: ClipOval(
            child: Image.asset(
              "images/logo.webp",
              width: 80,
              height: 80,
            ),
          ),
        ),
        Expanded(
            child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              note.title ?? "",
              style: TextStyle(color: Colors.black54, fontSize: 20),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            Container(
              margin: EdgeInsets.only(top: 20),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    note.typeid ?? "",
                    style: TextStyle(color: Colors.black45, fontSize: 16),
                  ),
                  Expanded(child: Container()),
                  Text(
                    note.updatedAt?.substring(0, 10) ?? "",
                    style: TextStyle(color: Colors.black45, fontSize: 16),
                  )
                ],
              ),
            ),
          ],
        )),
      ],
    );
  }

  Widget _buildContentWidget() {
    return Container(
      margin: EdgeInsets.only(top: 10),
      child: Expanded(
        child: Text(
          note.content ?? "",
          style: TextStyle(color: Colors.black54, fontSize: 16),
        ),
      ),
    );
  }

  Widget _buildIconWidget() {
    return Container(
      margin: EdgeInsets.only(top: 20),
      child: Flex(
        direction: Axis.horizontal,
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          Row(
            children: [
              Image.asset(
                "images/zan.webp",
                width: 20,
                height: 20,
              ),
              Container(
                margin: EdgeInsets.only(left: 5),
                child: Text(
                  note.zancount?.toString() ?? "",
                  style: TextStyle(fontSize: 14),
                ),
              )
            ],
          ),
          Row(
            children: [
              Image.asset(
                "images/replay.webp",
                width: 20,
                height: 20,
              ),
              Container(
                margin: EdgeInsets.only(left: 5),
                child: Text(
                  note.replaycount?.toString() ?? "",
                  style: TextStyle(fontSize: 14),
                ),
              )
            ],
          )
        ],
      ),
    );
  }

  Widget _buildReplayWidget() {
    return Container(
      margin: EdgeInsets.only(
        top: 20,
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
              child: TextField(
            decoration: InputDecoration(
              hintText: ' 开始你的评论吧',
              hintStyle: TextStyle(fontFamily: 'MaterialIcons', fontSize: 16),
              contentPadding: EdgeInsets.only(top: 8, bottom: 8),
              border: OutlineInputBorder(
                borderSide: BorderSide.none,
                borderRadius: BorderRadius.all(Radius.circular(5)),
              ),
              filled: true,
            ),
          )),
          Container(
            margin: EdgeInsets.only(left: 20),
            child: OutlinedButton(
              onPressed: () {},
              child: Text("评论"),
            ),
          )
        ],
      ),
    );
  }
}

实现结果为:

详情.png

2.4 Flutter根据帖子Id获取评论信息

2.4.1 数据获取

  // 根据帖子Id拉取评论信息
  static List<Comment> getCommentInfo(BuildContext context, String noteId) {
    BmobQuery<Comment> query = BmobQuery();
    query.addWhereEqualTo("noteid", noteId);
    query.queryObjects().then((value) {
      List<Comment> list = List();
      value.forEach((element) {
        list.add(Comment.fromJsonMap(element));
      });
      print(list.toString());
    }).catchError((e) {
      showError(context, BmobError.convert(e).error);
    });
  }

请求结果为:
comment请求结果.png

2.4.2 Json解析

import 'package:data_plugin/bmob/table/bmob_object.dart';

class Comment extends BmobObject {
  String content;
  String createdAt;
  String noteid;
  String objectId;
  String updatedAt;
  String userid;
  String username;

  Comment.fromJsonMap(Map<String, dynamic> map)
      : content = map["content"],
        createdAt = map["createdAt"],
        noteid = map["noteid"],
        objectId = map["objectId"],
        updatedAt = map["updatedAt"],
        userid = map["userid"],
        username = map["username"];

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['content'] = content;
    data['createdAt'] = createdAt;
    data['noteid'] = noteid;
    data['objectId'] = objectId;
    data['updatedAt'] = updatedAt;
    data['userid'] = userid;
    data['username'] = username;
    return data;
  }

  @override
  Map getParams() {
    toJson();
  }

  @override
  String toString() {
    return 'Comment{content: $content, createdAt: $createdAt, noteid: $noteid, objectId: $objectId, updatedAt: $updatedAt, userid: $userid, username: $username}';
  }
}

解析结果为:
屏幕快照 2021-03-31 下午8.57.15.png

2.4.3 UI展示

详情页的ListView改造为多类型ListView, 可以展示帖子, 分割线, 评论等内容

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          leading: BackButton(onPressed: () {}),
          title: Text("帖子详情"),
          centerTitle: true,
        ),
        body: ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            return _buildListViewCell(
                items[index]); //根据数据去构造不同的widget填充到ListView中
          },
        ));
  }

  Widget _buildListViewCell(Object object) {
    if (object is Note) { 
      return MomentDetailWidget(object); // 帖子信息
    } else if (object is Comment) {
      return CommentDetailWidget(object); // 评论信息
    } else if (object is DividerBean) {
      return DividerWidget(); // 分割线信息
    } else if (object is CommentEmptyBean) {
      return CommentEmptyWidget(); // 评论为空时的UI
    } else if (object is CommentTitleBean) {
      return CommentTitleWidget(object.commentNum); //  评论数量
    } else {
      return Container(); // 不识别的数据类型, 返回空Container
    }
  }

评论Widget

import 'package:flutter/material.dart';
import 'package:flutter_bbs/post_deatil/model/bean/comment.dart';

class CommentDetailWidget extends StatelessWidget {
  final Comment comment;

  CommentDetailWidget(this.comment);

  @override
  Widget build(BuildContext context) {
    return Container(
        margin: EdgeInsets.only(left: 20, top: 8, bottom: 8, right: 20),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(
              comment.username + " : ",
              style: TextStyle(fontSize: 16, color: Colors.blue),
            ),
            Expanded(
                child: Text(
              comment.content,
              style: TextStyle(fontSize: 16),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            )),
          ],
        ));
  }
}

此时呈现的效果为:
详情页.png

2.5 发表评论

由于发表评论的数据结构需要填充自己的userId和userName, 所以先实现Flutter从原生获取用户自己的uid和userName信息。
1617273319196.jpg

2.5.1 使用MethodChannel 从原生获取用户的uid

Flutter部分

import 'package:flutter/services.dart';

class MomentBridge {
  static const String BRIDGE_NAME = "flutter.bbs/moment";

  static const String METHOD_GET_USER_INFO = "getUserInfo";
  static const String KEY_USER_ID = "key_user_id";
  static const String KEY_USER_NAME = "key_user_name";

  static const _methodChannel = const MethodChannel(BRIDGE_NAME);

  static Future<Map> getUserInfo() async {
    try {
      Map res = await _methodChannel.invokeMethod(METHOD_GET_USER_INFO);
      print("getUserInfo suc" + res.toString());
      return res;
    } catch (e) {
      print("getUserInfo error" + e.toString());
    }
    return Map();
  }
}

Android 原生部分:

package com.wsg.xsybbs.flutter

import android.os.Bundle
import cn.bmob.v3.BmobUser
import com.wsg.xsybbs.bean.User
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant

/**
 * Create by wangshengguo on 2021/3/25.
 */
class MomentDetailActivity : FlutterActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
//        flutterEngine.let {
//            GeneratedPluginRegistrant.registerWith(it)
//        }

        // 注册MethodChannel
        MethodChannel(
            flutterEngine.dartExecutor,
            MomentBridge.BRIDGE_NAME
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                MomentBridge.METHOD_GET_USER_INFO -> {
                    val user = BmobUser.getCurrentUser(User::class.java)

                    val map: HashMap<String, String> = hashMapOf()
                    map[MomentBridge.KEY_USER_ID] = user.objectId
                    map[MomentBridge.KEY_USER_NAME] = user.username
                    result.success(map)
                }
                else -> {

                }
            }
        }
    }
}

发起调用后, 显示结果 为
1617273648441.jpg

2.5.2 发表评论

  // 发表评论
  static void addComment(BuildContext context, String noteId, String content,
      Function(Comment comment) update) async {
    Comment comment = Comment();
    comment.noteid = noteId;
    comment.content = content;

    Map map = await MomentBridge.getUserInfo();
    comment.userid = map[MomentBridge.KEY_USER_ID];
    comment.username = map[MomentBridge.KEY_USER_NAME];

    comment.save().then((value) {
      Toast.show("评论发表成功", context);
      update(comment);
    }).catchError((e) {
      showError(context, BmobError.convert(e).error);
    });
  }

2.5.3 刷新UI

评论发表成功后, 将评论插到评论列表最后一项

      return MomentDetailWidget(object, (String content) {
        NetWorkRepo.addComment(context, _noteId, content, (comment) {
          setState(() {
            if (items[items.length - 1] is CommentEmptyBean) { // 如果评论列表为空, 移除评论为空时的UI。将评论 插入数据集合展示
              items.removeAt(items.length - 1);
              items.add(comment);
            } else { // 直接插入
              items.add(comment);
            }
          });
        });
      });

后续有空的话,会继续完善点赞相关的功能, 并使用MVVM 对页面进行重写。

3、项目地址

项目地址为: github.com/stevenwsg/XSYBBS

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
65
获赞与收藏
104

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消