FlutterでPigeonを使ったカスタムプラグインの作成方法

このチュートリアルでは、Pigeonパッケージを使用して、AndroidとiOSプラットフォームをターゲットとするFlutterプラグインを作成します。

FlutterはUIフレームワークなので、ネイティブプラットフォームと通信することはいつも必要とされるものではありませんし、必要があったとしても、すでに特定のユースケースでこれを行うたくさんのパッケージがあります。

しかし、ネイティブプラットフォームが提供する機能がpub.devで公開されているパッケージにまだない場合もあります。そのような場合、私たちの出番です!Flutter側でDartコードを書いてプラットフォームを呼び出し、アプリがサポートしているそれぞれのプラットフォームで必要な機能を呼び出すネイティブプラットフォームコードを書く必要があります!

プラットフォームチャネル

Flutterでネイティブプラットフォームと通信する方法の1つは、MethodChannelを使用することです。以前の記事では、Flutterでメソッドチャネルを使用してプラグインを構築する方法やネイティブプラットフォームコードを呼び出す方法について説明しました。

MethodChannelの使用は比較的簡単ですが、実際はかなり時間がかかることがあります。MethodChannelを介してメソッドを呼び出す際、intdoubleboolStringなどの単純な型や、そのような型を値とするマップやリストだけを引数として渡すことができます。対応しているデータ型の完全なリストはこちらで見ることができます。複雑なオブジェクトを引数として渡す必要がある場合は、これを例えばキーが文字列で値がキーのマップに解析するロジックが必要になります。また、ネイティブプラットフォームから返されるデータに対しても同様のことをする必要があります。

json_serializableのようなパッケージを使用して、データをJSONから解析したり、逆にすることで解析ロジックを導入することを回避し、自分たちの時間を節約することができます。ただし、ネイティブプラットフォームが期待する通りの形式でデータを返していなければ、およびその逆も同様の場合は、解析が失敗します。

Pigeon

Pigeonは、Flutterとホストプラットフォーム間の通信に必要なすべてのコードを生成するコードジェネレータパッケージです。あなたがすることはAPIを定義するだけです。これは便利です、なぜなら解析ロジックについて心配する必要がなく、通信が型安全であることが保証されるからです。

2022年7月10日現在、PigeonはAndroidとiOSのみをサポートしており、それぞれJavaとObjective-Cのコードを生成します(Swiftは実験的)。生成されたコードはKotlinやSwiftからもアクセスできます。また、C++を使用した実験的なWindowsサポートもあります。

この投稿では、Pigeonを使用してシンプルなプラグインを作成します。結果としてできるものは、私たちが以前にここで構築した(フェイクの)アプリ使用状況プラグインと同じものですので、結果を比較できます。

プラグインはシンプルで、すべてのアプリとその使用状況をリストで返し、特定のアプリに時間制限を設定する機能をサポートするものです。このチュートリアルの範囲外であるため、ネイティブ側でこの機能を実際に実装せず、代わりにネイティブ側からダミーデータを返します。

プラグインテンプレート

始めるにあたって、flutter createを使用してプラグインテンプレートを使ってFlutterプラグインを作成します。

flutter create --org dev.dartling --template=plugin --platforms=android,ios app_usage_pigeon

これにより、このプラグインを使用するプロジェクトの例としてプラグインのコードが生成されます。デフォルトでは、生成されたAndroidコードはKotlinで、iOSはSwiftで書かれていますが、それぞれ -a-i フラグを使用してJavaまたはObjective-Cを指定することができます(-a java および/または -i objc)。

プラグインテンプレートにはかなりの量のコードが含まれています。このチュートリアルの文脈では、次のことを知っていれば十分です:

Dart側には、以下の3つのクラスがあります:

  • AppUsagePlatform - プラグインの「インターフェース」/APIであり、PlatformInterfaceを実装します。
  • MethodChannelAppUsage - メソッドチャネルを使用したAppUsagePlatformの実装です。
  • AppUsage - プラグインを使用する必要があるアプリでメソッドを公開するクラスです。

生成されたKotlinコードはAppUsagePlugin.ktで、メソッドチャネルを使用しています。これはpubspec.yamlでAndroidプラットフォームのプラグインクラスとして定義されているため、後でいくつかの変更を加えることになりますが、まだ必要です。同じことがSwiftAppUsagePlugin.swiftおよびAppUsagePlugin.hAppUsagePlugin.mを含むSwiftコードにも当てはまります。

このチュートリアルでは、メソッドチャネルではなくPigeonを使用してAppUsagePlatformの実装を書きます。MethodChannelAppUsageは必要ありませんので削除します。

注:PlatformInterfaceを使用する新しいプラグインテンプレートでは、アプリで単にネイティブコードを呼び出したいだけであれば本当に必要ない可能性のあるかなりの量のコードを導入します。プラグインや別パッケージを作成せずにPigeonを使用することも可能ですが、このチュートリアルではそのように進めます。

pigeonパッケージのインストール

パッケージをインストールしましょう:

flutter pub add --dev pigeon

または、pubspec.yamlにこれを追加します:

dev_dependencies:
  pigeon: ^3.2.3

App Usage APIの定義

Pigeonの動作方法はとてもシンプルです;APIはlibフォルダの外にDartクラスとして定義します(Pigeonは開発依存関係です)。APIクラスは@HostApi()デコレータを持つ抽象クラスであるべきで、そのメソッドは@asyncデコレータを持つべきです。

pigeonsという新しいディレクトリ内でApp Usage APIを定義しましょう:

// pigeons/app_usage_api.dart
import 'package:pigeon/pigeon.dart';

enum State { success, error }

class StateResult {
  final State state;
  final String message;

  StateResult(this.state, this.message);
}

class UsedApp {
  final String id;
  final String name;
  final int minutesUsed;

  UsedApp(this.id, this.name, this.minutesUsed);
}

@HostApi()
abstract class AppUsageApi {
  @async
  String? getPlatformVersion();

  @async
  List<UsedApp> getApps();

  @async
  StateResult setAppTimeLimit(String appId, int minutesUsed);
}

警告と制約

APIの定義は比較的単純でしたが、言及するべきいくつかの点があります:

Futures

戻り値をFutureとして指定する必要はありませんが、生成されたコードにおいてはそれになります。つまりgetPlatformVersionは実際には生成されたDartコードでFuture<String?>を返します。

インポートは許可されません

package:pigeon/pigeon.dart以外のインポートは許可されていません。これは、すべてのモデルクラスはこのPigeon APIファイル内で定義されるべきであるということです。

サポートされるデータ型

前に述べたように、単純なJSONのような値のみがサポートされます。これは、DateTimeDurationのような有用なDart型を使用することができないことを意味します。これにより、アプリ内で使用するモデルにPigeonモデルを変換するための追加のマッピングロジックがまだ必要になるかもしれません。UsedAppminutesUsedの場合、分からDurationを手作業で作成する必要がありますが、最初からこれがDurationであればよかったと思います。

基本的な戻り値の型に対するenumはまだサポートされていません

メソッドからenumを返すことはできませんが、メソッドのパラメータとしてenumを持つことはできます。別のクラスでenumをラップしている限り、enumを返すことは可能です。

// 無効です。enumはリターンされることはできません。
enum ResultState { success, error }

@HostApi()
abstract class AppUsageApi {
  @async
  ResultState getState();
}
// 有効です。enumはメソッドのパラメータと戻り値のオブジェクトのフィールドであれば使用できます。
enum ResultState { success, error }

class ApiResult {
  final ResultState state;
  final String message;

  ApiResult(this.state, this.message);
}

@HostApi()
abstract class AppUsageApi {
  @async
  ApiResult getResult();

  @async
  void setState(ResultState state);
}

ジェネリックはサポートされていますが、null許容型でのみ使用できます

HostApiの定義で非nullとして定義しても、生成されたDartクラスでは代わりにList<Something?>となります。

コードの生成

ジェネレータを実行する

APIを定義した後、flutter pub run pigeonを使用してコードを生成できます。このコマンドにはかなりの引数が必要です。

flutter pub run pigeon \
  --input pigeons/app_usage_api.dart \
  --dart_out lib/app_usage_api.dart \
  --java_package "dev.dartling.app_usage" \
  --java_out android/src/main/java/dev/dartling/app_usage/AppUsage.java \
  --experimental_swift_out ios/Classes/AppUsage.swift

将来的に実行しやすくするために、これをpigeon.shファイルに保存します。

今はSwiftを実験的なサポートのあるものとして選択していますが、Objective-Cの場合は、experimental_swift_out引数を次の3つの引数に置き換えるだけです。

  --objc_header_out ios/Classes/AppUsageApi.h \
  --objc_source_out ios/Classes/AppUsageApi.m \
  --objc_prefix FLT

Dart

input引数はAPIを定義したファイルであり、dart_outはアプリで実際に使用するコードであるため、libフォルダ内である必要があります。

Java

java_packageは完全なパッケージ名であり、この場合はdev.dartling.app_usageであり、java_outは生成されるJavaファイルのパスです。

注意:生成されたJavaクラス名がPigeonのHostApiと同じでないようにしてください。この場合、生成されるJavaクラスはAppUsageであり、PigeonダートクラスのHostApi名に定義されたAppUsageApiインターフェースを含む公開AppUsageApiインターフェースを含みます。同じ名前を使用した場合(初めに私がしたように!)、名前の重複が原因でコンパイルが失敗します。

注意:プラグインテンプレートがこの場合のようにKotlinを使用している場合、src/mainの下にあるjava/dev/dartling/app_usageディレクトリを手動で作成する必要があります。プラグインテンプレートの一部として生成されるのはkotlin/dev/dartling/app_usageだけだからです。

Swift

experimental_swift_outは生成されるSwiftファイルのパスです。

Objective-C

objc_header_outobjc_source_outの引数はObjective-C側で生成されるファイルを決定し、objc_prefixはオプションで生成されるクラス名のプレフィックスを決定します。

生成されたコードを理解する

flutter pub run pigeonを実行した後にPigeonによって生成されるコードは、私たちが頻繁に見る必要があるものではありません。私たちが知っておく必要があることは、Javaクラスには私たちの実装クラスが実装すべきAppUsageApiインターフェースがあるということです;これはJavaまたはKotlinのいずれかで行うことができます。Objective-Cの場合、FLTプレフィックス(ジェネレータを実行する際の引数)に

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/yallurium/how-to-create-a-custom-plugin-in-flutter-with-pigeon-24nh