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.Main
dokumentacji.
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) ObservableObject
i ViewModelStoreOwner
. 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ą @HiltViewModel
w commonMain
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ę.