What's flutter插件?

flutter作为一个ui绘制工具,是不能直接调用一些原生功能的,比如相机,传感器,屏幕亮度等等,所以需要原生实现,然后再用flutter和原生通信,这就是flutter插件。

How?

那么flutter怎么和原生通信呢,flutter官网有如下的图

可以看到,flutter是通过Platform Channels(官方翻译为平台通道)和原生通信的,图中的MethodChannel就是PlatformChannel中的一种,另外还有EventChannel。

Demo

官网有一个获取设备电池状态的demo,接下来我会写一个获取设备屏幕亮度的demo,包括实际开发flutter plugin的具体流程细节和一些可能会遇到的坑。

1.新建module

选择Flutter Plugin,在location那一栏,选择到当前flutter项目下的plugins文件夹(没有就新建),就会生成一个如图的flutter项目

2.配置gradle

android端,build.gradle下添加如下

//获取local.properties配置文件
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}
//获取flutter的sdk路径
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar")
    compileOnly 'androidx.annotation:annotation:1.1.0'
}

在local.properties文件下增加android sdk和flutter sdk的路径(如果没有),如下

sdk.dir=D:\\as\\sdk
flutter.sdk=F:\\flutter\\flutter

然后使用androidStudio打开screen_light下的android项目,编译运行,如果这一步没有问题的话,ScreenLightPlugin这个类可以正常编译通过,不会报错。

3. flutter传值到platform

在lib文件夹下编写flutter代码
3.1 创建MethodChannel对象

final MethodChannel _channel = const MethodChannel(_channel_name);

其中的_channel_name参数,需要保证在flutter、android、ios各端都是同一个值。
3.2 调用invokeMethod方法

///调用platform方法,并返回bool
  Future<bool> _invokeBooleanMethod(String method, [dynamic arguments]) async {
    final bool result = await _channel.invokeMethod<bool>(
      method,
      arguments,
    );
    return result;
  }

通过调用_channel的invokeMethod方法,可以把flutter的参数传到原生端
3.3 android端接受参数
在android目录下,ScreenLightPlugin类实现FlutterPlugin、ActivityAware和MethodChannel.MethodCallHandler
插件加载后,会执行onAttachedToEngine方法,可以做一些初始化操作;

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        context = flutterPluginBinding.applicationContext
        channel = MethodChannel(flutterPluginBinding.binaryMessenger, _channelName)
        channel.setMethodCallHandler(this)
    }

插件卸载执行onDetachedFromEngine方法;

 override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }

还有onAttachedToActivity可以获取到activity;

 override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = binding.activity
    }

另外一个最重要的方法是onMethodCall,当flutter通过_channel.invokeMethod调用方法后,会执行此方法,并且可以通过MethodCall参数获取到flutter传的参数

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        when (call.method) {
            _getSysScreenLight -> callGetSysScreenLight(call, result)
            _setAppScreenLight -> callSetAppScreenLight(call, result)
            _getAppScreenLight -> callGetAppScreenLight(call, result)
            _listenSysScreenLight -> callListenSysScreenLight(call, result)
            _unlistenSysScreenLight -> callUnlistenSysScreenLight(call, result)
        }
    }

    //参数获取flutter传的参数
 val light: Int? = call.argument<Int>(_argLight)

3.4 platform处理后,返回值到flutter
通过onMethodCall下的Result参数,可以返回相应的值到flutter

result.success(true)

以上的流程可以总结为,在flutter端,通过MethodChannel对象,调用invokeMethod方法,传值到原生端,原生端处理后,通过Result参数返回值到flutter端
有一个问题,是flutter和android端的ScreenLightPlugin类怎么关联在一起的,其实是在插件的pubspec.yaml文件里面配置的

flutter:
  plugin:
    platforms:
      android:
        package: com.perdev.screen_light
        pluginClass: ScreenLightPlugin
      ios:
        pluginClass: ScreenLightPlugin

4. 回调方法(原生传值到flutter)

和flutter传值到原生一样,也是先在原生端获得MethodChannel对象,然后调用invokeMethod方法,然后就会走到flutter的这个地方

 _channel.setMethodCallHandler((call) => _handleMethod(call));

  ///回调,platform主动调用channel.invokeMethod方法后,会走到此处
  Future<dynamic> _handleMethod(MethodCall call) {
    switch (call.method) {
      case _callbackSysScreenLightChanged:
        final map = call.arguments as Map<dynamic, dynamic>;
        final light = map[_argLight] as int;
        final selfChange = map[_argSelfChange] as bool;
        _list.forEach((v) {
          v(light, selfChange);
        });
        break;
    }
    return Future<dynamic>.value(null);
  }

5. 待续,ios平台实现代码

6. 发布插件

当插件开发完成后,可以把插件发布到pub.dev,供其他人使用,发布流程比较简单的,看一下这个文档就会了。

最后

至此,你已经完成了你的第一个flutter插件开发
插件pub地址
源码地址