reactivestate-core

Core APIs for multiplatform ViewModels (ReactiveViewModel), transforming StateFlow, intercepting the MutableStateFlow value setter, coroutine-locals (like thread-locals, or CompositionLocal for coroutines) and many other utilities.

Deriving/transforming StateFlow

val number = MutableStateFlow(0)

// For complex apps you often need reactive StateFlows. That's what derived() is for.
// This StateFlow is automatically recomputed whenever number's value is changed.
// IMPORTANT: You have to use get(number) instead of number.value
val doubledNumber: StateFlow<Int> = derived { 2 * get(number) }

// Here we only compute the value while someone is subscribed to changes (autoRun,
// derived or collect). This can be important for expensive computations/operations.
val onDemandDoubledNumber = derived(initial = 0, started = WhileSubscribed()) {
// Note: you could even call suspension functions from within this block
// to e.g. fetch something from the backend.
someSuspendFun(2 * get(number))
}

With derived you can construct a new StateFlow based on one or multiple other StateFlow instances. It behaves similar to the Jetpack Compose derivedStateOf, but based on StateFlow and is thus more universal (e.g. can even be used with a native UI).

If you want to call suspend functions, you have to use derived(initial = ...) with an initial value. If you don't have access to a CoroutineLauncher (e.g. ReactiveViewModel) or CoroutineScope you can use derivedWhileSubscribed. This variant only recomputes while someone is subscribed.

One of the biggest advantages of derived is that with StateFlow your code becomes much more readable than Flow based chains of combine (with Pair or Triple), map, flatMapLatest, conflate, stateIn, etc. Also, derived helps you write more correct code, while Flow based chains can lead to difficult bugs (e.g. an innocent filter can lead to outdated values in the UI).

Observing StateFlows

val base = MutableStateFlow(0)
val extra = MutableStateFlow(0)

autoRun {
// This code block is re-executed whenever either of the numbers change.
// IMPORTANT: You have to use get(base) instead of base.value
if (get(base) + get(extra) 10) {
alert("You're flying too high")
}
}

You can use autoRun to observe one or multiple StateFlows in a similar way to Jetpack Compose. The lambda block passed to autoRun is re-executed whenever any of the StateFlows passed to get() is changed. As shown in the example, you can even use multiple get() calls to observe several StateFlows at once.

In classic Android Fragment/Activity code you can use autoRun to keeping the UI in sync with your ViewModel. Depending on the context in which autoRun is executed, this observer is automatically tied to a CoroutineScope (e.g. the ViewModel's viewModelScope) or in case of a Fragment/Activity to the onStart()/onStop() lifecycle in order to prevent accidental crashes and unnecessary resource consumption.

Multiplatform ViewModels, loading state and error handling

class ExampleViewModel(scope: CoroutineScope, val repository: ExampleRepository) : ReactiveViewModel(scope) {
val inputFieldValue = MutableStateFlow("default")

fun submit() {
// The launch function automatically catches exceptions and increments/decrements the loading indicator.
// This way you can't forget the fundamentals that have to be always handled correctly.
launch {
repository.submit(inputFieldValue.value)
}
}
}

@Composable
fun MyScreen() {
val viewModel = reactiveViewModel(onError = { /* ...show error dialog... */ }) {
ExampleViewModel(scope)
}
val loading by viewModel.loading.collectAsStateWithLifecycle()
if (loading != 0) {
// ...show loading overlay...
}
// ...render UI...
}

This module provides a new experimental ReactiveViewModel base class for ViewModels, but also the older BaseReactiveState which is more work optimized for the older Android Fragment/Activity based UIs.

The differences compared to the androidx ViewModel class are:

  • Convention for automatic error handling: Exceptions in coroutines are caught automatically and won't crash your whole app.

  • Convention for loading indicators: There's a default loading indicator used for all launched coroutines. You can also use multiple different indicators via launch(withLoading = otherLoading) { ... }.

  • Supports coroutine-locals via ContextualVal (see next section), scoped to the ViewModel's CoroutineScope.

  • Gets the CoroutineScope as a constructor argument instead of creating its own. This might sound innocent/unnecessary, but it facilitates many useful things based on ContextualVal like OnInit or nested child ViewModels that implicitly reuse the parent's ContextualVals.

Coroutine-locals via ContextualVal (thread-locals / CompositionLocal for coroutines)

// A Boolean, defaulting to false, that can be overridden within the CoroutineContext
val ContextualIsFooEnabled = ContextualVal("ContextualIsFooEnabled") { false }

suspend fun foo() {
// This retrieves the value from the CoroutineContext
val isFooEnabled = ContextualIsFooEnabled.get()
println(isFooEnabled)
}

suspend fun bar() {
foo() // prints false
ContextualIsFooEnabled.with(true) {
foo() // prints true
}
}

// Alternative access without suspend
println(ContextualIsFooEnabled.get(coroutineScope))
println(ContextualIsFooEnabled.get(coroutineContext))

With ContextualVal you can define a val for which the value can be different depending on the coroutine and code block. This is somewhat similar to the Jetpack Compose CompositionLocal, but for coroutines.

IMPORTANT: The default value returned by the lambda block is not necessarily executed globally. Instead, the computed default value is attached to the root of the ViewModel's CoroutineScope. The global storage can be overridden by injecting ContextualValRoot into the CoroutineContext. Each ViewModel instance has a ContextualValRoot injected by default.

Another example where a ViewModel provides optionally customizable feature-specific loading indicators:

class ExampleViewModel(scope: CoroutineScope) : ReactiveViewModel(scope) {
val submitLoading = ContextualSubmitLoading.get(scope)

fun submit() {
launch(withLoading = submitLoading) {
// ...
}
}

companion object {
// Define a feature specific loading indicator that defaults to the default `loading` indicator
val ContextualSubmitLoading = ContextualVal("ContextualSubmitLoading") { ContextualLoading.get(it) }
}
}

@Composable
fun MyScreen() {
val viewModel = reactiveViewModel(onError = { /* ... */ }) {
// We override submitLoading to be its own loading indicator instance instead of the default
ExampleViewModel(scope + ExampleViewModel.ContextualSubmitLoading.valued { MutableStateFlow(0) })
}
}

// Alternatively, during app initialization you could set a new default globally.
ExampleViewModel.ContextualSubmitLoading.default = { MutableStateFlow(0) }

MutableStateFlow interceptors

You can use beforeUpdate/afterUpdate/withSetter on a MutableStateFlow to execute additional code on every update.

If you prefer the following (at first unusual) approach, you can also reduce boilerplate in ViewModels because you can turn this:

private val _state = MutableStateFlow("value")
public val state: StateFlow<String> = _state.asStateFlow()

public fun updateState(value: String) {
_state.value = value
// ...some extra logic...
}

into this:

public val state: MutableStateFlow<String> = MutableStateFlow("value").afterUpdate {
// ...some extra logic...
}

Convert StateFlow to MutableStateFlow

You can use toMutable to turn a StateFlow into a MutableStateFlow:

val readOnly: StateFlow<Int> = getSomeStateFlow()
val mutable: MutableStateFlow<Int> = readOnly.toMutable { value: Int ->
// This is executed whenever someone sets `mutable.value = ...`.
}

You have to ensure that readOnly also gets updated somehow whenever mutable.value is assigned. The example in the next section will give you a better idea of the whole picture.

Creating StateFlow and SharedFlow without CoroutineScope

The official coroutines library only provides stateIn and shareIn functions, but those take a CoroutineScope. Sometimes you can pass the ViewModel's CoroutineScope through multiple layers of code (ugly), but often you need (singleton) StateFlow instances shared between multiple ViewModels.

This module provides stateOnDemand and shareOnDemand and sharedFlow which only launch an internal coroutine while someone is subscribed, but can safely get garbage collected when nobody is subscribed, anymore.

For example, if you want to create a getIntFlow() function for a key-value store (e.g. SharedPreferences or NSUserDefaults) you might want to return the current result but also allow observing/collecting to receive updates. With stateOnDemand you can create such a result and using toMutable you can make it mutable:

suspend fun KeyValueStore.getIntFlow(key: String, default: Int): MutableStateFlow<Int> =
callbackFlow {
// With callbackFlow we define the collect() behavior.
// In this example, let's use a pseudo-API for getting notified whenever a key gets updated:
val listener = KeyValueStore.OnChangeListener { changedKey: String ->
if (changedKey == key) {
send(getInt(key))
}
}
addListener(listener)
awaitClose { removeListener(listener) }
}.stateOnDemand { previous: Wrapped<Int>? ->
// stateOnDemand takes a getter function which defines the StateFlow.value behavior when nobody collects.
// The previous value is also passed here, wrapped in a Wrapped() instance (which can be null if this is the
// first value access). This can be useful for caching.
getInt(key, default)
}.toMutable { value: Int ->
// toMutable defines the StateFlow.value = ... setter behavior
putInt(key, value)
}

Here we've turned a simple Flow (via callbackFlow) into a StateFlow that is safe for returning from a function and we didn't need any CoroutineScope. Then we've used toMutable to turn that into a MutableStateFlow.

State restoration

class MainViewModel(scope: CoroutineScope) : ReactiveViewModel(scope) {
// The StateFlowStore is accessible from anywhere via the CoroutineScope/CoroutineContext
val stateFlowStore: StateFlowStore = ContextualStateFlowStore.get(scope)
val count: StateFlow<Int> = stateFlowStore.getData("count", 0)
}

A StateFlowStore provides a similar API to Android's SavedStateHandle, but based on StateFlow instead of LiveData.

There's also InMemoryStateFlowStore which can be useful for unit tests.

Core APIs for multiplatform ViewModels (ReactiveViewModel), transforming StateFlow, intercepting the MutableStateFlow value setter, coroutine-locals (like thread-locals, or CompositionLocal for coroutines) and many other utilities.

Deriving/transforming StateFlow

val number = MutableStateFlow(0)

// For complex apps you often need reactive StateFlows. That's what derived() is for.
// This StateFlow is automatically recomputed whenever number's value is changed.
// IMPORTANT: You have to use get(number) instead of number.value
val doubledNumber: StateFlow<Int> = derived { 2 * get(number) }

// Here we only compute the value while someone is subscribed to changes (autoRun,
// derived or collect). This can be important for expensive computations/operations.
val onDemandDoubledNumber = derived(initial = 0, started = WhileSubscribed()) {
// Note: you could even call suspension functions from within this block
// to e.g. fetch something from the backend.
someSuspendFun(2 * get(number))
}

With derived you can construct a new StateFlow based on one or multiple other StateFlow instances. It behaves similar to the Jetpack Compose derivedStateOf, but based on StateFlow and is thus more universal (e.g. can even be used with a native UI).

If you want to call suspend functions, you have to use derived(initial = ...) with an initial value. If you don't have access to a CoroutineLauncher (e.g. ReactiveViewModel) or CoroutineScope you can use derivedWhileSubscribed. This variant only recomputes while someone is subscribed.

One of the biggest advantages of derived is that with StateFlow your code becomes much more readable than Flow based chains of combine (with Pair or Triple), map, flatMapLatest, conflate, stateIn, etc. Also, derived helps you write more correct code, while Flow based chains can lead to difficult bugs (e.g. an innocent filter can lead to outdated values in the UI).

Observing StateFlows

val base = MutableStateFlow(0)
val extra = MutableStateFlow(0)

autoRun {
// This code block is re-executed whenever either of the numbers change.
// IMPORTANT: You have to use get(base) instead of base.value
if (get(base) + get(extra) 10) {
alert("You're flying too high")
}
}

You can use autoRun to observe one or multiple StateFlows in a similar way to Jetpack Compose. The lambda block passed to autoRun is re-executed whenever any of the StateFlows passed to get() is changed. As shown in the example, you can even use multiple get() calls to observe several StateFlows at once.

In classic Android Fragment/Activity code you can use autoRun to keeping the UI in sync with your ViewModel. Depending on the context in which autoRun is executed, this observer is automatically tied to a CoroutineScope (e.g. the ViewModel's viewModelScope) or in case of a Fragment/Activity to the onStart()/onStop() lifecycle in order to prevent accidental crashes and unnecessary resource consumption.

Multiplatform ViewModels, loading state and error handling

class ExampleViewModel(scope: CoroutineScope, val repository: ExampleRepository) : ReactiveViewModel(scope) {
val inputFieldValue = MutableStateFlow("default")

fun submit() {
// The launch function automatically catches exceptions and increments/decrements the loading indicator.
// This way you can't forget the fundamentals that have to be always handled correctly.
launch {
repository.submit(inputFieldValue.value)
}
}
}

@Composable
fun MyScreen() {
val viewModel = reactiveViewModel(onError = { /* ...show error dialog... */ }) {
ExampleViewModel(scope)
}
val loading by viewModel.loading.collectAsStateWithLifecycle()
if (loading != 0) {
// ...show loading overlay...
}
// ...render UI...
}

This module provides a new experimental ReactiveViewModel base class for ViewModels, but also the older BaseReactiveState which is more work optimized for the older Android Fragment/Activity based UIs.

The differences compared to the androidx ViewModel class are:

  • Convention for automatic error handling: Exceptions in coroutines are caught automatically and won't crash your whole app.

  • Convention for loading indicators: There's a default loading indicator used for all launched coroutines. You can also use multiple different indicators via launch(withLoading = otherLoading) { ... }.

  • Supports coroutine-locals via ContextualVal (see next section), scoped to the ViewModel's CoroutineScope.

  • Gets the CoroutineScope as a constructor argument instead of creating its own. This might sound innocent/unnecessary, but it facilitates many useful things based on ContextualVal like OnInit or nested child ViewModels that implicitly reuse the parent's ContextualVals.

Coroutine-locals via ContextualVal (thread-locals / CompositionLocal for coroutines)

// A Boolean, defaulting to false, that can be overridden within the CoroutineContext
val ContextualIsFooEnabled = ContextualVal("ContextualIsFooEnabled") { false }

suspend fun foo() {
// This retrieves the value from the CoroutineContext
val isFooEnabled = ContextualIsFooEnabled.get()
println(isFooEnabled)
}

suspend fun bar() {
foo() // prints false
ContextualIsFooEnabled.with(true) {
foo() // prints true
}
}

// Alternative access without suspend
println(ContextualIsFooEnabled.get(coroutineScope))
println(ContextualIsFooEnabled.get(coroutineContext))

With ContextualVal you can define a val for which the value can be different depending on the coroutine and code block. This is somewhat similar to the Jetpack Compose CompositionLocal, but for coroutines.

IMPORTANT: The default value returned by the lambda block is not necessarily executed globally. Instead, the computed default value is attached to the root of the ViewModel's CoroutineScope. The global storage can be overridden by injecting ContextualValRoot into the CoroutineContext. Each ViewModel instance has a ContextualValRoot injected by default.

Another example where a ViewModel provides optionally customizable feature-specific loading indicators:

class ExampleViewModel(scope: CoroutineScope) : ReactiveViewModel(scope) {
val submitLoading = ContextualSubmitLoading.get(scope)

fun submit() {
launch(withLoading = submitLoading) {
// ...
}
}

companion object {
// Define a feature specific loading indicator that defaults to the default `loading` indicator
val ContextualSubmitLoading = ContextualVal("ContextualSubmitLoading") { ContextualLoading.get(it) }
}
}

@Composable
fun MyScreen() {
val viewModel = reactiveViewModel(onError = { /* ... */ }) {
// We override submitLoading to be its own loading indicator instance instead of the default
ExampleViewModel(scope + ExampleViewModel.ContextualSubmitLoading.valued { MutableStateFlow(0) })
}
}

// Alternatively, during app initialization you could set a new default globally.
ExampleViewModel.ContextualSubmitLoading.default = { MutableStateFlow(0) }

MutableStateFlow interceptors

You can use beforeUpdate/afterUpdate/withSetter on a MutableStateFlow to execute additional code on every update.

If you prefer the following (at first unusual) approach, you can also reduce boilerplate in ViewModels because you can turn this:

private val _state = MutableStateFlow("value")
public val state: StateFlow<String> = _state.asStateFlow()

public fun updateState(value: String) {
_state.value = value
// ...some extra logic...
}

into this:

public val state: MutableStateFlow<String> = MutableStateFlow("value").afterUpdate {
// ...some extra logic...
}

Convert StateFlow to MutableStateFlow

You can use toMutable to turn a StateFlow into a MutableStateFlow:

val readOnly: StateFlow<Int> = getSomeStateFlow()
val mutable: MutableStateFlow<Int> = readOnly.toMutable { value: Int ->
// This is executed whenever someone sets `mutable.value = ...`.
}

You have to ensure that readOnly also gets updated somehow whenever mutable.value is assigned. The example in the next section will give you a better idea of the whole picture.

Creating StateFlow and SharedFlow without CoroutineScope

The official coroutines library only provides stateIn and shareIn functions, but those take a CoroutineScope. Sometimes you can pass the ViewModel's CoroutineScope through multiple layers of code (ugly), but often you need (singleton) StateFlow instances shared between multiple ViewModels.

This module provides stateOnDemand and shareOnDemand and sharedFlow which only launch an internal coroutine while someone is subscribed, but can safely get garbage collected when nobody is subscribed, anymore.

For example, if you want to create a getIntFlow() function for a key-value store (e.g. SharedPreferences or NSUserDefaults) you might want to return the current result but also allow observing/collecting to receive updates. With stateOnDemand you can create such a result and using toMutable you can make it mutable:

suspend fun KeyValueStore.getIntFlow(key: String, default: Int): MutableStateFlow<Int> =
callbackFlow {
// With callbackFlow we define the collect() behavior.
// In this example, let's use a pseudo-API for getting notified whenever a key gets updated:
val listener = KeyValueStore.OnChangeListener { changedKey: String ->
if (changedKey == key) {
send(getInt(key))
}
}
addListener(listener)
awaitClose { removeListener(listener) }
}.stateOnDemand { previous: Wrapped<Int>? ->
// stateOnDemand takes a getter function which defines the StateFlow.value behavior when nobody collects.
// The previous value is also passed here, wrapped in a Wrapped() instance (which can be null if this is the
// first value access). This can be useful for caching.
getInt(key, default)
}.toMutable { value: Int ->
// toMutable defines the StateFlow.value = ... setter behavior
putInt(key, value)
}

Here we've turned a simple Flow (via callbackFlow) into a StateFlow that is safe for returning from a function and we didn't need any CoroutineScope. Then we've used toMutable to turn that into a MutableStateFlow.

State restoration

class MainViewModel(scope: CoroutineScope) : ReactiveViewModel(scope) {
// The StateFlowStore is accessible from anywhere via the CoroutineScope/CoroutineContext
val stateFlowStore: StateFlowStore = ContextualStateFlowStore.get(scope)
val count: StateFlow<Int> = stateFlowStore.getData("count", 0)
}

A StateFlowStore provides a similar API to Android's SavedStateHandle, but based on StateFlow instead of LiveData.

There's also InMemoryStateFlowStore which can be useful for unit tests.

Core APIs for multiplatform ViewModels (ReactiveViewModel), transforming StateFlow, intercepting the MutableStateFlow value setter, coroutine-locals (like thread-locals, or CompositionLocal for coroutines) and many other utilities.

Deriving/transforming StateFlow

val number = MutableStateFlow(0)

// For complex apps you often need reactive StateFlows. That's what derived() is for.
// This StateFlow is automatically recomputed whenever number's value is changed.
// IMPORTANT: You have to use get(number) instead of number.value
val doubledNumber: StateFlow<Int> = derived { 2 * get(number) }

// Here we only compute the value while someone is subscribed to changes (autoRun,
// derived or collect). This can be important for expensive computations/operations.
val onDemandDoubledNumber = derived(initial = 0, started = WhileSubscribed()) {
// Note: you could even call suspension functions from within this block
// to e.g. fetch something from the backend.
someSuspendFun(2 * get(number))
}

With derived you can construct a new StateFlow based on one or multiple other StateFlow instances. It behaves similar to the Jetpack Compose derivedStateOf, but based on StateFlow and is thus more universal (e.g. can even be used with a native UI).

If you want to call suspend functions, you have to use derived(initial = ...) with an initial value. If you don't have access to a CoroutineLauncher (e.g. ReactiveViewModel) or CoroutineScope you can use derivedWhileSubscribed. This variant only recomputes while someone is subscribed.

One of the biggest advantages of derived is that with StateFlow your code becomes much more readable than Flow based chains of combine (with Pair or Triple), map, flatMapLatest, conflate, stateIn, etc. Also, derived helps you write more correct code, while Flow based chains can lead to difficult bugs (e.g. an innocent filter can lead to outdated values in the UI).

Observing StateFlows

val base = MutableStateFlow(0)
val extra = MutableStateFlow(0)

autoRun {
// This code block is re-executed whenever either of the numbers change.
// IMPORTANT: You have to use get(base) instead of base.value
if (get(base) + get(extra) 10) {
alert("You're flying too high")
}
}

You can use autoRun to observe one or multiple StateFlows in a similar way to Jetpack Compose. The lambda block passed to autoRun is re-executed whenever any of the StateFlows passed to get() is changed. As shown in the example, you can even use multiple get() calls to observe several StateFlows at once.

In classic Android Fragment/Activity code you can use autoRun to keeping the UI in sync with your ViewModel. Depending on the context in which autoRun is executed, this observer is automatically tied to a CoroutineScope (e.g. the ViewModel's viewModelScope) or in case of a Fragment/Activity to the onStart()/onStop() lifecycle in order to prevent accidental crashes and unnecessary resource consumption.

Multiplatform ViewModels, loading state and error handling

class ExampleViewModel(scope: CoroutineScope, val repository: ExampleRepository) : ReactiveViewModel(scope) {
val inputFieldValue = MutableStateFlow("default")

fun submit() {
// The launch function automatically catches exceptions and increments/decrements the loading indicator.
// This way you can't forget the fundamentals that have to be always handled correctly.
launch {
repository.submit(inputFieldValue.value)
}
}
}

@Composable
fun MyScreen() {
val viewModel = reactiveViewModel(onError = { /* ...show error dialog... */ }) {
ExampleViewModel(scope)
}
val loading by viewModel.loading.collectAsStateWithLifecycle()
if (loading != 0) {
// ...show loading overlay...
}
// ...render UI...
}

This module provides a new experimental ReactiveViewModel base class for ViewModels, but also the older BaseReactiveState which is more work optimized for the older Android Fragment/Activity based UIs.

The differences compared to the androidx ViewModel class are:

  • Convention for automatic error handling: Exceptions in coroutines are caught automatically and won't crash your whole app.

  • Convention for loading indicators: There's a default loading indicator used for all launched coroutines. You can also use multiple different indicators via launch(withLoading = otherLoading) { ... }.

  • Supports coroutine-locals via ContextualVal (see next section), scoped to the ViewModel's CoroutineScope.

  • Gets the CoroutineScope as a constructor argument instead of creating its own. This might sound innocent/unnecessary, but it facilitates many useful things based on ContextualVal like OnInit or nested child ViewModels that implicitly reuse the parent's ContextualVals.

Coroutine-locals via ContextualVal (thread-locals / CompositionLocal for coroutines)

// A Boolean, defaulting to false, that can be overridden within the CoroutineContext
val ContextualIsFooEnabled = ContextualVal("ContextualIsFooEnabled") { false }

suspend fun foo() {
// This retrieves the value from the CoroutineContext
val isFooEnabled = ContextualIsFooEnabled.get()
println(isFooEnabled)
}

suspend fun bar() {
foo() // prints false
ContextualIsFooEnabled.with(true) {
foo() // prints true
}
}

// Alternative access without suspend
println(ContextualIsFooEnabled.get(coroutineScope))
println(ContextualIsFooEnabled.get(coroutineContext))

With ContextualVal you can define a val for which the value can be different depending on the coroutine and code block. This is somewhat similar to the Jetpack Compose CompositionLocal, but for coroutines.

IMPORTANT: The default value returned by the lambda block is not necessarily executed globally. Instead, the computed default value is attached to the root of the ViewModel's CoroutineScope. The global storage can be overridden by injecting ContextualValRoot into the CoroutineContext. Each ViewModel instance has a ContextualValRoot injected by default.

Another example where a ViewModel provides optionally customizable feature-specific loading indicators:

class ExampleViewModel(scope: CoroutineScope) : ReactiveViewModel(scope) {
val submitLoading = ContextualSubmitLoading.get(scope)

fun submit() {
launch(withLoading = submitLoading) {
// ...
}
}

companion object {
// Define a feature specific loading indicator that defaults to the default `loading` indicator
val ContextualSubmitLoading = ContextualVal("ContextualSubmitLoading") { ContextualLoading.get(it) }
}
}

@Composable
fun MyScreen() {
val viewModel = reactiveViewModel(onError = { /* ... */ }) {
// We override submitLoading to be its own loading indicator instance instead of the default
ExampleViewModel(scope + ExampleViewModel.ContextualSubmitLoading.valued { MutableStateFlow(0) })
}
}

// Alternatively, during app initialization you could set a new default globally.
ExampleViewModel.ContextualSubmitLoading.default = { MutableStateFlow(0) }

MutableStateFlow interceptors

You can use beforeUpdate/afterUpdate/withSetter on a MutableStateFlow to execute additional code on every update.

If you prefer the following (at first unusual) approach, you can also reduce boilerplate in ViewModels because you can turn this:

private val _state = MutableStateFlow("value")
public val state: StateFlow<String> = _state.asStateFlow()

public fun updateState(value: String) {
_state.value = value
// ...some extra logic...
}

into this:

public val state: MutableStateFlow<String> = MutableStateFlow("value").afterUpdate {
// ...some extra logic...
}

Convert StateFlow to MutableStateFlow

You can use toMutable to turn a StateFlow into a MutableStateFlow:

val readOnly: StateFlow<Int> = getSomeStateFlow()
val mutable: MutableStateFlow<Int> = readOnly.toMutable { value: Int ->
// This is executed whenever someone sets `mutable.value = ...`.
}

You have to ensure that readOnly also gets updated somehow whenever mutable.value is assigned. The example in the next section will give you a better idea of the whole picture.

Creating StateFlow and SharedFlow without CoroutineScope

The official coroutines library only provides stateIn and shareIn functions, but those take a CoroutineScope. Sometimes you can pass the ViewModel's CoroutineScope through multiple layers of code (ugly), but often you need (singleton) StateFlow instances shared between multiple ViewModels.

This module provides stateOnDemand and shareOnDemand and sharedFlow which only launch an internal coroutine while someone is subscribed, but can safely get garbage collected when nobody is subscribed, anymore.

For example, if you want to create a getIntFlow() function for a key-value store (e.g. SharedPreferences or NSUserDefaults) you might want to return the current result but also allow observing/collecting to receive updates. With stateOnDemand you can create such a result and using toMutable you can make it mutable:

suspend fun KeyValueStore.getIntFlow(key: String, default: Int): MutableStateFlow<Int> =
callbackFlow {
// With callbackFlow we define the collect() behavior.
// In this example, let's use a pseudo-API for getting notified whenever a key gets updated:
val listener = KeyValueStore.OnChangeListener { changedKey: String ->
if (changedKey == key) {
send(getInt(key))
}
}
addListener(listener)
awaitClose { removeListener(listener) }
}.stateOnDemand { previous: Wrapped<Int>? ->
// stateOnDemand takes a getter function which defines the StateFlow.value behavior when nobody collects.
// The previous value is also passed here, wrapped in a Wrapped() instance (which can be null if this is the
// first value access). This can be useful for caching.
getInt(key, default)
}.toMutable { value: Int ->
// toMutable defines the StateFlow.value = ... setter behavior
putInt(key, value)
}

Here we've turned a simple Flow (via callbackFlow) into a StateFlow that is safe for returning from a function and we didn't need any CoroutineScope. Then we've used toMutable to turn that into a MutableStateFlow.

State restoration

class MainViewModel(scope: CoroutineScope) : ReactiveViewModel(scope) {
// The StateFlowStore is accessible from anywhere via the CoroutineScope/CoroutineContext
val stateFlowStore: StateFlowStore = ContextualStateFlowStore.get(scope)
val count: StateFlow<Int> = stateFlowStore.getData("count", 0)
}

A StateFlowStore provides a similar API to Android's SavedStateHandle, but based on StateFlow instead of LiveData.

There's also InMemoryStateFlowStore which can be useful for unit tests.

Packages

Link copied to clipboard
common
jvmCommon
nonJvm