天天看點

Pigeon- Flutter多端接口一緻性以及規範化管理實踐

Pigeon- Flutter多端接口一緻性以及規範化管理實踐

導語: 跨端開發中,經常會遇到插件,接口管理上的問題。了解完本文,你将會了解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後最終效果如下

Pigeon- Flutter多端接口一緻性以及規範化管理實踐

可以看到接入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

    為我們的輸入檔案

生成模闆代碼後的項目目錄如下

Pigeon- Flutter多端接口一緻性以及規範化管理實踐

項目目錄

我們在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.dartDemoReply、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後,隻需要重寫對應的方法,邏輯分離,直接通過函數名區分,隻需要關心具體的業務邏輯即可。

Pigeon- Flutter多端接口一緻性以及規範化管理實踐

而在dart的調用側,接入前都是通過invokeMethod調用,傳入的參數map内也是dynamic類型的值。接入後直接調用api的執行個體對象上的方法,并且通過Pigeon生成的模闆代碼,直接執行個體化參數對象。

Pigeon- Flutter多端接口一緻性以及規範化管理實踐

總結:通過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]