Flutter学习8-Flutter和Native混合开发及其通讯机制

Flutter混合开发概述

Flutter可以和原生混合开发,其中有几种主要的形式

  • 作为单独页面嵌入原生App中,Flutter页面和原生页面可以互相打开.
  • 作为页面的一部分嵌入,Flutter页面可以嵌入到原生页面的一部分,同样原生页面也可以嵌入Flutter页面的一部分

混合开发之Native工程接入Flutter步骤

  1. 创建Flutter module
  2. 添加Flutter module依赖
  3. 在java或ObjectC中调用Flutter module
  4. 编写Dart代码
  5. 运行项目
  6. 热重启/重新加载
  7. 调试Dart代码
  8. 发布应用

1 创建Flutter Module

1.1 手动创建module方式

创建 flutter_hybird 文件夹并切换到该目录下,运行命令

flutter create -t module flutter_module
复制代码

使用AndroidStudio 打开该module并运行

需要注意的是如果想将Flutter的module集成到现有原生工程中,必须将原生工程和Flutter的module放在同一目录下

将Flutter的module添加到现有的Android项目中

需要注意的是Flutter目前最低支持api16+,所以在集成之前需要将项目的最低支持版本改为16或者之上版本

  1. 在Android项目的根目录的setting.gradle文件中添加
include ':app'                                     // assumed existing content
setBinding(new Binding([gradle: this]))    	  // new
evaluate(new File(                            // new
  settingsDir.parentFile,                     // new
  'flutter_module/.android/include_flutter.groovy' // new
)) 
复制代码
  1. 在Android项目的app目录下build.gradle添加
dependencies {
  implementation project(':flutter')
复制代码

1.2 AndroidStudio 自动创建

Android Studio会直接帮我们做完上述工作,开箱即用即可.

Native调用flutter

Android中以Activity形式调用Flutter

1. 在AndroidManifest.xml文件中配置FlutterActivity

  #application节点下
  <activity
            android:name="io.flutter.embedding.android.FlutterActivity"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:theme="@style/Theme.AndroidAppProject"
            android:windowSoftInputMode="adjustResize" />
复制代码

增加一个按钮点击事件,打开Flutter页面

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<Button>(R.id.bt_start).setOnClickListener { 
            startActivity(FlutterActivity.createDefaultIntent(this))
        }
    }
}
复制代码

效果

打开Flutter指定页面

Flutter目前可以打开指定页面,但是无法携带参数,因此我们可以把main.dart作为中转跳转并携带参数

  1. 首先定义json格式
{"route":"/page1","param":{"id":5}}
复制代码
  • 其中route表示跳转的路径
  • param表示携带的参数
  1. 给flutter设置跳转路径
 findViewById<Button>(R.id.bt_start).setOnClickListener {
            startActivity(FlutterActivity.withNewEngine().initialRoute("{\"route\":\"/page1\",\"param\":{\"id\":5}}").build(this))
        }
复制代码
  1. 在flutter的module中获取设置的路径值
import 'dart:ui';
String defaultRouteName=window.defaultRouteName;
复制代码
  • defaultRouteName 就是我们传过来的值 定义对象解析相关属性
class NativeToFlutter{
  String route;
  Map<String,dynamic> param;


  NativeToFlutter({this.route, this.param});

  factory NativeToFlutter.fromJson(Map<String,dynamic> map){
   return NativeToFlutter(route:map['route'],param:map['param']);
  }
}
复制代码

开始解析相关路径的参数

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String defaultRouteName=window.defaultRouteName;
    print("defaultRouteName=$defaultRouteName");
    Widget widget;
    if("/"==defaultRouteName){//initialRoute 配置的路径
      widget=MyHomePage();
    }else{
    //使用json解析json字符串
      var result=json.decode(defaultRouteName);
      print("result=${result.runtimeType}");
      NativeToFlutter nativeToFlutter= NativeToFlutter.fromJson(result);
      //判断路径进行跳转
      if(nativeToFlutter.route=="/page1"){
      //解析出对应的参数
      var id=nativeToFlutter.param['id'];
        widget=Page1();
      }else{
        widget=MyHomePage();
      }
    }
    return MaterialApp(
      initialRoute: "/",
      routes: {'/page1': (context) => Page1()},
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: widget,
    );
  }
}
复制代码

MaterialApp中

  • initialRoute 初始化页面的路径
  • routes 配置flutter页面路径

当然这只是简单的交互,目前Native打开Flutter页面比较缓慢,因为每次启动Flutter页面时,都会创建FlutterEngine

关于FlutterEngine的相关内容建议直接阅读flutter中文网站

使用缓存的 FlutterEngine

Android中以Fragment形式调用Flutter

新建一个Activity 并在其布局文件中添加个framelayout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".FirstActivity">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/design_default_color_secondary"
        android:gravity="center"
        android:text="我是头部title"
        android:textSize="60sp"
        app:layout_constraintTop_toTopOf="parent" />

    <FrameLayout
        android:id="@+id/fl_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_title" />

</androidx.constraintlayout.widget.ConstraintLayout>
复制代码

Activity

class FirstActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "flutter_fragment"
    }
    private var fragment: FlutterFragment?=null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first_actvity)
        fragment = supportFragmentManager.findFragmentByTag(TAG) as FlutterFragment?
        if (fragment == null) {
            fragment = FlutterFragment.withNewEngine().build()
            supportFragmentManager.beginTransaction().add(R.id.fl_container, fragment!!, TAG).commit()
        }
    }
}
复制代码

和Activity一样,Fragment也可以指定页面路径,但是无法携带参数

FlutterFragment.withNewEngine().initialRoute("/page1").build()
复制代码

同样也可以通过Activity的那种方式,指定页面并传递参数

FlutterFragment.withNewEngine().initialRoute("{\"route\":\"/page1\",\"param\":{\"id\":5}}").build()
复制代码

这样既可达到同样的效果

和Activity一样,让Flutter页面以fragment形式也需要FlutterEngine,关于预热FlutterEngine相关的知识,可以查看Flutter中文官网

使用缓存的 FlutterEngine

Flutter部分代码热更新

  1. 关闭app

  2. Flutter Attach

  3. 加载出Flutter部分代码

  4. 更改Flutter部分代码并启用热加载

Flutter与Native通讯机制

在做Flutter开发的时候通常离不开Flutter和Native的通讯,比如Dart调用native相册获取图片,Native将本地信息(GPS,陀螺仪等)主动传给Dart. Dart和Native的通讯通常有这几种场景

  • 初始化Flutter时Native给Flutter传递数据
  • Native发送数据给Dart
  • Dart给Native发送数据
  • Dart发送数据给Native,然后Native回传数据给Dart

Flutter和Native之前的通讯是通过Channel完成的 Flutter定义了三种类型的Channel

  • BasicMessageChannel: 用于传递字符串和半结构化的信息,持续通讯,收到消息后可以回复此次消息;如:Native遍历文件信息传递给Dart
  • MethodChannel: 用于传递方法调用的一次性通讯,如调用native的拍照和相册,读写日历,选择联系人等
  • EventChannel: 用于数据流通讯,持续通讯,收到消息后无法此次消息,通常用于Native向Dart发送数据,如:电量的变化,网络连接变化,陀螺仪,传感器等;

这三种类型的Channel都是全双工通讯,既通讯是双向的,

BasicMessageChannel 使用

首先看一下它的构造函数

BasicMessageChannel(name, codec, { BinaryMessenger? binaryMessenger })

  • name:是channel的名称,dart和native建立通讯的标示
  • codec:编码集,可以是如下几种

示例

class ChannelPage extends StatefulWidget {
  @override
  ChannelPageState createState() {
    return ChannelPageState();
  }
}

class ChannelPageState extends State<ChannelPage> {
  String nativeSentString = '';
  String nativeReturnString = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("测试flutter的channel通讯"),
      ),
      body: Column(
        children: [
          Text('测试BasicMethodChannel'),
          Text("Native主动回传消息:$nativeSentString"),
          Text("Native主动返回消息:$nativeReturnString"),
          RaisedButton(
            onPressed: () {
              sentMessage();
            },
            child: Text("点击给Native发送消息"),
          )
        ],
      ),
    );
  }

  static const String BASE_CHANNEL = 'BasicMessageChannel';
  BasicMessageChannel _basicMessageChannel = BasicMessageChannel(BASE_CHANNEL, StringCodec());

  _initChannel() {
    //当native发送消息后在这里处理
    _basicMessageChannel.setMessageHandler((message) {
      setState(() {
        nativeSentString = message;
      });
      print(message?.toString());
      //这里收到native消息后主动给native回消息
      return null;
    });
  }

  @override
  void initState() {
    _initChannel();
    super.initState();
  }

  sentMessage() async {
    try {
      //主动给native发送消息
      String result = await _basicMessageChannel.send("hello native");
      setState(() {
        nativeReturnString=result;
      });
      print(result);
    } catch (e) {
      print(e?.toString());
    }
  }
}
复制代码

native(Android端配置) 新建类

class FlutterBasicMessageChannel(context: Context) : FlutterPlugin, BasicMessageChannel.MessageHandler<String> {
private val  channelName:String="BasicMessageChannel"
    private var context:Context?=null
    companion object{
        private var basicMessageChannel:BasicMessageChannel<String>?=null
        fun sentContent(content:String){
            basicMessageChannel?.send(content)
        }
    }

    override fun onMessage(message: String?, reply: BasicMessageChannel.Reply<String>) {
        Toast.makeText(context, "收到了哈哈哈哈$message",Toast.LENGTH_LONG).show()
        reply.reply("给你一条消息哈哈哈哈")
    }

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    //注册消息通道
        context= binding.applicationContext
        basicMessageChannel= BasicMessageChannel<String>(binding.binaryMessenger,channelName,StringCodec.INSTANCE)
        basicMessageChannel?.setMessageHandler(this)
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    //销毁消息通道
        basicMessageChannel?.setMessageHandler(null)
        basicMessageChannel=null
    }

}
复制代码

在MainActivity中配置自定义Channel

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        flutterEngine.plugins.add(FlutterBasicMessageChannel(this))
        super.configureFlutterEngine(flutterEngine)

    }
}
复制代码

效果:

MethodChannel使用

构造函数 MethodChannel(name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ])

  • name:标识该channel,其实只传这一个参数即可
  static const String methodChannelName = "MethodChannel";
  MethodChannel _methodChannel = MethodChannel(methodChannelName);
    @override
  void initState() {
    _initChannel();
    super.initState();
  }
  _initChannel() {
    //当native通过该channel调用flutter的代码时在这里处理并返回结果
    _methodChannel.setMethodCallHandler((call){
      if ("hiFlutter"==call.method) {

      }else{
        call.noSuchMethod(null);
      }
      return null;
    });
  }
//点击按钮触发该方法调用native的方法并传递参数
 static const String methodName = "giveMeFive";
  callMethodChannel() async {
    Map<String, String> params = {};
    params["name"] = "张三1";
    params["age"] = "12";
    try{
      String result = await _methodChannel.invokeMethod(methodName, params);
      setState(() {
        nativeReturnString = result;
      });
    }catch(e){
      print(e?.toString());
    }
  }
复制代码

native端(Android) 新建类FlutterMethodChannel

  • 实现FlutterPlugin是为了注册该channel
  • 实现MethodChannel.MethodCallHandler 是为了处理dart端传来的方法调用
class FlutterMethodChannel(context: Context) : FlutterPlugin, MethodChannel.MethodCallHandler {
private val  channelName:String="BasicMessageChannel"
    private var context:Context?=null
    companion object{

        private var methodChannel:MethodChannel?=null
        fun sentContent(content:String){

        }
    }

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        context= binding.applicationContext
        //初始化并设置methodhandler
        methodChannel= MethodChannel(binding.binaryMessenger,"MethodChannel");
        methodChannel?.setMethodCallHandler(this)

    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        methodChannel=null
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        Toast.makeText(context, "收到了哈哈哈哈${call.method}",Toast.LENGTH_LONG).show()
        if (call.method=="giveMeFive") {
            var params: Map<*, *>? = call.arguments as? Map<*, *>;
            var name= params?.get("name")
            if ("张三"==name){
                result.success("5555555555555555")
            }else{
                result.error("100","不好意思在下只要张三","")
            }
        }else{
            result.notImplemented()
        }
    }
}
复制代码
  • result.notImplemented() flutter端调用的方法native未实现
  • result.success() native处理完成并返回结果
  • result.error() native处理异常并通知flutter该异常

这里需要注意的是一定要通过result对native处理的各个情况进行反馈,要不然flutter端就会等待并没有结果,尤其是native端出现了异常也一定要反馈给flutter端,

MainActivity中加上该配置

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        flutterEngine.plugins.add(FlutterMethodChannel(this))
        super.configureFlutterEngine(flutterEngine)
    }
}
复制代码

EventChannel 使用

构造方法 EventChannel(name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger])

  • name 和其他两个channel一样,
  @override
  void initState() {
    enableStream();
    super.initState();
  }
  
    static const String eventChannelName = "EventChannel";
  EventChannel _eventChannel = EventChannel(eventChannelName);
  StreamSubscription _streamSub;

  enableStream() {
  //监听native发送过来的信息
    _streamSub = _eventChannel.receiveBroadcastStream().listen((event) {
      print(event.toString());
    }, onError: (e) {
      print("onError");
    }, onDone: () {
      print("onDone");
    });
  }
  
   @override
  void dispose() {
  //注销并释放资源
    if (_streamSub != null) {
      _streamSub.cancel();
      _streamSub = null;
    }
    super.dispose();
  }
复制代码

native(Android端)

定义 FlutterEventChannel类

class FlutterEventChannel : FlutterPlugin, EventChannel.StreamHandler {
    private var context:Context?=null
    companion object{
        private var eventChannel:EventChannel?=null
        private var chargingStateChangeReceiver: BroadcastReceiver? = null
    }

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        context= binding.applicationContext
        //初始化EventChannel并设置Handler
        eventChannel= EventChannel(binding.binaryMessenger, "EventChannel");
        eventChannel?.setStreamHandler(this)

    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    //重置EventChannel并设置Handler为null
        eventChannel?.setStreamHandler(null)
        eventChannel=null
    }

	//flutter端开始监听回调该方法
    override fun onListen(arguments: Any?, events: EventSink?) {
      //注册监听手机电量变化 chargingStateChangeReceiver=createChargingStateChangeReceiver(events)
        context?.registerReceiver(chargingStateChangeReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    }

    override fun onCancel(arguments: Any?) {
    //释放监听
        context?.unregisterReceiver(chargingStateChangeReceiver)
        chargingStateChangeReceiver=null
    }

    private fun createChargingStateChangeReceiver(events: EventSink?): BroadcastReceiver {
        return object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
                if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) {
                    events?.error("UNAVAILABLE", "Charging status unavailable", null)
                } else {
                    val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                            status == BatteryManager.BATTERY_STATUS_FULL
                    events?.success(if (isCharging) "charging" else "discharging")
                }
            }
        }
    }
}
复制代码

可以看到日志

I/flutter (15313): charging
I/flutter (15313): charging
I/flutter (15313): charging
I/flutter (15313): charging
I/flutter (15313): charging
复制代码

这三种Channel应该怎么选呢?

1. 单次调用方法类型选择MethodChannel(如:选择相片,拍照)

2. Native持续向Flutter发送数据使用EventChannel(如:电量变化,陀螺仪,位置信息等)

3.发送字符串或者结构化数据,需要两端相互调用回调输出结果使用BasicMessageChannel(如:大量密集型运算时交给native运算,native得到结果后发送给flutter)

分类:
Android
标签: