该文已授权公众号 「码个蛋」,转载请指明出处
Flutter 说到底只是一个 UI 框架,很多功能都需要通过原生的 Api 来实现,那么就会涉及到 Flutter 和 Native 的交互,因为本人不懂 iOS 开发,所以只能讲下 Flutter 同 Android 的交互。
Android 项目配置 Flutter 依赖
既然是互相交互,那么需要准备一个 Android 项目。接着就需要创建 flutter module,让 Android 项目依赖,创建的方法可以参考官网 Flutter Wiki,虽然是官网提供的方法,但是完全按照这个步骤来,还是会有坑的,这边就慢慢一步步解决坑。
如果你用的是 Android Studio 进行开发的话,直接打开底部的 Terminal,直接创建 flutter module 依赖
flutter create -t module flutter_native_contact
至于 module 名可以随意填写,module 创建完后结构大概是这样的
接着切换到 module 下的 .android 文件夹,接着有坑来了,官网提供的方法是 ./gradlew flutter:assembleDebug
可能会提示命令不存在,那么直接通过 gradlew flutter:assembleDebug
来运行,等它自动跑完后,打开根目录下的 settings.gradle
文件,加入官网提供的 gradle 代码
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'flutter_native_contact/.android/include_flutter.groovy' // new
)) // new
复制代码
你以为这里没坑,真是图样图森破,没坑是不可能的,编译器大爷可能会给你甩这么个错误
很明显可以看出是找不到我们的文件,所以把文件名路径给补全
evaluate(new File( // new
settingsDir.parentFile, // new
'FlutterNativeContactDemo/flutter_native_contact/.android/include_flutter.groovy' // 这里补全路径
))
复制代码
接着打开原有项目下,原有项目下,原有项目下的 app 中的 build.gradle
文件,在 android 下加上如下代码
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
复制代码
这个必须要加,不要问为什么,我也不知道为什么,最后在项目下添加 flutter module 的依赖就完成了。这个过程告诉我们一个什么道理呢?*不要以为官网的都对,官网讲的也不是完全可信的,时不时给你来个坑就能卡你老半天。
原生界面加载 Flutter 页面
那么如何在原生界面显示 Flutter 界面呢,这个就需要通过 FlutterView 来实现了,Flutter 这个类提供了 createView
和 createFragment
两个方法,分别用于返回 FlutterView 和 FlutterFragment 实例,FlutterFragment 的实现原理也是通过 FlutterView 来实现的,可以简单看下 FlutterFragment 的源码
/**
* A {@link Fragment} managing a {@link FlutterView}.
*
* <p><strong>Warning:</strong> This file is auto-generated by Flutter tooling.
* DO NOT EDIT.</p>
*/
public class FlutterFragment extends Fragment {
public static final String ARG_ROUTE = "route";
private String mRoute = "/";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获取传入的路由值,默认为 '/'
if (getArguments() != null) {
mRoute = getArguments().getString(ARG_ROUTE);
}
}
@Override
public FlutterView onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// 最后还是挺过 createView 方法来生成页面,只不过直接放在 fragment,
// 放在 fragment 会比直接 使用 FlutterView 更方便管理,例如实现 ViewPager 等
return Flutter.createView(getActivity(), getLifecycle(), mRoute);
}
}
复制代码
createFragment 方式加载
在原生页面显示 Flutter 界面的第一种方式就是加载 FlutterFragment,看个比较简单的例子吧
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- 这个布局用于加载 fragment -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/flutter_fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginBottom="50dp"
android:src="@drawable/ic_add_white_36dp"
app:fabSize="auto"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout>
复制代码
在 Activity 可以直接通过返回 FlutterFragment 加载到 FrameLayout 即可
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, Flutter.createFragment("route_flutter"))
.commit()
}
}
复制代码
这样就把 Flutter 页面加载到原生界面了,会通过传递的路由值在 dart 层进行查找,所以接着就需要编写 Flutter 界面
/// runApp 内部值也可以直接传入 _buildWidgetForNativeRoute 方法
/// 这边在外层嵌套一层 MaterialApp 主要是防止一些不必要的麻烦,
/// 例如 MediaQuery 这方面的使用等
void main() => runApp(FlutterApp());
class FlutterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: _buildWidgetForNativeRoute(window.defaultRouteName),
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Color(0XFF008577),
accentColor: Color(0xFFD81B60),
primaryColorDark: Color(0xFF00574B),
iconTheme: IconThemeData(color: Color(0xFFD81B60)),
),
);
}
}
/// 该方法用于判断原生界面传递过来的路由值,加载不同的页面
Widget _buildWidgetForNativeRoute(String route) {
switch (route) {
case 'route_flutter':
return GreetFlutterPage();
// 默认的路由值为 '/',所以在 default 情况也需要返回页面,否则 dart 会报错,这里默认返回空页面
default:
return Scaffold();
}
}
class GreetFlutterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('NativeMessageContactPage'),
),
body: Center(
child: Text(
'This is a flutter fragment page',
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
),
);
}
}
复制代码
运行后可以看到页面加载出来了,不过会有一段时间的空白,这个在正式打包后就不会出现,所以不必担心。最后的页面应该是这样的
createView 方式加载
接着看下 createView 方法,说白了,第一种方法最后还是会通过该方式实现
@NonNull
public static FlutterView createView(@NonNull final Activity activity, @NonNull final Lifecycle lifecycle, final String initialRoute) {
// 交互前的一些初始化工作,需要完成才可以继续下一步,同时需要保证当前线程为主线程
// Looper.myLooper() == Looper.getMainLooper(),否则会甩你一脸的 IllegalStateException
FlutterMain.startInitialization(activity.getApplicationContext());
FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), null);
final FlutterNativeView nativeView = new FlutterNativeView(activity);
// 将 flutter 页面绑定到相应的 activity
final FlutterView flutterView = new FlutterView(activity, null, nativeView) {
// ......
};
// 将路由值传到 flutter 层,并加载相应的页面,
if (initialRoute != null) {
flutterView.setInitialRoute(initialRoute);
}
// 绑定 lifecycle,方便生命周期管理,同 activity 绑定
// 不熟悉 LifeCycle 的同学可以自行网上查找资料
lifecycle.addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void onCreate() {
// 配置一些参数,传递到 flutter 层
final FlutterRunArguments arguments = new FlutterRunArguments();
arguments.bundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext());
arguments.entrypoint = "main";
// 最终会调用方法 nativeRunBundleAndSnapshotFromLibrary,这是一个 native 方法,进行交互
flutterView.runFromBundle(arguments);
// 进行注册
GeneratedPluginRegistrant.registerWith(flutterView.getPluginRegistry());
}
// ......
});
return flutterView;
}
复制代码
通过 createView 方法返回的 FlutterView,通过设置 Layoutparams 参数就可以添加到相应的布局上,还有一种直接通过 addContentView 方式进行加载,这里直接修改原有代码,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main) 不需要这一步了
val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_flutter")
val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp) // 直接加载到 activity 页面
}
复制代码
但是通过这样加载的话,那么整个页面都是 flutter 的页面。那么之前的效果的 FAB 则不会被加载出来了,即使没有省略 setContentView(R.layout.activity_main)
方法,这个页面的 xml 布局也会被覆盖。
PlantformChannel
那么能够在原生界面显示 flutter 页面了,如何互相交互呢,这就需要通过 PlantformChannel 来执行了,PlantformChannel 主要有三种类型,BasicMessageChannel,MethodChannel,EventChannel。通过查看源码可以发现,三个 Channel 的实现机制类似,都是通过 BinaryMessenger 进行信息交流,每个 Channel 通过传入的 channel name 进行区分,所以在注册 Channel 的时候必须要保证 channel name 是唯一的,同时需要传入一个 BinaryMessageHandler 实例,用于传递信息的处理,当 Handler 处理完信息后,会返回一个 result,然后通过 BinaryMessenger 将 result 返回到 Flutter 层。如果需要深入理解这边推荐一篇文章深入理解Flutter PlatformChannel
接下来直接看例子吧,在创建 PlatformChannel 的时候需要传入一个 BinaryMessenger 实例,通过查看 FlutterView 的源码可以发现,FlutterView 就是一个 BinaryMessenger 在 Android 端的实现,所以呢,可以直接通过前面介绍的 Flutter.createView
方法获取注册 Channel 时的 BinaryMessenger 实例了,真是得来全部费工夫~因为通信的方法可能在多个界面会使用,所以还是封装一个通用类来处理会比较合理
BasicMessageChannel
BasicMessageChannel 用于传递字符串和半结构化的信息。
class FlutterPlugin(private val flutterView: FlutterView) :BasicMessageChannel.MessageHandler<Any>{
companion object {
private const val TAG = "FlutterPlugin"
@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
// channel name 需要保持两侧一致
val messageChannel =
BasicMessageChannel(flutterView, Constant.MESSAGE_CHANNEL_NAME, StandardMessageCodec.INSTANCE) // MessageCodec 有多种实现方式,可以参考推荐的文章
val instance = FlutterPlugin(flutterView)
messageChannel.setMessageHandler(instance) // 注册处理的 Hnadler
return instance
}
}
override fun onMessage(`object`: Any?, reply: BasicMessageChannel.Reply<Any>?) {
// 简单的将从 Flutter 传过来的消息进行吐司,同时返回自己的交互信息
// `object` 中包含的就是 Flutter 层传递过来的信息,reply 实例用于传递信息到 Flutter 层
Toast.makeText(flutterView.context, `object`.toString(), Toast.LENGTH_LONG).show()
reply?.reply("\"Hello Flutter\"--- an message from Android")
}
}
复制代码
接着就需要有个 FlutterView 用来注册,新建一个 Activity,用于加载 Flutter 页面
class ContactActivity : AppCompatActivity() {
private lateinit var plugin: FlutterPlugin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 传入路由值,需要在 flutter 层生成相应的界面
val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_contact")
val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp)
plugin = FlutterPlugin.registerPlugin(flutterView)
}
override fun onDestroy() {
super.onDestroy()
}
}
复制代码
那么我们就要在 Flutter 界面的 _buildWidgetForNativeRoute
方法加入新路由值对应的界面
Widget _buildWidgetForNativeRoute(String route) {
switch (route) {
// ...
case 'route_contact':
return FlutterContactPage();
default:
return Scaffold();
}
}
class FlutterContactPage extends StatelessWidget {
// 注册对应的 channel,要保证 channel name 和原生层是一致的
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Page'),
),
// 简单放一个按钮,通过 channel 传输消息过去,同时将原生层返回的消息打印出来
body: RaisedButton(
onPressed: () {
_messageChannel
.send('"Hello Native" --- an message from flutter')
.then((str) {
print('Receive message: $str');
});
},
child: Text('Send Message to Native'),
),
);
}
}
复制代码
最后的效果小伙伴可以自行执行,点击按钮后会弹出吐司,吐司内容就是 Flutter 传递的信息,同时在控制台可以看到从原生层返回的信息。
MethodChannel
MethodChannel 用于传递方法调用(method invocation)
直接在上述例子中进行修改,例如在 Flutter 页面中实现 Activity 的 finish 方法,并传递参数到前一个界面,先做 Flutter 页面的修改,在 AppBar 上增加一个返回按钮,用于返回上层页面
class FlutterContactPage extends StatelessWidget {
// 注册对应的 channel,要保证 channel name 和原生层是一致的
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());
final MethodChannel _methodChannel = MethodChannel(METHOD_CHANNEL_NAME);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Icon(Icons.arrow_back),
),
onTap: () {
_methodChannel
// invokeMethod 第一个值用于传递方法名,第二个值用于传递参数,
// 这边简单的传递一个字符串,当然也可以传递别的类型,map,list 等等
.invokeMethod<bool>('finishActivity', 'Finish Activity')
.then((result) { // 这边会返回一个结果值,通过判断是否成功来打印不同的信息
print('${result ? 'has finish' : 'not finish'}');
});
},
),
title: Text('Flutter Page'),
),
body: // ...
);
}
}
复制代码
同时,我们需要在 FlutterPlugin 这个类中,做些必要的修改,首先需要实现 MethodCallHandler
接口,该接口中需要实现 onMethodCall
方法,通过获取调用的方法名和参数值,进行相应的处理
class FlutterPlugin(private val flutterView: FlutterView) :
MethodChannel.MethodCallHandler, BasicMessageChannel.MessageHandler<Any> {
companion object {
private const val TAG = "FlutterPlugin"
@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
val instance = FlutterPlugin(flutterView)
val methodChannel = MethodChannel(flutterView, Constant.METHOD_CHANNEL_NAME)
// ...
messageChannel.setMessageHandler(instance)
return instance
}
}
// ....
// call 中携带了 Flutter 层传递过来的方法名和参数信息
// 可以分别通过 call.method 和 call.arguments 来获取
override fun onMethodCall(call: MethodCall?, result: MethodChannel.Result?) {
when (call?.method) {
"finishActivity" -> {
val activity = flutterView.context as Activity
val info = call.arguments.toString()
val intent = Intent().apply {
putExtra("info", info)
}
activity.setResult(Activity.RESULT_OK, intent)
activity.finish()
// 成功时候通过 result.success 返回值,
// 如果发生异常,通过 result.error 返回异常信息
// Flutter 通过 invokeMethod().then() 来处理正常结束的逻辑
// 通过 catchError 来处理发生异常的逻辑
result?.success(true)
}
// 如果未找到对应的方法名,则通过 result.notImplemented 来返回异常
else -> result?.notImplemented()
}
}
复制代码
最终的效果,当点击返回按钮的时候,会将 Flutter 层通过 invokeMethod 传递的 arguments 属性吐司出来,同时,控制台会打印出 "has finish" 的信息
EventChannel
EventChannel 用于数据流(event streams)的通信
EventChannel 的实现方式也类似,EventChannel 可以持续返回多个信息到 Flutter 层,在 Flutter 层的表现就是一个 stream,原生层通过 sink 不断的添加数据,Flutter 层接收到数据的变化就会作出新相应的处理。在 Android 端实现状态的监听可以通过广播来实现。直接看例子,还是修改上述代码
class FlutterPlugin(private val flutterView: FlutterView) :
MethodChannel.MethodCallHandler, EventChannel.StreamHandler, BasicMessageChannel.MessageHandler<Any> {
private var mStateChangeReceiver: BroadcastReceiver? = null
companion object {
private const val TAG = "FlutterPlugin"
const val STATE_CHANGE_ACTION = "com.demo.plugins.action.StateChangeAction"
const val STATE_VALUE = "com.demo.plugins.value.StateValue"
@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
// ...
val streamChannel = EventChannel(flutterView, Constant.STREAM_CHANNEL_NAME)
val instance = FlutterPlugin(flutterView)
methodChannel.setMethodCallHandler(instance)
streamChannel.setStreamHandler(instance)
messageChannel.setMessageHandler(instance)
return instance
}
}
// 实现 StreamHandler 需要重写 onListen 和 onCancel 方法
// onListen 不会每次数据改变就会调用,只在 Flutter 层,eventChannel 订阅广播
// 的时候调用,当取消订阅的时候则会调用 onCancel,
// 所以当开始订阅数据的时候,注册接收数据变化的关闭,
// 在取消订阅的时候,将注册的广播注销,防止内存泄漏
override fun onListen(argument: Any?, sink: EventChannel.EventSink?) {
mStateChangeReceiver = createEventListener(sink)
flutterView.context.registerReceiver(mStateChangeReceiver, IntentFilter(STATE_CHANGE_ACTION))
}
override fun onCancel(argument: Any?) {
unregisterListener()
}
// 在 activity 被销毁的时候,FlutterView 不一定会调用销毁生命周期,或者会延时调用
// 这就需要手动去注销一开始注册的广播了
fun unregisterListener() {
if (mStateChangeReceiver != null) {
flutterView.context.unregisterReceiver(mStateChangeReceiver)
mStateChangeReceiver = null
}
}
private fun createEventListener(sink: EventChannel.EventSink?):
BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (TextUtils.equals(intent?.action, STATE_CHANGE_ACTION)) {
// 这边广播只做简单的接收一个整数,然后通过 sink 传递到 Flutter 层
// 当然,sink 还有 error 方法,用于传递发生的错误信息,
// 以及 endOfStream 方法,用于结束接收
// 在 Flutter 层分别有 onData 对应 success 方法,onError 对应 error 方法
// onDone 对应 endOfStream 方法,根据不同的回调处理不同的逻辑
sink?.success(intent?.getIntExtra(STATE_VALUE, -1))
}
}
}
}
复制代码
在 Flutter 层,通过对 stream 的监听,对返回的数据进行处理,为了体现出变化,这边修改成 SatefulWidget 来存储状态
class FlutterContactPage extends StatefulWidget {
@override
_FlutterContactPageState createState() => _FlutterContactPageState();
}
class _FlutterContactPageState extends State<FlutterContactPage> {
final MethodChannel _methodChannel = MethodChannel(METHOD_CHANNEL_NAME);
final EventChannel _eventChannel = EventChannel(STREAM_CHANNEL_NAME);
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());
StreamSubscription _subscription;
var _receiverMessage = 'Start receive state'; // 初始的状态值
@override
void initState() {
super.initState();
// 当页面生成的时候就开始监听数据的变化
_subscription = _eventChannel.receiveBroadcastStream().listen((data) {
setState(() {
_receiverMessage = 'receive state value: $data'; // 数据变化了,则修改数据
});
}, onError: (e) {
_receiverMessage = 'process error: $e'; // 发生错误则显示错误信息
}, onDone: () {
_receiverMessage = 'receive data done'; // 发送完毕则直接显示完毕
}, cancelOnError: true);
}
@override
void dispose() {
super.dispose();
_subscription.cancel(); // 当页面销毁的时候需要将订阅取消,防止内存泄漏
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Icon(Icons.arrow_back),
),
onTap: () {
// MethodChannel demo
_methodChannel
.invokeMethod<bool>('finishActivity', _receiverMessage)
.then((result) {
print('${result ? 'has finish' : 'not finish'}');
}).catchError((e) {
print('error happend: $e');
});
},
),
title: Text('Flutter Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
// EventChannel demo,页面直接显示信息的变化
child: Text(
_receiverMessage,
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
),
// BasicMessageChannel demo
RaisedButton(
onPressed: () {
_messageChannel
.send('"Hello Native" --- an message from flutter')
.then((str) {
print('Receive message: $str');
});
},
child: Text('Send Message to Native'),
),
],
),
),
);
}
}
复制代码
同时,需要在 Activity 层调用一个定时任务不断的发送广播
class ContactActivity : AppCompatActivity() {
private var timer: Timer? = null
private var task: TimerTask? = null
private lateinit var random: Random
private lateinit var plugin: FlutterPlugin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
random = Random() // 生成随机整数
val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_contact")
val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp)
plugin = FlutterPlugin.registerPlugin(flutterView)
timer = Timer() // 定时器
task = timerTask { // 定时任务
sendBroadcast(Intent(FlutterPlugin.STATE_CHANGE_ACTION).apply {
putExtra(FlutterPlugin.STATE_VALUE, random.nextInt(1000))
})
}
timer?.schedule(task, 3000, 2000) // 延时 3s 开启定时器,并 2s 发送一次广播
}
override fun onDestroy() {
super.onDestroy()
// 页面销毁的时候需要将定时器,定时任务销毁
// 同时注销 Plugin 中注册的广播,防止内存泄漏
timer?.cancel()
timer = null
task?.cancel()
task = null
plugin.unregisterListener()
}
}
复制代码
最后的实现效果大概是这样的
Flutter 同 Android 端的交互到这讲的差不多了,和 iOS 的交互其实也类似,只不过在 Android 端通过 FlutterNativeView 来作为 Binarymessenger 的实现,在 iOS 端通过 FlutterBinaryMessenger 协议实现,原理是一致的。至于 Flutter 插件,其实现也是通过以上三种交互方式来实现的,可能我们目前通过 FlutterView 来作为 BinaryMessenger 实例,插件会通过 PluginRegistry.Registrar 实例的 messenger() 方法来获取 BinaryMessenger 实例。
需要了解插件的写法也可以直接查看官方提供的检测电量插件:Flutter Battery Plugin