最近一直在做公司新專案的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/X…