FlutterでPluginプロジェクトを作って実装してみた

FlutterでPluginを作る流れをやってみたのでご紹介します。
2018.09.24

大阪オフィスの山田です。あいぽんのショートカットアプリが楽しくて時間がみるみるうちに無くなります。FlutterでPluginを作る流れをやってみたのでご紹介します。(後述する理由により、公開はしてません)今回、サンプルとして、タイトルとメッセージを渡してネイティブのダイアログを表示するプラグインを作ってみます。こちらが公式のページです

実行画面

iOS

Android

開発環境

flutter doctor

[✓] Flutter (Channel master, v0.7.3-pre.24, on Mac OS X 10.13.6 17G65, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4)
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[✓] VS Code (version 1.27.2)
[✓] Connected devices (1 available)

Packageについて

Packageは、以下の2種類が存在します。

  • Dart package: Flutterフレームワークのみを使ったDartのみのPackage
  • Plugin package: ネイティブAPIを使ったPackage(今回はこちら)

Packageは最低限、以下のファイルを含みます。

  • pubspec.yaml: 名前やバージョンといったpackageのmetadataを記載します。
  • lib: packageのコードを含みます。最低限、<package-name>.dartファイルを含みます。

プロジェクトを作る

プロジェクトを作成する時に、--template=pluginを付与することで、plugin用のプロジェクトが作られます。

flutter create --org com.yamadaryo --template=plugin -i swift -a kotlin hogehoge

他のパラメータについての解説を載せておきます。

  • --org: organaizationを指定します。通常、Reverse domain nameで指定します
  • -i swift -a kotlin: デフォルトでプロジェクトのネイティブコードは、Objective-CとJavaで生成されますが、SwiftとKotlinを使いたい場合は、このオプションで指定します。

プロジェクトの設定

iOSの設定

Before editing the iOS platform code in Xcode, first make sure that the code has been built at least once

と公式ページに記載がありますので、Xcodeで開く前に一度、ビルドをします。こちらのコマンドでExampleプロジェクトのビルドを走らせます。

cd ./example
flutter build ios --no-codesign

ビルドした際にエラーが出ましたが、こちらのIssueで解決策が提示されていました。ios/Flutter/Debug.xcconfigFLUTTER_BUILD_MODE=debugの記述を追加します。

Androidの設定

AndroidもiOSと同様に

Before editing the Android platform code in Android Studio, first make sure that the code has been built at least once

と公式ページに記載がありますので、Android Studioで開く前に一度、ビルドをします。こちらのコマンドでExampleプロジェクトのビルドを走らせます。

cd ./example
flutter build apk

実装

以下の実装が必要になります。 - Flutter側の実装 - プラグインの実行モジュール - Example - iOS(MethodChannel) - Android(MethodChannel)

今回、ダイアログのタイトル文字列と、メッセージ文字列を渡しています。

Flutter側の実装

MethodChannelでの呼び出し

lib/<プラグイン名>.dartに実装します。

import 'dart:async';

import 'package:flutter/services.dart';

class PlatformOriginDialog {
  static const MethodChannel _channel =
      const MethodChannel('platform_origin_dialog');

  static Future<String> showDialog(String title, String message) async {
    final String result = await _channel.invokeMethod(
      'show_dialog',
      <String, dynamic>{
        'title': title,
        'message': message,
      });
    return result;
  }
}

Example

Pluginを動かすExampleもプロジェクトに含まれますので、example/lib/main.dartにExampleを実装します。

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

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

void main() => runApp(new MyApp());

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

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

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

  Future<void> initPlatformState() async {
    String dialogResult;
    try {
      dialogResult = await PlatformOriginDialog.showDialog("確認", "保存しますか?");
    } on PlatformException {
      dialogResult = 'Failed to show Dialog.';
    }

    // 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(() {
      _dialogResult = dialogResult;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(
          title: const Text('Plugin example app'),
        ),
        body: 
        Center(child: Column(children: <Widget>[
          new Text('Dialog result: $_dialogResult\n'),
          MaterialButton(
            child: Text("Button"),
            onPressed: () {
            initPlatformState();
          },)
        ],
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        ))
      ),
    );
  }
}

iOSの実装

iOS/Classes/SwiftPlatform<プラグイン名>.swiftに実装をしていきます。

public class SwiftPlatformOriginDialogPlugin: NSObject, FlutterPlugin {
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "platform_origin_dialog", binaryMessenger: registrar.messenger())
        let instance = SwiftPlatformOriginDialogPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
    }

    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        if call.method == "show_dialog" {
            let arguments = call.arguments as! [String: Any]
            guard let title = arguments["title"] as? String,
                let message = arguments["message"] as? String else {
                    result(FlutterError.init(code: "ArgumentError", message: "Required argument does not exist.", details: nil));
                    return
            }
            let alert: UIAlertController = UIAlertController(title: title, message: message, preferredStyle:  UIAlertControllerStyle.alert)
            let controller : FlutterViewController = UIApplication.shared.keyWindow?.rootViewController as! FlutterViewController;
            let defaultAction: UIAlertAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler:{
                (action: UIAlertAction!) -> Void in
                result("Tap OK")
            })
            let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler:{
                (action: UIAlertAction!) -> Void in
                result("Tap Cancel")
            })

            alert.addAction(cancelAction)
            alert.addAction(defaultAction)
            controller.present(alert, animated: true, completion: nil)
        }
    }
}

Androidの実装

android/src/main/kotlin/<ドメイン名>/<プラグイン名>Plugin.ktに実装をしていきます。

class PlatformOriginDialogPlugin(registrar: PluginRegistry.Registrar): MethodCallHandler {
  var registrar: Registrar = registrar

  companion object {
    @JvmStatic
    fun registerWith(registrar: Registrar): Unit {
      val channel = MethodChannel(registrar.messenger(), "platform_origin_dialog")
      channel.setMethodCallHandler(PlatformOriginDialogPlugin(registrar))
    }
  }

  override fun onMethodCall(call: MethodCall, result: Result): Unit {
    if (call.method.equals("show_dialog")) {
      val context = registrar.view().context
      val title = call.argument<String>("title")
      val message = call.argument<String>("message")
      AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog_Alert).apply {
        setTitle(title)
        setMessage(message)
        setPositiveButton("OK", DialogInterface.OnClickListener { _, _ ->
          result.success("Tap OK")
        })
        setNegativeButton("Cancel", DialogInterface.OnClickListener { _, _ ->
          result.success("Tap Cancel")
        })
        show()
      }
    }
  }
}

公開する

flutter packages pub publish --dry-runコマンドを実行し、不備がないかチェックします。問題がなければ、flutter packages pub publishで公開します。
一度公開すると削除はできないため、今回作ったテスト用Pluginは公開していません。詳細はこちら

最後に

久しぶりの投稿となってしまいました。アップデートも来ているようなので新しい部分も触っていこうと思います。

参考文献