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 账号,在运行命令时可能会因为网络环境延迟或失败。

相关推荐
BG13 小时前
Flutter 简仿Excel表格组件介绍
flutter
zhangmeng17 小时前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
恋猫de小郭17 小时前
对于普通程序员来说 AI 是什么?AI 究竟用的是什么?
前端·flutter·ai编程
卡尔特斯17 小时前
Flutter A GlobalKey was used multipletimes inside one widget'schild list.The ...
flutter
w_y_fan21 小时前
Flutter 滚动组件总结
前端·flutter
醉过才知酒浓21 小时前
Flutter Getx 的页面传参
flutter
火柴就是我2 天前
flutter 之真手势冲突处理
android·flutter
Speed1232 天前
`mockito` 的核心“打桩”规则
flutter·dart
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
恋猫de小郭2 天前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter