实现一个简单的 Flutter 插件

本文最后更新于 2021年4月4日 晚上

记录一次 Flutter 插件的实现, 目前仅实现了 iOS 端.

实现

插件调用原生 API, 需要 Pods 支持, 开发流程如下.

整体流程

  1. 使用 IDE 或命令行新建工程:

    1
    flutter create --org com.example --template=plugin 包名

    若想指定开发语言, 则需要:

    1
    flutter create --org com.example --template=plugin -i swift -a kotlin 包名

    插件包 API 在 lib 中实现.

  2. 针对 iOS 平台, 需要首先在 example 中执行如下命令:

    1
    flutter build ios --no-codesign
  3. 通过平台通道来实现 dart 和平台间的交互.

    Flutter 作为 UI 层和平台进行交互时, 使用的是 MethodChannel 机制.

    其实就是一套消息发送机制, 消息的发送和接收都是异步的, 从而保证不会阻塞 UI 线程.

    在 Flutter 端, 使用 MethodChannel 向平台发送消息.

    在平台端, 通过 MethodChannel(Android) 或 FlutterMethodChannel(iOS) 来接收消息并回传结果.

    并且平台端也可以反向地和 Flutter 端进行联系, 从而实现一套 Flutter 作为后端, 而平台作为前端的结构.(具体做法可以参考 quick actions 这个插件.)

  4. 具体平台上数据类型支持可以参考这个链接.

  5. Flutter 和平台间交互的实现可以看这个链接.

  6. 包支持三种方式的使用:

    • 发布到 Dart 包网站再通过包管理来引入使用.

    • 相对路径或绝对路径的方式:

      1
      2
      3
      dependencies:
      plugin1:
      path: ../plugin1/
    • git URL 的方式:

      1
      2
      3
      4
      5
      dependencies:
      package1:
      git:
      url: git://github.com/flutter/packages.git
      path: packages/package1

实践

  1. 建立一个 flutter_plugin_demo 的工程, 这里直接使用 IDE, 因为还会进行依赖的获取, 故第一次耗时较长.

    可以看到, 在 lib 中已经生成了部分可供使用的代码了, 其中使用 MethodChannel 类来实例化了一个消息通道对象, 通过这个对象的 invokeMethod 方法就可以向平台端发送消息:

    1
    final String str = await _channel.invokeMethod('消息名称', [1, 2, 'a']);

    其中第一个参数是消息名称.

    另外消息通道需要用到编解码器(codec), 默认情况下使用的是标准编解码器(另外还提供了一个 JSON 形式的编解码器, 可以将消息名和参数对应解析, 估计是用在网页端的).

  2. 定义插件中的 Flutter 层 API: 比如要提供平台上的电量信息.

    • 设置消息通道名为: top.rayy.demos/flutter_plugin_demo, 这样的写法只是一种格式约定, 并没有实际功能上的含义, 接收端初始化消息通道时使用相同字符串即可.

      1
      2
      3
      4
      class FlutterPluginDemoA {
      static const MethodChannel _channel =
      const MethodChannel('top.rayy.demos/flutter_plugin_demo');
      }
    • 设置异步 API, 向平台端发送消息以获取电量信息:

      1
      2
      3
      4
      5
      6
      7
      8
      static Future<String> getBatteryLevel() async {
      try {
      int value = await _channel.invokeMethod('getBatteryLevel');
      return '$value';
      } catch (e) {
      return 'unknown';
      }
      }

      发送消息时有三种可能的情况:

      1. 成功收到结果.
      2. 收到平台异常: PlatformException
      3. 收到消息未实现(对应平台没有实现这个消息的处理)异常: MissingPluginException

      故需要使用 try-catch 来包裹消息发送的调用.

  3. 在特定平台上实现:

    和普通的消息通道使用不同, 插件实现时, 在 iOS 端对应的处理类上实现了 FlutterPlugin 协议, 故不需要显式实例化消息通道, 而是在协议规定的类方法 registerWithRegistrar 中处理消息通道的实例化, 并且在 对象方法 handleMethodCall 中处理消息接收和结果发送.

    上述两个协议方法实现如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    FlutterMethodChannel* channel = [FlutterMethodChannel
    methodChannelWithName:@"top.rayy.demos/flutter_plugin_demo"
    binaryMessenger:[registrar messenger]];
    FlutterPluginDemo_aPlugin* instance = [[FlutterPluginDemo_aPlugin alloc] init];
    [registrar addMethodCallDelegate:instance channel:channel];
    }

    - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
    } else if ([@"getBatteryLevel" isEqualToString:call.method]) {
    // TODO: 消息具体的处理代码.
    } else {
    result(FlutterMethodNotImplemented);
    }
    }

    在 Xcode 中保证编译正常后继续.

  4. 实现具体处理代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
    } else if ([@"getBatteryLevel" isEqualToString:call.method]) {
    int level = [self batteryLevel];
    result([NSNumber numberWithInt:level]);
    } else {
    result(FlutterMethodNotImplemented);
    }
    }

    - (int)batteryLevel {
    UIDevice *device = UIDevice.currentDevice;
    device.batteryMonitoringEnabled = YES;
    if (device.batteryState == UIDeviceBatteryStateUnknown) {
    return -1;
    } else {
    return (int)(device.batteryLevel * 100);
    }
    }
  5. 在 example 中进行使用测试:

    因为要从异步转到同步, 在 Flutter 中利用了一个技巧, 即在某个方法中异步获取到值后进行 setState 操作来更新对应属性, 从而更新界面.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Future<void> initPlatformState() async {
    String batteryLevel;
    try {
    batteryLevel = await FlutterPluginDemoA.getBatteryLevel();
    } on PlatformException {
    // TODO: 异常处理
    }

    // 判断当前状态对象是否仍然和 Widget 关联, 如果没关联的话, setState 会崩溃.
    if (!mounted) return;

    setState( () {
    _batteryLevel = batteryLevel;
    });
    }

    剩下的就是尝试在界面上展示出来了.

  6. 使用 git 提供给其他 dart 工程使用:

    • 上传到一个可用的 git 仓库, 而后,

    • 尝试:

      1
      2
      3
      4
      5
      6
      7
      dependencies:
      package1:
      git:
      # 替换为库的 URL
      url: git://github.com/flutter/packages.git
      # 如果有多个包, 需要替换为具体路径(比如 Flutter 团队提供的就是若干插件在一块的)
      # path: packages/package1

      这里上传到的是 github 上面.

      证明可以直接使用.

在插件中使用 Swift

严格来说这个不应算到 Flutter 这里, 而是 iOS 端的一个实践而已.

只需要在 OC 实现文件中引用 swift 生成的头文件就可以了…

不过注意, 引入头文件的时候需要使用如下语法:

1
2
3
4
5
#import <模块名/模块名-Swift.h>

// 比如

#import <plugin_swift_demo/plugin_swift_demo-Swift.h>

另外 Swift 类需要是 public 的且标记为 @objc 才能在 OC 中访问到, 由于标记为 @objc, 故还有一个隐性要求就是 class, 且 class 必须继承自 NSObject.

但是要注意, 默认情况下, Flutter 插件生成命令生成的是静态库类型的 pod, 而使用 swift 的话默认是动态库类型的 Framework.

且暂时没有找到在 OC 静态库中混编 OC 和 Swift 的方法.

故目前如果想要在插件中自己编写 swift 代码的话, 最好创建 Swift 版本的插件, 而如果只用 OC 的话, 则可以通过 Pod 引入三方 Swift 库作为这个库的平台端依赖使用.

插件中的 Pod 库分析

首先是定义的 Podfile 文件, 看懂这个后再继续. 这个文件中主要是在为 iOS 平台 example 提供 Flutter 依赖和自定义 Pod 库依赖. pod 可以认出这个 pod 是开发 pod, 使用下面的代码放到 Podfile 中即可将文件放到顶层, 方便开发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}

if ENV['FLUTTER_FRAMEWORK_DIR'] == nil
abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework')
end

target 'Runner' do
use_frameworks!

# Pods for Runner

# Flutter Pods
pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR']

# 在上层目录找到当前插件的绝对路径和插件名称, 根据这两个信息来提供 Pod 库的引入规则.
# 这样开发库的所有文件可以在根目录下显示, 且仍然可以避免使用绝对路径.
if File.exists? '../.flutter-plugins'
flutter_root = File.expand_path('..')
File.foreach('../.flutter-plugins') { |line|
plugin = line.split(pattern='=')
if plugin.length == 2
name = plugin[0].strip()
path = plugin[1].strip()
resolved_path = File.expand_path("#{path}/ios", flutter_root)
pod name, :path => resolved_path
else
puts "Invalid plugin specification: #{line}"
end
}
end
end

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end

总结

插件开发的时候最好使用 swift 模板, 方便起步, 另外替换上述 Podfile 方便开发. 不过有个蛋疼的地方就是移动文件的时候无法放到指定的文件夹下.

另外可以看看 CocoaPod 的文档, 因为其他相关的问题都是由于 CocoaPod 上的配置问题引起的.

Flutter和平台间的交互

交互的时候在 Flutter 端是这个样的:

  • 用 method channel 发送消息
  • 用 event channel 接收消息

平台端正好相反:

  • 使用 method channel 接收消息
  • 使用 eventSink 发送消息

从平台向 Flutter 发送/接收消息的流程如下所示:

之前已经搞了从 Flutter 向平台发送消息的流程, 现在来看从平台往 Flutter 发送消息的流程

Flutter 端

  1. 建立 EventChannel:

    1
    2
    static const EventChannel eventChannel =
    EventChannel('samples.flutter.io/charging');
  2. 在合适的位置启动监听 EventChannel 并提供事件处理方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @override
    void initState() {
    super.initState();
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
    }

    void _onEvent(Object event) {
    // ...
    }

    void _onError(Object error) {
    // ...
    }

    其中 Object event 是可以进行编解码的数据类型, 它代表的是平台端传入的数据.

平台端

  1. 选择一个合适的类, 并在其中建立事件通道:

    1
    2
    let chargingChannel = FlutterEventChannel(name: "samples.flutter.io/charging",
    binaryMessenger: controller)
  2. 在这个类中声明一个事件槽, 同时让该类实现 FlutterStreamHandler 协议:

    1
    2
    3
    class SomeClass: NSObject, FlutterStreamHandler {
    private var eventSink: FlutterEventSink?
    }

    之后拿到事件槽的实例后, 就可以通过该实例向 Flutter 端发送消息.

  3. 将该类设置为事件通道的 streamHandler:

    1
    chargingChannel.setStreamHandler(self)
  4. 实现协议方法 onListenWithArgumentsonCancelWithArguments:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public func onListen(withArguments arguments: Any?,
    eventSink: @escaping FlutterEventSink) -> FlutterError? {
    self.eventSink = eventSink
    return nil
    }

    public func onCancel(withArguments arguments: Any?) -> FlutterError? {
    eventSink = nil
    return nil
    }
  5. 通过事件槽发送消息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private func sendBatteryStateEvent() {
    guard let eventSink = eventSink else { return }

    switch UIDevice.current.batteryState {
    case .full:
    eventSink("charging")
    case .charging:
    eventSink("charging")
    case .unplugged:
    eventSink("discharging")
    default:
    eventSink(FlutterError(code: "unavailable",
    message: "Charging status unavailable",
    details: nil))
    }
    }

    此时如果 Flutter 端想要接收消息, 则只需要在 onEvent 中写如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void _onEvent(Object event) {
    if (!mounted) { return; }
    String status;
    if (event == 'charging') {
    status = '正在充电';
    } else {
    status = '没有充电';
    }
    setState(() {
    _chargingStatus = '充电状态: $status';
    });
    }

typedef 的语法好弹弹堂:

1
2
3
4
5
6
typedef Compare = int Function(Object a, Object b);

typedef Compare<T> = int Function(T a, T b);

// 下面的写法和 typedef Compare = void Function(Object todo); 是一样的.
typedef void Compare(Object todo);

具体的实现已经放到了这个库中, 其中:

  • 通过电量数据的获取来展示 Flutter —> 平台 的消息发送和结果处理.

  • 通过监听网络状态变化来展示 平台 —> Flutter 的事件发送和处理.


实现一个简单的 Flutter 插件
https://blog.rayy.top/2019/01/06/2019-28-flutter-plugin/
作者
貘鸣
发布于
2019年1月6日
许可协议