Developing Flutter Plugins

November 12, 2018 Yuuki Nishiyama 0 Comments

AWARE用のFlutterのプラグインを開発する機会があったのでメモ。Flutterの開発環境が整った状態を想定して記述する。また、この記事の執筆時点では、Flutterバージョンは0.9.7-pre.26、Dartは2.1.0を用いて、macOS10.14上のAndroid StudioとXcodeで開発。

Flutterプラグインのテンプレート作成

まずは、flutterのコマンドを使ってテンプレートを作成

flutter create --org com.aware.flutter.sensor.sample --template=plugin -i swift -a kotlin aware_sensor_sample

NOTE: アプリ名にドット(.)を含めることはできない。Flutter Packagesでアプリ名を検索すると、小文字+アンダースコア(_)で構成されているので、命名規則はそれに従う。

テンプレートを生成すると、以下のファイル群が生成される。

CHANGELOG.md
[アプリ名]/lib
[アプリ名]/ios
[アプリ名]/android
[アプリ名]/example
[アプリ名]_android.iml
[アプリ名].iml
LICENSE
pubspec.yaml
README.md

Method & Event チャンネル作成

[アプリ名]/lib にライブラリの肝になる、Flutterのソースコード([アプリ名].dart)が生成されている。そこから繋がるiOSのソースコードは[アプリ名]/ios に 、Androidのソースコードは[アプリ名]/android に生成されている。最終的に開発したプラグインを動作させるサンプルアプリケーションのソースコードは[アプリ名]/example にある。

次にFlutter側のソースコード([アプリ名]/lib/[アプリ名].dart )を編集する。基本的な雛形はできているので、それに沿って開発する。詳細はFlutterの公式ページ(リンク)を参照。

Flutterでは、フロントエンドから、バックエンド(iOS&Android)のメソッドを使うには、プラグイン内でチャンネルを作り、そのチャンネル経由で各種iOS&Android側のメソッド呼び出す。これより、フロントエンドからは同じメソッドを使って異なるプラットフォーム(iOS & Android)のメソッドへのアクセスを可能にしている。その為、まずプラグイン上ではこれらチャンネルを実装する。

チャンネルには、メソッド呼び出し時に発火するMethodChannel と、イベント発生時に発火するEventChannelの二つがある。例えば、フロントエンドから加速度センサの設定の取得や変更を行う場合はMethodChannel が使う。一方で、加速度データ値の変化が発生した時など、バックエンドから値をフロントエンドに伝えたい場合などには、EventChannelを活用する。

MethodChannelの実装例

任意の文字列を使ってMethodChannelを生成するし、必要なメソッドを実装する。下記のサンプルコードでは、プラットフォームのバージョンを取得するメソッド、センサを起動するメソッドが記述されてる。

static const MethodChannel _channel = const MethodChannel('com_aware_flutter_sensor_sample/method');
// String側の戻り値が必要な場合は、Future<String>を使う
// Methodチャンネルにアクセスするには、.invokeMethod('[メソッド名]')を使う。
// バックエンドでは、[メソッド名]を使って、どのメソッドが呼ばれたかを判断し値を返す。
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
// Map<String,dynamic>型の設定値を用いて、センサを起動する。
// 戻り値が無い場合は、Future<Null>使う
Future<Null> start(Map<String,dynamic> config) async {
try {
// バックエンド側に値(センサの設定値など)を渡す場合は、第二引数に値を入れる。
await _channel.invokeMethod('start', config);
} on PlatformException catch (e) {
print(e.message);
}
}

EventChannelの実装例

MethodChannel同様にEventChannelを生成する。フロントエンド側がonChangedメソッドを実装することで、イベント発生時にonChangedメソッドが呼び出される。

static const EventChannel _stream = const EventChannel('com_aware_flutter_sensor_sample/stream');
Stream<Map<String,dynamic>> _onChanged;
Stream<Map<String,dynamic>> onChanged() {
if (_onChanged == null) {
_onChanged = _stream
.receiveBroadcastStream(['on_changed'])
.map<Map<String,dynamic>>(
(element) => element.cast<String, dynamic>());
}
return _onChanged;
}

プラグイン側の実装は以上。

iOSバックエンドの実装

iOS側のソースコード([アプリ名]/ios/Classes/Swift[アプリ名]Plugin.swift)をXcodeで開く。

EventChannel が必要な場合は、FlutterStreamhandler をクラスに継承し、onListen(arguments:events:) と onCancel(arguments:) を実装する。

public class SwiftComAwareFlutterSensorSamplePlugin:NSObject, FlutterPlugin,FlutterStreamHandler{
// codes
}

次に、MethodChannelとEventChannelを登録する。

public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "com_aware_flutter_sensor_sample/method", binaryMessenger: registrar.messenger())
let stream = FlutterEventChannel(name: "com_aware_flutter_sensor_sample/stream", binaryMessenger: registrar.messenger())
let instance = SwiftComAwareFlutterSensorDevicePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
stream.setStreamHandler(instance)
}

MethodChannelが呼ばれた場合は、handle(call:result:) が呼ばれる。引数としてFlutterMethodCallFlutterResult が渡されるので、FlutterMethodCallのmethod(String型)をチェックし、呼ばれたメソッド毎に処理を切り替える。

public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
 if call.method == "getPlatformVersion" {
  result("iOS " + UIDevice.current.systemVersion)
 } else if call.method == "start" {
  print("start")
 }
}

EventChannelが呼ばれた場合は、イベントモニタ開始時にはonListen(arguments:events:)が、イベントモニタ終了時にはonCancel(arguments:)が呼ばれる。onListenでは、イベント発生時に値を返す為の、FlutterEventSink変数をクラスに保存し、イベント発生時にその変数を通じてフロントエンドに値を返す。

var sinkOnChanged: FlutterEventSink?
var listeningOnChanged = false
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
 if let args = arguments as? Array<Any> {
  if args.count > 0 {
   if let eventName = args[0] as? String {
    if eventName == "on_changed" {
     self.sinkOnChanged = events
     listeningOnChanged = true
    }
   }
  }
 }
 return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
 if let args = arguments as? Array<Any> {
  if args.count > 0 {
   if let eventName = args[0] as? String {
    if eventName == "on_changed" {
     listeningOnChanged = false
    }
   }
  }
 }
 return nil
}
public func onChanged(data: Data) {
 if listeningOnChanged {
  if let sink = self.sinkOnChanged {
   sink(data.toDictionary())
  }
 }
}

iOS側バックエンドの開発は以上。Android側も同様に実装する。依存するライブラリ等がある場合は、[アプリ名]/ios/[アプリ名].podspecに追加する。記述方式はCocoapodsライブラリ作成時と同じなので過去の投稿を参考(過去投稿へはこちら)。

サンプルアプリの実装

最後に、開発したプラグインをサンプルアプリ上で動作させる。 [アプリ名]/example/lib/main.dart をAndroid Studioで開き、プラグインを呼び出す。

class _MyAppState extends State<MyApp> {
AwareSampleSensor _sampleSensor = new AwareSampleSensor()
StreamSubscription<Map<String,dynamic>> _onChangedSubscription;
Map<String, dynamic> _result;
String error;
@override
void initState() {
super.initState();
_onChangedSubscription = _sampleSensor.onDeviceChanged().listen((Map<String,dynamic> result){
  setState(() {
  _result = result;
  });
});
_sampleSensor.start(null);
}
@override
Widget build(BuildContext context) {
List<Widget> widgets;
if (widgets == null) {
widgets = new List();
}
 
 AwareDevice.platformVersion;
widgets.add(new Center(
child: new Text(_result != null
? 'Result: $_result\n'
: 'Error: $error\n')));
return new MaterialApp(
home: new Scaffold(
body: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: widgets,
),
),
);
}
}

サンプルコードは以上。

参考資料

https://flutter.io/docs/development/packages-and-plugins/developing-packages

+1

Leave a Reply:

Your email address will not be published. Required fields are marked *