Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutterから プラットフォーム固有のAPIを扱う

Flutterから プラットフォーム固有のAPIを扱う

Sota Hagiwara

October 18, 2022
Tweet

More Decks by Sota Hagiwara

Other Decks in Programming

Transcript

  1. Platform Channels • プラットフォーム固有のAPIを呼び出せるよ うにする仕組み ◦ Method Channel ▪ メソッドコール

    ◦ Event Channel ▪ イベントストリーム ◦ BasicMessageChannel • FlutterからPlatformChannelsを介して、ホ ストであるプラットフォームに呼び出しを送 信する • ホストは呼び出しを購読し、レスポンスを送 り返す • 呼び出しとレスポンスは非同期で行われる • これをもちいてたくさんのパッケージが開発 されている
  2. Method Channel(Flutter側) class BatteryState { // クライアント側とホスト側は、 MethodChannelのコンストラクタで渡されるチャネル名で接続される static const

    _platform = MethodChannel('samples.flutter.dev/battery'); Future<void> _getBatteryLevel() async { try { // methodを呼び出す。method名をStringで指定してあげる。 final result = await _platform.invokeMethod<int>('getBatteryLevel'); } on PlatformException catch (e) { // Stringで指定したメソッドが未定義などを場合エラーが throwされる } } }
  3. Method Channel(Android側) class MainActivity: FlutterActivity() { // チャネル名をFlutter側と揃えてあげる private val

    CHANNEL = "samples.flutter.dev/battery" override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> // TODO(次のページ) } } }
  4. Method Channel(Android側) when (call.method) { // call.methodにFlutter側で引数に渡したメソッド名が入っている "getBatteryLevel" -> {

    try { val batteryLevel = getBatteryLevel() result.success(batteryLevel) // 成功時は result.success } catch (throwable: Throwable) { result.error("1000", "Failed", null) // 失敗時は result.error } } else -> { result.notImplemented() // 未定義のメソッド名が入っていたときは result.notImplemented() } }
  5. Method Channel(iOS側) @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func

    application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController // チャネル名をFlutter側と揃えてあげる let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery", binaryMessenger: controller.binaryMessenger) batteryChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in // TODO: 次のページ }) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
  6. Method Channel(iOS側) switch call.method { case "getBatteryLevel": do { let

    batteryLevel = try getBatteryLevel() result(batteryLevel) } catch let error { result(FlutterError(...)) } break }
  7. flutter_blueのMethodChannelの扱い方 Flutter側のコード。プラグインのクラスでMethodChannelのインスタンスをそのまま作って 至って普通に使っている。 class FlutterBlue { // インスタンス生成 final MethodChannel

    _channel = const MethodChannel('$NAMESPACE/methods'); final EventChannel _stateChannel = const EventChannel('$NAMESPACE/state'); // invokeMethodしているところを一部抜粋 Future<bool> get isAvailable => _channel.invokeMethod('isAvailable').then<bool>((d) => d); Future<bool> get isOn => _channel.invokeMethod('isOn').then<bool>((d) => d);
  8. flutter_blueのMethodChannelの扱い方 Android側 @Override public void onMethodCall(MethodCall call, Result result) {

    ... switch (call.method) { ... case "isAvailable": { result.success(mBluetoothAdapter != null); break; } case "isOn": { result.success(mBluetoothAdapter.isEnabled()); break; }
  9. battery_plusのMethodChannelの扱い方(Flutter側) MethodChannel methodChannel = MethodChannel('dev.fluttercommunity.plus/battery'); @override Future<int> get batteryLevel =>

    methodChannel .invokeMethod<int>('getBatteryLevel') .then<int>((dynamic result) => result); @override Future<bool> get isInBatterySaveMode => methodChannel .invokeMethod<bool>('isInBatterySaveMode') .then<bool>((dynamic result) => result);
  10. battery_plusのパッケージ分割 Why split the various implementations across multiple packages rather

    than combining them all into a single package? There are a few reasons why this is better for the long-term maintainability and growth of a plugin: A plugin author does not need to have domain expertise for every supported Flutter platform (Android, iOS, Web, Mac OS, etc.). You can add support for a new platform without the original plugin author needing to review and pull in your code. Each package can be maintained and tested separately. Restructuring your plugin as a federated plugin allows anyone to implement support for new platforms without requiring you to do it yourself. For example, if Flutter supports Nintendo Switch in the future, then a Switch expert can add support for your plugin without you needing to learn all the new APIs. You can even vet the new Switch plugin, and if it meets your standards, you can make it an “endorsed implementation”, meaning that users of your plugin won’t even have to specifically depend on it in order to use it!
  11. federated pluginの実装をのぞき見 abstract class BatteryPlatform extends PlatformInterface { ... static

    BatteryPlatform _instance = MethodChannelBattery(); static BatteryPlatform get instance => _instance; static set instance(BatteryPlatform instance) { PlatformInterface.verifyToken(instance, _token); _instance = instance; } ~~~~~~~~~~~~~~~~~~~ Future<int> get batteryLevel { throw UnimplementedError('batteryLevel() has not been implemented.'); } ... }
  12. federated pluginの実装をのぞき見 class MethodChannelBattery extends BatteryPlatform { MethodChannel methodChannel =

    MethodChannel('dev.fluttercommunity.plus/battery'); @override Future<int> get batteryLevel => methodChannel .invokeMethod<int>('getBatteryLevel') .then<int>((dynamic result) => result);
  13. 自分で扱ってみた際の工夫(Android側で説明) // メソッドとメソッドの引数をsealed classで表現する. ここを見れば定義済みのメソッドがひと目でわかる  sealed class MethodChannelMethod { object

    Unknown : MethodChannelMethod() data class PrintLabel( val argumentRawLabelData: String, val argumentPrinterMacAddress: String, ) : MethodChannelMethod() object SearchPrinter : MethodChannelMethod() data class RetrieveStatus( val argumentPrinterMacAddress: String, ) : MethodChannelMethod() }
  14. 自分で扱ってみた際の工夫(Android側で説明) /// MethodChannelのcall(MethodCall)の拡張関数として /// 先程定義したsealed classへの変換メソッドを用意する fun MethodCall.toMethodChannelMethod(): MethodChannelMethod {

    return when (method) { "printLabel" -> { // 引数を取り出してdata classに埋め込む val argumentRawLabelData = argument<String>("rawLabelData")!! val argumentPrinterMacAddress = argument<String>("printerMacAddress")!! MethodChannelMethod.PrintLabel( argumentRawLabelData = argumentRawLabelData, argumentPrinterMacAddress = argumentPrinterMacAddress, ) } "searchPrinter" -> { ... } "retrieveStatus" -> { ... } else -> { ... } } }
  15. 自分で扱ってみた際の工夫(Android側で説明) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL_NAME).setMethodCallHandler { call, result -> when (val methodChannelMethod

    = call.toMethodChannelMethod()) { is MethodChannelMethod.PrintLabel -> { // このように引数が取得できて少し便利 methodChannelMethod.argumentRawLabelData } MethodChannelMethod.SearchPrinter -> {...} is MethodChannelMethod.RetrieveStatus -> { ... } is MethodChannelMethod.Unknown -> { ... } } }
  16. 自分で扱ってみた際の工夫(Android側で説明) // エラーもこんなかんじのsealed classに列挙しておくと可読性が上がる sealed class MethodChannelError( val errorCode: String,

    val errorMessage: String, val otherDetails: Map<String, Any>? = null, ) // こんなかんじの拡張関数はやしてあげる fun MethodChannel.Result.error(methodChannelError: MethodChannelError) = error( methodChannelError.errorCode, methodChannelError.errorMessage, methodChannelError.otherDetails, )
  17. プラットフォーム固有のAPIをFlutterから扱うときはこうす るとよさそうまとめ • 基本的にはプラグインとして作ってアプリで用いるほうが拡張性、汎用性ともに優れそ う ◦ federated pluginの型で作るのがよさそう • Method

    Channel, Event Channelに関して ◦ 可読性など気にせずシンプルにサンプルのような実装をする、でよさそう ◦ そもそもの使い方がシンプルなので特に手を加えなくても大丈夫そうなことがわ かった ◦ 可読性などを気にする場合は、工夫のしようもありそう