A series of KMM(Kotlin Multiplatform Mobile) foundation libraries.
Official release of KMM libraries provided by SuoxingTech. Including:
kmm-archwhich provides fundamental MVVM Architecture Components (i.e.ViewModel).kmm-kvwhich provides Key-value storage solution. JetpackDataStorefor Android andNSUserDefaultsfor iOS.kmm-databasewhich provides wrappedRealm's Kotlin SDK.kmm-analyticswhich provides wrappedFirebaseAnalytics&FirebaseCrashlytics.
For more information about released packages you can visit Packages under our organization space.
| Library | Dependency | Version |
|---|---|---|
kmm_arch |
dev.suoxing.kmm:kmm-arch |
github |
kmm_kv |
dev.suoxing.kmm:kmm-kv |
github |
kmm_database |
dev.suoxing.kmm:kmm-database |
github |
kmm_analytics |
dev.suoxing.kmm:kmm-analytics |
github |
Artifacts are currently published to GitHubPackages, which requires additional config on dependencyResolutionManagement block:
dependencyResolutionManagement {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/SuoxingTech/KMMFoundation")
val prop = java.util.Properties().apply {
load(java.io.FileInputStream(File(rootDir, "local.properties")))
}
val githubUser: String? = prop.getProperty("github.user")
val githubToken: String? = prop.getProperty("github.token")
credentials {
username = githubUser
password = githubToken
}
}
}
}sourceSets {
val commonMain by getting {
dependencies {
api("dev.suoxing.kmm:kmm-arch:$kmm_arch_ver")
api("dev.suoxing.kmm:kmm-kv:$kmm_kv_ver")
api("dev.suoxing.kmm:kmm-database:$kmm_database_ver")
}
}
}
kmm_analyticsmay have issue on iOS builds. you can use only android artifact by add to android dependency like:implementation("dev.suoxing.kmm:kmm_analytics-android:$kmm_analytics_ver")
dev.suoxing.kmm_arch.viewmodel.ViewModel aims to make ViewModel cross-platform. So that most bussiness logic code could be placed in shared module.
It's simple to implement your own ViewModel class, just subclassing dev.suoxing.kmm_arch.viewmodel.ViewModel and define UiState class (must be data class) like following code:
class HomeViewModel : ViewModel<HomeUiState>() {}
In addition, you might need koin to deal with dependency injection, in that case you need to wrap another BaseViewModel yourself:
import dev.suoxing.kmm_arch.viewmodel.ViewModel import org.koin.core.component.KoinComponent abstract class BaseViewModel<T: Any>() : ViewModel<T>(), KoinComponent
import androidx.lifecycle.compose.collectAsStateWithLifecycle fun HomeScene( ... viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel(), ... ) { val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle() }
For iOS you need to make a bridge helper, here is the sample code we are using internally:
import Foundation import shared import SwiftUI /// /// Wrap KMM ViewModel to `ObservableObject` with a published `uiState`. /// @MainActor class ObservableViewModel<UiState: AnyObject, VM: BaseViewModel<UiState>> : ObservableObject{ /// /// `UiState` type can be inferred from `vm` instance passed to wrapper. /// @Published var uiState: UiState /// /// Real KMM ViewModel reference. /// /// Named as `actor` in order to inform developer to invoke this only for handling user actions. /// /// Little bit ugly, but I think it's okay. π /// let actor: VM init (_ vm: VM) { // peek latest value to guarantee that `uiState` is always non-null. self.uiState = vm.peek() self.actor = vm vm.collect { value in // update `uiState` everytime `uiStateFlow` emits new value. self.uiState = value } } /// /// - It is recommended to call it in [onAppear], which will check whether [viewModelScope] is active (because it may have been cancelled). /// If it is not active, a new [viewModelScope] can be created in time. /// - In fact, it is a manual implementation of life cycle management, which is equivalent to starting the viewModel when [onAppear] and pausing it when [onDisapper]. /// (because it just cancels the viewModelScope), deinit is called by the system /// func activate() { // debugPrint(self.actor.description, ":vm:activate") self.actor.onViewAppear(onNewScope: { // onNewScope is called when the ViewModel creates a new [viewModelScope] // Because the viewModelScope was canceled, uiStateFlow needs to be collected again. Otherwise, it will not respond to the new state. self.actor.collect { value in // update `uiState` everytime `uiStateFlow` emits new value. self.uiState = value } }) } /// /// - It is recommended to call it in onDisappear, which will cancel [viewModelScope] /// func clear() { // manually cancel coroutine scope, since `deinit` may never be called. // debugPrint(self.actor.description, ":vm:clear") self.actor.onCleared() } deinit { // cancel coroutine scope debugPrint(self.actor.description, ":vm:deinit") self.actor.onCleared() } }
Then use it as any other @StateObject:
struct MainScene: View { @StateObject private var viewModel = ObservableViewModel(HomeViewModel()) var body: some View { MyView() .onAppear { viewModel.activate() viewModel.actor.start() // custom function initializing scene data } .onDisappear { viewModel.clear() } } }
If your App Targets 17+, ViewModels can also benifit from @Observable macro. Thus you can define ObservableViewModel like this:
@Observable class ObservableViewModel<UiState: AnyObject, VM: BaseViewModel<UiState>> { var uiState: UiState ... }
However there is a small trap here: DO NOT mix
uiStatewith@Bindablemacro. Keep in mind that our goal for making shared ViewModel is to achieve Unidirectional Data Flow (UDF)