Flutter与原生交互(将flutter嵌入已有项目
- 概述
- 缘由
- 思路
- 开始
- 配置项目
- 创建FlutterActivity
- Flutter中channel配置
- Flutter main.dart配置与中转页面
- 使用
- 原生打开flutter页面 (携带参数)
- flutter打开原生页面 (携带参数)
- 原生页面调用 flutter页面上的方法
- flutter页面调用原生页面上的方法
- 最后
目前已有android ios 开发好的项目 现在新的需求使用flutter开发 在嵌入搭配已有的原生项目,这样ios和android 开发任务理论上可以节约一半的开发时间
概述
缘由
目前有一些第三方框架解决了 flutter与原生交互,我们之前使用的flutterboost,由于flutterboost长期不更新(版本1.17.4),导致我们flutter也升级不了.所以不想要被第三方框架影响,所以我们自己来处理.
思路
关键是使用channel 交互基本靠它.
原生页面跳转到一个flutter页面 这个flutter页面是一个中转页面 它负责处理路由(具体调整到哪个flutter页面) 携带初始参数.
原生页面也有一个FlutterActivity /FlutterViewController 它负责处理flutter需要调用原生功能.
一共要解决这么几个问题
- 原生打开flutter页面 (携带参数)
- flutter打开原生页面 (携带参数)
- 原生页面调用 flutter页面上的方法
- flutter页面调用原生页面上的方法
开始
以android为例
配置项目
- 创建Flutter module 项目
- 依赖模块
这里my_flutter 是你的flutter module工程目录的名字 你直接运行原生项目 它会自动编译flutter 当作依赖
include ':app' // assumed existing content
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'my_flutter/.android/include_flutter.groovy' // new
)) // new
这里都比较简单 我没有写的很细 文档中每步都有 中文文档
创建FlutterActivity
- 创建FlutterActivity
自定义一个Activity 继承于FlutterFragmentActivity或FlutterActivity
个人建议继承于FlutterFragmentActivity 做过android应该都知道,要不然以后需要supportFragmentManager的时候 头就大了 - Activity里面先“重写”withCachedEngine方法 (静态方法不能叫重写 就这么理解吧) 为什么要重写 看下代码就知道了
companion object {
fun withCachedEngine(cachedEngineId: String): CachedEngineIntentBuilder {
return CachedEngineIntentBuilder(YDFlutterActivity::class.java, cachedEngineId)
}
}
3.定义channel
channel的初始化在configureFlutterEngine中进行
我这里使用了FlutterEngine 它是在application中配置的 具体配置官方文档很详细
这里关键就是channel的两个方法
setMethodCallHandler用于处理flutter页面发送过的消息
invokeMethod 用于发送消息给flutter页面
这里我们关注invokeMethod 我把我要跳转的具体页面path 和初始化参数params 发送给flutter中转页面
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.i("YDFlutterActivity", "执行了configureFlutterEngine")
initChannel(flutterEngine)
}
open fun initChannel(flutterEngine: FlutterEngine) {
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
channel.setMethodCallHandler { call, result ->
when (call.method) {
"nativeBack" -> {
this@YDFlutterActivity.finish()
}
"createFollowUpSuccess" -> {
EventBus.getDefault().post(FlutterReceiveEvent("createFollowUpSuccess"))
}
"testFun" -> {
T.showToast(this@YDFlutterActivity, "原生YDFlutterActivity:testFun")
Log.i("YDFlutterActivity", "原生拿到flutter 参数${call.arguments.toString()}")
result.success("互调成功")
channel.invokeMethod("testFlutterFun", "{原生参数:1231313}")
}
"openShare" -> {
val args = getMap(call.arguments.toString())
Log.i("AssistantFlutter", "弹窗弹窗${args.toString()}")
}
"ClientDetailV3" -> {
val args = getMap(call.arguments.toString())
navigationActivityFromPair(
RouterMerchant.CLIENT_DETAIL_V3,
Constant.IDK to CustomerDetailDto(
type = args["businessType"]?.toString()?.toInt(),
customerId = args["businessId"]?.toString()?.toLong()
)
)
}
else -> {
result.notImplemented()
}
}
}
intent?.apply {
val path = getStringExtra("path")
val params = getStringExtra("params") ?: ""
channel.invokeMethod(path, params)
Log.i("YDFlutterActivity", "执行了invokeMethod path:${path} params:${params}")
}
if(EventBus.getDefault().isRegistered(this)){
EventBus.getDefault().register(this)
}
}
Flutter中channel配置
单独创建一个dart文件 它供全局使用
MethodChannel commonNativeChannel =
MethodChannel('samples.flutter.dev/battery');
Flutter main.dart配置与中转页面
- main.dart配置
flutter的入口 很简单 只有一个默认路由 中转页面 NativeMiddlewarePage
Map<String, WidgetBuilder> nativeRoutes = {
'/': (BuildContext context) {
return NativeMiddlewarePage();
}
};
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_initScreenUtil();
}
_initScreenUtil() {
//初始化屏幕适配 当前设计图按照ios一倍图设计
setDesignWHD(375.0, 667.0, density: 1);
}
@override
Widget build(BuildContext context) {
return GetMaterialApp(
routes: nativeRoutes,
builder: (context, widget) {
return MediaQuery(
//设置文字大小不随系统设置改变
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: widget,
);
},
);
}
}
- 创建中转页面
就一个空白的Container
它的作用就是当作路由的中间件
直接拿到全局的commonNativeChannel 通过setMethodCallHandler判断原生要跳转具体哪个flutter页面 并带参数
class NativeMiddlewarePage extends StatefulWidget {
@override
_NativeMiddlewarePageState createState() => _NativeMiddlewarePageState();
}
class _NativeMiddlewarePageState extends State<NativeMiddlewarePage> {
@override
void initState() {
initNativeRouter();
super.initState();
}
void initNativeRouter() {
commonNativeChannel.setMethodCallHandler((MethodCall call) async {
print("flutter拿到原生 method:${call.method} arguments:${call.arguments}");
Map<String, dynamic> params = {};
try {
if (call.arguments != null) {
if ((call.arguments as String).isNotEmpty) {
params = json.decode(call.arguments);
}
}
} catch (e) {
print("json 解析异常 ${e}");
}
switch (call.method) {
case 'testFlutterFun':
print("flutter拿到原生 参数${call.arguments.toString()}");
break;
case 'CustomerRecyclePage':
skipPage(CustomerRecyclePage());
break;
case 'FollowCreatePage':
skipPage(FollowCreatePage(
taskId: params['taskId'],
businessId: params['businessId'],
taskName: params['taskName']));
break;
case 'AssistantHomePage':
skipPage(AssistantHomePage(
param: params,
));
break;
case 'FollowCreateTasksPage':
skipPage(FollowCreateTasksPage(
taskId: params['taskId'],
taskName: params['taskName'],
taskType: ['taskType'] as int,
));
break;
case 'FollowListPage':
skipPage(FollowListPage(
param: params,
));
break;
case 'FollowTaskDetailPage':
skipPage(FollowTaskDetailPage(
taskId: params['taskId'],
));
break;
case 'FollowRecordDetailPage':
skipPage(FollowRecordDetailPage(
taskId: params['taskId'],
));
break;
}
});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
);
}
void skipPage(Widget page) {
print("跳转至:${page}");
Get.offAll(page, duration: Duration(milliseconds: 0));
}
}
到这里 基本就配置完了
使用
原生打开flutter页面 (携带参数)
- 原生页面直接start到刚才封装的FlutterActivity.withCachedEngine
path就是你要跳转的具体flutter页面名称
params携带的参数 这里都用json字符串
context.startActivity(YDFlutterActivity
.withCachedEngine(NATIVE_MIDDLEWARE)
.build(context).apply {
putExtra("path", url)
if (params.isNotEmpty()) {
putExtra("params", gson.toJson(params))
}
})
- flutter中间页处理
完整代码刚才已经贴出来了
这里就是解析原生参数 判断call.method 决定跳转哪个页面
commonNativeChannel.setMethodCallHandler((MethodCall call) async {
print("flutter拿到原生 method:${call.method} arguments:${call.arguments}");
Map<String, dynamic> params = {};
try {
if (call.arguments != null) {
if ((call.arguments as String).isNotEmpty) {
params = json.decode(call.arguments);
}
}
} catch (e) {
print("json 解析异常 ${e}");
}
switch (call.method) {
case 'testFlutterFun':
print("flutter拿到原生 参数${call.arguments.toString()}");
break;
case 'CustomerRecyclePage':
skipPage(CustomerRecyclePage());
break;
flutter打开原生页面 (携带参数)
- 在任何flutter页面直接使用 Channel.invokeMethod
参数也是json形式
commonNativeChannel.invokeMethod("你要跳转原生页面", 参数);
- 原生处理
在自定义的FlutterActivity 中channel.setMethodCallHandler 中处理
channel.setMethodCallHandler { call, result ->
when (call.method) {
"你要跳转原生页面" -> {
val args = getMap(call.arguments.toString())
//这里startactivity就行
}
else -> {
result.notImplemented()
}
}
}
只要是在flutter页面里FlutterActivity 必定存在 它是flutter的载体
原生页面调用 flutter页面上的方法
- 其实看了页面调整后 调用方法更简单 同理
原生
channel.invokeMethod(方法名, 参数)
flutter页面 处理channel
commonNativeChannel.setMethodCallHandler((MethodCall call) async {
print("flutter拿到原生 method:${call.method} arguments:${call.arguments}");
}
flutter页面调用原生页面上的方法
- 同理
flutter
commonNativeChannel.invokeMethod('nativeBack', params);
- 原生
channel.setMethodCallHandler { call, result ->
when (call.method) {
"nativeBack" -> {
this@YDFlutterActivity.finish()
}
}
最后
- 为什么要通过中间flutter页面,不直接跳转到对应的页面
一开始确实 在flutter中配置路由
原生配置多个FlutterEngine 使用initialRoute 直接跳转到对应flutter页面 但是无法传递初始化参数 还用通过channel传参数
另外多个FlutterEngine 也影响原生性能 - 配置多个FlutterActivity ?
可以 对于有些交互复杂的页面 确实要单独配置一个Activity 要不然交互全写在一个Activity里面 - flutter的channel的全局的 原生的channel 属于 Activity
- 可以多个channel