![[Flutter] MethodChannelを使ってiOS/Androidのネイティブ側と双方向にやりとりする](https://images.ctfassets.net/ct0aopd36mqt/wp-thumbnail-97bd004eb227348cf028ece41fd4689e/b36c0bd625924c92c33ad88396cb5f71/flutter.png)
[Flutter] MethodChannelを使ってiOS/Androidのネイティブ側と双方向にやりとりする
こんにちは。きんくまです。
Flutter勉強中です。
今回はFlutterを使って、iOS/Androidのネイティブ側と双方向にやりとりをしたいと思います。
作ったもの
参考したもの
Writing custom platform-specific code
-> 公式マニュアル
注意!上記公式マニュアルはFlutter SDKのバージョンが反映されていないのかiOSは動かなかったです。
そのため、公式のサンプルコードを合わせて確認した方がよいです
その際、必ず自分が使っているFlutter SDKバージョンもページ左上からプルダウンで選ぶこと。SDKバージョンごとに実装方法が微妙に変わっているみたいです。-
下のリンクは、SDK 3.32のものです
platform_channel
platform_channel_swift
Flutter Platform Channels
-> この記事に仕組みがのっています
概要
やりとりの図
Flutter アプリ(クライアント)
↑
MethodChannel / EventChannelを媒介して通信する
↓
iOS/Androidなど各プラットフォーム(ホスト)
参考リンクにある仕組みの記事を読むと、クライアントとホストは一番低レベルはBinaryMessengerというものでやりとりしているようです。
ただしそのままだとバイナリの読み書きをするので、扱いにくい。
それを扱いやすくしたものがいくつか存在します。
そのうち、公式のページに書いてあるのは以下の2つ
- MethodChannel / EventChannel
- Pigeonライブラリ
Pigeonライブラリについて
先にPigeonライブラリについて。
少し試したので、もう少し調べてから記事にしようかと。
簡単にいうと、独自の型を定義しても使えるようになったりしてます。
MethodChannel / EventChannelについて
MethodChannel: Flutterから各プラットフォーム。逆に各プラットフォームからFlutterへの呼び出しが可能。
EventChannel: 各プラットフォームからFlutterへStreamデータを送るためのもの。例えば、端末のセンサーデータをFlutterに送り続けるみたいな想定。Flutterから各プラットフォームへは呼び出しできないです。
今回はMethodChannelについての記事です。
あと注意点としては、このサンプルは以下の環境で書いたものなので、もし別の環境でやった場合は動かない可能性もあります。そのときは、公式サンプルをみてください。
- Flutter SDK 3.32.1
- iOS SDK 16
- Android SDK 35
基本手順
通信するときは、以下の手順を行います。
- 名前をきめてチャンネルを作る
- 受け取り側はチャンネルにコールバックを張る
- 呼び出し側はチャンネルからメソッドを呼ぶ
これは、Flutterからの呼び出しでも、各プラットフォームからの呼び出しでも手順は変わらないです
チャンネルを作る時は、名前がかぶらないようにします。
リバースドメイン/チャンネル名
とするみたいです。
samples.flutter.dev/addition
今回やること全体は以下になります。
-
Flutterから各プラットフォームのaddメソッドを呼ぶ
-> 呼び出し方はここでわかると思います。ここで結果をうけとって普通は終了です。 -
各プラットフォーム内で、今度は逆にFlutterのaddメソッドを呼ぶ
-> 1とは逆の呼び出し方です。サンプル用に追加。ここで結果をうけとって終了ともできますが、サンプル用に3も行います。 -
2の結果をネイティブのAlertやToastを使って表示する
-> ネイティブ側にFlutterから返した値が伝わっていることを確認します。サンプルでネイティブのUI呼び出しを行います。
1と2はメソッド名がaddという名前で、Flutter側と各プラットフォーム側で同じ名前で実装していますが、特に意味はなく、別の名前で問題ないです。
1. Flutterから各プラットフォームのaddメソッドを呼ぶ
main.dart
class _MyHomePageState extends State<MyHomePage> {
static const additionChannel = MethodChannel('samples.flutter.dev/addition');
Future<void> callNativeAdd() async {
try {
final result = await additionChannel.invokeMethod<bool>('add');
print('Result from native add: $result');
} on PlatformException catch (e) {
print('Failed to call native add: \'${e.message}\'.');
}
}
Widget build(BuildContext context) {
略
ElevatedButton(
onPressed: callNativeAdd,
child: const Text('Call Native Add Method'),
),
],
),
),
);
}
}
additionChannel.invokeMethodについて。
今回は逆方向のサンプルも載せたいので、何か結果を返すというよりかは、ネイティブ側のメソッドを単純に呼び出すだけのものとなっています。
一応ネイティブからはboolが返ってきます。
iOS側
AppDelegate
private func setupFlutterMethodChannels() {
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
let additionChannel = FlutterMethodChannel(
name: ChannelName.addition.rawValue,
binaryMessenger: controller.binaryMessenger)
additionChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
switch call.method {
case AdditionMethod.add.rawValue:
// サンプル用に逆方向のメソッドを呼び出している
self?.callFlutterAdd(channel: additionChannel)
// ここで結果を返している
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
}
Android側
MainActivity.kt
private val ADDITION_CHANNEL = "samples.flutter.dev/addition"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ADDITION_CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "add") {
// サンプル用に逆方向のメソッドを呼び出している
add(flutterEngine)
// ここで結果を返している
result.success(true)
} else {
result.notImplemented()
}
}
}
2. 各プラットフォーム内で、今度は逆にFlutterのaddメソッドを呼ぶ
main.dart
void initState() {
additionChannel.setMethodCallHandler((call) async {
if (call.method == 'add') {
if (call.arguments != null && call.arguments is List && call.arguments.length == 2) {
print('Received add method call with arguments: ${call.arguments}');
return call.arguments[0] + call.arguments[1];
} else {
throw ArgumentError('Invalid arguments for add method');
}
}
});
super.initState();
}
コールバックを設定しています。
iOS側
AppDelegate
private func callFlutterAdd(channel: FlutterMethodChannel) {
let num1 = 35
let num2 = 8
channel.invokeMethod("add", arguments: [num1, num2]) { [weak self] result in
var resultStr = "no result"
if let result {
resultStr = "result: \(num1) + \(num2) = \(result)"
}
self?.showAlert(text: resultStr)
}
}
Android側
MainActivity.kt
private fun add(@NonNull flutterEngine: FlutterEngine) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ADDITION_CHANNEL)
val num1: Int = 35
val num2: Int = 8
channel.invokeMethod("add", intArrayOf(num1, num2), object : MethodChannel.Result {
override fun success(result: Any?) {
if (result != null) {
val text = "result: $num1 + $num2 = $result"
showToast(text)
}
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
// Handle error if needed
}
override fun notImplemented() {
// Handle not implemented if needed
}
})
}
3. 2の結果をネイティブのAlertやToastを使って表示する
iOS側
AppDelegate
private func showAlert(text: String) {
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
let action = UIAlertAction(title: "OK", style: .default) { _ in }
let alert = UIAlertController(title: "add result", message: text, preferredStyle: .alert)
alert.addAction(action)
controller.present(alert, animated: true)
}
Android側
MainActivity.kt
private fun showToast(text: String) {
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}
ソースコード全体
Flutter公式サンプルのバッテリー状態確認のソースコードも入っていますが、気にしないでください。
main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Commnunicate to platform'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const batteryChannel = MethodChannel('samples.flutter.dev/battery');
static const additionChannel = MethodChannel('samples.flutter.dev/addition');
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final result = await batteryChannel.invokeMethod<int>('getBatteryLevel');
batteryLevel = 'Battery level at $result %.';
} on PlatformException catch (e) {
batteryLevel = 'Failed to get battery level: \'${e.message}\'.';
}
setState(() {
_batteryLevel = batteryLevel;
});
}
Future<void> callNativeAdd() async {
try {
final result = await additionChannel.invokeMethod<bool>('add');
print('Result from native add: $result');
} on PlatformException catch (e) {
print('Failed to call native add: \'${e.message}\'.');
}
}
void initState() {
additionChannel.setMethodCallHandler((call) async {
if (call.method == 'add') {
if (call.arguments != null && call.arguments is List && call.arguments.length == 2) {
print('Received add method call with arguments: ${call.arguments}');
return call.arguments[0] + call.arguments[1];
} else {
throw ArgumentError('Invalid arguments for add method');
}
}
});
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: _getBatteryLevel,
child: const Text('Get Battery Level'),
),
Text('battery level: $_batteryLevel'),
SizedBox(height: 20,),
ElevatedButton(
onPressed: callNativeAdd,
child: const Text('Call Native Add Method'),
),
],
),
),
);
}
}
iOS側
AppDelegate.swift
import Flutter
import UIKit
enum ChannelName: String {
case battery = "samples.flutter.dev/battery"
case addition = "samples.flutter.dev/addition"
}
enum BatteryMethod: String {
case getBatteryLevel
}
enum AdditionMethod: String {
case add
}
enum MyFlutterErrorCode: String {
case unavailable = "UNAVAILABLE"
}
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
setupFlutterMethodChannels()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func setupFlutterMethodChannels() {
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
let batteryChannel = FlutterMethodChannel(
name: ChannelName.battery.rawValue,
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
switch call.method {
case BatteryMethod.getBatteryLevel.rawValue:
self?.receiveBatteryLevel(result: result)
default:
result(FlutterMethodNotImplemented)
}
}
let additionChannel = FlutterMethodChannel(
name: ChannelName.addition.rawValue,
binaryMessenger: controller.binaryMessenger)
additionChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
switch call.method {
case AdditionMethod.add.rawValue:
self?.callFlutterAdd(channel: additionChannel)
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
}
private func callFlutterAdd(channel: FlutterMethodChannel) {
let num1 = 35
let num2 = 8
channel.invokeMethod("add", arguments: [num1, num2]) { [weak self] result in
var resultStr = "no result"
if let result {
resultStr = "result: \(num1) + \(num2) = \(result)"
}
self?.showAlert(text: resultStr)
}
}
private func showAlert(text: String) {
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
let action = UIAlertAction(title: "OK", style: .default) { _ in
}
let alert = UIAlertController(title: "add result", message: text, preferredStyle: .alert)
alert.addAction(action)
controller.present(alert, animated: true)
}
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
guard device.batteryState != .unknown else {
result(FlutterError(code: MyFlutterErrorCode.unavailable.rawValue,
message: "Battery info unavailable",
details: nil))
return
}
result(Int(device.batteryLevel * 100))
}
}
Android側
MainActivity.kt
package com.sample.myapp.app250613_platform_code
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.widget.Toast
class MainActivity : FlutterActivity() {
private val BATTERY_CHANNEL = "samples.flutter.dev/battery"
private val ADDITION_CHANNEL = "samples.flutter.dev/addition"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
} else {
result.notImplemented()
}
}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ADDITION_CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "add") {
add(flutterEngine)
result.success(true)
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryLevel: Int
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
} else {
val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
}
return batteryLevel
}
private fun add(@NonNull flutterEngine: FlutterEngine) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ADDITION_CHANNEL)
val num1: Int = 35
val num2: Int = 8
channel.invokeMethod("add", intArrayOf(num1, num2), object : MethodChannel.Result {
override fun success(result: Any?) {
if (result != null) {
val text = "result: $num1 + $num2 = $result"
showToast(text)
}
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
// Handle error if needed
}
override fun notImplemented() {
// Handle not implemented if needed
}
})
}
private fun showToast(text: String) {
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}
}