
前言
很高兴遇见你~
在本系列的上一篇文章中,我们介绍了 Flutter 中的路由:
1、基本路由
2、命名路由
3、返回上一级
4、替换路由
5、返回到根路由
以及集成 http 库进行 https 请求实战。
还没有看过上一篇文章的朋友,建议先去阅读 Flutter 系列(七):Flutter 路由和 HTTPS 请求实战。接下来我们对 Flutter 与 Android 原生的交互与通信进行介绍
我做 Android 原生开发时,通常会以组件化的方式去进行,根据业务划分不同的组件,每个组件都是一个独立的工程,可以进行独立的运行和调试,当需要发版时,我们会将每个组件打成 aar 包并上传到 Maven 私服仓库,然后整合到 App 壳工程中,最终进行打包上线。在这个开发过程中,组件之间是需要进行通信的,如果需要通信的组件都是 Android 原生开发的,那么可以选择一个路由框架进行通信,例如:Arouter。
但是我们有些业务组件是使用 Flutter 开发的,因此这里就涉及到 Flutter 与 Android 原生的通信,那么它们是如何进行通信的呢?且听我细细道来
一、Android 壳工程集成 Flutter 组件
1、打开 AndroidStudio ,创建一个 Android 工程 AndroidAndFlutterInteractive:
2、接着在创建一个 Flutter 工程 fluttermodule:
main.dart 文件初始代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } }
class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override State<MyHomePage> createState() => _MyHomePageState(); }
class _MyHomePageState extends State<MyHomePage> { int _counter = 0;
void _incrementCounter() { setState(() { _counter++; }); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have clicked the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
|
3、在 Flutter 工程中执行 flutter build aar
命令或者直接使用 AndroidStudio 上的可视化操作:
执行完后会有如下提示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Consuming the Module 1. Open <host>/app/build.gradle 2. Ensure you have the repositories configured, otherwise add them:
String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com" repositories { maven { url '/Users/zhouying/codeandnotes/Flutter/fluttermodule/build/host/outputs/repo' } maven { url "$storageUrl/download.flutter.io" } }
3. Make the host app depend on the Flutter module:
dependencies { debugImplementation 'com.example.fluttermodule:flutter_debug:1.0' profileImplementation 'com.example.fluttermodule:flutter_profile:1.0' releaseImplementation 'com.example.fluttermodule:flutter_release:1.0' }
4. Add the `profile` build type:
android { buildTypes { profile { initWith debug } } }
|
大致意思就是在我们创建的 Android 工程中配置生成的 Flutter aar 的仓库地址,然后引用这个 aar,大家按照上述步骤配置即可
注意:上述演示生成的 Flutter aar 只是存在本地,实际开发中,我们会自己编写脚本生成 aar 并上传到 Maven 私服仓库
配置完成后,同步一下项目,如果没啥报错,我们就算是成功集成了 Flutter 组件
二、Android 调起 Flutter 页面(FlutterActivity)
接下来我们继续对 Android 工程进行配置,让 Flutter 页面显示出来
1、在 Android 工程的 AndroidManifest.xml 文件中添加 FlutterActivity
1 2 3 4 5 6
| <!--注册FlutterActivity--> <activity android:name="io.flutter.embedding.android.FlutterActivity" android:configChanges="orientation|keyboardHidden|screenSize" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" />
|
2、编写一个 button 跳转到 Flutter 页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity">
<Button android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="20dp" android:textAllCaps="false" android:onClick="toFlutterActivity" android:text="跳转 FlutterActivity" tools:ignore="HardcodedText,UsingOnClickInXml" />
</LinearLayout>
class MainActivity : AppCompatActivity() { @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) }
fun toFlutterActivity(view: View) { val intent = FlutterActivity.createDefaultIntent(this) startActivity(intent) } }
|
3、效果展示:
上述效果图虽然跳过去了,但是我们可以看到点击 button 时一个明显的停顿感,用户体验不好,接下来介绍一种预初始化 Flutter 的方式
2.1、Android 预初始化 Flutter 页面跳转
核心思想就是缓存 FlutterEngine,然后从缓存中取出 FlutterEngine 进行跳转
1、修改 MainActivity :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| class MainActivity : AppCompatActivity() {
companion object{ const val FLUTTER_ENGINE_ID = "default" } private lateinit var flutterEngine: FlutterEngine
@SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) flutterEngine = initFlutterEngine(FLUTTER_ENGINE_ID) }
fun toFlutterActivity(view: View) { val intent = FlutterActivity.withCachedEngine(FLUTTER_ENGINE_ID).build(this) startActivity(intent) }
一般在跳转前调用,从缓存中取出 FlutterEngine,这样可以加快我们页面的一个跳转
private fun initFlutterEngine(engineId: String): FlutterEngine { val flutterEngine = FlutterEngine(this) flutterEngine.navigationChannel.setInitialRoute("main") flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) val flutterEngineCache = FlutterEngineCache.getInstance() flutterEngineCache.put(engineId,flutterEngine) return flutterEngine }
override fun onDestroy() { super.onDestroy() 注意这里一定要销毁,否则会导致内存泄漏
当我们退出 FlutterActivity 时,FlutterEngine 可能还会继续执行代码
/ flutterEngine.destroy() } }
|
2、Flutter 端也要做相应的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void main() => runApp(getRouter(window.defaultRouteName));
Widget getRouter(String routeName) { switch(routeName){ case "main": return const MyApp(); default: return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text("Flutter Demo Home Page"), ), body: const Center( child: Text( "page not found", style: TextStyle( fontSize: 24, color: Colors.red ), ), ), ), ); } }
|
3、当我们修改 Flutter 工程的代码后,重新运行 Android 项目并不会生效,我们需要:
1、在 Flutter 工程重新执行 flutter build aar
命令
2、待 Flutter 命令执行完成,clean Android 工程
此时我们运行 Android 项目,就可以看到效果了:
可以看到,页面跳转变得非常丝滑
现在只是简单的跳转,那么如果我想在跳转时给 Flutter 页面传值要怎么做呢?
2.2、Android 给 Flutter 页面传值
分析 2.1 这个例子,我们在 Android 工程中设置了启动路由:
1
| flutterEngine.navigationChannel.setInitialRoute("main")
|
然后在 Flutter 中通过 window.defaultRouteName
获取了路由
那么我是否可以在启动路由中多添加一些数据,然后 Flutter 获取后进行解析呢?例如:
1 2 3 4 5 6 7 8 9 10 11
| flutterEngine.navigationChannel.setInitialRoute("main?{\"name\":\"erdai\",\"age\":18}")
String url = window.defaultRouteName;
String routeName = url.substring(0,url.indexOf("?"));
String paramsString = url.substring(url.indexOf("?") + 1); Map<String,dynamic> paramsMap = json.decode(paramsString);
|
实际上就是这么干的,我们修改 Flutter 端的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| void main(){ String url = window.defaultRouteName; String routeName = url.substring(0,url.indexOf("?")); String paramsString = url.substring(url.indexOf("?") + 1); Map<String,dynamic> paramsMap = json.decode(paramsString); print(paramsMap); runApp(getRouter(routeName)); }
|
当我们发布 aar,clean Android 工程并重新运行会进行参数的打印:
三、Android 嵌入 Flutter 页面(FlutterFragment)
类比 Android 启动 FlutterActivity,主要是通过两种方式构建 intent 对象:
1 2 3 4 5 6 7
| var intent: Intent = FlutterActivity.createDefaultIntent(this)
var intent = FlutterActivity .withCachedEngine(FLUTTER_ENGINE_ID) .build(this)
|
构建 FlutterFragment 类似:
1 2 3 4 5 6 7
| var flutterFragment: FlutterFragment = FlutterFragment.createDefault()
var flutterFragment: FlutterFragment = FlutterFragment .withCachedEngine(FLUTTER_ENGINE_ID) .build()
|
修改 Android 代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
|
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/flFragmentContainer" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".SecondActivity"/>
class SecondActivity : AppCompatActivity() { companion object{ const val FLUTTER_ENGINE_ID = "default" } private lateinit var flutterEngine: FlutterEngine private lateinit var flutterFragment: FlutterFragment
@SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) flutterEngine = initFlutterEngine(FLUTTER_ENGINE_ID)
flutterFragment = FlutterFragment .withCachedEngine(FLUTTER_ENGINE_ID) .build()
supportFragmentManager.beginTransaction().replace(R.id.flFragmentContainer,flutterFragment).commit() }
上述代码一般在跳转前调用,这样可以加快我们页面的一个跳转
private fun initFlutterEngine(engineId: String): FlutterEngine { val flutterEngine = FlutterEngine(this) flutterEngine.navigationChannel.setInitialRoute("main?{\"name\":\"erdai\",\"age\":18}") flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) val flutterEngineCache = FlutterEngineCache.getInstance() flutterEngineCache.put(engineId,flutterEngine) return flutterEngine }
override fun onPostResume() { super.onPostResume() flutterFragment.onPostResume() }
override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) flutterFragment.onNewIntent(intent) }
override fun onBackPressed() { super.onBackPressed() flutterFragment.onBackPressed() }
override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) flutterFragment.onRequestPermissionsResult(requestCode,permissions,grantResults) }
override fun onUserLeaveHint() { super.onUserLeaveHint() flutterFragment.onUserLeaveHint() }
override fun onTrimMemory(level: Int) { super.onTrimMemory(level) flutterFragment.onTrimMemory(level) }
override fun onDestroy() { super.onDestroy() flutterEngine.destroy() } }
<activity android:name=".SecondActivity" android:exported="false" android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"/>
fun toSecondActivity(view: View) { startActivity(Intent(this,SecondActivity::class.java)) }
|
上述 SecondActivity 中我们重写了很多方法,然后将其转发到了 FlutterFragment 中,主要目的是为了实现 Flutter 中所有预期的行为
接下来看下效果:
四、Android 与 Flutter 通信
Flutter 提供了一套 PlatformChannel 机制用于 Flutter 和 Android 的通信,主要分为三种类型:
1、MethodChannel:主要用于传递方法调用,Flutter 和 Native(Android)之间进行方法调用时可以使用,是一种双向的通信方式
2、EventChannel:主要用于用户数据流的通信,如:手机电量变化,网络连接变化等。这种方式只能 Native(Android)向 Flutter 发送数据,是一种单向的通信方式
3、BaseicMessageChannel:主要用于传递各种类型数据,它支持的类型有很多,如:String,半结构化信息等,是一种双向的通信方式
4.1、MethodChannel
上面我们介绍了 Android 给 Flutter 页面传值,主要是通过这行代码:
1
| flutterEngine.navigationChannel.setInitialRoute("main?{\"name\":\"erdai\",\"age\":18}")
|
点击查看 navigationChannel 的源码:
1 2 3 4 5 6 7 8 9 10 11 12
| public class NavigationChannel { private static final String TAG = "NavigationChannel";
@NonNull public final MethodChannel channel;
public NavigationChannel(@NonNull DartExecutor dartExecutor) { this.channel = new MethodChannel(dartExecutor, "flutter/navigation", JSONMethodCodec.INSTANCE); channel.setMethodCallHandler(defaultHandler); } }
|
发现它实际就是对 MethodChannel 做了一层封装,底层是通过 MethodChannel 来进行通信
这种方式在开发中用的比较多,使用也比较简单,我们直接通过例子说明
下面实现这么一个需求:首先从 MainActivity 跳转到 SecondActivity,然后 SecondActivity 每隔一秒给 Flutter 页面发送一个数字,Flutter 接收到数字并显示到中间的 Text 中,当接收到数字等于 5 ,通知 SecondActivty finish
先看一眼实现的效果:
1、首先来看 Android 端代码实现,SecondActivity 新增的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class SecondActivity : AppCompatActivity() { private lateinit var methodChannel: MethodChannel private var count = 0
@SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) methodChannel = MethodChannel(flutterEngine.dartExecutor,"com.dream.interactive") methodChannel.setMethodCallHandler { call, result -> if(call.method == "sendFinish"){ finish() } } startTimer() } private fun startTimer() { Timer().schedule(timerTask { runOnUiThread { val map = mapOf("count" to count++) methodChannel.invokeMethod("timer", map) } }, 0, 1000) }
}
|
2、Flutter 端代码实现,新增部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class _MyHomePageState extends State<MyHomePage> { int _counter = 0; final _channel = const MethodChannel("com.dream.interactive");
@override void initState() { super.initState(); _channel.setMethodCallHandler((call) async { String method = call.method; switch(method){ case "timer": setState(() { _counter = call.arguments["count"]; }); if(_counter == 5){ _channel.invokeMethod("sendFinish"); break; } break; default: break; } }); } }
|
4.2、EventChannel
我们使用 EventChannel 模拟 Android 发送一个充电信息给 Flutter ,Flutter 接收后在中间的 Text 展示出来
1、 Android 端代码实现,SecondActivity 新增的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| class SecondActivity : AppCompatActivity() {
/ private lateinit var eventSink: EventChannel.EventSink private var electricity = 0
@SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) val eventChannel = EventChannel(flutterEngine.dartExecutor,"com.dream.eventchannel") eventChannel.setStreamHandler(object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink) { Log.d("erdai", "onListen: $arguments") eventSink = events startTimer() } override fun onCancel(arguments: Any?) { Log.d("erdai", "onCancel: 断开连接") } }) }
private fun startTimer() { Timer().schedule(timerTask { runOnUiThread { electricity += 20 eventSink.success("电量:$electricity%") if(electricity == 100){ eventSink.endOfStream() } } }, 0, 1000) } }
|
2、Flutter 端代码实现,新增部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| class _MyHomePageState extends State<MyHomePage> { dynamic electricity; final _eventChannel = const EventChannel("com.dream.eventchannel"); StreamSubscription? _streamSubscription;
@override void initState() { super.initState(); _streamSubscription = _eventChannel .receiveBroadcastStream(["Hello,建立连接吧"]) .listen(_onData,onError: _onError,onDone: _onDone); }
void _onData(event){ print(event); setState(() { electricity = event; }); }
void _onError(error){ print(error); }
void _onDone(){ print('_onDone'); }
@override void dispose() { if(_streamSubscription != null){ _streamSubscription?.cancel(); _streamSubscription = null; } }
@override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( '$electricity', style: Theme.of(context).textTheme.headline4, ), ], ), ), ); } }
|
接下来我们看下效果和 Log 日志:
4.3、BaseicMessageChannel
我们使用 BaseicMessageChannel 实现一段 Andorid 和 Flutter 的对话,Flutter 收到 Android 的消息,在中间的 Text 展示出来,Android 收到 Flutter 的消息,使用 Toast 展示出来
1、 Android 端代码实现,SecondActivity 新增的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class SecondActivity : AppCompatActivity() { private lateinit var messageChannel: BasicMessageChannel<String> @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second)
messageChannel = BasicMessageChannel(flutterEngine.dartExecutor,"com.dream.messagechannel",StringCodec.INSTANCE) messageChannel.setMessageHandler { replay: String?, reply: BasicMessageChannel.Reply<String> -> Log.d("erdai", "onCreate: $replay") Toast.makeText(this,replay,Toast.LENGTH_SHORT).show() reply.reply("梧桐山") } messageChannel.send("周末去爬山吗?") { replay: String? -> Log.d("erdai", "onCreate: $replay") Toast.makeText(this,replay,Toast.LENGTH_SHORT).show() } }
}
|
2、Flutter 端代码实现,新增部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| class _MyHomePageState extends State<MyHomePage> { dynamic _content; final _messageChannel = const BasicMessageChannel("com.dream.messagechannel", StringCodec());
@override void initState() { super.initState(); _messageChannel.setMessageHandler((message) =>Future<String>((){ print(message); setState(() { _content = message; }); return "好啊"; })); } void _incrementCounter() async{ var result = await _messageChannel.send("去爬哪座山?"); print("$result"); setState(() { _content = result; }); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( '$_content', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
|
看下效果和 Log 日志:

4.4、通信原理
从图中我们可以看出:
1、Android 和 Flutter 都是以 ByteBuffer 为载体,然后通过 BinaryMessenger 来发送和接收数据
2、Android 和 Flutter 都是基于 PlatformChannel 机制来进行通信的
之所以我们能够如此简单的进行通信,实则是系统给我们做了大量的封装:线程的切换,数据拷贝等复杂操作
另外需要注意的是:在 Android 侧,BinaryMessenger 是一个接口,在 FlutterView 中实现了该接口,在 BinaryMessenger 的方法中通过 JNI 来与系统底层沟通。在 Flutter 侧,BinaryMessenger 是一个类,该类的作用就是与类 window 沟通,而类 window 才真正与系统底层沟通
五、总结
本篇文章我们介绍了:
1、Android 集成 Flutter
主要就是将 Flutter 端的代码打成 aar ,然后 Android 引用这个 aar
2、Android 调起 Flutter 页面(FlutterActivity,FlutterFragment),并给 Flutter 页面传值
传值底层使用的 MethodChannel
3、Android 与 Flutter 通信,主要使用到了 Flutter 的 PlatformChannel 机制,其实现主要有三种类型:
1、MethodChannel:用于 Flutter 和 Android 之间的方法通信,双向的
2、EventChannel:用于 Flutter 和 Android 之间的数据流通信,单向的:Android -> Flutter
3、BaseicMessageChannel:用于 Flutter 和 Android 之间的数据通信,双向的
4、简单的介绍了 Android 与 Flutter 通信的原理
好了,本篇文章到这里就结束了,希望能给你带来帮助 🤝
感谢你阅读这篇文章
下篇预告
下篇文章我会讲开发 Flutter 项目的一个技术选型,尽请期待吧🍺
参考和推荐
Android 集成 Flutter | 与交互
一篇看懂Android与Flutter之间的通信
你的点赞,评论,是对我巨大的鼓励!
欢迎关注我的公众号: sweetying ,文章更新可第一时间收到
如果有问题,公众号内有加我微信的入口,在技术学习、个人成长的道路上,我们一起前进!