Flutter集成旧项目并重构帖子详情页
最近一直在做公司新项目的Flutter工作,主要负责部分Flutter页面的编写以及与原生Android的桥接。主要的集成工作由于人员紧张,交给平台组同学来做 。 公司平台组提供了一整套的集成工具链, 开发工具, MVVM结构等一系列轮子,开箱即用。时间长了, 只停留在使用层面,很少深究,还是需要自己多看看。
这次为旧项目集成Flutter, 并使用Flutter重写帖子详情页。 来体会官方提供的, 混合模式的搭建以及开发。
本次需要重写的旧原生页面为:
重写之后的Flutter页面为:
好了, 话不多说了, 让我们开始吧。
1、旧项目集成Flutter
1.1 Flutter混合开发模式
Flutter混合开发模式一般有两种方式:
1、将原生项目作为Flutter项目的子项目, Flutter默认户创建Android和iOS的工程目录, 可以在该目录下进行原生客户端开发;
2、创建Flutter Module 作为依赖项,添加到现有的原生项目中。
第二种方式相对第一种方式更解耦, 尤其是针对现有项目改造成本更小。
1.2 Flutter Module的创建方式
使用 As 创建 Flutter Module
在 As 中选择 File->New->New Flutter Project,选择 Flutter Module 创建 Flutter Module 子项目,如下:
1.3 添加Flutter的两种方式
将Flutter添加到原生工程中, 有两种方式
- 以aar的方式集成到现有Android项目中
- 以 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));
实现效果为:
2、重写帖子详情页
此次使用Flutter重写的页面为帖子详情页
可以看出, 整个页面可以用一个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
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);
}
...
}
拉取结果为
2.3.2 Json解析
对Json数据 进行反序列化为bean实体。
这里使用Json2Dart插件(个人认为json_serializable库比较难使用, 坑也比较多)
生成的代码为:
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实体:
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("评论"),
),
)
],
),
);
}
}
实现结果为:
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);
});
}
请求结果为:
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}';
}
}
解析结果为:
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,
)),
],
));
}
}
此时呈现的效果为:
2.5 发表评论
由于发表评论的数据结构需要填充自己的userId和userName, 所以先实现Flutter从原生获取用户自己的uid和userName信息。
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 -> {
}
}
}
}
}
发起调用后, 显示结果 为
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
共同学习,写下你的评论
评论加载中...
作者其他优质文章