A series of KMM(Kotlin Multiplatform Mobile) foundation libraries.
Warning: We strongly recommend that you do not use this library directly. Although we use these libraries in our production apps, we suggest using this project only as an implementation reference. If you would like to fork and modify it yourself, please comply with the AGPL v3 license.
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.- This module has been removed from the main branch because the Realm SDK is no longer actively maintained. We have migrated our apps to Room Multiplatform. If you still rely on Realm, please refer to the relevant commit for implementation details.
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 |
|
kmm_kv |
dev.suoxing.kmm:kmm-kv |
|
kmm_database |
dev.suoxing.kmm:kmm-database |
|
kmm_analytics |
dev.suoxing.kmm:kmm-analytics |
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 {
commonMain.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>(), KoinComponentimport 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)