KMP 用の ViewModel を設定する

AndroidX ViewModel は、共有ビジネス ロジックと UI コンポーネントの間に明確なコントラクトを確立する橋渡し役として機能します。このパターンは、プラットフォーム間でデータの一貫性を確保し、各プラットフォームの異なる外観に合わせて UI をカスタマイズできるようにします。Android では Jetpack Compose、iOS では SwiftUI を使用して、引き続き UI を開発できます。

ViewModel の使用のメリットとすべての機能について詳しくは、ViewModel の主なドキュメントをご覧ください。

依存関係を設定する

プロジェクトで KMP ViewModel を設定するには、libs.versions.toml ファイルで依存関係を定義します。

[versions]
androidx-viewmodel = 2.9.3

[libraries]
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }

次に、KMP モジュールの build.gradle.kts ファイルにアーティファクトを追加し、依存関係を api として宣言します。この依存関係はバイナリ フレームワークにエクスポートされるためです。

// You need the "api" dependency declaration here if you want better access to the classes from Swift code.
commonMain.dependencies {
  api(libs.androidx.lifecycle.viewmodel)
}

Swift からアクセスできるように ViewModel API をエクスポートする

デフォルトでは、コードベースに追加したライブラリはバイナリ フレームワークに自動的にエクスポートされません。API がエクスポートされていない場合、共有コード(iosMain または commonMain ソースセットから)で使用する場合にのみ、バイナリ フレームワークから利用できます。その場合、API にはパッケージ接頭辞が含まれます。たとえば、ViewModel クラスは Lifecycle_viewmodelViewModel クラスとして使用できます。依存関係のエクスポートの詳細については、依存関係をバイナリにエクスポートするをご覧ください。

エクスペリエンスを向上させるには、iOS バイナリ フレームワークを定義する build.gradle.kts ファイルの export 設定を使用して、ViewModel の依存関係をバイナリ フレームワークにエクスポートします。これにより、ViewModel API に Swift コードから Kotlin コードと同じように直接アクセスできるようになります。

listOf(
  iosX64(),
  iosArm64(),
  iosSimulatorArm64(),
).forEach {
  it.binaries.framework {
    // Add this line to all the targets you want to export this dependency
    export(libs.androidx.lifecycle.viewmodel)
    baseName = "shared"
  }
}

(省略可)JVM Desktop で viewModelScope を使用する

ViewModel でコルーチンを実行する場合、viewModelScope プロパティは Dispatchers.Main.immediate に関連付けられます。これは、デフォルトではデスクトップで使用できない可能性があります。正しく動作させるには、kotlinx-coroutines-swing の依存関係をプロジェクトに追加します。

// Optional if you use JVM Desktop
desktopMain.dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:[KotlinX Coroutines version]")
}

詳細については、Dispatchers.Main ドキュメントをご覧ください。

commonMain または androidMain から ViewModel を使用する

共有 commonMain で ViewModel クラスを使用する場合や、androidMain sourceSet から使用する場合の特別な要件はありません。唯一の考慮事項は、プラットフォーム固有の API を使用できず、抽象化する必要があることです。たとえば、ViewModel コンストラクタ パラメータとして Android Application を使用している場合は、この API を抽象化して移行する必要があります。

プラットフォーム固有のコードの使用方法について詳しくは、Kotlin マルチプラットフォームのプラットフォーム固有のコードをご覧ください。

たとえば、次のスニペットは、commonMain で定義されたファクトリを持つ ViewModel クラスです。

// commonMain/MainViewModel.kt

class MainViewModel(
    private val repository: DataRepository,
) : ViewModel() { /* some logic */ }

// ViewModelFactory that retrieves the data repository for your app.
val mainViewModelFactory = viewModelFactory {
    initializer {
        MainViewModel(repository = getDataRepository())
    }
}

fun getDataRepository(): DataRepository = DataRepository()

UI コードでは、通常どおり ViewModel を取得できます。

// androidApp/ui/MainScreen.kt

@Composable
fun MainScreen(
    viewModel: MainViewModel = viewModel(
        factory = mainViewModelFactory,
    ),
) {
// observe the viewModel state
}

SwiftUI から ViewModel を使用する

Android では、ViewModel のライフサイクルは自動的に処理され、ComponentActivityFragmentNavBackStackEntry(Navigation 2)、または rememberViewModelStoreNavEntryDecorator(Navigation 3)にスコープ設定されます。ただし、iOS の SwiftUI には AndroidX ViewModel に相当する組み込み機能はありません。

ViewModel を SwiftUI アプリと共有するには、設定コードを追加する必要があります。

ジェネリックをサポートする関数を作成する

汎用 ViewModel インスタンスのインスタンス化では、Android のクラス参照リフレクション機能が使用されます。Objective-C ジェネリクスは Kotlin または Swift のすべての機能をサポートしていないため、Swift からジェネリック型の ViewModel を直接取得することはできません。

この問題を解決するために、ジェネリック型ではなく ObjCClass を使用するヘルパー関数を作成し、getOriginalKotlinClass を使用してインスタンス化する ViewModel クラスを取得できます。

// iosMain/ViewModelResolver.ios.kt

/**
 *   This function allows retrieving any ViewModel from Swift Code with generics. We only get
 *   [ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code
 *   doesn't preserve the generic class, but we can retrieve the original KClass in Kotlin.
 */
@BetaInteropApi
@Throws(IllegalArgumentException::class)
fun ViewModelStore.resolveViewModel(
    modelClass: ObjCClass,
    factory: ViewModelProvider.Factory,
    key: String?,
    extras: CreationExtras? = null,
): ViewModel {
    @Suppress("UNCHECKED_CAST")
    val vmClass = getOriginalKotlinClass(modelClass) as? KClass<ViewModel>
    require(vmClass != null) { "The modelClass parameter must be a ViewModel type." }

    val provider = ViewModelProvider.Companion.create(this, factory, extras ?: CreationExtras.Empty)
    return key?.let { provider[key, vmClass] } ?: provider[vmClass]
}

その後、Swift から関数を呼び出す場合は、型 T : ViewModel の汎用関数を記述し、T.self を使用して ObjCClassresolveViewModel 関数に渡すことができます。

ViewModel スコープを SwiftUI ライフサイクルに接続する

次のステップでは、ObservableObject インターフェースと ViewModelStoreOwner インターフェース(プロトコル)を実装する IosViewModelStoreOwner を作成します。ObservableObject を使用する理由は、SwiftUI コードでこのクラスを @StateObject として使用できるようにするためです。

// iosApp/IosViewModelStoreOwner.swift

class IosViewModelStoreOwner: ObservableObject, ViewModelStoreOwner {

    let viewModelStore = ViewModelStore()

    /// This function allows retrieving the androidx ViewModel from the store.
    /// It uses the utilify function to pass the generic type T to shared code
    func viewModel<T: ViewModel>(
        key: String? = nil,
        factory: ViewModelProviderFactory,
        extras: CreationExtras? = nil
    ) -> T {
        do {
            return try viewModelStore.resolveViewModel(
                modelClass: T.self,
                factory: factory,
                key: key,
                extras: extras
            ) as! T
        } catch {
            fatalError("Failed to create ViewModel of type \(T.self)")
        }
    }

    /// This is called when this class is used as a `@StateObject`
    deinit {
        viewModelStore.clear()
    }
}

このオーナーを使用すると、Android と同様に複数の ViewModel 型を取得できます。これらの ViewModel のライフサイクルは、IosViewModelStoreOwner を使用する画面が初期化解除され、deinit を呼び出すとクリアされます。初期化解除について詳しくは、公式ドキュメントをご覧ください。

この時点で、SwiftUI View で IosViewModelStoreOwner@StateObject としてインスタンス化し、viewModel 関数を呼び出して ViewModel を取得できます。

// iosApp/ContentView.swift

struct ContentView: View {

    /// Use the store owner as a StateObject to allow retrieving ViewModels and scoping it to this screen.
    @StateObject private var viewModelStoreOwner = IosViewModelStoreOwner()

    var body: some View {
        /// Retrieves the `MainViewModel` instance using the `viewModelStoreOwner`.
        /// The `MainViewModel.Factory` and `creationExtras` are provided to enable dependency injection
        /// and proper initialization of the ViewModel with its required `AppContainer`.
        let mainViewModel: MainViewModel = viewModelStoreOwner.viewModel(
            factory: MainViewModelKt.mainViewModelFactory
        )
        // ...
        // .. the rest of the SwiftUI code
    }
}

Kotlin マルチプラットフォームでは利用不可

Android で利用できる API の一部は、Kotlin Multiplatform では利用できません。

Hilt との統合

Kotlin マルチプラットフォーム プロジェクトでは Hilt を使用できないため、commonMain sourceSet で @HiltViewModel アノテーション付きの ViewModel を直接使用することはできません。その場合は、Koinkotlin-injectMetroKodein などの別の DI フレームワークを使用する必要があります。Kotlin Multiplatform で動作するすべての DI フレームワークは、klibs.io で確認できます。

SwiftUI でフローを監視する

SwiftUI でコルーチンの Flow を直接監視することはできません。ただし、KMP-NativeCoroutines ライブラリまたは SKIE ライブラリを使用すると、この機能を有効にできます。