Use FlowMVI with Android
There are multiple options on how to organize your code when working with Android. The choice depends on your project's specific needs and each option has certain tradeoffs.
ViewModel
Direct ViewModels
The simplest way to organize code is to implement Container
in your ViewModel.
You are not required to implement any interfaces, however. They are only served as markers/nice dsl providers.
Example that uses models from quickstart and MVVM+ style. This example is also fully implemented in the sample app.
class CounterViewModel(
repo: CounterRepository,
handle: SavedStateHandle,
) : ViewModel(), ImmutableContainer<CounterState, CounterIntent, CounterAction> {
// the store is lazy here, which is good for performance if you use other properties of the VM.
// if you don't want a lazy store, use the regular store() function here
override val store by lazyStore(
initial = Loading,
scope = viewModelScope,
) {
configure {
debuggable = BuildConfig.DEBUG
}
enableLogging()
parcelizeState(handle)
/* ... everything else ... */
reduceLambdas() // <-- don't forget that lambdas must still be reduced
}
fun onClickCounter() = store.intent {
action(ShowCounterIncrementedMessage)
updateState<DisplayingCounter, _> {
copy(counter = counter + 1)
}
}
}
Prefer to extend ImmutableContainer
as that will hide the intent
function from outside code, otherwise you'll leak
the PipelineContext
of the store to subscribers.
The upside of this approach is that it's easier to implement and use some navigation-specific features
like savedState
(you can still use them for KMP though)
The downside is that you lose KMP compatibility. If you have plans to make your ViewModels multiplatform,
it is advised to use the delegated approach instead, which is only slightly more verbose.
Delegated ViewModels
A slightly more advanced approach would be to avoid subclassing ViewModels altogether and using ContainerViewModel
that delegates to the Store.
This is a more robust and multiplatform-friendly approach that is slightly more boilerplatish, but does not require you to subclass ViewModels.
The only caveat is injecting your Container
into an instance of StoreViewModel
, and then injecting the
StoreViewModel
correctly. The implementation varies based on which DI framework you will be using, with some examples
are provided in the DI Guide
View Integration
For a View-based project, subscribe in an appropriate lifecycle callback and create two functions to render states and consume actions.
- Subscribe in
Fragment.onViewCreated
orActivity.onCreate
. The library will handle the lifecycle for you. - Make sure your
render
function is idempotent, andconsume
function does not loop itself with intents. - Always update all views in
render
, for any state change, to circumvent the problems of old-school stateful view-based Android API.
class CounterFragment : Fragment() {
private val binding by viewBinding<CounterFragmentBinding>()
private val store: CounterContainer by container() // see DI guide for implementation
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribe(container, ::consume, ::render)
with(binding) {
tvCounter.setOnClickListener(store::onClickCounter) // let's say we are using MVVM+ style.
}
}
private fun render(state: CounterState): Unit = with(binding) {
with(state) {
tvCounter.text = counter.toString()
/* ... update ALL views! ... */
}
}
private fun consume(action: CounterAction): Unit = when (action) {
is ShowMessage -> Snackbar.make(binding.root, action.message, Snackbar.LENGTH_SHORT).show()
}
}