Menyiapkan ViewModel untuk KMP

ViewModel AndroidX berfungsi sebagai jembatan, yang menetapkan kontrak yang jelas antara logika bisnis bersama dan komponen UI Anda. Pola ini membantu memastikan konsistensi data di seluruh platform, sekaligus memungkinkan UI disesuaikan untuk tampilan unik setiap platform. Anda dapat melanjutkan pengembangan UI dengan Jetpack Compose di Android dan SwiftUI di iOS.

Baca selengkapnya manfaat penggunaan ViewModel dan semua fitur dalam dokumentasi utama untuk ViewModel.

Menyiapkan dependensi

Untuk menyiapkan ViewModel KMP di project Anda, tentukan dependensi dalam file libs.versions.toml:

[versions]
androidx-viewmodel = 2.9.3

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

Kemudian, tambahkan artefak ke file build.gradle.kts untuk modul KMP Anda dan deklarasikan dependensi sebagai api, karena dependensi ini akan diekspor ke framework biner:

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

Mengekspor ViewModel API untuk akses dari Swift

Secara default, library apa pun yang Anda tambahkan ke codebase tidak akan otomatis diekspor ke framework biner. Jika API tidak diekspor, API hanya tersedia dari framework biner jika Anda menggunakannya dalam kode bersama (dari set sumber iosMain atau commonMain). Dalam hal ini, API akan berisi awalan paket, misalnya class ViewModel akan tersedia sebagai class Lifecycle_viewmodelViewModel. Lihat mengekspor dependensi ke biner untuk mengetahui informasi selengkapnya tentang cara mengekspor dependensi.

Untuk meningkatkan pengalaman, Anda dapat mengekspor dependensi ViewModel ke framework biner menggunakan penyiapan export di file build.gradle.kts tempat Anda menentukan framework biner iOS, yang membuat API ViewModel dapat diakses langsung dari kode Swift seperti dari kode 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"
  }
}

(Opsional) Menggunakan viewModelScope di Desktop JVM

Saat menjalankan coroutine di ViewModel, properti viewModelScope terikat ke Dispatchers.Main.immediate, yang mungkin tidak tersedia di desktop secara default. Agar berfungsi dengan benar, tambahkan dependensi kotlinx-coroutines-swing ke project Anda:

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

Lihat dokumentasi Dispatchers.Main untuk mengetahui detail selengkapnya.

Menggunakan ViewModel dari commonMain atau androidMain

Tidak ada persyaratan khusus untuk menggunakan class ViewModel di commonMain bersama, atau dari sourceSet androidMain. Satu-satunya pertimbangan adalah Anda tidak dapat menggunakan API khusus platform dan Anda harus mengabstraksikannya. Misalnya, jika Anda menggunakan Application Android sebagai parameter konstruktor ViewModel, Anda harus bermigrasi dari API ini dengan mengabstraksikannya.

Informasi selengkapnya tentang cara menggunakan kode khusus platform tersedia di kode khusus platform di Multiplatform Kotlin.

Misalnya, dalam cuplikan berikut adalah class ViewModel dengan factory-nya, yang ditentukan dalam commonMain:

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

Kemudian, dalam kode UI, Anda dapat mengambil ViewModel seperti biasa:

// androidApp/ui/MainScreen.kt

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

Menggunakan ViewModel dari SwiftUI

Di Android, siklus proses ViewModel ditangani dan dicakup secara otomatis ke ComponentActivity, Fragment, NavBackStackEntry (Navigation 2), atau rememberViewModelStoreNavEntryDecorator (Navigation 3). Namun, SwiftUI di iOS tidak memiliki padanan bawaan untuk ViewModel AndroidX.

Untuk membagikan ViewModel dengan aplikasi SwiftUI, Anda perlu menambahkan beberapa kode penyiapan.

Membuat fungsi untuk membantu generik

Membuat instance ViewModel generik menggunakan fitur refleksi referensi class di Android. Karena generik Objective-C tidak mendukung semua fitur Kotlin atau Swift, Anda tidak dapat langsung mengambil ViewModel jenis generik dari Swift.

Untuk membantu mengatasi masalah ini, Anda dapat membuat fungsi helper yang akan menggunakan ObjCClass, bukan jenis generik, lalu menggunakan getOriginalKotlinClass untuk mengambil class ViewModel yang akan dibuat instance-nya:

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

Kemudian, saat ingin memanggil fungsi dari Swift, Anda dapat menulis fungsi generik berjenis T : ViewModel dan menggunakan T.self, yang dapat meneruskan ObjCClass ke fungsi resolveViewModel.

Menghubungkan cakupan ViewModel ke Siklus Proses SwiftUI

Langkah selanjutnya adalah membuat IosViewModelStoreOwner yang menerapkan antarmuka (protokol) ObservableObject dan ViewModelStoreOwner. Alasan ObservableObject adalah agar dapat menggunakan class ini sebagai @StateObject dalam kode SwiftUI:

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

Pemilik ini memungkinkan pengambilan beberapa jenis ViewModel, seperti di Android. Siklus proses ViewModel tersebut akan dihapus saat layar yang menggunakan IosViewModelStoreOwner di-deinisialisasi dan memanggil deinit. Anda dapat mempelajari lebih lanjut deinisialisasi di dokumentasi resmi.

Pada tahap ini, Anda cukup membuat instance IosViewModelStoreOwner sebagai @StateObject di Tampilan SwiftUI dan memanggil fungsi viewModel untuk mengambil 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
    }
}

Tidak Tersedia di Multiplatform Kotlin

Beberapa API yang tersedia di Android tidak tersedia di Kotlin Multiplatform.

Integrasi dengan Hilt

Karena Hilt tidak tersedia untuk project Multiplatform Kotlin, Anda tidak dapat langsung menggunakan ViewModel dengan anotasi @HiltViewModel di sourceSet commonMain. Dalam hal ini, Anda perlu menggunakan framework DI alternatif, misalnya, Koin, kotlin-inject, Metro, atau Kodein. Anda dapat menemukan semua framework DI yang kompatibel dengan Kotlin Multiplatform di klibs.io.

Mengamati Alur di SwiftUI

Mengamati Flow coroutine di SwiftUI tidak didukung secara langsung. Namun, Anda dapat menggunakan library KMP-NativeCoroutines atau SKIE untuk mengizinkan fitur ini.