👻

MethodChannelをわかりやすく解説したい

2024/12/17に公開

🤔やってみたいこと

FlutterでiOSとAndroidのOS特有の機能を使いたいことあります。その場合は、MethodChannelと呼ばれる機能を使います。

公式の解説

Flutter は、プラットフォーム固有の API を、それらの API と直接連携する言語で呼び出すことができる柔軟なシステムを使用します。

  • Android 上の Kotlin または Java
  • iOS 上の Swift または Objective-C
  • Windows 上の C++
  • macOS 上の Objective-C
  • Linux 上の C

Flutter に組み込まれているプラ​​ットフォーム固有の API サポートは、コード生成に依存せず、柔軟なメッセージ パッシング スタイルに依存します。あるいは、Pigeonパッケージを使用して、コード生成で構造化された型安全なメッセージを送信することもできます。

アプリの Flutter 部分は、プラットフォーム チャネルを介して、アプリのホスト(Dart 以外の部分) にメッセージを送信します。

ホストはプラットフォーム チャネルをリッスンし、メッセージを受信します。次に、ネイティブ プログラミング言語を使用して任意の数のプラットフォーム固有の API を呼び出し、アプリの Flutter 部分であるクライアントに応答を返します。

Pigeonは使いません笑
使い方がわからん😇
企業によっては使ってないので標準機能で頑張る💪

🚀やってみたこと

海外の動画を見てやってみると理解しやすい。文章だけだと理解し難い人はみると良いだろう。
https://www.youtube.com/watch?v=WguIIylkN8A

🤔公式の解説によると

ステップ2: Flutterプラットフォームクライアントを作成する
アプリのStateクラスは現在のアプリの状態を保持します。これを拡張して、現在のバッテリー状態を保持します。

まず、チャネルを構築します。MethodChannelバッテリー レベルを返す単一のプラットフォーム メソッドを使用します。

チャネルのクライアント側とホスト側は、チャネル コンストラクターで渡されるチャネル名を介して接続されます。 1 つのアプリ内で使用されるすべてのチャネル名は一意である必要があります。チャネル名の前に、一意の「ドメイン プレフィックス」を付けます。

ここがポイントですね。自分で名前は決めることができます。com.なんとか?に/platformsをつける。
com.jboycode/platformsとなります。

1 つのアプリ内で使用されるすべてのチャネル名は一意である必要があります。チャネル名の前に、一意の「ドメイン プレフィックス」を付けます。

class _MyHomePageState extends State<MyHomePage> {
  
  final platform = MethodChannel("com.jboycode/platforms");
  String message = "";

  
  Widget build(BuildContext context) {

メソッドは非同期で書く必要があるのと、UI Stateが更新されないと表示されないので、setState()のコールバックの中に状態変数を書いて、その中にNativeから取得した値が入っている変数を代入する。

_callNativeMethod() async {
    /// メソッドには任意の名前をつけることできる。("")
    /// Native側も同じ名前にする必要がある。
    try {
      String message = await platform.invokeMethod("callNative");
      setState(() {
        this.message = message;
      });
      talker.debug("👻 Message From Native: $message");
    } on PlatformException catch (e) {
      talker.error(e.message);
    } finally {
      talker.info("MethodChannelを実行した");
  }
}

Kotlinの操作をする目的で、Android StudiodでNativeの設定を開く場面があるがもし右クリックでFlutterのアイコンがない場合は、こちらの記事を参考にすると良い。
https://qiita.com/arikawa0712/items/50328e4c5078abbfd85f

私も昔ハマったの思い出して記事にした。
https://zenn.dev/joo_hashi/articles/2a1b6fbc8e7d54

Android

Kotlinのコードを認識してもらうには、Koltlinのプロジェクトで開く必要がある。プロジェクトを開いたら公式のソースコードを参考にテキストを渡すだけの処理を記載する。

こちらを参考にする。

import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
  private val CHANNEL = "samples.flutter.dev/battery"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // This method is invoked on the main thread.
      // TODO
    }
  }
}
Kotlinのコード
package com.jboycode.methld_chanels_demo

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    // Flutterで定義した final platform channelと同じ名前にする。
    // final platform = MethodChannel("com.jboycode/platforms");
    private val channel = "com.jboycode/platforms"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel)
            .setMethodCallHandler { call, result ->
                // Flutterのメソッドと同じ名前にする。
                if (call.method == "callNative") {
                    result.success("Android ${android.os.Build.VERSION.RELEASE}")
                } else {
                    result.notImplemented()
                }
            }
    }
}

iOSの設定

Finderで、xcodeを開く。ファイルを編集する。公式のコードと少し違う???
コードの保管機能もないので驚いた💦
最初はエラー出てるかもしれませんが▶️再生ボタン押すとエラー消えました。

Swiftのコード
import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    
    let methodChannel =
        FlutterMethodChannel(name: "com.jboycode/platforms",
                                                      binaryMessenger: controller.binaryMessenger)
    
        methodChannel.setMethodCallHandler{
            (call, result) in
            if call.method == "callNative"{
                let message = "👻 Hello From IOS Native"
                result(message)
            }
        }

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

これでネイティブ側の設定は完了。

Flutterのコードは少しアレンジしてます。ログのパッケージ入れてますがなくもOK
https://pub.dev/packages/talker

Flutterのコード

logのコード

import 'package:talker/talker.dart';
/// https://pub.dev/packages/talker
/// log出力用のモジュール
Talker talker = Talker(
  /// ログの種類で色を変える
  settings: TalkerSettings(
    colors: {
      TalkerLogType.debug.key: AnsiPen()..green(),
      TalkerLogType.error.key: AnsiPen()..red(),
      TalkerLogType.warning.key: AnsiPen()..yellow(),
      TalkerLogType.info.key: AnsiPen()..blue(),
    }
  )
);

ロジックのコード

main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:methld_chanels_demo/talker.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),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  final platform = MethodChannel("com.jboycode/platforms");
  String message = "";

  
  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: _callNativeMethod,
              child: const Text('Call Native Methods'),
            ),
            Text("Message From Native $message"),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }

  _callNativeMethod() async {
    /// メソッドには任意の名前をつけることできる。("")
    /// Native側も同じ名前にする必要がある。
    try {
      String message = await platform.invokeMethod("callNative");
      setState(() {
        this.message = message;
      });
      talker.debug("👻 Message From Native: $message");
    } on PlatformException catch (e) {
      talker.error(e.message);
    } finally {
      talker.info("MethodChannelを実行した");
  }
  }
}

実行した結果

iOSだとボタンを押すとお化け出ます。

Androidだとアレンジはしてないのでそのまま

おろさっきは色が変わってなかった。

🙂最後に

なんとなくわかるぐらい。そして私でも理解できるようにAIに代わりに書かせることをすることもなくチュートリアル動画と公式の解説を見ながらやってみました。流石に、情報が出てこない、実装に時間がかかりそうな機能は任せた方がいいかも💦

Discussion