26

Pigeon- Flutter多端接口一致性以及规范化管理实践

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA%3D%3D&%3Bmid=2651233455&%3Bidx=1&%3Bsn=292a2c1f4f3d092326764828469bbcd3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

RVrERbV.jpg!mobile

导语: 跨端开发中,经常会遇到插件,接口管理上的问题。了解完本文,你将会了解Flutter是如何通过Pigeon去解决plugin中多端开发难以管理的问题。

demo 源码地址

https://github.com/linpenghui958/flutterPigeonDemo

warning :目前Pigeon还是prerelease版本,所以可能会有breaking change。下文以0.1.7版本为例。

为何需要Pigeon

在hybird开发中,前端需要native能力,需要native双端开发提供接口。这种情况下就如何规范命名,参数等就成了一个问题,如果单独维护一份协议文件,三端依照协议文件进行开发,很容易出现协议更改后,没有及时同步,又或者在实际开发过程没有按照规范,可能导致各种意外情况。在Flutter插件包的开发中,因为涉及到native双端代码实现能力,dart侧暴露统一的接口给使用者,也会出现同样的问题,这里Flutter官方推荐使用Pigeon进行插件管理。

Pigeon的作用

Flutter官方提供的Pigeon插件,通过dart入口,生成双端通用的模板代码,Native部分只需通过重写模板内的接口,无需关心methodChannel部分的具体实现,入参,出参也均通过生成的模板代码进行约束。假设接口新增,或者参数修改,只需要在dart侧更新协议文件,生成双端模板,即可达到同步更新。

以Flutter官方plugin中的video_player为例,接入pigeon后最终效果如下

qeYV3i7.png!mobile

可以看到接入pigeon后整体代码简洁了不少,而且规范了类型定义。接下来我们看一下如何从零接入Pigeon。

接入Pigeon

先看一下pub.dev上Pigeon的介绍,Pigeon只会生成Flutter与native平台通信所需的模板代码,没有其他运行时的要求,所以也不用担心Pigeon版本不同而导致的冲突。(这里的确不同版本使用起来差异较大,笔者这里接入的时候0.1.7与0.1.10,pigeon默认导出和使用都不相同)

创建package

ps:如果接入已有plugin库,可以跳过此部分,直接看接入部分。

执行生成插件包命令:

flutter create --org com.exmple --template plugin flutterPigeonDemo

要创建插件包,使用 --template=plugin 参数执行 flutter create

  • lib/flutter_pigeon_demo.dart
    • 插件包的dart api

  • android/src/main/kotlin/com/example/flutter_pigeon_demo/FlutterPigeonPlugin.kt
    • 插件包Android部分的实现

  • ios/Classes/FlutterPigeonDemoPlugin.m
    • 插件包ios部分的实现。

  • example/
    • 使用该插件的flutterdemo。

这里常规通过methodChannel实现plugin的部分省略,主要讲解一下如何接入pigeon插件。

添加依赖

首先在 pubspec.yaml 中添加依赖

dev_dependencies:
  flutter_test:
    sdk: flutter
  pigeon:
    version: 0.1.7

然后按照官方的要求添加一个pigeons目录,这里我们放dart侧的入口文件,内容为接口、参数、返回值的定义,后面通过pigeon的命令,生产native端代码。

这里以 pigeons/pigeonDemoMessage.dart 为例

import 'package:pigeon/pigeon.dart';

class DemoReply {
  String result;
}

class DemoRequest {
  String methodName;
}

// 需要实现的api
@HostApi()
abstract class PigeonDemoApi {
  DemoReply getMessage(DemoRequest params);
}

// 输出配置
void configurePigeon(PigeonOptions opts) {
  opts.dartOut = './lib/PigeonDemoMessage.dart';
  opts.objcHeaderOut = 'ios/Classes/PigeonDemoMessage.h';
  opts.objcSourceOut = 'ios/Classes/PigeonDemoMessage.m';
  opts.objcOptions.prefix = 'FLT';
  opts.javaOut =
  'android/src/main/kotlin/com/example/flutter_pigeon_demo/PigeonDemoMessage.java';
  opts.javaOptions.package = 'package com.example.flutter_pigeon_demo';
}

pigeonDemoMessage.dart 文件中定义了请求参数类型、返回值类型、通信的接口以及pigeon输出的配置。

这里 @HostApi() 标注了通信对象和接口的定义,后续需要在native侧注册该对象,在Dart侧通过该对象的实例来调用接口。

configurePigeon 为执行pigeon生产双端模板代码的输出配置。

  • dartOut 为dart侧输出位置
  • objcHeaderOut、objcSourceOut 为iOS侧输出位置
  • prefix 为插件默认的前缀
  • javaOut、javaOptions.package 为Android侧输出位置和包名

之后我们只需要执行如下命令,就可以生成对应的代码到指定目录中。

flutter pub run pigeon --input pigeons/pigeonDemoMessage.dart
  • --input 为我们的输入文件

生成模板代码后的项目目录如下

AruM73V.png!mobile

项目目录

我们在Plugin库中只需要管理标红的dart文件,其余标绿的则为通过Pigeon自动生成的模板代码。

我们接下来看一下双端如何使用Pigeon生成的模板文件。

Android端接入

这里Pigeon生产的 PigeonDemoMessage.java 文件中,可以看到入参和出参的定义 DemoRequest、DemoReply ,而 PigeonDemoApi 接口,后面需要在plugin中继承PigeonDemoApi并实现对应的方法,其中setup函数用来注册对应方法所需的methodChannel。

ps: 这里生成的PigeonDemoApi部分,setup使用了接口中静态方法的默认实现,这里需要api level 24才能支持,这里需要注意一下。

考虑到兼容性问题,可以将setup的定义转移到plugin中。

首先需要在plugin文件中引入生成的PigeonDemoMessage中的接口和类。FlutterPigeonDemoPlugin先要继承PigeonDemoApi。然后在onAttachedToEngine中进行PigeonDemoApi的setup注册。并在plugin中重写PigeonDemoApi中定义的getMessage方法

伪代码部分

// ... 省略其他引入
import com.example.flutter_pigeon_demo.PigeonDemoMessage.*

// 继承PigeonDemoApi
public class FlutterPigeonDemoPlugin: FlutterPlugin, MethodCallHandler, PigeonDemoApi {

 //...
 override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
     channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_pigeon_demo")
     channel.setMethodCallHandler(this);
     // pigeon生成的api进行初始化
     PigeonDemoApi.setup(flutterPluginBinding.binaryMessenger, this);
   }
   
   // 重写PigeonDemoApi中的getMessage方法
   override fun getMessage(arg: DemoRequest): DemoReply {
      var reply = DemoReply();
      reply.result = "pigeon demo result";
      return reply;
 }
}

iOS接入

ios相关目录下的 PigeonDemoMessage.m 也有 FLTDemoReply、FLTDemoRequest、FLTPigeonDemoApiSetup 的实现。首先需要在plugin中引入头文件 PigeonDemoMessage.h ,需要在registerWithRegistrar中注册setup函数,并实现getMessage方法。

#import "FlutterPigeonDemoPlugin.h"
#import "PigeonDemoMessage.h"

@implementation FlutterPigeonDemoPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    FlutterPigeonDemoPlugin* instance = [[FlutterPigeonDemoPlugin alloc] init];
    // 注册api
    FLTPigeonDemoApiSetup(registrar.messenger, instance);
}

// 重写getMessage方法
- (FLTDemoReply*)getMessage:(FLTDemoRequest*)input error:(FlutterError**)error {
    FLTDemoReply* reply = [[FLTDemoReply alloc] init];
    reply.result = @"pigeon demo result";
    return reply;
}

@end

Dart侧使用

最终在dart侧如何调用呢 首先看一下lib下Pigeon生成的dart文件 PigeonDemoMessage.dart DemoReply、DemoRequest 用来实例化入参和出参 然后通过 PigeonDemoApi 的实例去调用方法。

import 'dart:async';

import 'package:flutter/services.dart';
import 'PigeonDemoMessage.dart';

class FlutterPigeonDemo {
  static const MethodChannel _channel =
      const MethodChannel('flutter_pigeon_demo');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  static Future<DemoReply> testPigeon() async {
    // 初始化请求参数
    DemoRequest requestParams = DemoRequest()..methodName = 'requestMessage';
    // 通过PigeonDemoApi实例去调用方法
    PigeonDemoApi api = PigeonDemoApi();
    DemoReply reply = await api.getMessage(requestParams);
    return reply;
  }

}

至此,Pigeon的接入就已经完成了。

接入Pigeon后的效果

本文demo代码较为简单,接入Pigeon前后的差异并不明显,我们可以看下一Flutter官方plugin中的video_player接入前后的对比。

左侧为接入Pigeon前,处理逻辑都在onMethodCall中,不同的方法通过传入的call.method来区分,代码复杂后很容易变成面条式代码,而且返回的参数也没有约定,有较多不确定因素。

右侧接入Pigeon后,只需要重写对应的方法,逻辑分离,直接通过函数名区分,只需要关心具体的业务逻辑即可。

M3uaquu.png!mobile

而在dart的调用侧,接入前都是通过invokeMethod调用,传入的参数map内也是dynamic类型的值。接入后直接调用api的实例对象上的方法,并且通过Pigeon生成的模板代码,直接实例化参数对象。

NzIvQvf.png!mobile

总结:通过Pigeon来管理Flutter的plugin库,只需要在dart侧维护一份协议即可,即使在多端协同开发的情况下,也能达到约束和规范的作用。

在实现原生插件时我们可以省去很多重复代码,并且不需要关心具体methodchannel的name,也避免了常规情况下,可能出现的面条式代码,只需通过重写pigeon暴露的方法就可以完成双端的通信。而dart侧也只需要通过模板暴露的实例对象来调用接口方法。

源码分析

使用的时候,我们只知道运行命令 flutter pub run pigeon --input xxx 就可以生成双端模板代码,接下来我们深入了解一下,这其中Pigeon到底做了什么。

首先,看一下plugin库默认导出的pigeon_lib.dart入口文件,这里主要有几个定义PigeonOptions、ParseResults、Pigeon。

  • PigeonOptions,是执行命令生成模板时的选项。

  • ParseResults,表示解析的结果集合包含了AST对象root,和解析过程产生的错误信息集合erros。

  • Pigeon,是实际进行代码生成的类。

其中Pigeon的入口为run方法,这里进行了模板代码的生成。

run函数的入参是一个String类型的List,这里对应的是通过命令行输入的,PigeonOptions的选项。

函数开始先实例化了pigeon对象,并对传入的options进行解析生成编译所需的PigeonOptions。

这里提供了两种方式,一种是通过命令直接传入,一种是通过入口文件内configurePigeon的定义传入。

// Pigeon实例初始化
final Pigeon pigeon = Pigeon.setup();
// 解析命令行穿传入的参数
final PigeonOptions options = Pigeon.parseArgs(args);
// 解析入口文件内的参数
_executeConfigurePigeon(options);

//校验input(输入文件)或者dartOut(dart输出路径)是否为空
if (options.input == null || options.dartOut == null) {
  print(usage);
  return 0;
}

接下来会对objcHeaderOut、javaOut为空的情况取默认值处理。

// 解析apis
final ParseResults parseResults = pigeon.parse(apis);
for (Error err in parseResults.errors) {
  errors.add(Error(message: err.message, filename: options.input));
}

这里parse解析生成的parseResults对象,最终用parseResults中的ast对象root来生成多端模板代码。

这里首先将需要实现的api类和参数类进行了区分。(ps:这里_isApi中便是通过dart入口中@HostApi注解进行区分)

for (Type type in types) {
  final ClassMirror classMirror = reflectClass(type);
  if (_isApi(classMirror)) {
    apis.add(classMirror);
  } else {
    classes.add(classMirror);
  }
}

然后对参数类型进行区分,并给root对象添加了classes和apis属性。

这里classes对应模板中参数的类。而apis则对应模板中含有函数的方法类。

root.classes =
      _unique(_parseClassMirrors(classes), (Class x) => x.name).toList();

  root.apis = <Api>[];
  for (ClassMirror apiMirror in apis) {
    final List<Method> functions = <Method>[];
    for (DeclarationMirror declaration in apiMirror.declarations.values) {
      if (declaration is MethodMirror && !declaration.isConstructor) { 
        // 省略处理过程
      }
    }
    final HostApi hostApi = _getHostApi(apiMirror);
    root.apis.add(Api(
        name: MirrorSystem.getName(apiMirror.simpleName),
        location: hostApi != null ? ApiLocation.host : ApiLocation.flutter,
        methods: functions,
        dartHostTestHandler: hostApi?.dartHostTestHandler));
  }

最后根据解析后的root对象,来生成对应各端的代码。

if (options.dartOut != null) {
  await _runGenerator(
    options.dartOut,
    (StringSink sink) =>
    generateDart(options.dartOptions, parseResults.root, sink));
}
if (options.objcHeaderOut != null) {
  await _runGenerator(
    options.objcHeaderOut,
    (StringSink sink) => generateObjcHeader(
      options.objcOptions, parseResults.root, sink));
}
if (options.objcSourceOut != null) {
  await _runGenerator(
    options.objcSourceOut,
    (StringSink sink) => generateObjcSource(
      options.objcOptions, parseResults.root, sink));
}
if (options.javaOut != null) {
  await _runGenerator(
    options.javaOut,
    (StringSink sink) =>
    generateJava(options.javaOptions, parseResults.root, sink));
}

这里每个具体生成输出的函数就比较简单,这里以dart端的generateDart函数为例,通过root对象,遍历其中的class和api来生成对应的模板代码,这里模板都是已经预先定义好的。如果项目本身有定制化输出模板的需求,只需要修改对应的部分就好了。

void generateDart(DartOptions opt, Root root, StringSink sink) {
  final List<String> customClassNames =
      root.classes.map((Class x) => x.name).toList();
  final Indent indent = Indent(sink);
  indent.writeln('// $generatedCodeWarning');
  indent.writeln('// $seeAlsoWarning');
  indent.writeln(
      '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import');
  indent.writeln('// @dart = ${opt.isNullSafe ? '2.10' : '2.8'}');
  indent.writeln('import \'dart:async\';');
  indent.writeln('import \'package:flutter/services.dart\';');
  indent.writeln(
      'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;');
  indent.writeln('');

  final String nullBang = opt.isNullSafe ? '!' : '';
  // 遍历输出参数类
  for (Class klass in root.classes) {
    sink.write('class ${klass.name} ');
    indent.scoped('{', '}', () {
      for (Field field in klass.fields) {
        final String datatype =
            opt.isNullSafe ? '${field.dataType}?' : field.dataType;
        indent.writeln('$datatype ${field.name};');
      }
      indent.writeln('// ignore: unused_element');
      indent.write('Map<dynamic, dynamic> _toMap() ');
      indent.scoped('{', '}', () {
        indent.writeln(
            'final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};');
        for (Field field in klass.fields) {
          indent.write('pigeonMap[\'${field.name}\'] = ');
          if (customClassNames.contains(field.dataType)) {
            indent.addln(
                '${field.name} == null ? null : ${field.name}$nullBang._toMap();');
          } else {
            indent.addln('${field.name};');
          }
        }
        indent.writeln('return pigeonMap;');
      });
      indent.writeln('// ignore: unused_element');
      indent.write(
          'static ${klass.name} _fromMap(Map<dynamic, dynamic> pigeonMap) ');
      indent.scoped('{', '}', () {
        indent.writeln('final ${klass.name} result = ${klass.name}();');
        for (Field field in klass.fields) {
          indent.write('result.${field.name} = ');
          if (customClassNames.contains(field.dataType)) {
            indent.addln(
                'pigeonMap[\'${field.name}\'] != null ? ${field.dataType}._fromMap(pigeonMap[\'${field.name}\']) : null;');
          } else {
            indent.addln('pigeonMap[\'${field.name}\'];');
          }
        }
        indent.writeln('return result;');
      });
    });
    indent.writeln('');
  }
  // 省略apis接口部分的输出
}

腾讯音乐QQ音乐/全民K歌招聘客户端、web前端、后台开发,点击查看原文投递简历! 或邮箱联系: [email protected]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK