Multiplatform ViewModels¶
This library allows creating multiplatform ViewModels (inherited from BaseReactiveState
) and also provides a by reactiveState
helper for attaching it to Android’s Activity
or Fragment
with proper lifecycle handling.
// This is a multiplatform "ViewModel". It doesn't inherit from Android's ViewModel
// and doesn't depend on any Android code.
// It can persist saved instance state via StateFlowStore. On iOS you could pass
// e.g. an InMemoryStateFlowStore.
// The base class for such ViewModels (and other "living" stateful objects)
// is BaseReactiveState. You can alternatively use the ReactiveState interface
// e.g. together with delegation.
class MultiPlatformViewModel(
scope: CoroutineScope,
// The StateFlowStore allows for state restoration (like onSaveInstanceState).
// See next section for details.
private val store: StateFlowStore,
// For dependency injection
private val dependency: SomeDependency,
) : BaseReactiveState<MyEvents>(scope) {
val data = MutableStateFlow("hello")
fun doSomething() {
// In contrast to scope.launch, the BaseReactiveState.launch function
// automatically catches exceptions and forwards them to eventNotifier
// via ErrorEvents.onError(throwable).
launch {
val result = callSomeSuspendFun()
data.value = result
// BaseReactiveState comes with a built-in eventNotifier
eventNotifier { ... }
}
}
}
interface MyEvents : ErrorEvents {
fun onResultReceived()
}
// Alternatively, this is an example in case you want to use Android-native ViewModels.
// This ViewModel can persist state with SavedStateHandle (no more onSaveInstanceState() boilerplate)
class StateViewModel(val handle: SavedStateHandle, dependency: SomeDependency) : ViewModel() {
// ...
}
// Example integration with Android
class MainFragment : Fragment() {
// Attaches a multiplatform ViewModel (ReactiveState) to the fragment.
// Within the "by reactiveState" block you have access to scope and stateFlowStore which are taken from an
// internally created Android ViewModel that hosts the ReactiveState instance.
private val multiPlatformViewModel by reactiveState {
MultiPlatformViewModel(scope, stateFlowStore, SomeDependency())
}
// Alternatively, for Android ViewModels there's stateViewModel and buildViewModel
private val viewModel by stateViewModel { handle -> StateViewModel(handle, SomeDependency()) }
// With buildOnViewModel you can create an arbitrary object that lives on an internally created wrapper ViewModel.
// The "by reactiveState" helper is using this internally.
private val someObjectOnAViewModel by buildOnViewModel { SomeObject() }
}
With buildOnViewModel
you can create your fully custom ViewModel if prefer. However, BaseReactiveState
comes with batteries included:
- event handling: Send one-time events to the UI via
eventNotifier
. - error handling:
launch
catches all errors and forwards them toeventNotifier
viaErrorEvents.onError(throwable)
. - lifecycle handling: With
by reactiveState
theeventNotifier
is automatically observed in the>= STARTED
state. - loading indicators:
launch
automatically maintains a loadingStateFlow
, so you can show a loading indicator in the UI while the coroutine is running. This can use either the defaultloading
or any customMutableValueFlow<Int>
, so you can distinguish different loading states, each having its own loading indicator in the UI.
For Android, ReactiveState’s by reactiveState
, by buildViewModel
, by stateViewModel
, by buildOnViewModel
, and similar extension functions allow creating a ViewModel
by directly instantiating it.
This results in more natural code and allows passing arguments to the ViewModel
.
Internally, these helper functions are simple wrappers around viewModels
, ViewModelProvider.Factory
and AbstractSavedStateViewModelFactory
.
They just reduce the amount of boilerplate for common use-cases.
Launching coroutines¶
To give you a deeper understanding what happens when you run:
launch {
// ...code block...
}
That piece of code is similar to writing this:
scope.launch {
loading.atomicIncrement() // however that works
try {
// ...code block...
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
eventNotifier.invoke { onError(e) } // explicitly writing invoke for clarity only
} finally {
loading.atomicDecrement() // however that works
}
}
Customizing by reactiveState
¶
You can implement the OnReactiveStateAttached
interface on your Fragment
/Activity
in order to customize the attachment procedure:
class MyFragment : Fragment(), OnReactiveStateAttached, ErrorEvents {
val viewModel by reactiveState { ... }
override fun onReactiveStateAttached(reactiveState: ReactiveState<out ErrorEvents>) {
autoRun { setLoading(get(reactiveState.loading) > 0) }
}
fun setLoading(isLoading: Boolean) {
// ...
}
}
Alternatively, if you want to support multiple ViewModels and merge all their loadings states into one:
class MyFragment : Fragment(), OnReactiveStateAttached, ErrorEvents {
// We'll merge the loading states of all ReactiveState instances into this one
val loading = MutableValueFlow(0)
val viewModel by reactiveState { ... }
val viewModel2 by reactiveState { ... }
val viewModel3 by reactiveState { ... }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
autoRun { setLoading(get(loading) > 0) }
}
fun setLoading(isLoading: Boolean) {
// ...
}
override fun onReactiveStateAttached(reactiveState: ReactiveState<out ErrorEvents>) {
lifecycleScope.launch {
// Sum all loading states
loading.incrementFrom(reactiveState.loading)
}
}
}
MutableStateFlow interceptors¶
You can use beforeUpdate
/afterUpdate
/withSetter
on a MutableStateFlow
to execute additional code on every
update. This can also be helpful to 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...
}