The magic of combination of dart and platform.
あんまり役に立たない開発が好きです
The magic of combination of dart and platform.
あんまり役に立たない開発が好きです
任意のコードをバックグラウンドで実行することができるパッケージの一例
Framework / Engine / Embedder の3層
今回出てくるAPIは主にEmbedder
"Architectural diagram" by Flutter official is licensed under CC BY 4.0
https://flutter.dev/docs/resources/architectural-overview#architectural-layers
既存のネイティブアプリにFlutterを組み込むAdd-to-App (Androidでのコード例)
class MyApplication : Application() { lateinit var flutterEngine : FlutterEngine override fun onCreate() { super.onCreate() // Instantiate a FlutterEngine. flutterEngine = FlutterEngine(this) // Configure an initial route. flutterEngine.navigationChannel.setInitialRoute("your/route/here"); // Start executing Dart code to pre-warm the FlutterEngine. flutterEngine.dartExecutor.executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ) // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment. } }
class MyApplication : Application() { lateinit var flutterEngine : FlutterEngine override fun onCreate() { super.onCreate() // Instantiate a FlutterEngine. flutterEngine = FlutterEngine(this) // Configure an initial route. flutterEngine.navigationChannel.setInitialRoute("your/route/here"); // Start executing Dart code to pre-warm the FlutterEngine. flutterEngine.dartExecutor.executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ) // Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment. } }
Flutterを起動するためのクラス
並列処理ができる仕組み
import 'dart:isolate'; /// Root Isolate void main() { await Isolate.spawn(anotherMain, sendPort); } /// Child Isolate /// top-level function or static method void anotherMain() { print('Hello from anotherMain!'); }
import 'dart:isolate'; /// Root Isolate void main() { await Isolate.spawn(anotherMain, sendPort); } /// Child Isolate /// top-level function or static method void anotherMain() { print('Hello from anotherMain!'); }
main()
もIsolate 詳しくは monoさんの記事 がわかりやすいです
とても分かりやすい公式ドキュメント
https://flutter.dev/docs/development/platform-integration/platform-channels
基本的にやることは
MethodChannel
を名前付きでインスタンス化invokeMethod()
メソッドを呼ぶ setMethodCallHandler()
を使ってメソッド呼び出しを検知する"Architectural diagram" by Flutter official is licensed under CC BY 4.0
https://github.com/Kurogoma4D/flutter_background_example
main.dart
class _MyHomePageState extends State<MyHomePage> { @override void initState() { super.initState(); TimerManager.initialize(); } @override Widget build(BuildContext context) { ... ElevatedButton( onPressed: () => TimerManager.startTimer(timerCallback), child: const Text('Start'), ), ElevatedButton( onPressed: () => TimerManager.stopTimer(), child: const Text('Stop'), ), ... } }
class _MyHomePageState extends State<MyHomePage> { @override void initState() { super.initState(); TimerManager.initialize(); } @override Widget build(BuildContext context) { ... ElevatedButton( onPressed: () => TimerManager.startTimer(timerCallback), child: const Text('Start'), ), ElevatedButton( onPressed: () => TimerManager.stopTimer(), child: const Text('Stop'), ), ... } }
timer_manager.dart
class TimerManager { static const _channel = MethodChannel('dev.krgm4d/timer_manager'); static Future<void> initialize() async { final callback = PluginUtilities.getCallbackHandle(callbackDispatcher); await _channel.invokeMethod( 'TimerManager.initializeService', [callback.toRawHandle()]); } static Future<void> startTimer(void Function(int time) callback) async { final handle = PluginUtilities.getCallbackHandle(callback); await _channel.invokeMethod( 'TimerManager.startTimer', [handle.toRawHandle()], ); } }
class TimerManager { static const _channel = MethodChannel('dev.krgm4d/timer_manager'); static Future<void> initialize() async { final callback = PluginUtilities.getCallbackHandle(callbackDispatcher); await _channel.invokeMethod( 'TimerManager.initializeService', [callback.toRawHandle()]); } static Future<void> startTimer(void Function(int time) callback) async { final handle = PluginUtilities.getCallbackHandle(callback); await _channel.invokeMethod( 'TimerManager.startTimer', [handle.toRawHandle()], ); } }
MainActivity.kt
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "dev.krgm4d/timer_manager" ).setMethodCallHandler { call, result -> run { val args = call.arguments<ArrayList<*>>() when (call.method) { "TimerManager.initializeService" -> { initializeService(context, args) result.success(true) } "TimerManager.startTimer" -> startTimer(context, args, result) "TimerManager.stopTimer" -> stopTimer(context, result) else -> result.notImplemented() } } } }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "dev.krgm4d/timer_manager" ).setMethodCallHandler { call, result -> run { val args = call.arguments<ArrayList<*>>() when (call.method) { "TimerManager.initializeService" -> { initializeService(context, args) result.success(true) } "TimerManager.startTimer" -> startTimer(context, args, result) "TimerManager.stopTimer" -> stopTimer(context, result) else -> result.notImplemented() } } } }
MainActivity.kt companion object
@JvmStatic private fun initializeService(context: Context, args: ArrayList<*>?) { Log.d(TAG, "Initializing TimerService") // Callback Dispatcher のアドレス*を SharedPreference に保存する val callbackHandle = args!![0] as Long context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit() .putLong( CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle ).apply() }
@JvmStatic private fun initializeService(context: Context, args: ArrayList<*>?) { Log.d(TAG, "Initializing TimerService") // Callback Dispatcher のアドレス*を SharedPreference に保存する val callbackHandle = args!![0] as Long context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit() .putLong( CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle ).apply() }
* Flutterの内部では CallbackHandle
というクラスで扱われる値の
ナマの数値
便宜上アドレスと表現するが、
厳密にメモリ上のアドレスというわけではない
timer_manager.dart
class TimerManager { static const _channel = MethodChannel('dev.krgm4d/timer_manager'); static Future<void> initialize() async { final callback = PluginUtilities.getCallbackHandle(callbackDispatcher); await _channel.invokeMethod( 'TimerManager.initializeService', [callback.toRawHandle()]); } static Future<void> startTimer(void Function(int time) callback) async { final handle = PluginUtilities.getCallbackHandle(callback); await _channel.invokeMethod( 'TimerManager.startTimer', [handle.toRawHandle()], ); } }
class TimerManager { static const _channel = MethodChannel('dev.krgm4d/timer_manager'); static Future<void> initialize() async { final callback = PluginUtilities.getCallbackHandle(callbackDispatcher); await _channel.invokeMethod( 'TimerManager.initializeService', [callback.toRawHandle()]); } static Future<void> startTimer(void Function(int time) callback) async { final handle = PluginUtilities.getCallbackHandle(callback); await _channel.invokeMethod( 'TimerManager.startTimer', [handle.toRawHandle()], ); } }
MainActivity.kt
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "dev.krgm4d/timer_manager" ).setMethodCallHandler { call, result -> run { val args = call.arguments<ArrayList<*>>() when (call.method) { "TimerManager.initializeService" -> { initializeService(context, args) result.success(true) } "TimerManager.startTimer" -> startTimer(context, args, result) "TimerManager.stopTimer" -> stopTimer(context, result) else -> result.notImplemented() } } } }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "dev.krgm4d/timer_manager" ).setMethodCallHandler { call, result -> run { val args = call.arguments<ArrayList<*>>() when (call.method) { "TimerManager.initializeService" -> { initializeService(context, args) result.success(true) } "TimerManager.startTimer" -> startTimer(context, args, result) "TimerManager.stopTimer" -> stopTimer(context, result) else -> result.notImplemented() } } } }
MainActivity.kt companion object
@JvmStatic private fun startTimer( context: Context, args: ArrayList<*>?, result: MethodChannel.Result ) { // [A] のアドレスを引数から取り出し SharedPreference に保存する val callbackHandle = args!![0] as Long context.getSharedPreferences( SHARED_PREFERENCES_KEY,Context.MODE_PRIVATE).edit() .putLong( CALLBACK_HANDLE_KEY, callbackHandle ).apply() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Foreground Service を起動 context.startForegroundService( Intent(context, IsolateHolderService::class.java) ) } result.success(true) }
@JvmStatic private fun startTimer( context: Context, args: ArrayList<*>?, result: MethodChannel.Result ) { // [A] のアドレスを引数から取り出し SharedPreference に保存する val callbackHandle = args!![0] as Long context.getSharedPreferences( SHARED_PREFERENCES_KEY,Context.MODE_PRIVATE).edit() .putLong( CALLBACK_HANDLE_KEY, callbackHandle ).apply() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Foreground Service を起動 context.startForegroundService( Intent(context, IsolateHolderService::class.java) ) } result.success(true) }
IsolateHolderService.kt
class IsolateHolderService : Service(), MethodChannel.MethodCallHandler { private lateinit var mBackgroundChannel: MethodChannel // onCreate から呼ばれる private fun startIsolateHolderService(context: Context) { val callbackHandle = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_KEY,Context.MODE_PRIVATE) .getLong(MainActivity.CALLBACK_DISPATCHER_HANDLE_KEY, 0) // CallbackDispatcherのアドレスからコールバック情報を取得する val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) ... } }
class IsolateHolderService : Service(), MethodChannel.MethodCallHandler { private lateinit var mBackgroundChannel: MethodChannel // onCreate から呼ばれる private fun startIsolateHolderService(context: Context) { val callbackHandle = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_KEY,Context.MODE_PRIVATE) .getLong(MainActivity.CALLBACK_DISPATCHER_HANDLE_KEY, 0) // CallbackDispatcherのアドレスからコールバック情報を取得する val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) ... } }
IsolateHolderService.kt
class IsolateHolderService : Service(), MethodChannel.MethodCallHandler { private lateinit var mBackgroundChannel: MethodChannel // onCreate から呼ばれる private fun startIsolateHolderService(context: Context) { ... // CallbackDispatcherをエントリーポイントとして Flutter Engine を起動 sBackgroundFlutterEngine = FlutterEngine(context) sBackgroundFlutterEngine!!.dartExecutor.executeDartCallback(DartExecutor.DartCallback( context.assets, FlutterInjector.instance().flutterLoader().findAppBundlePath(), callbackInfo )) // CallbackDispatcherから初期化が完了した通知を受け取れるようにする mBackgroundChannel = MethodChannel(..., "dev.krgm4d/timer_manager_background") mBackgroundChannel.setMethodCallHandler(this) } }
class IsolateHolderService : Service(), MethodChannel.MethodCallHandler { private lateinit var mBackgroundChannel: MethodChannel // onCreate から呼ばれる private fun startIsolateHolderService(context: Context) { ... // CallbackDispatcherをエントリーポイントとして Flutter Engine を起動 sBackgroundFlutterEngine = FlutterEngine(context) sBackgroundFlutterEngine!!.dartExecutor.executeDartCallback(DartExecutor.DartCallback( context.assets, FlutterInjector.instance().flutterLoader().findAppBundlePath(), callbackInfo )) // CallbackDispatcherから初期化が完了した通知を受け取れるようにする mBackgroundChannel = MethodChannel(..., "dev.krgm4d/timer_manager_background") mBackgroundChannel.setMethodCallHandler(this) } }
callback_dispatcher.dart
void callbackDispatcher() { const _backgroundChannel = MethodChannel('dev.krgm4d/timer_manager_background'); WidgetsFlutterBinding.ensureInitialized(); ... }
void callbackDispatcher() { const _backgroundChannel = MethodChannel('dev.krgm4d/timer_manager_background'); WidgetsFlutterBinding.ensureInitialized(); ... }
callback_dispatcher.dart
void callbackDispatcher() { ... _backgroundChannel.setMethodCallHandler((call) async { final List<dynamic> args = call.arguments; // バックグラウンドで実行する任意のDartコードのアドレスを取得 // (図では [A] ) final callback = PluginUtilities.getCallbackFromHandle( CallbackHandle.fromRawHandle(args[0]), ); // バックグランドでカウントした数値を取得 final time = args[1] as int; // [A] を実行 callback(time); }); // 初期化が完了したことをプラットフォーム側に通知する _backgroundChannel.invokeMethod('TimerService.initialized'); }
void callbackDispatcher() { ... _backgroundChannel.setMethodCallHandler((call) async { final List<dynamic> args = call.arguments; // バックグラウンドで実行する任意のDartコードのアドレスを取得 // (図では [A] ) final callback = PluginUtilities.getCallbackFromHandle( CallbackHandle.fromRawHandle(args[0]), ); // バックグランドでカウントした数値を取得 final time = args[1] as int; // [A] を実行 callback(time); }); // 初期化が完了したことをプラットフォーム側に通知する _backgroundChannel.invokeMethod('TimerService.initialized'); }
IsolateHolderService.kt
// Foreground Serviceを起動させたときに呼ばれる override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // Foreground Service を使うための準備(ここでは省略) // [A] のアドレスを取得 callbackHandle = this.getSharedPreferences(MainActivity.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) .getLong(MainActivity.CALLBACK_HANDLE_KEY, 0) ... }
// Foreground Serviceを起動させたときに呼ばれる override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // Foreground Service を使うための準備(ここでは省略) // [A] のアドレスを取得 callbackHandle = this.getSharedPreferences(MainActivity.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) .getLong(MainActivity.CALLBACK_HANDLE_KEY, 0) ... }
IsolateHolderService.kt
// Foreground Serviceを起動させたときに呼ばれる override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { ... runnableTask = object : Runnable { var count = 0 override fun run() { if (sServiceStarted.get()) { // 1秒間隔でこのスコープ内が定期実行される // countをインクリメントし、CallbackDispatcherで受け取ることができる // MethodCallを行う count++ mBackgroundChannel.invokeMethod("", listOf(callbackHandle, count)) } handler?.postDelayed(this, 1000) } } handler?.post(runnableTask!!) ... }
// Foreground Serviceを起動させたときに呼ばれる override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { ... runnableTask = object : Runnable { var count = 0 override fun run() { if (sServiceStarted.get()) { // 1秒間隔でこのスコープ内が定期実行される // countをインクリメントし、CallbackDispatcherで受け取ることができる // MethodCallを行う count++ mBackgroundChannel.invokeMethod("", listOf(callbackHandle, count)) } handler?.postDelayed(this, 1000) } } handler?.post(runnableTask!!) ... }
callback_dispatcher.dart
void callbackDispatcher() { ... _backgroundChannel.setMethodCallHandler((call) async { final List<dynamic> args = call.arguments; // バックグラウンドで実行する任意のDartコードのアドレスを取得 // (図では [A] ) final callback = PluginUtilities.getCallbackFromHandle( CallbackHandle.fromRawHandle(args[0]), ); // バックグランドでカウントした数値を取得 final time = args[1] as int; // [A] を実行 callback(time); }); ... }
void callbackDispatcher() { ... _backgroundChannel.setMethodCallHandler((call) async { final List<dynamic> args = call.arguments; // バックグラウンドで実行する任意のDartコードのアドレスを取得 // (図では [A] ) final callback = PluginUtilities.getCallbackFromHandle( CallbackHandle.fromRawHandle(args[0]), ); // バックグランドでカウントした数値を取得 final time = args[1] as int; // [A] を実行 callback(time); }); ... }
main.dart
void timerCallback(int time) => debugPrint('time: $time');
void timerCallback(int time) => debugPrint('time: $time');
🎉 🙌 🎉
Dartコードをバックグラウンド実行するためには
main()
の代わりになる関数、②実行したい関数を静的な形で用意するMethodChannel
経由で呼び出すあくまでFlutterはプラットフォームより上のレイヤー、プラットフォームとの連携がキモ