Flutter-自定義插件

Flutter插件是什么?

在開發(fā)Flutter應(yīng)用過程中會(huì)涉及到平臺(tái)相關(guān)接口調(diào)用将鸵,例如數(shù)據(jù)庫操作勉盅、相機(jī)調(diào)用、定位等業(yè)務(wù)場景顶掉。Flutter自身并不支持直接在平臺(tái)上實(shí)現(xiàn)這些功能草娜,而是通過插件包接口去調(diào)用指定平臺(tái)API從而實(shí)現(xiàn)原生平臺(tái)上特定功能

創(chuàng)建Flutter插件工程

在Android Studio里點(diǎn)擊Flie - New - New Flutter Project,在左側(cè)里選中Flutter痒筒,然后點(diǎn)擊Next宰闰。
wecom-temp-d83e54cdf760937727daf020f4128b67.png
  • 在Project Name里輸入項(xiàng)目名,只能是小寫英文
  • 在Project type里選擇Plugin
  • 在Organization里寫包名簿透,.Project Name會(huì)拼在包名的最后面成為包名的一部分
使用命令創(chuàng)建插件

flutter create --org com.example --template=plugin plugin_name -I swift
其中 com.example 是插件包名的一部分移袍,plugin_name 是插件的名稱。插件的完整包名為 com.example.plugin_name

插件目錄結(jié)構(gòu)
wecom-temp-7353b068adcc6be10902b1ae2a3c2cf1.png

我們需要關(guān)注的主要有以下4個(gè):

  • android目錄是用來開發(fā)Android端的插件功能
  • ios目錄是用來開發(fā)iOS端的插件功能
  • lib是實(shí)現(xiàn)Flutter插件接口的代碼
  • example目錄是測試項(xiàng)目老充,用來測試開發(fā)出來的插件的

插件功能開發(fā)

App端實(shí)現(xiàn)

在register方法里葡盗,我們注冊(cè)了一個(gè)通道(已經(jīng)默認(rèn)注冊(cè)了),通道名默認(rèn)就是項(xiàng)目名啡浊,該名字在通信里必須是唯一的觅够,可以修改胶背,一旦修改,需要把dart和android里的名字也一并修改喘先。
在handle方法里钳吟,實(shí)現(xiàn)Flutter調(diào)用原生的API,其中call.method就是方法名窘拯,call.arguments就是Flutter傳遞過來的參數(shù)红且。使用result(返回值)可以把結(jié)果返回給Flutter。
當(dāng)找不到方法名時(shí)涤姊,可以返回FlutterMethodNotImplemented給Flutter表示該方法還沒實(shí)現(xiàn)直焙,以此來做版本兼容

public class SwiftTestPlugin: NSObject, FlutterPlugin {
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "test_plugin", binaryMessenger: registrar.messenger())
        let instance = SwiftTestPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
    }
    
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        if call.method == "getNetWorkType" { // 獲取網(wǎng)絡(luò)類型的實(shí)現(xiàn)
            result("WIFI")
        } else if call.method == "bonusPoints" { // 使用參數(shù)的實(shí)現(xiàn)
            let array = call.arguments as! Array<Int>
            result(array[0] + array[1])
        } else if call.method == "getPlatformVersion" { // 默認(rèn)的實(shí)現(xiàn)
            result("iOS " + UIDevice.current.systemVersion)
        } else {
            // 找不到方法
            result(FlutterMethodNotImplemented)
        }
    }
}
Flutter端實(shí)現(xiàn)

增加方法用來調(diào)用iOS端的方法,方法名不需要和iOS端的保持一致砂轻,主要是通道里調(diào)用iOS端的方法名就行了

abstract class TestPluginPlatform extends PlatformInterface {
  /// Constructs a TestPluginPlatform.
  TestPluginPlatform() : super(token: _token);

  static final Object _token = Object();

  static TestPluginPlatform _instance = MethodChannelTestPlugin();

  /// The default instance of [TestPluginPlatform] to use.
  ///
  /// Defaults to [MethodChannelTestPlugin].
  static TestPluginPlatform get instance => _instance;
  
  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [TestPluginPlatform] when
  /// they register themselves.
  static set instance(TestPluginPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }

  Future<String> getNetWorkType() async {
    throw UnimplementedError('getNetWorkType() has not been implemented.');
  }

  Future<int> add() async {
    throw UnimplementedError('add() has not been implemented.');
  }
}

插件自動(dòng)生成的抽象類

class MethodChannelTestPlugin extends TestPluginPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('test_plugin');

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

  /// 實(shí)現(xiàn)iOS端新增的方法
  @override
  Future<String> getNetWorkType() async {
    final String state = await methodChannel.invokeMethod('getNetWorkType');
    return state;
  }

  /// 實(shí)現(xiàn)iOS端新增的方法
  @override
  Future<int> add() async {
    final int result = await methodChannel.invokeMethod('bonusPoints', [5, 8]); /// 接收一個(gè)數(shù)組或者字典作為參數(shù)傳遞給原生端
    return result;
  }
}

定義methodChannel奔誓,最終由methodChannel通過invokeMethod調(diào)用原生方法,invokeMethod內(nèi)的方法名需要與原生定義的方法名一致

class TestPlugin {
  Future<String?> getPlatformVersion() {
    return TestPluginPlatform.instance.getPlatformVersion();
  }

  /// 實(shí)現(xiàn)iOS端新增的方法
  Future<String> getNetWorkType() async {
    return TestPluginPlatform.instance.getNetWorkType();
  }

  /// 實(shí)現(xiàn)iOS端新增的方法
  Future<int> add() async {
    return TestPluginPlatform.instance.add();
  }
}

需要注意的是搔涝,F(xiàn)lutter和原生通信都是異步的厨喂,所以都需要使用await和async

三方庫使用

寫插件不可避免的會(huì)用到第三方庫,在使用第三方庫的時(shí)候庄呈,會(huì)遇到3種情況:

  • 僅原生端使用第三方庫
    當(dāng)僅原生端需要依賴某些第三方庫時(shí)蜕煌,可以在podspec文件里加上s.dependency '第三方庫名'
  • 僅Flutter端使用第三方庫
    當(dāng)僅Flutter端需要依賴某些第三方庫時(shí),可以在pubspec.yaml文件里的dependencies部分
dependencies:
  flutter:
    sdk: flutter

  url_launcher: ^6.0.16
  • 都使用同一個(gè)第三方庫
    假設(shè)Flutter里需要用到url_launcher诬留,然后原生里也需要用到斜纪,那我們就得在Flutter的pubspec.yaml文件里的dependencies部分添加依賴包,同時(shí)也要在iOS端的podspec文件里加上s.dependency 'url_launcher'
私有庫使用

寫插件不可避免的會(huì)用到私有庫文兑,在使用第三方庫的時(shí)候:

  • 原生端需要在podspec文件里加上s.dependency '私有庫名'

  • Flutter項(xiàng)目需要在podFile里添加私有的source和path

tips: 可將私有庫打包成.a盒刚,直接集成在插件里, 避免了私有庫引用帶來的麻煩

通信的數(shù)據(jù)類型

原生與Flutter互相通信時(shí)使用的數(shù)據(jù)類型是有限制的,以下是可用的數(shù)據(jù)類型:

Dart kotlin Java Objective-C Swift
null null null NSNull NSNull
bool Boolean java.lang.Boolean NSNumber numberWithBool: NSNumber(value: Bool)或者Bool
int 32位平臺(tái) Int java.lang.Integer NSNumber numberWithInt: NSNumber(value: Int32)或者Int32
int Long java.lang.Long NSNumber numberWithLong: NSNumber(value: Int)或者Int
double Double java.lang.Double NSNumber numberWithDouble: NSNumber(value: Double)或者Double
String String java.lang.String NSString String或者NSString
Uint8List ByteArray byte[] FlutterStandardTypedData typedDataWithBytes: FlutterStandardTypedData(bytes: Data)
Int32List IntArray int[] FlutterStandardTypedData typedDataWithInt32: FlutterStandardTypedData(int32: Data)
Int64List LongArray long[] FlutterStandardTypedData typedDataWithInt64: FlutterStandardTypedData(int64: Data)
Float32List FloatArray float[] FlutterStandardTypedData typedDataWithFloat32: FlutterStandardTypedData(float32: Data)
Float64List DoubleArray double[] FlutterStandardTypedData typedDataWithFloat64: FlutterStandardTypedData(float64: Data)
List List java.util.ArrayList NSArray Array或者NSArray
Map HashMap java.util.HashMap NSDictionary Dictionary或者NSDictionary
  • Swift的基礎(chǔ)類型可以用Objective-C的對(duì)象類型绿贞,集合類型可以兼容Objective-C的集合類型(不過這些都是Swift本身的特性)
  • 在使用Swift時(shí)因块,最好還是使用它本身的類型,如果使用Objective-C的類型籍铁,就無法判斷詳細(xì)類型涡上,比如Int和Double,在使用Objective-C類型的時(shí)候拒名,都是NSNumber
思考

能不能統(tǒng)一生成兩端通用的代碼吩愧?開發(fā)是否可以不需要關(guān)心Method Channe的具體實(shí)現(xiàn),只需要實(shí)現(xiàn)對(duì)應(yīng)接口即可增显?

pigeon工具插件

一個(gè)代碼生成工具雁佳,讓Flutter和宿主平臺(tái)更安全、更簡單、更快地通信甘穿。通過Dart入口腮恩,生成兩端通用的模板代碼梢杭,原生則只需重寫模板內(nèi)的接口温兼,無需管理Method Channel的實(shí)現(xiàn)。參數(shù)可以通過模板來同步生成武契。

目前的pigeon只支持生成OC和Java代碼

1. 添加依賴

dev_dependencies:
  pigeon: ^3.1.0

2. 添加插件交互類和方法

  • @HostApi() 標(biāo)記的募判,是用于 Flutter 調(diào)用原生的方法。
  • @FlutterApi() 標(biāo)記的咒唆,是用于原生調(diào)用 Flutter 的方法届垫。
  • @async 如果原生的方法,是異步回調(diào)那種全释,你就可以使用這個(gè)標(biāo)記
  • 只支持 dart 的基礎(chǔ)類型
import 'package:pigeon/pigeon.dart';

class Everything {
  bool? aBool;
  int? anInt;
  double? aDouble;
  String? aString;
  Uint8List? aByteArray;
  Int32List? a4ByteArray;
  Int64List? a8ByteArray;
  Float64List? aFloatArray;
  // ignore: always_specify_types
  List? aList;
  // ignore: always_specify_types
  Map? aMap;
  List<List<bool?>?>? nestedList;
  Map<String?, String?>? mapWithAnnotations;
}

/// Flutter調(diào)用原生的方法
@HostApi()
abstract class HostEverything {
  Everything giveMeEverything();
  Everything echo(Everything everything);
}

/// 原生調(diào)用Flutter的方法
@FlutterApi()
abstract class FlutterEverything {
  Everything giveMeEverythingFlutter();
  Everything echoFlutter(Everything everything);
}

3. 生成各端插件代碼

在項(xiàng)目目錄下装处,執(zhí)行以下命令:

$flutter pub run pigeon
--input pigeon/schema.dart
--dart_out lib/api_generated.dart
--objc_header_out ios/Classes/AllTypesPigeon.h
--objc_source_out ios/Classes/AllTypesPigeon.m
--objc_prefix FLT
--java_out android/src/main/java/com/akulaku/test_plugin_pigeon/AllTypesPigeon.java
--java_package "com.akulaku.test_plugin_pigeon"

或者執(zhí)行 ./run_pigeon.sh 腳本內(nèi)容如下:

flutter pub run pigeon \
--input pigeon/schema.dart \
--dart_out lib/api_generated.dart \
--objc_header_out ios/Classes/AllTypesPigeon.h \
--objc_source_out ios/Classes/AllTypesPigeon.m \
--objc_prefix FLT \
--java_out android/src/main/java/com/akulaku/test_plugin_pigeon/AllTypesPigeon.java \
--java_package "com.akulaku.test_plugin_pigeon"
  • input 第二步定義的交互類
  • dart_out 對(duì)應(yīng)的dart文件
  • objc_header_out/objc_source_out 對(duì)應(yīng)的iOS實(shí)現(xiàn)文件
  • java_out 對(duì)應(yīng)的android實(shí)現(xiàn)文件

執(zhí)行腳本或者命令生成以下文件:

wecom-temp-92d7254da48d39c25b198cc85c0b27cc.png

iOS實(shí)現(xiàn)Flutter調(diào)用原生的方法
① 刪掉項(xiàng)目中之前的獲取版本的原生的和Flutter側(cè)的相關(guān)channel代碼

public class SwiftTestPlugin: NSObject, FlutterPlugin {
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "test_plugin", binaryMessenger: registrar.messenger())
        let instance = SwiftTestPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
    }
    
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
         result("iOS " + UIDevice.current.systemVersion)
    }
}

② 在AllTypesPigeon.m中自動(dòng)生成了一個(gè)方法HostEverythingSetup

void FLTHostEverythingSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<FLTHostEverything> *api) {
  {
    FlutterBasicMessageChannel *channel =
      [[FlutterBasicMessageChannel alloc]
        initWithName:@"dev.flutter.pigeon.HostEverything.giveMeEverything"
        binaryMessenger:binaryMessenger
        codec:FLTHostEverythingGetCodec()        ];
    if (api) {
      NSCAssert([api respondsToSelector:@selector(giveMeEverythingWithError:)], @"FLTHostEverything api (%@) doesn't respond to @selector(giveMeEverythingWithError:)", api);
      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
        FlutterError *error;
        FLTEverything *output = [api giveMeEverythingWithError:&error];
        callback(wrapResult(output, error));
      }];
    }
    else {
      [channel setMessageHandler:nil];
    }
  }
  {
    FlutterBasicMessageChannel *channel =
      [[FlutterBasicMessageChannel alloc]
        initWithName:@"dev.flutter.pigeon.HostEverything.echo"
        binaryMessenger:binaryMessenger
        codec:FLTHostEverythingGetCodec()        ];
    if (api) {
      NSCAssert([api respondsToSelector:@selector(echoEverything:error:)], @"FLTHostEverything api (%@) doesn't respond to @selector(echoEverything:error:)", api);
      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
        NSArray *args = message;
        FLTEverything *arg_everything = GetNullableObjectAtIndex(args, 0);
        FlutterError *error;
        FLTEverything *output = [api echoEverything:arg_everything error:&error];
        callback(wrapResult(output, error));
      }];
    }
    else {
      [channel setMessageHandler:nil];
    }
  }
}

③ 在SwiftTestPluginPigeonPlugin的注冊(cè)方法里,調(diào)用這個(gè)setup方法進(jìn)行初始化和設(shè)置

    public static func register(with registrar: FlutterPluginRegistrar) {
        let messenger: FlutterBinaryMessenger = registrar.messenger()
        let api: HostEverything & NSObjectProtocol = SwiftTestPluginPigeonPlugin.init()
        FLTHostEverythingSetup(messenger, api)
    }

④ iOS/Classes目錄下浸船,創(chuàng)建test_plugin_pigeon.h文件妄迁,導(dǎo)入頭文件,此文件在iOS自動(dòng)生成的<test_plugin_pigeon/test_plugin_pigeon-Swift.h>文件中會(huì)自動(dòng)引用李命。

#ifndef test_plugin_pigeon_h
#define test_plugin_pigeon_h

#import "AllTypesPigeon.h"

#endif /* test_plugin_pigeon_h */

⑤ SwiftFlutterPigeonPlugin遵循HostEverything協(xié)議登淘,實(shí)現(xiàn)Flutter調(diào)原生的方法

import Flutter
import UIKit

/// 遵循HostEverything協(xié)議,實(shí)現(xiàn)Flutter調(diào)原生的方法
public class SwiftTestPluginPigeonPlugin: NSObject, FlutterPlugin, FLTHostEverything {
    
    
    public static func register(with registrar: FlutterPluginRegistrar) {
        let messenger: FlutterBinaryMessenger = registrar.messenger()
        let api: FLTHostEverything & NSObjectProtocol = SwiftTestPluginPigeonPlugin.init()
        FLTHostEverythingSetup(messenger, api)
    }
    
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        result("iOS " + UIDevice.current.systemVersion)
    }
    
    public func giveMeEverythingWithError(_ error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> FLTEverything? {
        let everyThing = FLTEverything()
        everyThing.aString = "原生返給Flutter的字符串"
        everyThing.aBool = false
        everyThing.anInt = 11
        return everyThing
    }
    
    public func echo(_ everything: FLTEverything, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> FLTEverything? {
        let everyThing = FLTEverything()
        everyThing.aString = "原生返給Flutter的字符串"
        everyThing.aBool = false
        everyThing.anInt = 11
        return everyThing
    }
    
}

上面都是講Flutter怎么調(diào)原生的封字,那具體是怎么通信的呢黔州?

原生與Flutter通信

Flutter 提供了 Platform Channel 機(jī)制,讓消息能夠在 native 與 Flutter 之間進(jìn)行傳遞

Flutter定義了三種不同類型的Channel通信類阔籽,

  • BasicMessageChannel:用于傳遞字符串和半結(jié)構(gòu)化的信息流妻。
  • MethodChannel:用于傳遞方法調(diào)用(method invocation)。
  • EventChannel: 用于數(shù)據(jù)流(event streams)的通信笆制。

Channel 使用 codec 消息編解碼器合冀,支持從基礎(chǔ)數(shù)據(jù)到二進(jìn)制格式數(shù)據(jù)的轉(zhuǎn)換、解析。
三種Channel之間互相獨(dú)立隧枫,每種Channel均有三個(gè)重要成員變量:

  • name: String類型横腿,代表Channel的名字,也是其唯一標(biāo)識(shí)符棕叫。
  • messager:BinaryMessenger類型,代表消息信使奕删,是消息的發(fā)送與接收的工具俺泣。
  • codec: MessageCodec類型或MethodCodec類型,代表消息的編解碼器。

一個(gè)Flutter應(yīng)用中可能存在多個(gè)Channel伏钠,每個(gè)Channel在創(chuàng)建時(shí)必須指定一個(gè)獨(dú)一無二的name横漏,Channel之間使用name來區(qū)分彼此。當(dāng)有消息從Flutter端發(fā)送到Platform端時(shí)熟掂,會(huì)根據(jù)其傳遞過來的channel name找到該Channel對(duì)應(yīng)的Handler(消息處理器)缎浇。

Channel 注冊(cè)

以FlutterMethodChannel為例看一個(gè)注冊(cè)流程

FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
      methodChannelWithName:@"samples.flutter.io/battery"
            binaryMessenger:controller];
            
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call,
                                         FlutterResult result) {
    if ([@"getBatteryLevel" isEqualToString:call.method]) {
      int batteryLevel = [weakSelf getBatteryLevel];
      if (batteryLevel == -1) {
        result([FlutterError errorWithCode:@"UNAVAILABLE"
                                   message:@"Battery info unavailable"
                                   details:nil]);
      } else {
        result(@(batteryLevel));
      }
      ......
  }];

初始化一個(gè)橋接,傳入橋接名和處理消息發(fā)送接收的類赴肚,橋接名為"samples.flutter.io/battery"

通過setMethodCallHandler 方法素跺,在 native 項(xiàng)目中注冊(cè)該橋接的回調(diào) handler,其中參數(shù) result 表示向 Flutter 回傳的結(jié)果

- (void)setMethodCallHandler:(FlutterMethodCallHandler)handler {
  if (!handler) {
    [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:nil];
    return;
  }
 #此處又包裝了一個(gè)回調(diào) messageHandler誉券,用于解碼二進(jìn)制消息 message指厌,并向 Flutter 側(cè)回傳執(zhí)行結(jié)果 reply
  FlutterBinaryMessageHandler messageHandler = ^(NSData* message, FlutterBinaryReply callback) {
    FlutterMethodCall* call = [_codec decodeMethodCall:message];
    handler(call, ^(id result) {
      if (result == FlutterMethodNotImplemented)
        callback(nil);
      else if ([result isKindOfClass:[FlutterError class]])
        callback([_codec encodeErrorEnvelope:(FlutterError*)result]);
      else
        callback([_codec encodeSuccessEnvelope:result]);
    });
  };
  [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:messageHandler];
}

- (void)setMessageHandlerOnChannel:(NSString*)channel
              binaryMessageHandler:(FlutterBinaryMessageHandler)handler {
  NSAssert(channel, @"The channel must not be null");
  [_engine.get() setMessageHandlerOnChannel:channel binaryMessageHandler:handler];
}

- (void)setMessageHandlerOnChannel:(NSString*)channel
              binaryMessageHandler:(FlutterBinaryMessageHandler)handler {
  NSAssert(channel, @"The channel must not be null");
  FML_DCHECK(_shell && _shell->IsSetup());
  self.iosPlatformView->GetPlatformMessageRouter().SetMessageHandler(channel.UTF8String, handler);
}

// PlatformMessageRouter
void PlatformMessageRouter::SetMessageHandler(const std::string& channel,
                                              FlutterBinaryMessageHandler handler) {
  message_handlers_.erase(channel);
  if (handler) {
    message_handlers_[channel] =
        fml::ScopedBlock<FlutterBinaryMessageHandler>{handler, fml::OwnershipPolicy::Retain};
  }
}

PlatformMessageRouter的message_handlers_ 是個(gè)哈希表,key 是橋接名踊跟,value 放 handle踩验。原生注冊(cè)橋接方法,其實(shí)就是維護(hù)一個(gè) map 對(duì)象

Channel調(diào)用方法

dart 側(cè)調(diào)用獲取電量的橋接方法

static const MethodChannel methodChannel =
      MethodChannel('samples.flutter.io/battery');
      
final int result = await methodChannel.invokeMethod('getBatteryLevel');

channel調(diào)用invokeMethod商玫,查看invokeMethod源碼:

// platform_channel.dart
const MethodChannel(this.name, [this.codec = const StandardMethodCodec()]); 
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
    assert(method != null);
    final dynamic result = await BinaryMessages.send(
      name,
      codec.encodeMethodCall(MethodCall(method, arguments)),
    );
    if (result == null)
      throw MissingPluginException('No implementation found for method $method on channel $name');
    return codec.decodeEnvelope(result);
  }

nvokeMethod 方法將方法名和參數(shù)轉(zhuǎn)化為二進(jìn)制數(shù)據(jù)箕憾,并通過 BinaryMessages send 方法發(fā)送出去

// platform_messages.dart
static Future<ByteData> send(String channel, ByteData message) {
  final _MessageHandler handler = _mockHandlers[channel];
    if (handler != null)
      return handler(message);
    return _sendPlatformMessage(channel, message);
  }
  
static Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
    final Completer<ByteData> completer = Completer<ByteData>();
    ui.window.sendPlatformMessage(channel, message, (ByteData reply) {
      try {
        completer.complete(reply);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
          context: 'during a platform message response callback',
        ));
      }
    });
    return completer.future;
  }

// window.dart
  void sendPlatformMessage(String name,
                           ByteData data,
                           PlatformMessageResponseCallback callback) {
    final String error =
        _sendPlatformMessage(name, _zonedPlatformMessageResponseCallback(callback), data);
    if (error != null)
      throw new Exception(error);
  }
  
  
  String _sendPlatformMessage(String name,
                              PlatformMessageResponseCallback callback,
                              ByteData data) native 'Window_sendPlatformMessage';

_sendPlatformMessage 將調(diào)用 native 的方法 Window_sendPlatformMessage。

注冊(cè)流程圖:


292976-4d087d00c5fac7fe.png.png

dart 側(cè)的代碼跟蹤結(jié)束了决帖,接下來又到了 native

void Window::RegisterNatives(tonic::DartLibraryNatives* natives) {
  natives->Register({
      {"Window_defaultRouteName", DefaultRouteName, 1, true},
      {"Window_scheduleFrame", ScheduleFrame, 1, true},
      {"Window_sendPlatformMessage", _SendPlatformMessage, 4, true},
      {"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true},
      {"Window_render", Render, 2, true},
      {"Window_updateSemantics", UpdateSemantics, 2, true},
      {"Window_setIsolateDebugName", SetIsolateDebugName, 2, true},
  });
}

注冊(cè) native 方法厕九,供 dart 端調(diào)用,其中 Window_sendPlatformMessage 被 dart 調(diào)用地回,對(duì)應(yīng)的 _SendPlatformMessage 方法扁远。而 _SendPlatformMessage 又調(diào)用了SendPlatformMessage

// WindowClient
Dart_Handle SendPlatformMessage(Dart_Handle window,
                                const std::string& name,
                                Dart_Handle callback,
                                const tonic::DartByteData& data) {
  UIDartState* dart_state = UIDartState::Current();

  if (!dart_state->window()) {
    // Must release the TypedData buffer before allocating other Dart objects.
    data.Release();
    return ToDart("Platform messages can only be sent from the main isolate");
  }

  fml::RefPtr<PlatformMessageResponse> response;
  if (!Dart_IsNull(callback)) {
    response = fml::MakeRefCounted<PlatformMessageResponseDart>(
        tonic::DartPersistentValue(dart_state, callback),
        dart_state->GetTaskRunners().GetUITaskRunner());
  }
  if (Dart_IsNull(data.dart_handle())) {
    dart_state->window()->client()->HandlePlatformMessage(
        fml::MakeRefCounted<PlatformMessage>(name, response));
  } else {
    const uint8_t* buffer = static_cast<const uint8_t*>(data.data());

    dart_state->window()->client()->HandlePlatformMessage(
        fml::MakeRefCounted<PlatformMessage>(
            name, std::vector<uint8_t>(buffer, buffer + data.length_in_bytes()),
            response));
  }

  return Dart_Null();
}

// Engine.cc
void Engine::HandlePlatformMessage(
    fml::RefPtr<blink::PlatformMessage> message) {
  if (message->channel() == kAssetChannel) {
    HandleAssetPlatformMessage(std::move(message));
  } else {
    delegate_.OnEngineHandlePlatformMessage(std::move(message));
  }
}

// |shell::Engine::Delegate|
void Shell::OnEngineHandlePlatformMessage(
    fml::RefPtr<blink::PlatformMessage> message) {
  FML_DCHECK(is_setup_);
  FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread());

  task_runners_.GetPlatformTaskRunner()->PostTask(
      [view = platform_view_->GetWeakPtr(), message = std::move(message)]() {
        if (view) {
          view->HandlePlatformMessage(std::move(message));
        }
      });
}

// |shell::PlatformView|
void PlatformViewIOS::HandlePlatformMessage(fml::RefPtr<blink::PlatformMessage> message) {
  platform_message_router_.HandlePlatformMessage(std::move(message));
}

void PlatformMessageRouter::HandlePlatformMessage(
    fml::RefPtr<blink::PlatformMessage> message) const {
  fml::RefPtr<blink::PlatformMessageResponse> completer = message->response();
  auto it = message_handlers_.find(message->channel());
  if (it != message_handlers_.end()) {
    FlutterBinaryMessageHandler handler = it->second;
    NSData* data = nil;
    if (message->hasData()) {
      data = GetNSDataFromVector(message->data());
    }
    handler(data, ^(NSData* reply) {
      if (completer) {
        if (reply) {
          completer->Complete(GetMappingFromNSData(reply));
        } else {
          completer->CompleteEmpty();
        }
      }
    });
  } else {
    if (completer) {
      completer->CompleteEmpty();
    }
  }
}

注冊(cè) native 方法,供 dart 端調(diào)用刻像,其中 Window_sendPlatformMessage 被 dart 調(diào)用畅买,對(duì)應(yīng)的 _SendPlatformMessage 方法。而 _SendPlatformMessage 又調(diào)用了SendPlatformMessage

最終PlatformView把消息轉(zhuǎn)發(fā)給PlatformMessageRouter细睡,然后從message_handlers_中取出channelName對(duì)應(yīng)的 handle 并執(zhí)行

handle 完成后谷羞,將結(jié)果回調(diào)給 Flutter

調(diào)用流程圖:


292976-94777a411f6313bd.png.jpg

插件功能測試

① 在test_plugin_pigeon.dart文件里添加測試代碼:

import 'api_generated.dart';
import 'test_plugin_pigeon_platform_interface.dart';

class TestPluginPigeon {

  final HostEverything _hostEverything = HostEverything();

  Future<String?> getPlatformVersion() {
    return TestPluginPigeonPlatform.instance.getPlatformVersion();
  }

  /// Flutter 調(diào)用原生方法
  Future<Everything> getEverything()  async {
    return await _hostEverything.giveMeEverything();
  }

  /// Flutter 調(diào)用原生方法
  Future<Everything> echoEveryThing(Everything everything) async {
    return await _hostEverything.echo(everything);
  }
}

② 在example目錄里的lib目錄,里面有一個(gè)main.dart文件溜徙,調(diào)用測試代碼

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:test_plugin_pigeon/api_generated.dart';
import 'package:test_plugin_pigeon/test_plugin_pigeon.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';
  String _aString = 'Unknown';
  final _testPluginPigeonPlugin = TestPluginPigeon();

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

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String platformVersion;
    String aString;
    // Platform messages may fail, so we use a try/catch PlatformException.
    // We also handle the message potentially returning null.
    try {
      // platformVersion =
      //     await _testPluginPigeonPlugin.getPlatformVersion() ?? 'Unknown platform version';
      Everything everything = await _testPluginPigeonPlugin.getEverything();
      aString = everything.aString ?? "";
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
      aString = "Failed to get aString.";
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _platformVersion = "Unknown";
      _aString = aString;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body:Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Center(
                child: Text('Running on: $_platformVersion\n'),
              ),
              Center(
                child: Text('asString: $_aString\n'),
              ),
            ]) ,
      ),
    );
  }
}

測試結(jié)果如圖:

wecom-temp-8fc64c83c5aa1aa6c0fe80cde4c66ae8.png

插件發(fā)布

  1. 上傳插件前湃缎,需要完善一些資料:
  • README.md介紹包的文件
  • CHANGELOG.md記錄每個(gè)版本中的更改
  • LICENSE包含軟件包許可條款的文件
  • pubspec.yaml的資料
  • 所有公共API的API文檔

pubspec.yaml,對(duì)Flutter插件來說蠢壹,pubspec.yaml里除了插件的依賴嗓违,還包含一些元信息,根據(jù)需要图贸,把這些補(bǔ)上

name: test_plugin_pigeon# 要發(fā)布的項(xiàng)目名稱
description: A new Flutter project. # 項(xiàng)目描述
version: 0.0.1 # 發(fā)布的版本
homepage: http://www.google.com/  # 項(xiàng)目主頁
publish_to: https://xxxxxx.com/  # 發(fā)布地址
email: "xxxxx.com" # 開發(fā)者郵箱
author: Wangjs  # 開發(fā)者
repository: "https:xxxxxxxxx" # 一般寫當(dāng)前插件源代碼的Github地址 
  1. 上傳前的需要清理插件蹂季,避免插件過大無法上傳:
flutter clean
  1. cd到插件目錄下冕广,執(zhí)行發(fā)布操作
flutter pub publish

可以使用以下命令來測試發(fā)布:flutter pub publish --dry-run --server="xxxx"

插件使用

插件使用方式:

  • pub
  • git
  • 私有pub庫
pub依賴

這種是最常見的方式,直接在工程的pubspec.yaml中寫上你需要的插件名和版本偿洁,之后執(zhí)行Pub get就行了撒汉。

dependencies:
  flutter:
    sdk: flutter
  xxxxxx: ^0.0.1 # 添加庫
git依賴

如果我們不想發(fā)布到pub,但又想團(tuán)隊(duì)共享插件涕滋,那么我們可以把庫上傳到git倉庫里面睬辐,然后在pubspec.yaml中配置,之后執(zhí)行Pub get就行了何吝。

dependencies:
  flutter:
    sdk: flutter
    
  nakiri:
    git:
      url: https://github.com/xxx/xxxx.git
      ref: tag/branch

url:git地址
ref:表示git引用溉委,可以是commit hash鹃唯,tag或者分支

私有pub倉庫依賴

一般而言爱榕,pub管理插件比git管理方便,所以一般大公司都會(huì)搭建自己的私有pub倉庫坡慌,依賴私有pub倉庫也很簡單黔酥,只需要在pubspec.yaml中配置完成后,之后執(zhí)行Pub get就行了洪橘。

dependencies:
  flutter:
    sdk: flutter

  xxxx: 
   hosted: 'https:xxxxxxxxx'
   version: '^0.0.1'
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末跪者,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子熄求,更是在濱河造成了極大的恐慌渣玲,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弟晚,死亡現(xiàn)場離奇詭異忘衍,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)卿城,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門枚钓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瑟押,你說我怎么就攤上這事搀捷。” “怎么了多望?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵嫩舟,是天一觀的道長。 經(jīng)常有香客問我怀偷,道長家厌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任枢纠,我火速辦了婚禮像街,結(jié)果婚禮上黎棠,老公的妹妹穿的比我還像新娘。我一直安慰自己镰绎,他們只是感情好脓斩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著畴栖,像睡著了一般随静。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吗讶,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天燎猛,我揣著相機(jī)與錄音,去河邊找鬼照皆。 笑死重绷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的膜毁。 我是一名探鬼主播昭卓,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瘟滨!你這毒婦竟也來了候醒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤杂瘸,失蹤者是張志新(化名)和其女友劉穎倒淫,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體败玉,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡敌土,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绒怨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纯赎。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖南蹂,靈堂內(nèi)的尸體忽然破棺而出犬金,到底是詐尸還是另有隱情,我是刑警寧澤六剥,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布晚顷,位于F島的核電站,受9級(jí)特大地震影響疗疟,放射性物質(zhì)發(fā)生泄漏该默。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一策彤、第九天 我趴在偏房一處隱蔽的房頂上張望栓袖。 院中可真熱鬧匣摘,春花似錦、人聲如沸裹刮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捧弃。三九已至赠叼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間违霞,已是汗流浹背嘴办。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留买鸽,地道東北人涧郊。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像癞谒,于是被迫代替她去往敵國和親底燎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子刃榨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容