Closed12

Flutter の MethodChannel を使ってみる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

はじめに

このスクラップでは Flutter の Method Channel クラスを使って下記をやってみる

  • MethodChannel を使って iOS SDK の機能を呼び出す
  • MethodChannel を使って iOS サードパーティライブラリを呼び出す
  • MethodChannel を使って Android SDK の機能を呼び出す
  • MethodChannel を使って Android サードパーティライブラリ を呼び出す

https://api.flutter.dev/flutter/services/MethodChannel-class.html

Method Channel については今のところよくわからないがプラットフォームに固有の機能を呼び出す方法らしい

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

FlutterMethodChannel と MethodChannel

FlutterMethodChannel は iOS 、MethodChannel は Android のようだ

On the platform side, MethodChannel on Android (MethodChannelAndroid) and FlutterMethodChannel on iOS (MethodChanneliOS) enable receiving method calls and sending back a result.

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ドキュメントの導入部を少し読んだ

https://docs.flutter.dev/development/platform-integration/platform-channels?tab=type-mappings-java-tab

  • UI と Platform は非同期メッセージパッシングでやり取りする
  • タイプセーフなメッセージを送受信するには Pigeon を使う
  • defaultTargetPlatform を使うことでプラットフォーム固有の Dart コードを書けるらしい
  • メッセージはメインスレッドで送受信する必要がある
  • 通常は UI → Platform にリクエストするが逆も可能
  • メッセージは StandardMessageCode を使ってエンコード/デコードされる
  • 普通に MethodChannel を使う分には flutter create でプロジェクトを作成して OK
  • パッケージを作成する場合には Developping plugin packages のページを参考にする
  • Client と Android Platform は MethodChannel を使い、iOS Platform は FlutterMethodChannel を使う

Step 2: Create the Flutter platform client からはチュートリアル内容なので、これに従って手順を進めてみようと思う

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Android コードを呼び出す

準備コマンド

flutter create batterylevel
cd batterylevel

コード

batterylevel/lib/main.dart
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 const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

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

class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/battery');
  String _batteryLevel = 'Unknown battery level.';

  Future<void> _getBatteryLevel() async {
    String batteryLevel;

    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level as $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "failed to get battery level: '${e.message}'.";
    }

    setState(
      () {
        _batteryLevel = batteryLevel;
      },
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            ElevatedButton(
              onPressed: _getBatteryLevel,
              child: const Text('Get Battery Level'),
            ),
            Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}
batterylevel/android/app/src/main/kotlin/com/example/batterylevel/MainActivity.kt
package com.example.batterylevel

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 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 ->
      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()
      }
    }
  }

  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
  }
}

実行コマンド

Android 実機で実行する

flutter run

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

iOS コードを呼び出す

準備コマンド

flutter create batterylevel
cd batterylevel

コード

batterylevel/lib/main.dart
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 const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

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

class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/battery');
  String _batteryLevel = 'Unknown battery level.';

  Future<void> _getBatteryLevel() async {
    String batteryLevel;

    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level as $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "failed to get battery level: '${e.message}'.";
    }

    setState(
      () {
        _batteryLevel = batteryLevel;
      },
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            ElevatedButton(
              onPressed: _getBatteryLevel,
              child: const Text('Get Battery Level'),
            ),
            Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}
batterylevel/ios/Runner/AppDelegate.swift
import Flutter
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(
      name: "samples.flutter.dev/battery",
      binaryMessenger: controller.binaryMessenger
    )

    batteryChannel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      guard call.method == "getBatteryLevel" else {
        result(FlutterMethodNotImplemented)
        return
      }
      self?.receiveBatteryLevel(result: result)
    })
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  private func receiveBatteryLevel(result: FlutterResult) {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true

    if device.batteryState == UIDevice.BatteryState.unknown {
      result(
        FlutterError(
          code: "UNAVAILABLE",
          message: "Battery level not available",
          details: nil
        ))
    } else {
      result(Int(device.batteryLevel * 100))
    }
  }
}

実行コマンド

iOS 実機で実行する

flutter run

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

build.gradle について

依存関係を追加したい場合は buildscriptdependencies ではなくルートの dependencies に追加すると良さそう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Android のサードパーティライブラリを呼び出す

準備コマンド

flutter create hello_gson
cd hello_gson

コード

hello_gson/lib/main.dart
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 const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

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

class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('loremipsum.co.jp/gson');

  String _message = '';

  Future<void> _getMessage() async {
    String message;

    try {
      message = await platform.invokeMethod('getMessage');
    } on PlatformException catch (e) {
      message = 'Failed to get a message: ${e.message}';
    }

    setState(() {
      _message = message;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            ElevatedButton(
              onPressed: _getMessage,
              child: const Text('Get Message'),
            ),
            Text(_message),
          ],
        ),
      ),
    );
  }
}
hello_gson/android/app/src/main/kotlin/com/example/hello_gson/MainActivity.kt
package com.example.hello_gson

import androidx.annotation.NonNull
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "loremipsum.co.jp/gson"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "getMessage") {
                val gson = Gson()
                val message = gson.toJson("Message")
                result.success(message)
            } else {
                result.notImplemented()
            }
        }
    }
}
android/app/build.gradle
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation "com.google.code.gson:gson:2.10"
}

実行コマンド

Android 実機で実行する

flutter run

実行結果

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

iOS のサードパーティライブラリを呼び出す

準備コマンド

flutter create hello_swifty_json
cd hello_swifty_json
pod init

コード

hello_swifty_json/lib/main.dart
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 const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

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

class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('loremipsum.co.jp/gson');

  String _message = '';

  Future<void> _getMessage() async {
    String message;

    try {
      message = await platform.invokeMethod('getMessage');
    } on PlatformException catch (e) {
      message = 'Failed to get a message: ${e.message}';
    }

    setState(() {
      _message = message;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            ElevatedButton(
              onPressed: _getMessage,
              child: const Text('Get Message'),
            ),
            Text(_message),
          ],
        ),
      ),
    );
  }
}
hello_swifty_json/ios/Podfile
# Uncomment the next line to define a global platform for your project
# platform :ios, '11.0'

target 'Runner' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for Runner
  pod 'SwiftyJSON', '~> 5.0'
end
hello_swifty_json/ios/Runner/AppDelegate.swift
import Flutter
import SwiftyJSON
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(
      name: "loremipsum.co.jp/gson",
      binaryMessenger: controller.binaryMessenger
    )

    channel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      guard call.method == "getMessage" else {
        result(FlutterMethodNotImplemented)
        return
      }
      result(JSON(["message": "I'm a json"]).rawString())
    })

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

実行コマンド

iOS 実機で実行する

pod install
flutter run

実行結果

このスクラップは2023/01/10にクローズされました