Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A series of KMM(Kotlin Multiplatform Mobile) foundation libraries.

License

Notifications You must be signed in to change notification settings

SuoxingTech/KMMFoundation

Repository files navigation

KMMFoundation

A series of KMM(Kotlin Multiplatform Mobile) foundation libraries.

Introduction

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. Jetpack DataStore for Android and NSUserDefaults for iOS.
  • kmm-database which provides wrapped Realm's Kotlin SDK.
  • kmm-analytics which provides wrapped FirebaseAnalytics & FirebaseCrashlytics.

For more information about released packages you can visit Packages under our organization space.

Latest version

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

Using GitHub Registry

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

Add Dependency

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")

Start using kmm-arch ViewModel

dev.suoxing.kmm_arch.viewmodel.ViewModel aims to make ViewModel cross-platform. So that most bussiness logic code could be placed in shared module.

Implementing your ViewModel 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

Android Compose with LifecycleExt

import androidx.lifecycle.compose.collectAsStateWithLifecycle
fun HomeScene(
 ...
 viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
 ...
) {
 val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
}

iOS with SwiftUI @StateObject

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 uiState with @Bindable macro. Keep in mind that our goal for making shared ViewModel is to achieve Unidirectional Data Flow (UDF)

About

A series of KMM(Kotlin Multiplatform Mobile) foundation libraries.

Resources

License

Stars

Watchers

Forks

Packages

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /