Flutter プラグインの作成方法

「Flutter プラグインの作成方法」Codelab へようこそ。

プラグインとは

プラグインとは、アプリに機能を追加できるソフトウェアです。たとえば、デバイス上のカメラをモバイルアプリで操作したい場合に利用できます。プラグインは Flutter エコシステムの重要な要素です。まず、pub.dev に必要なプラグインがすでに存在するかどうかを確認してください。 Flutter SDK の作成者と Flutter コミュニティのメンバーは、多数のプラグインを作成し、それらを pub.dev に公開してコミュニティと共有しています。

特に、Flutter Favorite のパッケージとプラグインを確認する必要があります。Flutter の Favorite Favorites タグは、アプリの作成にあたり、最初に検討するプラグインを特定するためのものです。

Flutter を使うと、ユーザーはクロスプラットフォームの Dart ライブラリを簡単に操作できますが、プラットフォーム固有のコードを使って操作した方がよい場合もあります。たとえば、Dart ライブラリを作成していないデータベースと通信したい場合があります。Flutter には、プラットフォーム固有のコードと通信できるプラグインを作成するメカニズムが用意されています。また、pub.dev でプラグインを公開して、他のユーザーが使えるようにすることも可能です。

この Codelab では、iOS および Android 向けに独自のプラグインを作成する方法を学習します。ホスト プラットフォーム上で音声を処理する簡単な音楽プラグインを実装してから、プラグインを使って音楽キーボードを作成するサンプルアプリを作成します。

完成したアプリのスクリーンショットは次のようになります。

f4275505c0be0bd7.png 746b8f48aa63e2ff.png

学習内容

  • iOS および Android 向け Flutter プラグインの作成方法
  • プラグインの API を作成する方法。
  • プラグインを使うアプリの作成方法。
  • 他のユーザーが使えるようにプラグインを公開する方法。

この Codelab で学びたいことは次のどれですか?

このトピックは初めて受講するので、簡単に概要を知りたい。このトピックについてはある程度知っているが、復習したい。プロジェクトで使用するサンプルコードを確認したい。特定の項目に関する説明を確認したい。

このラボを完了するには、Flutter SDKエディタの 2 つのソフトウェアが必要です。Android Studio や IntelliJ(Flutter プラグインや Dart プラグインがインストールされている)、Visual Studio Code(Dart Code と Flutter の拡張機能を備えている)などの任意のエディタを使用できます。

プラグイン開発のための一部のツールが変更されました。この Codelab では、Flutter SDK v1.15.19 以降を使用することを前提としています。バージョンは次のコマンドで確認できます。

$ flutter doctor

この Codelab は、次のいずれかのデバイスを使って実行できます。

  • パソコンに接続され、デベロッパー モードに設定されている iOS 物理デバイスまたは Android 物理デバイス
  • iOS シミュレータ(Xcode ツールのインストールが必要)
  • Android Emulator(Android Studio でセットアップが必要)

Flutter にはプラグインに適したテンプレートが用意されており、簡単に開始できます。プラグインのテンプレートを生成するときに、使用する言語を指定します。デフォルトでは、iOS の場合が Swift、Android の場合が Kotlin になっています。この Codelab では、Objective-C と Java を使用します。

作業ディレクトリで次のコマンドを実行して、プラグイン テンプレートを作成し、null 安全に移行します。

$ flutter create --template=plugin --org com.example --template=plugin --platforms=android,ios -a java -i objc plugin_codelab
$ cd plugin_codelab
$ dart migrate --apply-changes
$ cd example
$ flutter pub upgrade
$ dart migrate --apply-changes

これらのコマンドで、次のディレクトリ構造が生成されます。

plugin_codelab
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── local.properties
│   ├── plugin_codelab_android.iml
│   ├── settings.gradle
│   └── src
│       └── main
│           ├── AndroidManifest.xml
│           └── java
│               └── com
│                   └── example
│                       └── plugin_codelab
│                           └── PluginCodelabPlugin.java
├── example
│   ├── README.md
│   ├── android
│   ├── build
│   │   └── ios
│   │       └── Runner.build
│   │           └── Release-iphoneos
│   │               └── Runner.build
│   │                   └── dgph
│   ├── ios
│   ├── lib
│   │   └── main.dart
│   ├── plugin_codelab_example.iml
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   └── test
│       └── widget_test.dart
├── ios
│   ├── Assets
│   ├── Classes
│   │   ├── PluginCodelabPlugin.h
│   │   └── PluginCodelabPlugin.m
│   └── plugin_codelab.podspec
├── lib
│   └── plugin_codelab.dart
├── plugin_codelab.iml
├── pubspec.lock
├── pubspec.yaml
└── test
    └── plugin_codelab_test.dart

次に、重要なファイルについて説明します。

  • pubspec.yaml - プラグインを定義する YAML ファイル。プラグインの名前、依存関係、バージョン、サポートされているオペレーティング システムなどを指定します。プラグインの pub.dev ページで使用されます。
  • CHANGELOG.md - プラグインの新しいバージョンを公開するたびに、新しいバージョンでの変更がわかるように、このマークダウン ファイルを更新する必要があります。
  • README.md - このマークダウン ファイルは、プラグインの pub.dev リスティングの最初のページに表示されます。このファイルは、プラグインの定義とその使用方法を詳しく説明しています。
  • lib/plugin_codelab.dart - プラグインにフロントエンドを実装する Dart コード。プラグイン クライアントは、このディレクトリ内のパブリック クラスと関数にアクセスできます。
  • android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java - plugin_codelab.dart で説明されている Android 機能を実装するネイティブ Java コード。
  • ios/Classes/PluginCodelabPlugin.m - plugin_codelab.dart. で説明されている iOS 機能を実装する Objective-C コード(対応するヘッダー ファイルもあります)。
  • example/ - このディレクトリにはプラグインのクライアントが含まれています。プラグインの開発中に、このファイルを編集してプラグインの動作を確認します。
  • example/lib/main.dart - プラグインを実行する Dart コード。ここでサンプルの UI を作成します。

次の手順に沿って、iOS または Android デバイスでサンプルを実行します。

$ cd plugin_codelab/example
$ flutter run

次のように表示されます。

52b7d03a33b9cbfa.png

プラグインのフロントエンド用に生成された次のコードを確認します。

lib/plugin_codelab.dart

class PluginCodelab {
  static const MethodChannel _channel =
      const MethodChannel('plugin_codelab');

  static Future<String> get platformVersion async {
    final String version =
      await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

cf1e10b838bf60ee.png 確認内容

  • PluginCodelab は、プラグインのユーザーが呼び出すクラスです。
  • このクラスは、MethodChannel を作成し、Dart コードがホスト プラットフォームと通信できるようにします。
  • このプラグインの API にはプロパティ ゲッター platformVersion というメソッドが 1 つだけあります。誰かが Dart でこのゲッターを呼び出すと、MethodChannelgetPlatformVersion() メソッドを呼び出し、String が返されるまで非同期的に待機します。
  • getPlatformVersion メッセージの内容の解釈は、プラットフォーム固有のコードにより異なります(これについては後で説明します)。

example/lib/main.dart

Future<void> initPlatformState() async {
  String platformVersion;
  // Platform messages may fail, so we use a try/catch PlatformException.
  try {
    platformVersion = await PluginCodelab.platformVersion;
  } on PlatformException {
    platformVersion = 'Failed to get platform version.';
  }

  // 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 = platformVersion;
  });
}

cf1e10b838bf60ee.png 確認内容

  • 上記のコードはプラグインのクライアントです。
  • このコードは、lib/plugin_codelab.dart で定義されたゲッターを呼び出します。
  • 呼び出しは try-block でラップされています。iOS のプラットフォーム固有のコードが FlutterError を返すか、Java で例外をスローした場合、これらのエラーや例外は Dart 側で再表示されます。

ios/Classes/PluginCodelabPlugin.m

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  FlutterMethodChannel* channel = [FlutterMethodChannel
      methodChannelWithName:@"plugin_codelab"
            binaryMessenger:[registrar messenger]];
  PluginCodelabPlugin* instance = [[PluginCodelabPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

cf1e10b838bf60ee.png 確認内容

  • この初期化コードは、新しいエンジンが設定されるときに呼び出されます。
  • このコードは、プラグインと通信するためのチャネルを生成します。
  • ここで指定するチャネル名は、lib/plugin_codelab.dart で定義した名前と一致させる必要があります。
  • methodCallDelegate として自身を設定すると、作成されたインスタンスは、指定したバイナリ メッセンジャーに関連付けられているメッセージを受信します。

ios/Classes/PluginCodelabPlugin.m

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else {
    result(FlutterMethodNotImplemented);
  }
}

cf1e10b838bf60ee.png 確認内容

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
  channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "plugin_codelab");
  channel.setMethodCallHandler(this);
}

cf1e10b838bf60ee.png 確認内容

  • この Java コードは Android 用に getPlatformVersion() を実装します。

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  if (call.method.equals("getPlatformVersion")) {
    result.success("Android " + android.os.Build.VERSION.RELEASE);
  } else {
    result.notImplemented();
  }
}

cf1e10b838bf60ee.png 確認内容

  • この Java コードは、Dart から送信されたメッセージを処理します。このコードは iOS プラグインと類似していますが、わずかな違いがあります。

72ad6e60941a67c6.png次のようなコンテンツができあがりました。

次に、プラットフォーム固有の実装を用意しました。キーボードの鍵を操作するとシンセサイザーの音が鳴るようにします。このコードは、Flutter に表示するライブラリと考えることができます。通常、プラグインを作成するときは、この例のようにプラット フォームの API がすでに定義されていて、そこから作業を行うことになります。

これで、同じ機能を iOS 用と Android 用に別々に実装したことになります。プラグインからアプリに呼び出せるように、これらの実装をアプリの一部としてコンパイルする必要があります。

iOS に追加する

次のファイルをプロジェクトに追加します。

これらのファイルを ios/Classes に配置すると、プラグイン用の iOS ビルドの一部としてコンパイルされます。ios/plugin_codelab.podspec を見ると、デフォルトで glob を使って、コンパイルするソースを定義していることがわかります。必要な作業は、ファイルを必要な場所に配置するだけです。

Android に追加する

次のファイルをプロジェクトに追加します。

android/src/main/java/com/example/plugin_codelab/Synth.java

この Java ファイルを android/src/main/java/com/example に配置すると、プラグインの Android ビルドの一部としてコンパイルされます。Gradle ビルドシステムを見ると、このファイルを適切なディレクトリに配置するだけで、コンパイルされることがわかります。

シンセサイザー インターフェースの説明

シンセサイザー インターフェースは iOS と Android でほぼ同じであり、次の 4 つのメソッドで構成されています。

class Synthesizer {
  void start();
  void stop();
  int keyDown(int key);
  int keyUp(int key);
}

keyUp() メソッドと keyDown() メソッドは、音楽キーボードの鍵が押されたか離されたときに送信されるイベントを表します。key 引数は、鍵が押されたか離されたかを表しており、音楽キーボードのすべての鍵が列挙されます。MIDI 標準では、これらの鍵の列挙について定義されています。60 は 中央ハの値で、黒鍵または白鍵ごとに 1 つずつ増えます(半音)をご覧ください)。プラグインではこの定義が使用されます。

プラグイン作成の次のステップでは、Flutter とホスト プラットフォームの間で送受信する情報の種類を検討する必要があります。すでに API が定義されているライブラリを表す場合は、そのインターフェースを模倣してワークロードを減らすことができます。

この Codelab では、各プラットフォームごとにシンセサイザー コードが提供されるため、Dart コードでそのインターフェースを模倣できます。

lib/plugin_codelab.dart

import 'dart:async';

import 'package:flutter/services.dart';

class PluginCodelab {
  static const MethodChannel _channel = const MethodChannel('plugin_codelab');

  static Future<String?> get platformVersion async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  static Future<int?> onKeyDown(int key) async {
    final int? numNotesOn = await _channel.invokeMethod('onKeyDown', [key]);
    return numNotesOn;
  }

  static Future<int?> onKeyUp(int key) async {
    final int? numNotesOn = await _channel.invokeMethod('onKeyUp', [key]);
    return numNotesOn;
  }
}

invokeMethod() の 2 番目のパラメータは、メソッド呼び出しに送信されるパラメータの一覧です。

これで、サウンド生成用のプラットフォーム固有のライブラリと、そのコードを制御する Dart コードが作成できましたが、これらは接続されていません。これらの Dart メソッドのいずれかを呼び出すと、プラグインにホスト側が実装されていないため、「未実装」の例外が発生します。これを次のステップで確認します。

iOS で接続を確立する

まず、シンセサイザー インスタンスを作成して起動するようにプラグインを変更します。

ios/Classes/PluginCodelabPlugin.m

@implementation PluginCodelabPlugin {
  int _numKeysDown;
  FLRSynthRef _synth;
}
- (instancetype)init {
  self = [super init];
  if (self) {
    _synth = FLRSynthCreate();
    FLRSynthStart(_synth);
  }
  return self;
}

- (void)dealloc {
  FLRSynthDestroy(_synth);
}

次に、チャネルを介して送信されたメッセージの処理を開始します。

- (void)handleMethodCall:(FlutterMethodCall *)call
                  result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS "
        stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else if ([@"onKeyDown" isEqualToString:call.method]) {
    FLRSynthKeyDown(_synth, [call.arguments[0] intValue]);
    _numKeysDown += 1;
    result(@(_numKeysDown));
  } else if ([@"onKeyUp" isEqualToString:call.method]) {
    FLRSynthKeyUp(_synth, [call.arguments[0] intValue]);

    _numKeysDown -= 1;
    result(@(_numKeysDown));
  } else {
    result(FlutterMethodNotImplemented);
  }
}

このコードでは、onKeyDown メッセージと onKeyUp メッセージも検索するようになりました。key 引数を取得するには、call.arguments から取り出してください。戻り値は NSNumber としてボックス化されているため(プラットフォーム チャネルのドキュメントに記載されています)、intValue を使ってこの値を変換します。

完成したファイル PluginCodelabPlugin.m をご覧ください。

Android で接続を確立する

まず、シンセサイザー インスタンスを作成して起動するようにプラグインを変更します。

android/src/main/java/com/example/plugin_codelab/PluginCodelabPlugin.java

public class PluginCodelabPlugin implements FlutterPlugin, MethodCallHandler {
  private MethodChannel channel;
  private Synth synth;
  private static final String channelName = "plugin_codelab";

  private static void setup(PluginCodelabPlugin plugin, BinaryMessenger binaryMessenger) {
    plugin.channel = new MethodChannel(binaryMessenger, channelName);
    plugin.channel.setMethodCallHandler(plugin);
    plugin.synth = new Synth();
    plugin.synth.start();
  }

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    setup(this, flutterPluginBinding.getBinaryMessenger());
  }

次に、チャネルを介して送信されたメッセージの処理を開始します。

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  if (call.method.equals("getPlatformVersion")) {
    result.success("Android " + android.os.Build.VERSION.RELEASE);
  } else if (call.method.equals("onKeyDown")) {
    try {
      ArrayList arguments = (ArrayList) call.arguments;
      int numKeysDown = synth.keyDown((Integer) arguments.get(0));
      result.success(numKeysDown);
    } catch (Exception ex) {
      result.error("1", ex.getMessage(), ex.getStackTrace());
    }
  } else if (call.method.equals("onKeyUp")) {
    try {
      ArrayList arguments = (ArrayList) call.arguments;
      int numKeysDown = synth.keyUp((Integer) arguments.get(0));
      result.success(numKeysDown);
    } catch (Exception ex) {
      result.error("1", ex.getMessage(), ex.getStackTrace());
    }
  } else {
    result.notImplemented();
  }
}

iOS と同様に、このコードでは onKeyDown メッセージと onKeyUp メッセージを検索するようになりました。arguments.get() を使って、key 値もここに抽出します。Android で、発生する可能性のある例外をプラグインがすべて処理することを確認してください。

完成したファイル PluginCodelabPlugin.java をご覧ください。

この時点で、プラグインはすべての接続を実装しました。プラグインの実際の動作を確認してみましょう。そのためには、シンプルなキーボード UI サンプルアプリを実装します。

example/lib/main.dart

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

import 'package:flutter/services.dart';
import 'package:plugin_codelab/plugin_codelab.dart';

enum _KeyType { Black, White }

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight])
      .then((_) {
    runApp(new MyApp());
  });
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String? _platformVersion = 'Unknown';

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

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String? platformVersion;
    try {
      platformVersion = await PluginCodelab.platformVersion;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  void _onKeyDown(int key) {
    print("key down:$key");
    PluginCodelab.onKeyDown(key).then((value) => print(value));
  }

  void _onKeyUp(int key) {
    print("key up:$key");
    PluginCodelab.onKeyUp(key).then((value) => print(value));
  }

  Widget _makeKey({@required _KeyType keyType, @required int key}) {
    return AnimatedContainer(
      height: 200,
      width: 44,
      duration: Duration(seconds: 2),
      curve: Curves.easeIn,
      child: Material(
        color: keyType == _KeyType.White
            ? Colors.white
            : Color.fromARGB(255, 60, 60, 80),
        child: InkWell(
          onTap: () => _onKeyUp(key),
          onTapDown: (details) => _onKeyDown(key),
          onTapCancel: () => _onKeyUp(key),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Color.fromARGB(255, 250, 30, 0),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Text('Running on: $_platformVersion\n'),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  _makeKey(keyType: _KeyType.White, key: 60),
                  _makeKey(keyType: _KeyType.Black, key: 61),
                  _makeKey(keyType: _KeyType.White, key: 62),
                  _makeKey(keyType: _KeyType.Black, key: 63),
                  _makeKey(keyType: _KeyType.White, key: 64),
                  _makeKey(keyType: _KeyType.White, key: 65),
                  _makeKey(keyType: _KeyType.Black, key: 66),
                  _makeKey(keyType: _KeyType.White, key: 67),
                  _makeKey(keyType: _KeyType.Black, key: 68),
                  _makeKey(keyType: _KeyType.White, key: 69),
                  _makeKey(keyType: _KeyType.Black, key: 70),
                  _makeKey(keyType: _KeyType.White, key: 71),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

次の点に注意してください。

  • プラグインを使用するには、'package:plugin_codelab/plugin_codelab.dart' をインポートする必要があります。プラグイン上のサンプルの依存関係は、この処理を行う example/pubspec.yaml, で定義されています。
  • main(), では、キーボード全体が画面に表示されるように画面の向きが横向きに固定されます。
  • _onKeyDown() メソッドと _onKeyUp() メソッドはいずれも、前の手順で設計したプラグイン API のクライアントです。
  • このコードでは、InkWell(インタラクティブな長方形)を使って個々の鍵を描画します。

アプリを実行して、音楽キーボードが正常に動作するか確認します。

cd example
flutter run

次のような画面が表示されます。

f4275505c0be0bd7.png

これで、iOS 用と Android 用の Flutter プラグインを作成し、便利な音楽キーボードを楽しめるようになりました。完成したプロジェクトは https://github.com/flutter/codelabs/tree/master/plugin_codelab からダウンロードして比較できます。

次の手順

  • エンドツーエンド テストを追加します。Flutter チームには、e2e というエンドツーエンドの統合テストを作成するためのライブラリが用意されています。
  • pub.dev に公開します。作成したプラグインは、他のユーザーが使用できるよう、オンラインで共有できます。プラグインを pub.dev に公開する方法に関するドキュメントの全文については、プラグイン パッケージの開発をご覧ください。

シンセサイザーを拡張する

シンセサイザーを実際に操作し、改善して楽しみたい場合は、次のステップをご検討ください。

  • 現在、シンセサイザーは正弦波を生成しています。のこぎり波 が発生するとどうなりますか?
  • 鍵を押すか離すと、ポンと音がするのに気付きましたか?これは、オシレーターが突然オンまたはオフになっているためです。一般に、シンセサイザーは振幅エンベロープを使用します。
  • 現時点では、一度に押すことができる鍵は 1 つだけです。これは単旋律と呼ばれます。本物のピアノは単旋律です。