刚接触flutter的时候,以为flutter是一个全新开发app的语言,独立于Android原生之外的操作,入坑之后发现不是的。因为Flutter不能完成所有Native的功能,比如不同平台的底层服务如电量变化、网络连接变化以及最近项目中使用的直接拨号功能以及地图功能都无法用flutter实现其功能,因此需要借助Native层的接口来实现flutter的开发,所以Flutter提供了一套Platform Channel的机制,来满足Flutter与Native通信的需求。
图中可以看到,Flutter是Client端,Native是Host,Client和host通信是通过PlatformChannel,Client通过PlatformChannel向Host发送消息,Host监听PlatformChannel并接收消息,然后将响应结果发送给Client。即Flutter 调用 Native 方法时,需要传递的信息是通过平台通道传递到宿主端的,Native 收到调用的信息后方可执行指定的操作。消息和响应以异步方式传递,以确保UI不阻塞。另外,PlatformChannel是双工的,这意味着Flutter和Native可以交替做Client和Host。
1、EventChannel是一种native向flutter发送数据的单向通信方式,flutter无法返回任何数据给native。主要用于native向flutter发送手机电量变化、网络连接变化、陀螺仪、传感器等。
2、BaseMessageChannel :用于传递字符串和半结构化的信息(在大内存数据块传递的情况下使用)
3、通过MethodChannel来实现,MethodChannel支持数据双向传递,有返回值。本文只讲这个,因为项目中只用了这个。
总的来说就是:这三种方式均各有适用的场景:MethodChannel用于native与flutter的方法调用,EventChannel用于native单向的向flutter发送广播消息,BasicMessageChannel用于native与flutter之间的消息互发。
首先了解下flutter创建的项目结构
android 就是我们开发安卓部分的位置
iOS 就是我们开发 iOS 的位置
lib 是与 Android 、iOS 联调的位置。也可以理解为Flutter 实现的位置
因此flutter与android原生的通信代码就写在这2个目录文件下,iOS的就让iOS的同学去写就完事了。
Android原生数据与flutter通道建立的基础是在实现FlutterPlugin之上的,看下FlutterPlugin代码注释
public interface FlutterPlugin {
/ * *
@code FlutterPlugin与@link FlutterEngine实例相关联。
*
{@code FlutterPlugin}可能需要的相关资源是通过{@code提供的
*绑定}。{@code binding}可以被缓存和引用,直到{@link
#onDetachedFromEngine(FlutterPluginBinding)}被调用并返回。
* /
void onAttachedToEngine(@NonNull FlutterPluginBinding binding);
/ * *
*这个{@code FlutterPlugin}已从{@link FlutterEngine}实例中删除。
* 传递给这个方法的{@code binding}与传递给{@link的实例相同
* # onAttachedToEngine (FlutterPluginBinding)}。在此方法中,它作为
*方便。{@code绑定}可能在这个方法的执行过程中被引用,但是它
*不能在此方法返回后被缓存或引用。
{@code FlutterPlugin}s应该释放该方法中的所有资源。
* /
void onDetachedFromEngine(@NonNull FlutterPluginBinding binding);
}
显而易见,onAttachedToEngine是FlutterPlugin与FlutterEngine实例关联,主要实现了发送二进制消息和设置消息处理回调的方法,其底层实现的目的就是让flutter层与android原生的交互进行绑定,而onDetachedFromEngine这个是释放资源。
onAttachedToEngine是绑定双向的通道后,数据的通信则需要BinaryMessenger来处理,因为BinaryMessenger是Platform端与Flutter端通信的工具,其通信使用的消息格式为二进制格式数据。可以看下FlutterPlugin目录结构组成如下
可以明显看到 onAttachedToEngine方法里面传入参数FlutterPluginBinding,而FlutterPluginBinding里面可以获取到BinaryMessenger对象,因此目前流程应该是实现FlutterPlugin接口,重写onAttachedToEngine方法,通过FlutterPluginBinding获取BinaryMessenger对象,通过BinaryMessenger对象是实现与flutter的数据通信。
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
class MyFlutterPlugins : FlutterPlugin {
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
val binaryMessenger = binding.binaryMessenger
}
override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
}
flutter与Android原生不可能只存在调用一个调用的插件,所以建立通道后,需要创建双方可以识别的唯一标识。在native和flutter之间,数据的交互是双向的。我们可以从Native层调用flutter层的dart代码,也可以在flutter层调用Native的代码。而作为通讯桥梁就是MethodChannel了,这个类在初始化的时候需要注册一个渠道值。这个值必须是唯一的,并且在使用到的Native层和Flutter层互相对应。看下MethodChannel的注释
/**
创建一个与指定的{@link BinaryMessenger}关联的新通道
*指定的名称和标准{@link MethodCodec}。
*
@param messenger a {@link BinaryMessenger}。
@param name一个通道名字符串。
*/
public MethodChannel(BinaryMessenger messenger, String name) {
this(messenger, name, StandardMethodCodec.INSTANCE);
}
可以看到MethodChannel方法里面有name字段为一个通道名字符串。而BinaryMessenger这个上面已经获取到了,现在就是要传入双方约定的唯一标识传进去,比如传入“myFlutterPlugin”作为唯一标识
val methodChannel=MethodChannel(binaryMessenger, "myFlutterPlugin")
拿到MethodChannel对象后,我们需要通过setMethodCallHandler回调接口对flutter调用Android中的方法进行监听,通过回调中的MethodCall对象方法名判断、获取方法参数,并且返回调用结果。至此我们的通道代码可以写到如下地步
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import io.flutter.plugin.common.MethodChannel
class MyFlutterPlugins : FlutterPlugin {
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
//获取binaryMessenger对象
val binaryMessenger=binding.binaryMessenger
//创建methodChannel对象
val methodChannel=MethodChannel(binaryMessenger, "myFlutterPlugin")
//使用setMethodHandle对对方调用自己的方法进行监听,
// 通过回调中的MethodCall对象方法名判断、获取方法参数,并且返回调用结果。
methodChannel?.setMethodCallHandler { call, result ->
if (call.method == "printLog") {
printLogFromAndroid();
}
}
}
override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
private fun printLogFromAndroid(){
Log.e("TAG","调用了android原生的数据")
}
其中回调接口中的call和result参数是MethodCallHandler接口的方法体
public interface MethodCallHandler {
@UiThread
void onMethodCall(@NonNull MethodCall call, @NonNull Result result);
}
前者判断调用的方法,后面返回处理的结果,这个后面会提下。现在FlutterPlugin里面的代码已经撸完了,然后就是注册这个插件了
应用创建时,MainActivity 默认继承FlutterActivity ,
class MainActivity: FlutterActivity() {
}
而FlutterActivity里提供了注册引擎的接口
void configureFlutterEngine(@NonNull FlutterEngine flutterEngine)
所以我们在MainActivity里重写configureFlutterEngine方法,然后添加我们的插件就行了
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
flutterEngine.plugins.add(MyFlutterPlugins())
}
}
至此,android原生端的代码已经完成了。
以上是我们手动编写通道插件手动注册的,但是flutter也提供了一些第三方调用原生的插件,那它们是如何自动注册插件的呢?
创建flutter项目的时候 android目录下会自动新建一个io.flutter.plugins目录下GeneratedPluginRegistrant类里面会有个registerWith方法,然后自动添加一些三方调用原生的插件,举个栗子:
在flutter目录里pubspec.yaml里添加高德定位调用原生插件,如下
执行flutter pub get后会在android的目录下去注册这个通道,如下
以上代码不用手动撸它,撸了也会被自动覆盖。这个操作是flutter项目自动生成的且运行的时候自动注册的,之前说过flutter创建项目后,Android项目里的MainActivity默认是继承flutterActivity的,所以启动MainActivity的时候,其父类flutterActivity的时候会通过FlutterActivityAndFragmentDelegate这个代理类去配置通信通道的调用。
这里便是自动注册的入口,通过在FlutterActivityAndFragmentDelegate的onAttach里配置,
configureFlutterEngine是Host接口的一个方法,因为项目继承activity 所以其接口方法的具体实现在FlutterActivity里寻找,
顺藤摸瓜式看源码,就能找到在FlutterActivity类中找到configureFlutterEngine的实现体,里面可以看到内部调用了GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine);而该方法就是通过反射机制去调用android目录下 io.flutter.plugins下GeneratedPluginRegistrant类下的默认方法registerWith(),继续顺藤摸瓜看
所以说为什么我们在flutter里面添加一些三方调用原生的插件的时候不需要手动配置通道的注册的原因就是在这里,然后需要注意的一点就是 我们自定义flutter与android原生通道的时候,需要去重写configureFlutterEngine方法并去注入我们的自己插件 这样会覆盖了第三方的插件,所以我们需要手动添加第三方的插件注册
首先一样的需要建立通道 ,还是用MethodChannel方法,
注册渠道:在两端同时创建一个MethodChannel对象,注册相同的字符串值
class MethodChannel {
const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ])
: assert(name != null),
assert(codec != null),
_binaryMessenger = binaryMessenger;
}
可以看到flutter里MethodChannel需要传入name,而android原生定义MethodChannel里也需要传入唯一标识name,所以此处name就是android原生传入的myFlutterPlugin,so 如下定义
var _channel = MethodChannel('myFlutterPlugin');
MethodChannel对象实例有了,接着就是实现调用的具体方法了
Future print() async{
_channel.invokeMethod("printLog");
}
invokeMethod源码注释很长,自行查看,
Future invokeMethod(String method, [ dynamic arguments ]) {
return _invokeMethod(method, missingOk: false, arguments: arguments);
}
两个参数,method就是要调用的方法名,arguments 就是传入的参数,这里没有传,因为前面原生我没有调用传参的方法。直接写个按钮调用下
ElevatedButton(onPressed: ()=>print(), child: Text("调用原生方法")),
打印结果:
E/TAG: printLog: 调用了android原生的方法
还是拿上面的例子说明
之前讲MethodChannel的时候 没有具体说明它的回调接口里的参数,现在look下
MethodChannel->setMethodCallHandler->MethodCallHandler->onMethodCall->
MethodCall
主要看下这个MethodCall
public final class MethodCall {
/** The name of the called method. */
public final String method;
/**
* Arguments for the call.
*
* Consider using {@link #arguments()} for cases where a particular run-time type is expected.
* Consider using {@link #argument(String)} when that run-time type is {@link Map} or {@link
* JSONObject}.
*/
public final Object arguments;
/**
* Creates a {@link MethodCall} with the specified method name and arguments.
*
* @param method the method name String, not null.
* @param arguments the arguments, a value supported by the channel's message codec.
*/
public MethodCall(String method, Object arguments) {
if (BuildConfig.DEBUG && method == null) {
throw new AssertionError("Parameter method must not be null.");
}
this.method = method;
this.arguments = arguments;
}
method则是我们调用的方法,而arguments就是我们接收flutter传过来的参数类型
所以结合上面的例子,传入的参数和方法名都是通过MethodCall来实现的,那么改下上面的例子
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import io.flutter.plugin.common.MethodChannel
class MyFlutterPlugins : FlutterPlugin {
var methodChannel: MethodChannel?=null
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
methodChannel=MethodChannel(binding.binaryMessenger, "myFlutterPlugin")
methodChannel?.setMethodCallHandler { call, result ->
if (call.method == "printLog") {
val key= call.argument("key")!!
printLogFromAndroid(key);
}
}
}
override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
private fun printLogFromAndroid(key:String){
Log.e("TAG","调用了android原生的数据 $key")
}
}
传入参数的标识码为“key",通过“key”获取flutter传入的值,同时也修改下flutter的代码
其它都不变,就改下调用的方法
Future print() async{
_channel.invokeMethod("printLog",{"key":"华锐捷"});
}
最后打印下:
E/TAG: 调用了android原生的数据 华锐捷
实现setMethodCallHandler回调接口的时候,目前我们只使用了MethodCall,result还有动过,现在看下源码里Result的注释
public interface Result {
/**
* Handles a successful result.
*
* @param result The result, possibly null. The result must be an Object type supported by the
* codec. For instance, if you are using {@link StandardMessageCodec} (default), please see
* its documentation on what types are supported.
*/
@UiThread
void success(@Nullable Object result);
/**
* Handles an error result.
*
* @param errorCode An error code String.
* @param errorMessage A human-readable error message String, possibly null.
* @param errorDetails Error details, possibly null. The details must be an Object type
* supported by the codec. For instance, if you are using {@link StandardMessageCodec}
* (default), please see its documentation on what types are supported.
*/
@UiThread
void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails);
/** Handles a call to an unimplemented method. */
@UiThread
void notImplemented();
}
很显然,就是flutter调用原生接口是否成功或失败 的返回接口,就是把flutter调用android原生的结果包括调用android原生方法的返回结果返回给flutt,evoid success(@Nullable Object result);返回的是object类型的
修改下 android原生调用的方法,带有返回参数
private fun printLogFromAndroid(key:String):String{
Log.e("TAG","调用了android原生的数据 $key")
return "大华"
}
并且 添加setMethodCallHandler result回调接口
result.success( Object result))
代码最后撸成以下:
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import android.util.Log
import io.flutter.plugin.common.MethodChannel
class MyFlutterPlugins : FlutterPlugin {
var methodChannel: MethodChannel?=null
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
methodChannel=MethodChannel(binding.binaryMessenger, "myFlutterPlugin")
methodChannel?.setMethodCallHandler { call, result ->
if (call.method == "printLog") {
val key= call.argument("key")!!
result.success(printLogFromAndroid(key))
}
}
}
override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
private fun printLogFromAndroid(key:String):String{
Log.e("TAG","调用了android原生的数据 $key")
return "大华"
}
}
调用原生接口printLogFromAndroid 传入参数key 并返回“大华”,setMethodCallHandler的MethodCallHandler的中result返回该数据给flutter ,下面实现下flutter的实现
final _channel = const MethodChannel('myFlutterPlugin');
void printMsg() async{
String result= await _channel.invokeMethod("printLog",{"key":"华锐捷"}) ;
print("来自Android原生返回的数据;$result");
}
传入key为“华锐捷”的字段给原生接口,原生接口返回“大华”给flutter ,然后flutter调用该方法
说明 flutter端传给android原生的数据发送成功,并成功接收到android原生返回的数据了。
至此,关于flutter与Android通信的代码已经撸完了,下面说下一些常见的需求
原理都和之前的调用的方法一样,主要是如何获取界面跳转的上下文对象,这里分2种,一种是context.startActivity进行界面跳转,另外一种就是activity对象进行跳转
实现FlutterPlugin的接口的时候,有onAttachedToEngine方法里面的参数FlutterPluginBinding 里面有applicationContext参数,所以context可以从此处获取
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
mContext= binding.applicationContext
}
获取到上下文后界面直接走android原生 intent跳转逻辑了,
private fun intentTest(context: Context) {
val intent=Intent()
intent.flags=Intent.FLAG_ACTIVITY_NEW_TASK
intent.setClass(context.applicationContext, MainActivity2::class.java)
context.startActivity(intent)
}
注意此处需要添加 intent.flags=Intent.FLAG_ACTIVITY_NEW_TASK、否则会报Calling startActivity() from outside of an Activity context异常,不晓得你们报不报错,反正我的是崩了。Context中有一个startActivity方法,Activity继承自Context,重载了startActivity方法。如果使用Activity的startActivity方法,不会有任何限制,而如果使用Context的startActivity方法的話,就需要开启一个新的的task,遇到这个异常,是因为使用了Context的startActivity方法。解决办法是,加一个flag。intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK );这样就可以在新的task里面启动这个Activity了。然后调用的方法还是一样的 在setMethodCallHandler 判断调用的方法名,然后调用跳转界面的代码。其次,也可以在application里获取全局的context对象,然后拿来用就行了。
一个新的接口ActivityAware ,主要注意
void onAttachedToActivity(@NonNull ActivityPluginBinding binding);
void onDetachedFromActivity();
前者绑定activity时调用,后者与activity分离时回调 ,而前者ActivityPluginBinding参数中则有包含当前activity的实例对象
所以要获取activity实例对象,先实现ActivityAware接口,然后实现onAttachedToActivity方法,从ActivityPluginBinding 获取到activity对象
class MyFlutterPlugins : ActivityAware{
var activity: Activity?=null
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity=binding.activity
}
}
然后修改下界面跳转的代码逻辑,此处不需要添加flag了
private fun intentTest(activity: Activity) {
val intent=Intent()
intent.setClass(activity, MainActivity2::class.java)
activity.startActivity(intent)
}
调用的逻辑和之前的一样,不多BB了
另外onDetachedFromActivity接口可以做一些回收操作,比如activity回收,免得内存泄漏
说到内存泄漏,还有个application类的Application.ActivityLifecycleCallbacks接口
可以在ActivityAware中的activity绑定回调中注册,获取activity的生命周期,更好地去维护activity的生命周期状态,
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
// 注册生命周期回调, 保证界面初始化的时候对应的是正确的activity状态
activity?.application?.registerActivityLifecycleCallbacks(this)
}
往往能坚持到最后的人不是靠着最初的三分激情,而是恰到好处的喜欢和投入!