文章目录
一、前言
Flutter本身是一个跨平台的框架,所以不可能面面俱到的把Native平台的特性都实现出来,这里面就需要用到插件。本文基于Android平台来实现一个Android客户端自定义的View,并可以进行一定的混合通信能力。在学会本文后,不仅可以进行原生View的封装,也可以进行Flutter插件的编写
二、Android代码的编写
首先编写Plugin插件,新版的插件是通过实现FlutterPlugin
来做的,简单代码如下:
TestPlugin.kt
import io.flutter.embedding.engine.plugins.FlutterPlugin
/**
* 测试插件
*/
class TestPlugin: FlutterPlugin{
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
//程序在加载插件会调用该函数
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
//程序在卸载插件时候会调用该函数
}
}
三、插件仓库
上一段内容就是简单的编写了一个插件,虽然什么功能都没有,接下来需要在程序启动时候进行加载插件,这里我们编写一个统一的插件管理仓库来进行对插件进行管理
DqPluginRegistrant.kt
import androidx.annotation.Keep
import com.dq.flutter_dq_app.plugin.TestPlugin
import io.flutter.embedding.engine.FlutterEngine
/**
* 自己写的插件在这里进行注册
*/
@Keep
class DqPluginRegistrant {
companion object{
fun registerWith(flutterEngine: FlutterEngine){
flutterEngine.plugins.add(TestPlugin())//通过FlutterEngine进行加载插件
}
}
}
四、加载插件
由上文知道插件需要一个FlutterEngine
来进行加载的,一般可以在FlutterActivity
和FlutterFragmentActivity
中获取到该值,代码如下:
import android.content.Intent
import android.util.Log
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
DqPluginRegistrant.registerWith(flutterEngine)
}
}
至此,整个插件的加载就已经完成了,在Flutter程序启动时候,会把作为首页的MainActivity启动,然后通过调用configureFlutterEngine(flutterEngine: FlutterEngine)
进行插件配置。
五、编写Android端的自定义View
在Flutter中可以通过实现PlatformView
来实现对原生View的封装,使原生View作为Flutter组件进行显示,简要代码如下:
import android.content.Context
import android.view.View
import android.widget.TextView
import io.flutter.plugin.platform.PlatformView
//自定义View
class TestView: PlatformView{
private var view:View
constructor(context: Context) {
val textView = TextView(context)
textView.text = "这是一个TextView"
this.view = textView
}
override fun getView(): View = view
override fun dispose() {
//这里可以对一些资源进行清理
}
}
六、创建工厂模式对自定义View进行加载
在Flutter中加载PlatformView
的需要通过PlatformViewFactory
来进行加载,简要代码如下:
import android.content.Context
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
class FlutterTestViewFactory: PlatformViewFactory(StandardMessageCodec.INSTANCE) {
//参数中args的问号不可以省略,否则程序出错
override fun create(context: Context, viewId: Int, args: Any?): PlatformView = FlutterTestView(context)
}
七、在插件中加载PlatformView
PlatformViewFactory
创建完后需要在插件中进行加载,代码如下:
import com.dq.flutter_dq_app.view.FlutterTestViewFactory
import io.flutter.embedding.engine.plugins.FlutterPlugin
/**
* 测试插件
*/
class TestPlugin:
FlutterPlugin//对Flutter加载、卸载的监听
{
private val gameViewType = "flutter.plugins.io/testView"//本地View的类型,这个名字要和Flutter那边的组件名字保持一致
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
//程序在加载插件会调用该函数
binding
.platformViewRegistry
.registerViewFactory(
gameViewType, FlutterTestViewFactory())
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
//程序在卸载插件时候会调用该函数
}
}
八、Flutter中进行显示Native的自定义View
简单代码如下:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Scaffold(
appBar: AppBar(
title: Text('Material App Bar'),
),
body: Center(
child: Container(
child: AndroidView(viewType: 'flutter.plugins.io/testView'),//这个名字要和Native那边的名字保持一致
),
),
),
);
}
}
至此,通过插件编写原生View的方式基本结束
九、在原生的VIew中获取Activity
在实际开发中业务往往是复杂的,里面常常会用到Activity,针对这个问题,可以通过Android的ActivityLifecycleCallbacks
获取顶层View的方式来实现,当然也可以通过Flutter自带的方式,接下来使用Flutter来在View中获取Activity,这里需要使用到ActivityAware
,并且需要对原先的代码进行改造,代码如下:
FlutterTestView.kt
//自定义View
class FlutterTestView: PlatformView{
private var view:View
constructor(context: Context,act: Activity? = null) {
val textView = TextView(context)
textView.text = "这是一个TextView"
this.view = textView
if(null != act){
Toast.makeText(act,"获取到了Activity",Toast.LENGTH_SHORT).show();
}
}
override fun getView(): View = view
override fun dispose() {
//这里可以对一些资源进行清理
}
}
FlutterTestViewFactory.kt
//自定义View的工厂方法
class FlutterTestViewFactory: PlatformViewFactory(StandardMessageCodec.INSTANCE) {
private var activity: Activity? = null
//参数中的问号不可以省略,否则程序出错
//create函数是在Flutter中显示该自定义组件的时候进行回调,请注意这个时机
override fun create(context: Context, viewId: Int, args: Any?): PlatformView{
return FlutterTestView(context,activity)
}
//之所以分开而不在构造函数里面传递是因为各个回调函数调用时机不一致的问题
fun setActivity(activity: Activity){
this.activity = activity
}
}
TestPlugin.kt
import com.dq.flutter_dq_app.view.FlutterTestViewFactory
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
/**
* 测试插件
*/
class TestPlugin:
FlutterPlugin,//对Flutter加载、卸载的监听
ActivityAware//对Activity加载、卸载的监听
{
private val gameViewType = "flutter.plugins.io/testView"//本地View的类型,这个名字要和Flutter那边的组件名字保持一致
private lateinit var flutterTestViewFactory: FlutterTestViewFactory
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
//程序在加载插件会调用该函数
flutterTestViewFactory = FlutterTestViewFactory()
binding
.platformViewRegistry
.registerViewFactory(
gameViewType, flutterTestViewFactory)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
//程序在卸载插件时候会调用该函数
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
//加载Activity的监听
//该函数会在onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding)之后调用
flutterTestViewFactory.setActivity(binding.activity)
}
override fun onDetachedFromActivityForConfigChanges() {
//配置切换监听,比如横竖屏旋转
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
//配置切换监听,比如横竖屏旋转
}
override fun onDetachedFromActivity() {
//移除Activity的监听
//该函数会在onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding)之前调用
}
}
十、混合通信及完整代码
在实际业务中,通常不仅仅只显示一个View这样,还有其它一些交互操作,这里就涉及到通信问题,以下来通过Flutter端的点击事件来改变Native端View显示的内容,点击按钮时候,会将Android端的内容发送给Flutter端。完整代码如下:
FlutterTestView.kt
//自定义View
class FlutterTestView: PlatformView{
private var callBack : (() -> Unit) ?= null //一个回调
private var view:View
constructor(context: Context,act: Activity? = null) {
val btn = Button(context)
btn.text = "这是一个按钮"
this.view = btn
// Toast.makeText(act,"获取到了Activity",Toast.LENGTH_SHORT).show();
Log.e("YM","----TextView显示--->")
btn.setOnClickListener {
callBack?.invoke()
}
}
override fun getView(): View = view
override fun dispose() {
//这里可以对一些资源进行清理
}
fun changeText(text: String){
if (view is Button){
(view as Button).text = text
}
}
//添加监听
fun addCallBack(callBack : (() -> Unit)){
this.callBack = callBack
}
}
FlutterTestViewFactory.kt
//自定义View的工厂方法
class FlutterTestViewFactory: PlatformViewFactory(StandardMessageCodec.INSTANCE) {
private var callBack : (() -> Unit) ?= null //一个回调
private var activity: Activity? = null
private lateinit var flutterTestView: FlutterTestView
//参数中的问号不可以省略,否则程序出错
//create函数是在Flutter中显示该自定义组件的时候进行回调,请注意这个时机
override fun create(context: Context, viewId: Int, args: Any?): PlatformView{
flutterTestView = FlutterTestView(context,activity)
flutterTestView.addCallBack(callBack!!)
return flutterTestView
}
//之所以分开而不在构造函数里面传递是因为各个回调函数调用时机不一致的问题
fun setActivity(activity: Activity){
this.activity = activity
}
fun changeText(text: String){
flutterTestView.changeText(text)
}
//添加监听
fun addCallBack(callBack : (() -> Unit)){
this.callBack = callBack
}
}
TestPlugin.kt
/**
* 测试插件
*/
class TestPlugin:
FlutterPlugin,//对Flutter加载、卸载的监听
ActivityAware,//对Activity加载、卸载的监听
MethodChannel.MethodCallHandler//对Flutter与Native通信的监听
{
private val testViewType = "flutter.plugins.io/testView"//本地View的类型,这个名字要和Flutter那边的组件名字保持一致
//Flutter与Native通信的渠道,调用View中的函数时候,由于View可能不存在,所以可以将相关的通信渠道转移到View中进行监听
private val testViewChannel = "dq.plugins.flutter/testView/channel"//Flutter那里对通信渠道监听的时候需要和该渠道名字保持一致
private val methodName = "testMethod"//Flutter那里调用的函数名字需要和这个保持一致
private val methodNameSender = "sendFlutterMethod"//发送消息到Flutter的函数名,Flutter那里调用的函数名字需要和这个保持一致
private lateinit var flutterTestViewFactory: FlutterTestViewFactory
private var testChannel: MethodChannel ?= null
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
//程序在加载插件会调用该函数
testChannel = MethodChannel(binding.binaryMessenger, testViewChannel)
testChannel?.setMethodCallHandler(this)//对通信渠道的监听
flutterTestViewFactory = FlutterTestViewFactory()
binding
.platformViewRegistry
.registerViewFactory(
testViewType, flutterTestViewFactory)
flutterTestViewFactory.addCallBack {
sendMessageToFlutter()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
//程序在卸载插件时候会调用该函数
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
//加载Activity的监听
//该函数会在onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding)之后调用
flutterTestViewFactory.setActivity(binding.activity)
}
override fun onDetachedFromActivityForConfigChanges() {
//移除Activity的监听
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
}
override fun onDetachedFromActivity() {
//该函数会在onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding)之前调用
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == methodName) {//通信渠道中调用相关的函数
result.success("Android ${android.os.Build.VERSION.RELEASE}")//该方法会将内容回传给Flutter
val arguments = call.arguments as String //传递什么都行,这里记得改成一样的类型
flutterTestViewFactory.changeText(arguments)
} else {
result.notImplemented()
}
}
//发消息给Flutter
private fun sendMessageToFlutter(){
testChannel?.invokeMethod(methodNameSender,"Native端发送过来的消息",object : MethodChannel.Result{
override fun success(result: Any?) {
}
override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) {
}
override fun notImplemented() {
}
})
}
}
flutter_native_view.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
_listenerNativeMessage();
return MaterialApp(
title: 'Material App',
home: Scaffold(
appBar: AppBar(
title: Text('Material App Bar'),
),
body: Center(
child: Container(
child: GestureDetector(
onTap: (){
//这里接收不到
print("Android控件点击情况");
},
onVerticalDragDown: (d){//示例代码源于platform_view.dart
print("垂直滑动事件");
platformTest;
},
child: Container(
height: 200,
width: 200,
color: Colors.red,
//这个名字要和Native那边的名字保持一致
//PlatformViews没有办法接收到点击事件,但是底层的屏幕事件是可以接收到的
//详情看:https://zhuanlan.zhihu.com/p/108770601
child: AndroidView(viewType: 'flutter.plugins.io/testView'),
),
),
)
),
),
);
}
static const MethodChannel _channel = const MethodChannel('dq.plugins.flutter/testView/channel');//参数记得改成和Native端一致的内容
static const methodNameSender = "sendFlutterMethod";//发送消息到Flutter的函数名,Flutter那里调用的函数名字需要和这个保持一致
Future<String> get platformTest async {
final String version = await _channel.invokeMethod('testMethod',"我要改值了");//第一个方法名记得和Native保持一致
return version;
}
_listenerNativeMessage(){
_channel.setMethodCallHandler((methodCall) async {
String methodName = methodCall.method;
String params = methodCall.arguments;
switch(methodName){
case methodNameSender:
print('接收到的参数:$params');
break;
}
print('flutter listen:$methodCall');
return "";
});
}
}
十一、总结
以上实现了日常开发中的简单的Flutter和Native的交互问题,但是其中还有一些问题需要了解,比如ActivityAware
类的含义以及与其类似的其它类,如:BroadcastReceiverAware
、ContentProviderAware
、ServiceAware
。再比如通信中用到的MethodChannel
以及其他相关的类,如:BasicMessageChannel
等等,还有Flutter中是否可以嵌入Android的Fragment
等问题。
十二、参考资料
本文主要参考了部分源码、官方示例代码及以下资料:
- https://book.flutterchina.club/chapter12/android_implement.html
- https://medium.com/flutter/modern-flutter-plugin-development-4c3ee015cf5a
- https://www.jianshu.com/p/bba0f615d59c
- https://www.cnblogs.com/moluy/p/14132564.html
- https://www.jianshu.com/p/7367492e7bf1
- 三种MessageChannel的使用方式:
https://www.cnblogs.com/wjw334/p/12693220.html - Flutter混合开发-通信:
https://www.cnblogs.com/wjw334/archive/2004/01/13/12693220.html