A series of KMM(Kotlin Multiplatform Mobile) foundation libraries.
Official release of KMM libraries provided by SuoxingTech. Including:
kmm-arch
which provides fundamental MVVM Architecture Components (i.e.ViewModel
).kmm-kv
which provides Key-value storage solution. JetpackDataStore
for Android andNSUserDefaults
for iOS.kmm-database
which provides wrappedRealm
's Kotlin SDK.kmm-analytics
which 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 {
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_analytics
may 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()
}
}
}