Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

ハギー(萩原爽太) @soprog_ 買物事業部 -> プラットフォーム戦略部 Android, Flutterエンジニア N高等学校在学中,22新卒 コーヒーにハマっています。飲みすぎてなかなか寝付けな い日々です。

Slide 3

Slide 3 text

今日話すこと ● Flutterフレームワークからプラットフォーム固有のAPIを扱う仕組みの概要について ● プラットフォーム固有のAPIを扱っているパッケージから実例を覗いてみた👀 ● 自分で扱ってみた際の工夫 ● 実践プラットフォーム固有のAPIをFlutterから扱うときはこうするとよさそうまとめ

Slide 4

Slide 4 text

Flutterフレームワークから プラットフォーム固有のAPIを扱う仕組み

Slide 5

Slide 5 text

Flutterフレームワークからプラットフォーム固有のAPIを扱 う仕組み ● Googleが開発しているクロスプラット フォームのアプリケーションフレーム ワーク ○ 単一のコードベースでiOS, Android, Web (beta: Windows, MacOS, Linux) に対応したアプリケー ションを構築できる ● Dart言語で記述 ● Flutter使ってますか?

Slide 6

Slide 6 text

Flutterフレームワークからプラットフォーム固有のAPIを扱 う仕組み Flutterからプラットフォーム固有のAPIを扱いたいユースケース ● e.g. ○ Flutterフレームワークから取得できない端末情報などの利用(位置情報, Bluetooth, etc..) ○ そのプラットフォーム固有のSDKしか提供されていない(Flutter向けSDKがない)

Slide 7

Slide 7 text

Flutterフレームワークからプラットフォーム固有のAPIを扱 う仕組み 👉 Platform Channelsという仕組みを用いると実現できる Flutterからプラットフォーム固有のAPIを扱いたいというユースケース ● e.g. ○ Flutterフレームワークから取得できない端末情報などの利用(位置情報, Bluetooth, etc..) ○ そのプラットフォーム固有のSDKしか提供されていない(Flutter向けSDKがない)

Slide 8

Slide 8 text

Platform Channels ● プラットフォーム固有のAPIを呼び出せるよ うにする仕組み ○ Method Channel ■ メソッドコール ○ Event Channel ■ イベントストリーム ○ BasicMessageChannel ● FlutterからPlatformChannelsを介して、ホ ストであるプラットフォームに呼び出しを送 信する ● ホストは呼び出しを購読し、レスポンスを送 り返す ● 呼び出しとレスポンスは非同期で行われる ● これをもちいてたくさんのパッケージが開発 されている

Slide 9

Slide 9 text

Method Channelを用いた実装(公式サンプルより) Flutter<-> iOS(Swift), Android(Kotlin)

Slide 10

Slide 10 text

Method Channel(Flutter側) class BatteryState { // クライアント側とホスト側は、 MethodChannelのコンストラクタで渡されるチャネル名で接続される static const _platform = MethodChannel('samples.flutter.dev/battery'); Future _getBatteryLevel() async { try { // methodを呼び出す。method名をStringで指定してあげる。 final result = await _platform.invokeMethod('getBatteryLevel'); } on PlatformException catch (e) { // Stringで指定したメソッドが未定義などを場合エラーが throwされる } } }

Slide 11

Slide 11 text

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(次のページ) } } }

Slide 12

Slide 12 text

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() } }

Slide 13

Slide 13 text

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) } }

Slide 14

Slide 14 text

Method Channel(iOS側) switch call.method { case "getBatteryLevel": do { let batteryLevel = try getBatteryLevel() result(batteryLevel) } catch let error { result(FlutterError(...)) } break }

Slide 15

Slide 15 text

プラットフォーム固有のAPIを扱っている Packageの実装を覗いてみた👀

Slide 16

Slide 16 text

プラットフォーム固有のAPIを扱っているPackageの実装を 覗く👀 ● flutter_blue https://pub.dev/packages/flutter_blue ○ iOS, Android, MacOSに対応 ● battery_plus(battery) https://pub.dev/packages/battery_plus ○ iOS, Android, Web, MacOS, Windows, Linuxに対応

Slide 17

Slide 17 text

flutter_blue 所感 ● 変わったことはしておらず、サンプルと同じような仕組みで愚直に実装されている

Slide 18

Slide 18 text

flutter_blueのMethodChannelの扱い方 Flutter側のコード。プラグインのクラスでMethodChannelのインスタンスをそのまま作って 至って普通に使っている。 class FlutterBlue { // インスタンス生成 final MethodChannel _channel = const MethodChannel('$NAMESPACE/methods'); final EventChannel _stateChannel = const EventChannel('$NAMESPACE/state'); // invokeMethodしているところを一部抜粋 Future get isAvailable => _channel.invokeMethod('isAvailable').then((d) => d); Future get isOn => _channel.invokeMethod('isOn').then((d) => d);

Slide 19

Slide 19 text

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; }

Slide 20

Slide 20 text

battery_plus ● Flutter側のMethodChannelからのメソッド呼び出し部分と、Android,iOSそれぞれ MethodChannelの実装はシンプルにサンプルと同じように実装されている ● MethodChannelの扱い自体はサンプル通りでシンプルだが、パッケージ自体ががいくつ かに分割されている

Slide 21

Slide 21 text

battery_plusのMethodChannelの扱い方(Flutter側) MethodChannel methodChannel = MethodChannel('dev.fluttercommunity.plus/battery'); @override Future get batteryLevel => methodChannel .invokeMethod('getBatteryLevel') .then((dynamic result) => result); @override Future get isInBatterySaveMode => methodChannel .invokeMethod('isInBatterySaveMode') .then((dynamic result) => result);

Slide 22

Slide 22 text

battery_plusのパッケージ分割 ● このようにパッケージが分かれている ● なぜ..? ○ https://medium.com/flutter/ho w-to-write-a-flutter-web-plugin -part-2-afdddb69ece6 ○ ここに答えがあった

Slide 23

Slide 23 text

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!

Slide 24

Slide 24 text

battery_plusのパッケージ分割 ● プラグインの保守性・発展性のため ● Flutterからどのような要求・応答がもとめられているかのインターフェースを用意して おき、そのインターフェースに従って実装できるような仕組みを作ればプラグインの保 守・新規プラットフォーム対応を作者以外でも行えるようになり保守性・発展性が上が る ● プラットフォーム固有のAPIを扱うプラグインはこの形式で実装しておくのがベターらし い e.g. ● もしFlutterがNintendo Switchをサポートしたとき、インターフェスに従えばNintendo Switchに詳しい人がプラグインのSwitchサポートを実装できる。

Slide 25

Slide 25 text

battery_plusのパッケージ分割 ● このプラグインの分割はfederated pluginと呼ばれる ○ https://flutter.dev/docs/development/packages-and-plugins/developing-pac kages#federated-plugins ○ 公式の解説、実装サンプルもあった

Slide 26

Slide 26 text

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 get batteryLevel { throw UnimplementedError('batteryLevel() has not been implemented.'); } ... }

Slide 27

Slide 27 text

federated pluginの実装をのぞき見 class MethodChannelBattery extends BatteryPlatform { MethodChannel methodChannel = MethodChannel('dev.fluttercommunity.plus/battery'); @override Future get batteryLevel => methodChannel .invokeMethod('getBatteryLevel') .then((dynamic result) => result);

Slide 28

Slide 28 text

自分で扱ってみたときの工夫

Slide 29

Slide 29 text

自分で扱ってみた際の工夫 ● ラベルプリンターのSDK(iOS, Android)とFlutterをつなぐ実装をした。  ● Method Channelを用いた。 ○ Flutter側は普通 ● ネイティブ側の実装をちょっとだけ工夫してみた ○ 👉

Slide 30

Slide 30 text

自分で扱ってみた際の工夫(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() }

Slide 31

Slide 31 text

自分で扱ってみた際の工夫(Android側で説明) /// MethodChannelのcall(MethodCall)の拡張関数として /// 先程定義したsealed classへの変換メソッドを用意する fun MethodCall.toMethodChannelMethod(): MethodChannelMethod { return when (method) { "printLabel" -> { // 引数を取り出してdata classに埋め込む val argumentRawLabelData = argument("rawLabelData")!! val argumentPrinterMacAddress = argument("printerMacAddress")!! MethodChannelMethod.PrintLabel( argumentRawLabelData = argumentRawLabelData, argumentPrinterMacAddress = argumentPrinterMacAddress, ) } "searchPrinter" -> { ... } "retrieveStatus" -> { ... } else -> { ... } } }

Slide 32

Slide 32 text

自分で扱ってみた際の工夫(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 -> { ... } } }

Slide 33

Slide 33 text

自分で扱ってみた際の工夫(Android側で説明) // エラーもこんなかんじのsealed classに列挙しておくと可読性が上がる sealed class MethodChannelError( val errorCode: String, val errorMessage: String, val otherDetails: Map? = null, ) // こんなかんじの拡張関数はやしてあげる fun MethodChannel.Result.error(methodChannelError: MethodChannelError) = error( methodChannelError.errorCode, methodChannelError.errorMessage, methodChannelError.otherDetails, )

Slide 34

Slide 34 text

まとめ

Slide 35

Slide 35 text

プラットフォーム固有のAPIをFlutterから扱うときはこうす るとよさそうまとめ ● 基本的にはプラグインとして作ってアプリで用いるほうが拡張性、汎用性ともに優れそ う ○ federated pluginの型で作るのがよさそう ● Method Channel, Event Channelに関して ○ 可読性など気にせずシンプルにサンプルのような実装をする、でよさそう ○ そもそもの使い方がシンプルなので特に手を加えなくても大丈夫そうなことがわ かった ○ 可読性などを気にする場合は、工夫のしようもありそう

Slide 36

Slide 36 text

ありがとうございました󰢛