この記事はFlutter Advent Calendar 2024 の19日目の記事です。
前置き
Google 謹製のクロスプラットフォーム 技術であるFlutterは素晴らしいツールです。しかし、Flutter(Dart )の実装のみでアプリの全てのユースケース をカバーできるわけではありません。場合によってはネイティブコード(SwiftやKotlin)を書く機会もあります。下記は一例です。
音楽や動画再生において、マッチするライブラリがなく自作する場合
電話、SMS、ネットワーク、NFC など低層の技術を扱う場合
ウィジェット を作成する場合
アプリを作り始める前にこれらを実装することがわかっている場合、Flutterを採用せずに最初からKotlin、Swiftで作ることが大半だと思います。しかし、Flutterアプリに後からこれらのようなネイティブコードを追加していく、といったケースもあるでしょう。そうした場合に、できればコードをある程度共通化 したい!と思うかもしれません。
今回はFlutterのネイティブコードを書く時に参考にできるかもしれない?FlutterのネイティブコードにKotlin Multiplatformを混ぜ込んでみる実装を試してみました。
FlutterやReact Nativeと同様なクロスプラットフォーム 技術の選択肢として、Kotlin Multiplatformが近年注目されています。
kotlinlang.org
Kotlin MultiplatformはKotlinを使用して実装を行った後、コンパイル でネイティブのフレームワーク に変換されます。これによりiOS でもネイティブで書いたアプリと同じようなパフォーマンスを担保しやすいです。また、実行前にコンパイル されたものを使用するため、Flutterとは別軸で開発することも可能です。
For example, in mobile applications, shared code written in Kotlin is compiled to JVM bytecode for Android with Kotlin/JVM and compiled to native binaries for iOS with Kotlin/Native. It makes the integration with Kotlin Multiplatform seamless on both platforms.
https://www.jetbrains.com/help/kotlin-multiplatform-dev/faq.html#what-is-kotlin-native-and-how-does-it-relate-to-kotlin-multiplatform
Sample Code
github.com
アプリ説明
Flutter側で日時のデータを保存して、KMPから呼び出すことができます。
iOS
Android
解説
それでは、FlutterプロジェクトにKMPを混ぜ込む流れを説明します。
プロジェクト設定
Flutterプロジェクトの作成
通常のFlutter開発と同様に、Flutterプロジェクトを作成します。
Dart and Flutter learning pathway
以下のサイトからプロジェクトの雛形を作ります。なお、Android Studio の機能など、他に雛形を作れる方法があればそちらを使っても問題ありません。
Kotlin Multiplatform Wizard | JetBrains
iOS の選択では、今回はUIはFlutterで表示するためDo not share UI (use only SwiftUI)でOKです。
KMPのベースをFlutterへ配置する
前段で作成したKMPのプロジェクトをFlutterプロジェクトへ持っていきます。
今回はFlutterプロジェクトからの使用を想定しているためsharedフォルダとその設定用のGradleファイル以外は必要ありません。
FlutterプロジェクトにKMPのshared周りのファイルを埋め込み。本来はもっと整頓したほうがよいです。
Gradleの設定をGroovy向けに修正
KMPのプロジェクトのGradle設定はKotlin DSL ですが、Flutterは執筆時点での安定版(3.24.5)のデフォルトはGroovyを使用しています。そのため、今回はKMP側のbuild.gradle.ktsをGroovyでも動作するように修正を行なっています。
主な変更点は以下です。
androidTarget()はandroid()に変更
sourceSetsの書き方を調整
jvmToolchainを追加
こちらはお使いのJDK に合わせてバージョンを設定してください。
元の設定もコメントとして残していますので、詳しくはGradleファイルをご確認ください。
kmm_flutter_sample/shared/build.gradle.kts at main · alpha2048/kmm_flutter_sample · GitHub
なお、既にKotlin DSL への対応を済ませている方は上記の変更は必要ないかもしれません。
先日発表された3.27のリリースでは、Kotlin DSL への対応についてアナウンスされています。いずれはKotlin DSL がデフォルトになりそうですね!
medium.com
FlutterのiOS 設定を編集する
Wizardで生成されたiOS プロジェクトを参考に、KMPのコードをビルドするRun ScriptをFlutterのXcode プロジェクトに追加します。
ここまでの設定で、FlutterでKMPのsharedモジュールを読み込む準備ができました。
(何か書き忘れた設定があればお知らせください🙏)
(実際の運用では動かないパターンがあるかもしれません。その場合はご自身で設定の調整をお願いします。)
実装
Method Channelを使用して連携
FlutterからMethod Channelを使用してiOS /Android のネイティブコードを呼び出し、さらにKMPの処理を呼び出します。
AppDelegateとMainActivityにFlutterから呼び出すためのMethod Channelを実装し、Flutter側 <--> Kotlin、Swiftとの連携箇所を作ります。そして、KMPのデフォルト実装のGreetingクラスを呼び出しています。
static const methodChannel = MethodChannel('platform_method/kmp' );
...
final res = await methodChannel.invokeMethod('getGreeting' );
kmm_flutter_sample/lib/main.dart at main · alpha2048/kmm_flutter_sample · GitHub
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let kmpChannel = FlutterMethodChannel(name: "platform_method/kmp" , binaryMessenger: controller as! FlutterBinaryMessenger )
kmpChannel.setMethodCallHandler({
(methodCall: FlutterMethodCall , result: @escaping FlutterResult ) -> Void in
if (methodCall.method == "getGreeting" ) {
let greet = Greeting().platformNameWithString(context: NSObject ())
result(greet)
} else {
result(FlutterMethodNotImplemented)
}
})
kmm_flutter_sample/android/app/src/main/kotlin/com/example/kmp_flutter_sample/MainActivity.kt at main · alpha2048/kmm_flutter_sample · GitHub
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "platform_method/kmp" )
channel.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->
if (methodCall.method == "getGreeting" ) {
val greet = Greeting().platformNameWithString(this )
result.success(greet)
}
else {
result.notImplemented()
}
}
kmm_flutter_sample/ios/Runner/AppDelegate.swift at main · alpha2048/kmm_flutter_sample · GitHub
これによってKMP側で共通化 されたコードを呼び出すことができました👍
shared_preferenceを使用してデータを同期する
ただKMPの処理にアクセスができただけでは味気ないので、今回はFlutterアプリで保存したデータをKMPから呼び出す以下の実装を行なっています。
まず、Flutterからshared_preferences で日時を保存します。
final data = DateTime.now().toIso8601String();
final prefs = await SharedPreferences.getInstance();
await prefs.setString('savedDataFromFlutter' , "保存日: $data" );
そして、KMP側でUserDefaults/SharedPreferencesから保存したデータを取り出します。iOS はUserDefaults(platform.Foundation.NSUserDefaults)で、Android はSharedPreferencesでshared_preferencesのキーから情報を取得しています。
共通部分
class Greeting {
private val platform = getPlatform()
fun platformNameWithString(context: SPref): String {
val savedData = context.getFlutterSavedData()
return "Hello, ${ platform.name} ! with data: $savedData "
}
}
expect class SPref
expect fun SPref.getFlutterSavedData(): String
Android の実装部分
import android.app.Activity
import android.content.Context
actual typealias SPref = Activity
actual fun SPref.getFlutterSavedData(): String {
val preferences = this .getSharedPreferences(
"FlutterSharedPreferences" ,
Context.MODE_PRIVATE
)
return preferences.getString("flutter.savedDataFromFlutter" , "データなし" ) ?: "データなし"
}
iOS の実装部分
import platform.Foundation.NSUserDefaults
import platform.darwin.NSObject
actual typealias SPref = NSObject
actual fun SPref.getFlutterSavedData(): String {
return NSUserDefaults.standardUserDefaults.stringForKey("flutter.savedDataFromFlutter" ) ?: ""
}
なお、shared_preferencesパッケージに新しく追加されたSharedPreferencesAsyncなどは、Android で内部でDataStoreを使っており、同じプロセス内で複数のインスタンスを作成できない ことからKMP側でアクセスできないため、古いAPI を使用しています。
今回はshared_preferencesを利用していますが、実運用を考えた場合、独自のDBなどを使って管理することを検討したほうがよいかもしれません。
おわりに
ということで、今回はFlutterにKMPを埋め込んでみる挑戦でした。
今回は綺麗に整理して埋め込んだわけではないため、実プロジェクト等で協調して動かすにはコツがいりそうですが、思ったよりも簡単に導入できそうですね。何かの参考になれば幸いです。