MVI for Compose (Part 5)
- Problems and benefits of MVVM
- Custom MVI
- Practical examples, continued implementation
- Automating/reducing boilerplate
- Best practices and conclusions (you are here)
In this part I’m going to talk about some rules and recommendations which should be followed in order to avoid non-obvious problems, and draw some conclusions regarding the use of MVI approach If you are here but haven’t read the previous articles, please follow the links above to find them.
Exhaustive ‘when’
When processing intents using handleIntent
and reduce
methods, you have to use the construction ‘when’ and avoid using ‘else’ block in order to make compilator process all new intents and signal that intent needs processing if it has just been added. In the case of reduce you just have to return previous screen state if nothing is happening.
Intent on first load
There are cases when it’s necessary to start some intent along with screen start and initialising MVI. In this case two approaches can be used:
- Start LaunchedEffect(Unit) from @Composable
- Use the init method in MVI class
However, there are cases when LaunchedEffect(Unit) starts several times, for instance, when a configuration has been changed, so it is preferable to use the second method.
Fields in MviProcessor class
If you need to add some additional fields to your MVI which contain data, it’s better to stop and refactor the contract. All data should be transferred to screen state and store only there and in intents. In order to prevent human error, Lint rules can be written for large projects which will prohibit creating fields at the class level.
Use Holders to handle state and intents
As Google recommends, when you work with Jetpack Compose it is better to use middleware function which will contain all non-UI rendering-related operations.
Screen or component should only depend on state and provide callbacks for user interactions
An example of a more complex Holder you can see in the project.
Split MVI’s for unrelated components
Compose allows you to use LocalViewModelStoreOwner.current to inject ViewModel into different screen components. If your screen parts are independent, it doesn’t make sense to handle them in one contract, just break the contract into independent parts and assign a different MVI to each part.
Full code you can find in the project
Navigation
Nowhere in the code you will find examples of navigation using this approach. The first reason is that presentational patterns are not supposed to be responsible for it. And another reason is that there is a variety of different methods. When working with the approach described above I used inject of the router into MviProcessor** class constructor. It allowed with certain intents to trigger navigation events and separate them from MVI logic.
However, there is a caveat: intents come from the user and from the system without any filters, so they might get duplicated (for example, a user double-clicked the Next button). There are two options in this case. Either the button goes into disabled state after the first intent, or navigation events are passed through “debounce”. In the first case, state contract grows a little, in the second one, processing of navigation events becomes more complicated. The choice is up to the reader.
Testing
Testing is no different from testing MVVM. You create an instance of your MVI, subscribe to viewState updates, and after running intents, validate the UI state
Conclusions
The approach described in previous articles shows a simple way to implement MVI and examples of its use. It was on a production application and allows you to quickly develop UI and UI logic for screens.
This approach is a great replacement for the old development of mvp applications with MVI if you use Jetpack Compose, and with proper automation it perfectly replaces old MVP (Model-view-presenter) approaches for designing small screen, it also scales and decomposes well for large projects.
It does not work with cross-platform and relies on ViewModel, if this is a problem for you, you can use Ribs or Decompose.
It is not a library, and it is in your power to expand and customise the functionality as you need.
It doesn’t take a lot of space, just two code files which can be placed in a separate module or on artifactory.
It is easy to use. A lot of MVI libraries provide more functionality but have a higher entry threshold and complex api.
That’s all for now, your comments and thoughts are welcome. See you soon!