Photo by Zach Reiner on Unsplash

Android Model-View-Intent with Kotlin Flow

Update from Unidirectional Data Flow with LiveData

Adam Hurwitz
ProAndroidDev
Published in
9 min readJul 21, 2020

--

You may think of ‘magic mushrooms’ when you hear ‘fungi network’. However, just like the Model-View-Intent (MVI) pattern, the forest’s organized fungi network of a connected info sharing system, like Model-View-Intent’s pattern, is indeed not magic.

The Unidirectional Data Flow (UDF) pattern does a great job to ensure information is sent in one direction from the view to the business logic, is retrieved, and processed in order to build the view. UDF alone does not organize how the data processed. The Model-View-Intent pattern structures the design of how information is shared and views are created. MVI defines a clear contract with the view and business logic using UDF. MVI🌲 ️is like an intelligently designed forest’s ecosystem vs. a one-way wave of info and events with UDF 🌊 used by itself.

Coinverse code

Also available on the Play Store.

Overview

Advantages

Components

Implement with Kotlin Flow

Alternatives

Summary

Advantages

Photo by Brandon Green on Unsplash

UDF

By itself, the Unidirectional Data Flow provides organization and performance benefits.

  • 🌊 Linear flow — View initiates events from the user and system to generate and return immutable data.
  • 📡 Async event control — Set the lifecycle and threading for the creation of the view data in the business logic.
  • 💼 Reusability — Utilize common business logic across multiple views.

MVI + UDF

Model-View-Intent builds upon Unidirectional Data Flow, better organizing the UI and business logic into view states and creating a clear contract between the UI and business logic with an interface.

  • 🖼️ UI modularization — Define the final UI results emitted and rendered in an interface contract and view state.
  • 🏗 ️Scaling — Reuse code with clear organization.
  • 🥼Unit test simply —Build tests by observing the existing view state results from the business logic.
  • 🐛 Debug quicker— Examine the current view state emitted to determine the root cause.

Kotlin Flow

  • 🏎️ Quick setup — Requires fewer libraries compared to Rx.
  • ♻️ Lifecycle management — Inherently controls the lifecycle of the operation upon launch.
  • 🧵 Threading management — Can be handled in one place when the flow is launched rather than each time data is observed. Additional customization can be added if needed.
  • 👻 Avoid nesting nightmares — The callback style pattern is harder to work with and creates the need to initialize instance variables within a function to retrieve information from within nested logic.
  • 🎨 Customization — The core c lasses, methods, and inherited classes and interfaces are clearly readable making it possible to edit or add functionality with extension functions.
  • 🚦 Ease-of-use — It is easy to learn new topics via reading the good ol’ documentation. The docs provide interactive examples.

Components

Photo by Elisa Kerschbaumer on Unsplash

By implementing Model-View-Intent with immutable intents and observing immutable state data, the architecture takes advantage of a Unidirectional Data Flow pattern.

Note: ‘Intent’ is used as a naming convention for events or actions in an app and is not the same as Android system level Intents.

Model

  • Implement with a ViewModel, also referred to as a Presenter in other implementations.
  • Observes the requests for new actions or data requests from incoming intents and processes the business logic from the intent requests
  • Creates the view states
  • Stores the view state data on lifecycle changes
  • Emits an immutable state

View

  • Implement with an activity/fragment.
  • Displays the view state
  • Initiates events/actions by the user or the Android system

Intent

  • Implement with a view interface.
  • Defines the contract between the UI and business logic
  • Creates the immutable intents that return an observable object from the view to the ViewModel
  • Creates the method(s) to render the view state(s) from the ViewModel to the view

Implement with Kotlin Flow

Photo by Łukasz Maźnica on Unsplash

Model-View-Intent can be implemented with any reactive framework. StateFlow and MutableStateFlow’s introduction in Kotlin coroutines’ 1.3.6 release makes Flow a fantastic framework from the view to the repository level. Mutable data can be updated privately as instance variables within a class and observed publicly as an immutable value.

Add the kotlinx.coroutines library with the latest version to build.gradle.

Model — ViewModel

FeedViewState.kt

  1. A Sealed class defines the view state group FeedViewState
  2. Each UI module in the view state, Feed and OpenContent are defined as a sub-class. If there is a state that emits the same data values, but represents a unique event, use a class as a data class will consider these classes’ hash code equivalent, and not emit the state. For example, when the same info can be returned more than once consecutively.

To re-use the ViewModel for more advanced use cases, the ViewModel can handle multiple types of sealed class view states.

FeedViewState.kt

FeedViewModel.kt

  1. The view state is a private MutableStateFlow. Values can only be added from the ViewModel ensuring only the ViewModel can create/update the state.
  2. The FeedView defines the intents and the contract between the model and view. It is passed into the ViewModel through a public function bindIntents that initiates observing all of the view intents. The intent Flows loadFromNetwork and selectContent are observed with onEach or the custom extension method onEachEvent for one-time events, described below.
  3. The state is created or updated based on the Resource.kt status LOADING, SUCCESS, and ERROR of the data request.
  4. The view state changes are observed in the initState intent Flow and sent to the Fragment via the FeedView interface render function.
FeedViewModel.kt

One-time Events

One-time events don’t persist and include navigation, dialogs, or errors. To handle this with Flow, an Event.kt wraps one-time Flows and is emitted with the custom extension function onEachEvent. LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) by Jose Alcérreca explains this issue well in a discussion about LiveData. The event concept applies to all reactive frameworks.

CoroutineScope and CoroutineDispatcher

For JUnit tests, it is important to manually pass the CoroutineScope into the ViewModel and use the custom getViewModelScope function to return the appropriate scope inside the ViewModel.

The same thing goes for the CoroutineDispatcher which is not used in this example but can be implemented with Craig Russell’s DispatcherProvider included in ViewModelUtils.kt.

Dagger 2 Dependency Injection for ViewModels with AssistedInject

Gabor Varadi’s Dagger Tips article provides a clear implementation guide. Moving forward, Dagger Hilt will simplify ViewModel injection once released in production.

View — Activity/Fragment

FeedViewIntent.kt

Private mutable intents can be initialized directly in the view or abstracted to a class like in FeedViewIntent.kt that defines the intents and data classes used.

  1. Intents the Fragment will use to pass Flow data to the ViewModel through the FeedView interface.
  2. Data classes the intents use to pass info.
FeedViewIntent.kt

FeedFragment.kt

  1. Initialize the mutable intents stored in FeedViewIntent.
  2. Bind the FeedView interface to the ViewModel in onViewCreated to begin observing the intents defined and emitted from FeedView.
  3. Implement the functions defined in FeedView that emit Flow data to the ViewModel by overriding the interface functions.
  4. Emit Flow data intents. a)initState returns a true Flow by default as it is responsible for binding all of the view states in the ViewModel when the view is created. b) loadFromNetwork occurs when the view is created for the first time. c) selectContent emits when an item in the newsfeed is selected by the user in FeedAdapter.kt.
  5. render binds the view state from the ViewModel to the view. The state values are used to create the UI as one would normally build and bind UI views.
FeedFragment.kt
FeedAdapter.kt

Intent — View Interface

The view interface is the connective network of Model-View-Intent, as it defines the contract of how the view and business logic interact, shares intents to the ViewModel, and renders the view states to the UI.

  1. initState, loadFromNetwork, and selectContent are functions that return a flow emitted from the view to the ViewModel.
  2. render binds the view state emitted from the ViewModel to the Fragment. FeedView manually injects states into the view from the ViewModel vs. observing many pieces of data in different places liked done before in the Unidirectional Data Flow.

Multiple render functions can be defined if more than one view state sealed class is needed.

FeedView.kt

Alternatives

Photo by Vlad Shapochnikov on Unsplash

RxJava’s Subject or Relay also work well with MVI. LiveData can be used too but will add undue complexity.

LiveData

Prior to utilizing Kotlin Flow, LiveData was the reactive framework in both the repository responses and ViewModel view state in version one, Android Unidirectional Data Flow with LiveData.

Not a good fit for Repositories — As outlined in Android Unidirectional Data Flow with LiveData — 2.0, ‘LiveData Can be Problematic in the Repositorybecause its’ subscription is tied to a view lifecycle. LiveData is not relevant for non-UI related transformations where data needs to be processed in the Repository or ViewModel prior to populating the view state.

Adds view state complexity — After refactoring Flow to replace LiveData in the Repository, LiveData is still used in the view state. Additional complexity remains due to the need for a mutable and immutable view state class. Again, because LiveData is bound to a view’s lifecycle, a mutable view state cannot be initiated and observed in the ViewModel as with Flow or RxJava. As a workaround, the mutable view state adds values in the ViewModel, and the immutable view state is observed directly by the view instead of being emitted in the ViewModel through an interface contract like withFeedView.

FeedFragment.kt and FeedViewState.kt

With Flow, the ViewModel observes state changes bound to the ViewModel as seen above where getViewModelScope sets the lifecycle to viewModelScope. With the release of StateFlow, Roman Elizarov, Kotlin Libraries Team Lead, also recommends to consider using MutableStateFlow as a MutableLiveData replacement.

RxJava

For a comparison of Kotlin Flow and RxJava strengths see Android Unidirectional Data Flow — Kotlin Flow vs. RxJava. Spoiler, Kotlin Flow requires less setup than RxJava, manages the lifecycle by default, handles threading cleanly, avoids nesting, allows customization with extensions, and is relatively easy to use. Rx is still a great and powerful option as it has been battle-tested over the past six years and at the time of this post, holds the vast majority of market share in adoption for Android reactive frameworks.

Summary

Photo by Dave Robinson on Unsplash

The Unidirectional Data Flow 🌊 is a great initial step. With the addition of Model-View-Intent 🌲, and considering well-defined view states, it improves performance, read, and testability for the Coinverse beta app. Best yet, as MVI was originally popularized in JavaScript’s Cycle.js, this pattern is effective across languages, frameworks, and platforms.

View Contract

The biggest takeaway for me is the view interface intents contract, FeedView. The FeedView contract is a concise map of all of the functionality between the view and the ViewModel. This concept was introduced to me by Yigit Boyar on the core Android team at the 2019 Android Dev Summit during an ‘office hours’ style session and took me time to realize how to refactor UDF into MVI + UDF properly.

Unit Tests

With the initial UDF Coinverse local JUnit 5 tests I spent too much time to debug the order of events fired and values observed. MVI will simplify this because instead of observing many individual values, each view state is observed as the direct result of an intent. Follow me on Medium for updates on using MVI for local Junit 5 tests.

This pattern is largely inspired by Hannes Dorfmann’s epic eight-part series, Reactive Apps With Model-View-Intent using RxJava. Hannes is also the creator of the Android MVI library Mosby. The series is summarized in episode 103 of the Fragmented podcast with Kaushik Gopal and Donn Felker. I highly recommend these resources. Also, thanks to Michael Huh for our discussions on your team’s version of MVI. It took me multiple times to fully digest the concepts so it is worth taking your time.

Explore Coinverse on the Google Play Store or the open-source code. Please clap if you like the above. The longer the clap button is pressed the more claps. 👏🏻

📝 Notes

I’m Adam Hurwitz, writer of code and more. Follow me on Medium.

--

--