Konfigurowanie ViewModel w KMP

ViewModel AndroidX pełni funkcję pomostu, ustanawiając jasną umowę między wspólną logiką biznesową a komponentami interfejsu. Ten wzorzec pomaga zapewnić spójność danych na różnych platformach, a jednocześnie umożliwia dostosowywanie interfejsów do odmiennego wyglądu każdej platformy. Możesz dalej tworzyć interfejs użytkownika za pomocą Jetpack Compose na Androidzie i SwiftUI na iOS.

Więcej informacji o zaletach korzystania z klasy ViewModel i wszystkich jej funkcjach znajdziesz w głównej dokumentacji klasy ViewModel.

Konfigurowanie zależności

Aby skonfigurować KMP ViewModel w projekcie, zdefiniuj zależność w pliku libs.versions.toml:

[versions]
androidx-viewmodel = 2.9.3

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

Następnie dodaj artefakt do pliku build.gradle.kts modułu KMP i zadeklaruj zależność jako api, ponieważ ta zależność zostanie wyeksportowana do struktury binarnej:

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

Eksportowanie interfejsów API ViewModel na potrzeby dostępu z poziomu Swift

Domyślnie żadna biblioteka dodana do kodu nie jest automatycznie eksportowana do struktury binarnej. Jeśli interfejsy API nie są eksportowane, są dostępne w ramach binarnych tylko wtedy, gdy używasz ich w kodzie współdzielonym (z zestawu źródeł iosMain lub commonMain). W takim przypadku interfejsy API zawierałyby prefiks pakietu, np. klasa ViewModel byłaby dostępna jako klasa Lifecycle_viewmodelViewModel. Więcej informacji o eksportowaniu zależności znajdziesz w artykule Eksportowanie zależności do plików binarnych.

Aby poprawić komfort korzystania, możesz wyeksportować zależność ViewModel do binarnego frameworka za pomocą konfiguracji export w pliku build.gradle.kts, w którym definiujesz binarny framework iOS. Dzięki temu interfejsy API ViewModel będą dostępne bezpośrednio z kodu Swift, tak samo jak z kodu 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"
  }
}

(Opcjonalnie) Korzystanie z viewModelScope na komputerze z JVM

Podczas uruchamiania korutyn w obiekcie ViewModel właściwość viewModelScope jest powiązana z obiektem Dispatchers.Main.immediate, który może być domyślnie niedostępny na komputerze. Aby działał prawidłowo, dodaj do projektu zależność kotlinx-coroutines-swing:

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

Więcej informacji znajdziesz w Dispatchers.Maindokumentacji.

Używanie ViewModel z commonMain lub androidMain

Nie ma konkretnych wymagań dotyczących używania klasy ViewModel w przypadku zestawu źródeł sharedcommonMain ani zestawu źródeł androidMain. Jedynym ograniczeniem jest to, że nie możesz używać żadnych interfejsów API specyficznych dla platformy i musisz je wyodrębnić. Jeśli na przykład używasz AndroidaApplication jako parametru konstruktora ViewModel, musisz zrezygnować z tego interfejsu API, tworząc jego abstrakcję.

Więcej informacji o tym, jak używać kodu specyficznego dla platformy, znajdziesz w artykule Kod specyficzny dla platformy w Kotlinie Multiplatform.

Na przykład w tym fragmencie kodu znajduje się klasa ViewModel z fabryką zdefiniowaną w 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()

Następnie w kodzie interfejsu możesz pobrać ViewModel w zwykły sposób:

// androidApp/ui/MainScreen.kt

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

Używanie ViewModel w SwiftUI

Na Androidzie cykl życia obiektu ViewModel jest obsługiwany automatycznie i ograniczony do ComponentActivity, Fragment, NavBackStackEntry (Navigation 2) lub rememberViewModelStoreNavEntryDecorator (Navigation 3). SwiftUI na iOS nie ma jednak wbudowanego odpowiednika AndroidX ViewModel.

Aby udostępnić ViewModel aplikacji SwiftUI, musisz dodać kod konfiguracji.

Tworzenie funkcji ułatwiającej korzystanie z typów generycznych

Tworzenie instancji ogólnej klasy ViewModel wykorzystuje na Androidzie funkcję odbicia odwołania do klasy. Ponieważ generics w Objective-C nie obsługują wszystkich funkcji języków Kotlin ani Swift, nie możesz bezpośrednio pobrać z języka Swift obiektu ViewModel typu ogólnego.

Aby rozwiązać ten problem, możesz utworzyć funkcję pomocniczą, która będzie używać ObjCClass zamiast typu ogólnego, a następnie użyć getOriginalKotlinClass, aby pobrać klasę ViewModel do utworzenia instancji:

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

Gdy zechcesz wywołać funkcję z poziomu Swift, możesz napisać funkcję ogólną typu T : ViewModel i użyć T.self, która może przekazać ObjCClass do funkcji resolveViewModel.

Łączenie zakresu ViewModel z cyklem życia SwiftUI

Następnym krokiem jest utworzenie IosViewModelStoreOwner, które implementuje interfejsy (protokoły) ObservableObjectViewModelStoreOwner. Powodem użycia ObservableObject jest możliwość użycia tej klasy jako @StateObject w kodzie 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()
    }
}

Ten właściciel umożliwia pobieranie wielu typów ViewModel, podobnie jak w Androidzie. Cykl życia tych obiektów ViewModel kończy się, gdy ekran korzystający z IosViewModelStoreOwner zostanie odinstalowany i wywoła deinit. Więcej informacji o deinicjalizacji znajdziesz w oficjalnej dokumentacji.

W tym momencie możesz po prostu utworzyć instancję IosViewModelStoreOwner jako @StateObject w widoku SwiftUI i wywołać funkcję viewModel, aby pobrać 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
    }
}

Niedostępne w Kotlin Multiplatform

Niektóre interfejsy API dostępne na Androidzie nie są dostępne w Kotlin Multiplatform.

Integracja z Hilt

Ponieważ Hilt nie jest dostępny w projektach Kotlin Multiplatform, nie możesz bezpośrednio używać elementów ViewModel z adnotacją @HiltViewModelcommonMain sourceSet. W takim przypadku musisz użyć alternatywnego frameworka DI, np. Koin, kotlin-inject, Metro lub Kodein. Wszystkie platformy DI, które działają z Kotlin Multiplatform, znajdziesz na stronie klibs.io.

Obserwowanie przepływów w SwiftUI

Obserwowanie przepływów w SwiftUI nie jest bezpośrednio obsługiwane. Możesz jednak użyć biblioteki KMP-NativeCoroutines lub SKIE, aby włączyć tę funkcję.