Flutter 插件开发入门

1、初识 Flutter Plugin

Flutter 的插件类似于我们在 Android 中说的第三方库,通过使用插件,可以借助插件中的代码实现一些额外功能。

Flutter 的插件以 package 的形式存在,使用 package 的目的是为了达到模块化,可以让代码被共享和复用。这些 package 可以直接在 pubspec.yaml 中被依赖。

一个 package 至少包含两个部分:

  • pubspec.yaml 文件:元数据文件,声明了 package 的名称、版本、作者等信息
  • lib 文件夹:包含 package 的公开代码,至少会存在 <pakcage-name>.dart 这个文件,该文件用于使用者快速 import 这个 package 以便使用代码内容,因此必须存在

package 的种类分为两种:

  • Dart packages:纯 Dart 代码的 package。它包含 Flutter 的特定功能,因此它依赖于 Flutter Framework,只能用在 Flutter 上
  • Plugin packages:包含 Dart 代码编写的 API,也包含平台(Android/iOS)特定实现的 package,可以被 Android 和 iOS 调用

2、认识插件结构

2.1 通过源码了解插件工作原理

我们创建一个 Flutter 插件项目,它的目录结构如下:

关注其中的三个目录:

  1. android:Android 原生代码,相对于 Flutter 而言,就是 Native 代码
  2. example:AS 生成的使用 Flutter 插件的示例项目
  3. lib:Flutter 插件源码

在 /example/lib/ 目录下有一个 main.dart 文件是示例程序的入口,在页面中央会展示平台版本 _platformVersion,该变量通过插件内定义的 getPlatformVersion() 获取:

dart 复制代码
class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';
  final _pluginDemoPlugin = PluginDemo();

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

  Future<void> initPlatformState() async {
    String platformVersion;
    try {
      platformVersion =
          await _pluginDemoPlugin.getPlatformVersion() ?? 'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Text('Running on: $_platformVersion\n'),
        ),
      ),
    );
  }
}

插件 PluginDemo 通过 AS 创建的模板代码生成:

dart 复制代码
import 'plugin_demo_platform_interface.dart';

class PluginDemo {
  Future<String?> getPlatformVersion() {
    return PluginDemoPlatform.instance.getPlatformVersion();
  }
}

插件在这里进行了一层封装,需要再进一步看 PluginDemoPlatform:

dart 复制代码
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'plugin_demo_method_channel.dart';

abstract class PluginDemoPlatform extends PlatformInterface {
  PluginDemoPlatform() : super(token: _token);

  static final Object _token = Object();

  // _instance 是一个 MethodChannelPluginDemo
  static PluginDemoPlatform _instance = MethodChannelPluginDemo();

  // instance 的 getter 和 setter
  static PluginDemoPlatform get instance => _instance;

  static set instance(PluginDemoPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  // 默认的方法没有提供实现
  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }
}

PluginDemoPlatform.instance 实际上是子类对象 MethodChannelPluginDemo,创建一个 MethodChannel 来执行 getPlatformVersion():

dart 复制代码
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'plugin_demo_platform_interface.dart';

/// An implementation of [PluginDemoPlatform] that uses method channels.
class MethodChannelPluginDemo extends PluginDemoPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('plugin_demo');

  @override
  Future<String?> getPlatformVersion() async {
    final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
    return version;
  }
}

MethodChannel 需要输入一个名字作为参数:

dart 复制代码
class MethodChannel {
  // name 是 Channel 的名字
  // codec 是消息编解码器
  const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ])
      : _binaryMessenger = binaryMessenger;
}

MethodChannel 这里我们要注意两个参数:

  1. name:String 类型,Channel 的名字,用于区分 Channel,因此必须独一无二
  2. codec:消息编解码器,Flutter 插件的作用包含在 Flutter 应用与各个 Platform(Android/iOS)之间进行通信,比如方法调用与数据传递都是通信的一部分,那么这个通信过程可以看作协议,发送方需要通过编码器对协议内容进行编码,接收方需要通过解码器对协议内容进行解码

这里我们是从源码角度来看 Channel,后续我们也会再从架构角度对 Platform Channel 进行解析,还会再看到相关内容。

然后在执行 invokeMethod() 时会调用 Native(Flutter 的 Native 就是 Android 或 iOS)的插件源码,比如 Android 端的 Flutter/plugin_demo/android/src/main/kotlin/<PackageName>/PluginDemoPlugin.kt

kotlin 复制代码
class PluginDemoPlugin: FlutterPlugin, MethodCallHandler {
  private lateinit var channel : MethodChannel

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

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    if (call.method == "getPlatformVersion") {
      result.success("Android ${android.os.Build.VERSION.RELEASE}")
    } else {
      result.notImplemented()
    }
  }

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

onMethodCall() 内在被调用的方法名是 getPlatformVersion 时会返回一个字符串,标记出 Android 的系统版本。

需要注意的是,在 Flutter 插件的 MethodChannelPluginDemo 内创建 MethodChannel 时传入的名字(plugin_demo),需要与 Native 这边创建的 MethodChannel 传入的名字一致,才可成功调用方法。

2.2 实现一个简单的插件

熟悉了插件结构,就可以开发一个简单的插件了。比如 Flutter 应用中打印 log 只能通过 print() 或 debugPrint() 打印,默认都是 Info 等级,使用起来并不友好。因此我们可以通过 Flutter 插件提供打印各种日志等级的方法,具体操作过程如下:

  1. Native 端:修改 Android 端的 PluginDemoPlugin,在 onMethodCall() 中添加输出 Error 级别 Log 的方法:

    kotlin 复制代码
    	override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
            if (call.method == "getPlatformVersion") {
                result.success("Android ${android.os.Build.VERSION.RELEASE}")
            } else if (call.method == "logE") {
                val tag = call.argument("tag");
                val msg = call.argument("message");
                android.util.Log.e(tag, msg);
            } else {
                result.notImplemented()
            }
        }
  2. Flutter 插件:

    • 先在插件提供的平台基类 PluginDemoPlatform 中提供 logE 的默认实现:

      dart 复制代码
        Future<void> logE(String tag, String message) {
          throw UnimplementedError('logE() has not been implemented.');
        }
    • 然后在 PluginDemoPlatform 的子类 MethodChannelPluginDemo 中提供 logE 的实现,通过 MethodChannel 调用 Native 的方法:

      dart 复制代码
        @override
        Future<void> logE(String tag, String message) async {
          // arguments 是 dynamic 的,因为我们要传两个参数,因此做成一个 Map
          await methodChannel
              .invokeMethod<String>('logE', {'tag': tag, 'message': message});
        }
    • 最后在 PluginDemo 中提供一个封装方法:

      dart 复制代码
        void logE(String tag, String message) {
          PluginDemoPlatform.instance.logE(tag, message);
        }
  3. example 示例:在示例的 main 文件中通过插件调用 logE():

    dart 复制代码
    class _MyAppState extends State<MyApp> {
      final _pluginDemoPlugin = PluginDemo();
    
      @override
      void initState() {
        super.initState();
        initPlatformState();
        // 通过插件调用输入 Error 等级 log 的方法
        _pluginDemoPlugin.logE('FlutterPlugin', 'init work');
      }
    }

3、Plugin 通信原理

介绍 Plugin 之前需要先了解一下 Flutter 框架。

3.1 Flutter 框架

Flutter 框架包括 Framework 和 Engine,他们运行在各自的 Platform 上:

Framework 由 Dart 语言开发,包含 Material Design 风格(Android)和 Cupertino 风格(iOS)的 Widget,还有文本、图片、按钮等基础 Widget;此外,还包括渲染、动画、绘制、手势等基础能力。

Engine 是 C++ 实现的,包括 Skia(二维图形库)、Dart VM(Dart Runtime)、Text(文本渲染)等。Flutter 的上层能力实际上是 Engine 提供的。通过 Engine 将各个 Platform 的差异抹平,Flutter Plugin 就是通过 Engine 提供的 Platform Channel 实现的通信。

Flutter 插件调用 Android Native 代码的过程,实际上就是从 Framework 到 Engine,Engine 到 Android 底层再向上到 Java/Kotlin 的 U 型结构。

3.2 Platform Channel

Flutter Plugin 本质上是一个特殊的 package,它提供了 Android 或 iOS 的底层封装,在 Flutter 层提供组件功能,使得 Flutter 可以方便的调取 Native 代码。

很多平台相关性或者对于 Flutter 实现起来比较复杂的部分,都可以封装成 Plugin,其原理图如下:

图中可见,Flutter app 通过 Plugin 创建的 Platform Channel 调用 Native API,下面来详细介绍 Platform Channel 的各个部分。

还是先看上面的图,Flutter app 作为 client,通过 MethodChannel 向其他 Platform(Android/iOS)发送调用消息,而 Android Platform 作为 Host 通过 MethodChannel 接收调用消息,而另一个 Host iOS Platform 则通过 FlutterMethodChannel 接收调用消息。

Android Platform 中有一个 FlutterActivity 作为 Android Plugin 的管理器,它记录了所有 Plugin 并将它们绑定到 FlutterView 中。

Flutter 提供了三种不同类型的 Channel:

  • BasicMessageChannel:用于传递字符串和半结构化的信息
  • MethodChannel:用于传递方法调用(method invocation),方法调用也可以反向发送调用消息
  • EventChannel: 用于数据流(event streams)的通信

三种 Channel 相互独立,各有用途,但是在设计上非常相近,均有三个重要成员变量:

  • name: String 类型,代表 Channel 的名字,也是其唯一标识符。Flutter 应用可能会有多个 Channel,它们通过 name 区分,因此每个 Channel 必须指定一个独一无二的名字。并且,消息从 Flutter 发送到 Platform 时,在 Platform 这边也要通过传递过来的 channel name 找到该 Channel 对应的 Handler(消息处理器)
  • messager:BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具
  • codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器。消息编解码器,是 JSON 格式的二进制序列化,所以调用方法的参数类型必须是可 JSON 序列化的。

平台之间传递的数据必须是支持二进制序列化的,否则无法通过消息编解码器传递消息。标准平台通道使用标准消息编解码器,以支持简单的类似 JSON 值的高效二进制序列化,例如 booleans、numbers,、Strings,、byte buffers,、List,、Maps(详细信息参考 StandardMessageCodec)。 当发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。

下表显示了如何在宿主上接收 Dart 值,反之亦然:

Dart Android iOS
null null nil (NSNull when nested)
bool java.lang.Boolean NSNumber numberWithBool:
int java.lang.Integer NSNumber numberWithInt:
int, if 32 bits not enough java.lang.Long NSNumber numberWithLong:
int, if 64 bits not enough java.math.BigInteger FlutterStandardBigInteger
double java.lang.Double NSNumber numberWithDouble:
String java.lang.String NSString
Uint8List byte[] FlutterStandardTypedData typedDataWithBytes:
Int32List int[] FlutterStandardTypedData typedDataWithInt32:
Int64List long[] FlutterStandardTypedData typedDataWithInt64:
Float64List double[] FlutterStandardTypedData typedDataWithFloat64:
List java.util.ArrayList NSArray
Map java.util.HashMap NSDictionary

4、开发步骤总结

4.1 开发 Dart package

AS 创建新的 Flutter 项目,Project type 选择 Package,或者通过 flutter create 命令,将 template 参数指定为 package 创建:

shell 复制代码
flutter create --template=package hello

生成的 Package 模板结构如下:

  • lib/hello.dart:Package 的 Dart 代码
  • test/hello_test.dart:Package 的单元测试代码
  • 实现 Package:
    • 对于纯 Dart 包,需要在 lib/<package name>.dart 文件内或在 lib 下添加新文件实现
    • 对于测试包,在 test 目录中添加单元测试代码

4.2 开发 Plugin package

创建 Plugin

AS 创建新的 Flutter 项目,Project type 选择 Plugin,或者通过 flutter create 命令,将 template 参数指定为 plugin,同时将 org 参数使用反向域名表示法指定组织名并创建:

shell 复制代码
flutter create --org com.example --template=plugin hello

生成的 Plugin 模板结构如下:

  • lib/hello.dart:插件包的 Dart API

  • android/src/main/java/com/<yourcompany>/hello/HelloPlugin.java:插件包 API 的 Android 实现代码

  • ios/Classes/HelloPlugin.m:插件包 API 的 iOS 实现代码

  • example 目录:依赖于 Plugin 的示例程序。默认情况下,示例代码在 Android 和 iOS 上分别使用 Java 和 Objective-C,如果想使用 Kotlin 或 Swift,可以用过 -i 或 -a 参数指定语言:

    shell 复制代码
    flutter create --template=plugin -i swift -a kotlin hello

实现 Plugin

因为 Plugin 是包含多个平台的代码的,因此实现 Plugin 需要在多个平台上联动:

  • 用 Dart 语言实现 Flutter Plugin(代码位置在根目录的 lib 文件夹下)
  • 用 Kotlin/Java 实现 Android 原生代码(代码位置在根目录下的 /android/src/main 文件夹下)
  • 用 Objective-C/Swift 实现 iOS 原生代码(代码位置在根目录下的 ios 文件夹下)

编辑 Android 代码之前需要确保代码至少构建过一次,可以在 AS 的终端执行 cd 命令进入到示例代码的目录,然后用 flutter 命令进行 apk 构建:

shell 复制代码
cd hello/example
flutter build apk

最后需要通过 Platform Channel 将 Dart 代码与平台相关的特定实现连接起来,这一部分在认识插件结构一节中已经讲过。

添加文档

建议将以下文档添加到所有软件包:

  1. README.md:介绍包的文件
  2. CHANGELOG.md:记录每个版本中的更改
  3. LICENSE:包含软件包许可条款的文件

4.3 发布 package

实现完毕的 package 可以在 Pub 上发布供他人使用。发布的步骤如下:

  1. 发布前,需要先检查 pubspec.yamlREADME.md 以及 CHANGELOG.md 文件,以确保其内容的完整性和正确性

  2. 运行 dry-run 命令查看是否都准备 OK 了(如果有问题需要根据错误信息修改):

    shell 复制代码
    flutter packages pub publish --dry-run
  3. 最后,运行发布命令:

    shell 复制代码
    flutter packages pub publish

发布 package 需要使用 Google 账号,在运行命令时可能会因为网络环境延迟或失败。

相关推荐
sunly_8 小时前
Flutter:打包apk,详细图文介绍(一)
flutter
哥谭居民00018 小时前
primevue的<Menu>组件
flutter
LuiChun10 小时前
flutter在windows平台中运行报错
flutter
通域17 小时前
Mac 安装 Flutter 提示 A network error occurred while checking
flutter·macos
AdSet聚合广告21 小时前
解锁节日季应用广告变现潜力,提升应用广告收入
flutter·搜索引擎·uni-app·个人开发·节日
程序员老刘·1 天前
我在成都教人用Flutter写TDD(补充)——关于敏捷教练
flutter·敏捷开发·tdd
lichong9512 天前
【Flutter&Dart】构建布局(1/100)
android·flutter·api·postman·smartapi·postapi·foxapi
HH思️️无邪2 天前
Flutter-插件 scroll-to-index 实现 listView 滚动到指定索引位置
android·flutter·ios
Time@traveler2 天前
Flutter中添加全局防护水印的实现
flutter·flutter添加全局水印·flutter水印防伪·flutter水印·flutter飞书水印效果·flutter企微水印效果
lichong9512 天前
【Flutter&Dart】交互~创建一个有状态的widget &StatefulWidget(2/100)
flutter·yapi·交互·api·postman·dart·smartapi