PS:多くの状況において、80% の既知の効果は 20% の可能な原因に起因しています。
前の数記事では、Navigator コンポーネント、Flex レイアウト、画像の読み込み、Widget のライフサイクル、混合開発など、Flutter 開発の基本知識を紹介しました。記事は以下の通りです:
- Flutter シリーズの Navigator コンポーネント使用
- Flutter シリーズの Flex レイアウト詳解
- Flutter シリーズの画像読み込み詳解
- Flutter シリーズの Widget ライフサイクル
- Flutter シリーズの混合開発 Android 編
次に、Flutter 混合開発における Platform Channel の使用について紹介します。主な内容は以下の通りです:
- プラットフォームチャネルの紹介
- プラットフォームデータ型の対照
- BasicMessageChannel
- MethodChannel
- EventChannel
プラットフォームチャネルの紹介#
Platform Channel は非同期メッセージチャネルで、メッセージは送信前にバイナリメッセージにエンコードされ、受信したバイナリメッセージは Dart 値にデコードされます。伝達されるメッセージの型は、対応するデコーダーがサポートする値のみであり、すべてのデコーダーは空のメッセージをサポートしています。Native と Flutter の通信アーキテクチャは以下の図の通りです:
Flutter では、3 種類の異なる PlatformChannel が定義されており、主に以下の 3 つがあります:
- BasicMessageChannel:データ伝達に使用;
- MethodChannel:メソッド呼び出しの伝達に使用;
- EventChannel:イベントの伝達に使用;
そのコンストラクタは、チャネル識別子、デコーダー、および BinaryMessenger を指定する必要があります。BinaryMessenger は Flutter とプラットフォームの通信ツールで、バイナリデータの伝達、対応するメッセージハンドラーの設定などに使用されます。
デコーダーには MethodCodec と MessageCodec の 2 種類があり、前者はメソッドに、後者はメッセージに対応します。BasicMessageChannel は MessageCodec を使用し、MethodChannel と EventChannel は MethodCodec を使用します。
プラットフォームデータ型の対照#
Platform Channel は異なるメッセージデコードメカニズムを提供します。例えば、StandardMessageCodec は基本データ型のデコードを提供し、JSONMessageCodec は Json のデコードをサポートします。プラットフォーム間で通信する際には自動的に変換されます。各プラットフォームのデータ型対照は以下の通りです:
BasicMessageChannel#
BasicMessageChannel は主にデータ伝達に使用され、バイナリデータを含みます。BasicMessageChannel を使用することで、MethodChannel と EventChannel の機能を実現できます。ここでは、BasicMessageChannel を使用して Android プロジェクトで Flutter リソースファイルを使用するケースを示します。重要なプロセスは以下の通りです:
- Flutter 側で画像リソースに対応するバイナリデータを取得します。ここでは BinaryCodec を使用し、データ形式は ByteData です;
- BasicMessageChannel を使用して画像に対応するデータを送信します;
- Android 側で ByteBuffer を使用して受信し、ByteArray に変換して Bitmap として表示します。
Flutter 側の重要なコードは以下の通りです:
// BasicMessageChannelを作成
_basicMessageChannel = BasicMessageChannel<ByteData>("com.manu.image", BinaryCodec());
// assets内の画像に対応するByteDataデータを取得
rootBundle.load('images/miao.jpg').then((value) => {
_sendStringMessage(value)
});
// 画像データを送信
_sendStringMessage(ByteData byteData) async {
await _basicMessageChannel.send(byteData);
}
Android 側の重要なコードは以下の通りです:
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.i(tag, "configureFlutterEngine")
// メッセージハンドラーを設定
BasicMessageChannel<ByteBuffer>(
flutterEngine.dartExecutor, "com.manu.image", BinaryCodec.INSTANCE
).setMessageHandler { message, reply ->
Log.i(tag, "configureFlutterEngine > message:$message")
// データ変換:ByteBuffer->ByteArray
val byteBuffer = message as ByteBuffer
imageByteArray = ByteArray(byteBuffer.capacity())
byteBuffer.get(imageByteArray)
}
// FlutterからAndroidへの遷移メソッドハンドラーを設定
MethodChannel(flutterEngine.dartExecutor, channel).setMethodCallHandler { call, result ->
Log.i(tag, "configureFlutterEngine > method:${call.method}")
if ("startBasicMessageChannelActivity" == call.method) {
// 画像データを持参
BasicMessageChannelActivity.startBasicMessageChannelActivity(this,imageByteArray)
}
}
}
// Flutterのassetsからの画像を表示
val imageByteArray = intent.getByteArrayExtra("key_image")
val bitmap = BitmapFactory.decodeByteArray(imageByteArray,0,imageByteArray.size)
imageView.setImageBitmap(bitmap)
さらに、BasicMessageChannel は BinaryCodec と組み合わせることで、大きなメモリデータブロックの伝達をサポートしています。
MethodChannel#
MethodChannel は主にメソッドの伝達に使用され、Native メソッドと Dart メソッドを伝達できます。つまり、MethodChannel を介して Flutter 内で Android のネイティブメソッドを呼び出し、Android 内で Dart メソッドを呼び出すことができます。相互呼び出しはすべて MethodChannel の invokeMethod メソッドを介して行われ、通信時には同じチャネル識別子を使用する必要があります。具体的には以下の通りです:
- Flutter が Android メソッドを呼び出す
以下の例では、MethodChannel を使用して Flutter から Android のネイティブ画面 MainActivity に遷移します。Android 側は以下の通りです:
/**
* @desc FlutterActivity
* @author jzman
*/
val tag = AgentActivity::class.java.simpleName;
class AgentActivity : FlutterActivity() {
val tag = AgentActivity::class.java.simpleName;
private val channel = "com.manu.startMainActivity"
private var platform: MethodChannel? = null;
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.d(tag,"configureFlutterEngine")
platform = MethodChannel(flutterEngine.dartExecutor, channel)
// メソッドハンドラーを設定
platform!!.setMethodCallHandler(StartMethodCallHandler(this@AgentActivity))
}
companion object{
/**
* NewEngineIntentBuilderを再作成する必要があります
*/
fun withNewEngine(): MNewEngineIntentBuilder? {
return MNewEngineIntentBuilder(AgentActivity::class.java)
}
}
/**
* カスタムNewEngineIntentBuilder
*/
class MNewEngineIntentBuilder(activityClass: Class<out FlutterActivity?>?) :
NewEngineIntentBuilder(activityClass!!)
/**
* MethodCallHandlerを実装
*/
class StartMethodCallHandler(activity:Activity) : MethodChannel.MethodCallHandler{
private val context:Activity = activity
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if ("startMainActivity" == call.method) {
Log.i(tag,"arguments:"+call.arguments)
startMainActivity(context)
// Flutterに実行結果をコールバック
result.success("success")
} else {
result.notImplemented()
}
}
}
}
上記のように、MethodChannel.Result オブジェクトを使用して Flutter に実行結果をコールバックできます。Flutter 側は以下の通りです:
/// State
class _PageState extends State<PageWidget> {
MethodChannel platform;
@override
void initState() {
super.initState();
platform = new MethodChannel('com.manu.startMainActivity');
}
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: EdgeInsets.fromLTRB(8, 8, 8, 0),
child: RaisedButton(
onPressed: () {
_startMainActivity();
},
child: Text("FlutterからAndroidへ"),
),
);
}
/// ネイティブActivityに遷移
void _startMainActivity() {
platform.invokeMethod('startMainActivity', 'flutter message').then((value) {
// 戻り値を受け取る
print("value:$value");
}).catchError((e) {
print(e.message);
});
}
}
- Android が Dart メソッドを呼び出す
以下の例では、MethodChannel を使用して Flutter 内の Dart メソッド getName を呼び出します。Android 側のコードは以下の通りです:
/**
* @desc MainActivity
* @author jzman
*/
class MainActivity : FlutterActivity() {
private val tag = MainActivity::class.java.simpleName;
private val channel = "com.manu.startMainActivity"
private var methodChannel: MethodChannel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnGetDart.setOnClickListener {
getDartMethod()
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.i(tag,"configureFlutterEngine")
methodChannel = MethodChannel(flutterEngine.dartExecutor,channel)
}
private fun getDartMethod(){
methodChannel?.invokeMethod("getName",null, object :MethodChannel.Result{
override fun success(result: Any?) {
Log.i(tag,"success: "+result.toString())
Toast.makeText(this@MainActivity,result.toString(),Toast.LENGTH_LONG).show()
}
override fun error(errorCode: String,errorMessage: String?,errorDetails: Any?) {
Log.i(tag,"error")
}
override fun notImplemented() {
Log.i(tag,"notImplemented")
}
})
}
companion object{
fun startMainActivity(context: Context) {
val intent = Intent(context, MainActivity::class.java)
context.startActivity(intent)
}
}
}
Flutter 側は以下の通りです:
/// State
class _PageState extends State<PageWidget> {
MethodChannel platform;
@override
void initState() {
super.initState();
platform = new MethodChannel('com.manu.startMainActivity');
// AndroidがFlutterメソッドを呼び出すのをリッスン
platform.setMethodCallHandler(platformCallHandler);
}
@override
Widget build(BuildContext context) {
return Container();
}
/// Flutterメソッド
Future<dynamic> platformCallHandler(MethodCall call) async{
switch(call.method){
case "getName":
return "flutterからの名前";
break;
}
}
}
EventChannel#
EventChannel は主に Flutter からネイティブへの一方向の呼び出しに使用され、その使用方法は Android のブロードキャストに似ています。ネイティブインターフェースがイベントの送信を担当し、Flutter 側がリスナーを登録するだけです。詳細はコードを見てください。Android 側のコードは以下の通りです:
/// Android
class MFlutterFragment : FlutterFragment() {
// ここではFragmentを使用していますが、Activityでも同様です
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Log.d(tag,"configureFlutterEngine")
EventChannel(flutterEngine.dartExecutor,"com.manu.event").setStreamHandler(object:
EventChannel.StreamHandler{
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.i(tag,"configureFlutterEngine > onListen")
// EventSinkがイベント通知を送信
events?.success("event message")
}
override fun onCancel(arguments: Any?) {
Log.i(tag,"configureFlutterEngine > onCancel")
}
})
}
companion object{
fun withNewEngine(): NewEngineFragmentBuilder? {
return MNewEngineIntentBuilder(
MFlutterFragment::class.java
)
}
}
class MNewEngineIntentBuilder(activityClass: Class<out FlutterFragment?>?) :
NewEngineFragmentBuilder(activityClass!!)
}
Flutter 側は以下の通りです:
/// State
class EventState extends State<EventChannelPage> {
EventChannel _eventChannel;
String _stringMessage;
StreamSubscription _streamSubscription;
@override
void initState() {
super.initState();
_eventChannel = EventChannel("com.manu.event");
// Eventイベントをリッスン
_streamSubscription =
_eventChannel.receiveBroadcastStream().listen((event) {
setState(() {
_stringMessage = event;
});
}, onError: (error) {
print("event error$error");
});
}
@override
void dispose() {
super.dispose();
if (_streamSubscription != null) {
_streamSubscription.cancel();
_streamSubscription = null;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("EventChannel"),
centerTitle: true,
),
body: Center(
child: Text(_stringMessage == null ? "default" : _stringMessage),
));
}
}
以上が Flutter プラットフォームチャネルの使用方法です。