A Song of Praise to MvRx

Indri Yunita
Ruangguru Engineering
4 min readJan 31, 2020
Photo by Fidel Fernando / Unsplash

One-Way All the Way

I guess by now we can all agree that unidirectional (one-way) data flow has a big role in creating a reliable presentation layer. Just to make sure we are on the same page, unidirectional data flow is a technique of applying one-way mutations on an immutable data state. In this pattern, mutations can only be done outside the presentation/view, while presentation/view is made to be dumb (stateless), only passively waiting to be notified when some state changes occur. This concept was mainly popularized by Redux, and most commonly used alongside with React.

Current trend shows that the concept has been adopted in Android development as well, although more commonly known as MVI (Model-View-Intent) architecture pattern in the Android world. Several frameworks built by established enterprises based on this pattern for example are MvRx by AirBnb, Mobius by Spotify, StateMachine by Tinder, and MVICore by Badoo.

Several months ago, we at Ruangguru decided that we needed this approach in our Android app (well, it’s a long-overdue action but I’m just glad we took it anyway). So we looked into the options, and MvRx quite stood up because of its maturity and excellency of the documentation. Also, it relies heavily on RxJava, a library that we’ve been using extensively already. After looking into the example project and making a PoC based on an actual feature in our app, we were sold.

As a side note, at some point our discussion also touched the issue of how we should use reusable and composable UI components and whether Airbnb’s Epoxy library would suit our needs. After some thinking and discussions, we decided it would not be worth it as the steps we would need to take would be huge. And with Jetpack Compose’s bright promise in the future, we thought we would just wait until Jetpack Compose is ready to be used in production.

Back to MvRx. Actually I’m kind of disappointed that I don’t find too many references of MvRx on the Internet. Or rather, I’m surprised it doesn’t get enough praise that it deserves. So consider this post that, a praise to MvRx for making Android development a little less of a pain in the a** :D

Benefits

What does that mean for us in concrete terms?

  • First off, since we made the change from the MVP pattern, where the View and the Presenter communicates through a contract interface, it means with MvRx we now don’t need to write it anymore (and yes, it gets tedious over time). So that means one less file to write :)
  • Secondly, by wrapping all the properties necessary to render a screen in a MvRxState, it makes it easier to scan over them. What we encountered before, what with less clear boundary between States and View, States often found their way to reside in View (e.g. Fragment) instead of residing strictly in Presenter. This is marked by the existence of instance variables representing States in said Fragment, all cluttered and headache-inducing. Some tip based on our experience: putting MvRxState in its own file (parallel with its Fragment and ViewModel files) makes it all the cleaner.
  • MvRx is lifecycle aware, because it’s built respecting Jetpack’s LifecycleOwner. That means we don’t have to manually handle subscriptions ourselves. We used to have to dispose CompositeDisposable in Presenter whenever view is detached (through some “onViewDetached()” callback stated in the contract. Now with MvRxViewModel, we kissed it goodbye because it uses CompositeDisposable that’s automatically disposed in onCleared. Also, in the MvRxView, invalidate function — where you’re supposed to handle UI according to state changes — is only called when current View’s lifecyle is at least STARTED (that means STARTED and RESUMED).
  • savedInstanceState made easy by @PersistStateannotation.

Some Things to Considerate

Besides that, I’d like to recap some things for consideration based on our experience migrating to MvRx.

  • As MvRx is Fragment-based, you should put your feature code inside Fragments. So if you’re not doing that already, you should be willing to make that change.
  • Choose the scope of your ViewModel wisely. MvRxViewModel can be Fragment-scoped or Activity-scoped. When you need to share your States across multiple Fragments, use Activity-scoped ViewModel. One scenario where you might want to share your states, for example, is when you have a screen containing list in Fragment A, and when an item is clicked it will lead to a detail screen in Fragment B.
// Activity-scoped
private val viewModel: SomeActivityViewModel by activityViewModel()
// Fragment-scoped
private val viewModel: SomeFragmentViewModel by fragmentViewModel()
  • It’s mostly enough that you implement invalidate in the Fragment and handle all operations of updating UI based on state changes there, but sometimes you might want to subscribe to state changes manually. MvRx provides three subscription methods to create your own subscriptions: subscribe() ,selectSubscribe(), and asyncSubscribe. One common parameter for these functions is deliveryMode: DeliveryMode, which is a sealed class with two subclasses: RedeliverOnStart (the default mode), and UniqueOnly. From the Kotlin code documentation:
* @param deliveryMode If [UniqueOnly], when this MvRxView goes from a stopped to start lifecycle a state value
* will only be emitted if the state changed. This is useful for transient views that should only
* be shown once (toasts, poptarts), or logging. Most other views should use false, as when a view is destroyed
* and recreated the previous state is necessary to recreate the view.

For example, if I want to subscribe to an event where user reports on some content, which will show Toast when the request is successful and handle error when it happens, I would write the following:

viewModel.asyncSubscribe(
SomeState::contentReportAsync,
uniqueOnly(),
{ handleError(it) },
{ activity?.showToast(R.string.msg_content_reported) }
)

Conclusion

Was the migration seamless? No. It took some time to get the hang of it, especially converting from imperative coding style. Was it worth it though? Hell yes. We could see that our code is cleaner and has better separation of concerns now.

--

--

Indri Yunita
Ruangguru Engineering

A work in progress; I’m always changing. My aspiration is to be the all-singing, all-dancing crap of the world, unsubjugated by this relentless world.