ViewModel¶
1. Add dependency¶
- Add
mavenCentral()torepositorieslist inbuild.gradle.kts/settings.gradle.kts.
// settings.gradle.kts
dependencyResolutionManagement {
[...]
repositories {
mavenCentral()
[...]
}
}
- Add dependency to
build.gradle.ktsof your shared module (must useapiconfiguration).
// build.gradle.kts
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api("io.github.hoc081098:kmp-viewmodel:0.8.0")
}
}
}
}
- Expose
kmp-viewmodeltoDarwinnative side.
// Cocoapods
kotlin {
cocoapods {
[...]
framework {
baseName = "shared"
export("io.github.hoc081098:kmp-viewmodel:0.8.0") // required to expose the classes to iOS.
}
}
}
// -- OR --
// Kotlin/Native as an Apple framework
kotlin {
ios {
binaries {
framework {
baseName = "shared"
export("io.github.hoc081098:kmp-viewmodel:0.8.0") // required to expose the classes to iOS.
}
}
}
}
Snapshots of the development version are available in Sonatype's snapshots repository.
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
[...]
}
}
// build.gradle.kts
dependencies {
api("io.github.hoc081098:kmp-viewmodel:0.7.2-SNAPSHOT")
}
2. Overview¶
public expect abstract class ViewModel {
public constructor()
public constructor(vararg closeables: Closeable)
public val viewModelScope: CoroutineScope
public fun addCloseable(closeable: Closeable)
protected open fun onCleared()
}
- The ViewModel has a
viewModelScopewhich is aCoroutineScopethat is cancelled when the ViewModel is cleared. Once the ViewModel is cleared, all coroutines launched in this scope will be cancelled.
addCloseablemethod is used to addCloseablethat will be closed directly beforeonClearedis called.
onClearedis called when the ViewModel is cleared, you can override this method to do some clean up work. But it is recommended to use theaddCloseablemethod instead of overridingonClearedmethod.
3. Create your ViewModel in commonMain source set.¶
import com.hoc081098.kmp.viewmodel.Closeable
import com.hoc081098.kmp.viewmodel.ViewModel
import com.hoc081098.kmp.viewmodel.wrapper.NonNullFlowWrapper
import com.hoc081098.kmp.viewmodel.wrapper.NonNullStateFlowWrapper
import com.hoc081098.kmp.viewmodel.wrapper.wrap
class ProductsViewModel(
private val getProducts: GetProducts,
) : ViewModel() {
private val _eventChannel = Channel<ProductSingleEvent>(Int.MAX_VALUE)
private val _actionFlow = MutableSharedFlow<ProductsAction>(Int.MAX_VALUE)
val stateFlow: NonNullStateFlowWrapper<ProductsState>
val eventFlow: NonNullFlowWrapper<ProductSingleEvent> = _eventChannel.receiveAsFlow().wrap()
init {
// Close _eventChannel when ViewModel is cleared.
addCloseable(_eventChannel::close)
stateFlow = _actionFlow
.transformToStateFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = ProductsState.INITIAL,
)
.wrap()
}
// Do business logic here, to convert `ProductsAction`s to `ProductsState`s.
private fun SharedFlow<ProductsAction>.transformToStateFlow(): Flow<ProductsState> = TODO()
fun dispatch(action: ProductsAction) {
_actionFlow.tryEmit(action)
}
}
4. Use common ViewModel in each platform.¶
4.1. Android¶
Use the ViewModel as a normal AndroidX Lifecycle ViewModel.
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun ProductsScreen(
viewModel: ProductsViewModel = viewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
// Render UI based on state.
}
4.2. non-Android¶
- Make sure that you call
clear()on your ViewModel when it’s no longer needed, to properly cancel theCoroutineScopeand close resources. For example, you should callclear()indeinitblock when usingViewModelinDarwintargets (ios,macos,tvos,watchos).
- In addition, you should create a wrapper of the common
ViewModelin each platform and use flow wrappers provided by this library (NonNullFlowWrapper,NullableFlowWrapper,NonNullStateFlowWrapper,NullableStateFlowWrapper), to consume the commonViewModeleasily and safely.
For more details, please check kmp viewmodel sample.
The below example is using NonNullStateFlowWrapper.subscribe(scope:onValue:) method
to consume the Flows in Darwin targets (Swift language).
@MainActor
class IosProductsViewModel: ObservableObject {
private let commonVm: ProductsViewModel
@Published private(set) var state: ProductsState
init(commonVm: ProductsViewModel) {
self.commonVm = commonVm
self.state = self.commonVm.stateFlow.value
self.commonVm.stateFlow.subscribe(
scope: self.commonVm.viewModelScope,
onValue: { [weak self] in self?.state = $0 } // use weak self to avoid retain cycle.
)
}
func dispatch(action: ProductsAction) { self.commonVm.dispatch(action: action) }
deinit { self.commonVm.clear() }
}