Android - Best Practices for ViewModel State in MVVM? Android - Best Practices for ViewModel State in MVVM? android android

Android - Best Practices for ViewModel State in MVVM?


I struggled with the same problem at work and can share what is working for us. We're developing 100% in Kotlin so the following code samples will be as well.

UI state

To prevent the ViewModel from getting bloated with lots of LiveData properties, expose a single ViewState for views (Activity or Fragment) to observe. It may contain the data previously exposed by the multiple LiveData and any other info the view might need to display correctly:

data class LoginViewState (    val user: String = "",    val password: String = "",    val checking: Boolean = false)

Note, that I'm using a Data class with immutable properties for the state and deliberately don't use any Android resources. This is not something specific to MVVM, but an immutable view state prevents UI inconsistencies and threading problems.

Inside the ViewModel create a LiveData property to expose the state and initialize it:

class LoginViewModel : ViewModel() {    private val _state = MutableLiveData<LoginViewState>()    val state : LiveData<LoginViewState> get() = _state    init {        _state.value = LoginViewState()    }}

To then emit a new state, use the copy function provided by Kotlin's Data class from anywhere inside the ViewModel:

_state.value = _state.value!!.copy(checking = true)

In the view, observe the state as you would any other LiveData and update the layout accordingly. In the View layer you can translate the state's properties to actual view visibilities and use resources with full access to the Context:

viewModel.state.observe(this, Observer {    it?.let {        userTextView.text = it.user        passwordTextView.text = it.password        checkingImageView.setImageResource(            if (it.checking) R.drawable.checking else R.drawable.waiting        )    }})

Conflating multiple data sources

Since you probably previously exposed results and data from database or network calls in the ViewModel, you may use a MediatorLiveData to conflate these into the single state:

private val _state = MediatorLiveData<LoginViewState>()val state : LiveData<LoginViewState> get() = _state_state.addSource(databaseUserLiveData, { name ->    _state.value = _state.value!!.copy(user = name)})...

Data binding

Since a unified, immutable ViewState essentially breaks the notification mechanism of the Data binding library, we're using a mutable BindingState that extends BaseObservable to selectively notify the layout of changes. It provides a refresh function that receives the corresponding ViewState:

Update: Removed the if statements checking for changed values since the Data binding library already takes care of only rendering actually changed values. Thanks to @CarsonHolzheimer

class LoginBindingState : BaseObservable() {    @get:Bindable    var user = ""        private set(value) {            field = value            notifyPropertyChanged(BR.user)        }    @get:Bindable    var password = ""        private set(value) {            field = value            notifyPropertyChanged(BR.password)        }    @get:Bindable    var checkingResId = R.drawable.waiting        private set(value) {            field = value            notifyPropertyChanged(BR.checking)        }    fun refresh(state: AngryCatViewState) {        user = state.user        password = state.password        checking = if (it.checking) R.drawable.checking else R.drawable.waiting    }}

Create a property in the observing view for the BindingState and call refresh from the Observer:

private val state = LoginBindingState()...viewModel.state.observe(this, Observer { it?.let { state.refresh(it) } })binding.state = state

Then, use the state as any other variable in your layout:

<layout ...>    <data>        <variable name="state" type=".LoginBindingState"/>    </data>    ...        <TextView            ...            android:text="@{state.user}"/>        <TextView            ...            android:text="@{state.password}"/>        <ImageView            ...            app:imageResource="@{state.checkingResId}"/>    ...</layout>

Advanced info

Some of the boilerplate would definitely benefit from extension functions and Delegated properties like updating the ViewState and notifying changes in the BindingState.

If you want more info on state and status handling with Architecture Components using a "clean" architecture you may checkout Eiffel on GitHub.

It's a library I created specifically for handling immutable view states and data binding with ViewModel and LiveData as well as glueing it together with Android system operations and business use cases.The documentation goes more in depth than what I'm able to provide here.


Android Unidirectional Data Flow (UDF) 2.0

Update 12/18/2019: Android Unidirectional Data Flow with LiveData — 2.0

I've designed a pattern based on the Unidirectional Data Flow using Kotlin with LiveData.

UDF 1.0

Check out the full Medium post or YouTube talk for an in-depth explanation.

Medium - Android Unidirectional Data Flow with LiveData

YouTube - Unidirectional Data Flow - Adam Hurwitz - Medellín Android Meetup

Code Overview

Step 1 of 6 — Define Models

ViewState.kt

// Immutable ViewState attributes.data class ViewState(val contentList:LiveData<PagedList<Content>>, ...)// View sends to business logic.sealed class ViewEvent {  data class ScreenLoad(...) : ViewEvent()  ...}// Business logic sends to UI.sealed class ViewEffect {  class UpdateAds : ViewEffect()   ...}

Step 2 of 6 — Pass events to ViewModel

Fragment.kt

private val viewEvent: LiveData<Event<ViewEvent>> get() = _viewEventprivate val _viewEvent = MutableLiveData<Event<ViewEvent>>()override fun onCreate(savedInstanceState: Bundle?) {    ...    if (savedInstanceState == null)      _viewEvent.value = Event(ScreenLoad(...))}override fun onResume() {  super.onResume()  viewEvent.observe(viewLifecycleOwner, EventObserver { event ->    contentViewModel.processEvent(event)  })}

Step 3 of 6 — Process events

ViewModel.kt

val viewState: LiveData<ViewState> get() = _viewStateval viewEffect: LiveData<Event<ViewEffect>> get() = _viewEffectprivate val _viewState = MutableLiveData<ViewState>()private val _viewEffect = MutableLiveData<Event<ViewEffect>>()fun processEvent(event: ViewEvent) {    when (event) {        is ViewEvent.ScreenLoad -> {          // Populate view state based on network request response.          _viewState.value = ContentViewState(getMainFeed(...),...)          _viewEffect.value = Event(UpdateAds())        }        ...}

Step 4 of 6 — Manage Network Requests with LCE Pattern

LCE.kt

sealed class Lce<T> {  class Loading<T> : Lce<T>()  data class Content<T>(val packet: T) : Lce<T>()  data class Error<T>(val packet: T) : Lce<T>()}

Result.kt

sealed class Result {  data class PagedListResult(    val pagedList: LiveData<PagedList<Content>>?,     val errorMessage: String): ContentResult()  ...}

Repository.kt

fun getMainFeed(...)= MutableLiveData<Lce<Result.PagedListResult>>().also { lce ->  lce.value = Lce.Loading()  /* Firestore request here. */.addOnCompleteListener {    // Save data.    lce.value = Lce.Content(ContentResult.PagedListResult(...))  }.addOnFailureListener {    lce.value = Lce.Error(ContentResult.PagedListResult(...))  }}

Step 5 of 6 — Handle LCE States

ViewModel.kt

private fun getMainFeed(...) = Transformations.switchMap(repository.getFeed(...)) {   lce -> when (lce) {    // SwitchMap must be observed for data to be emitted in ViewModel.    is Lce.Loading -> Transformations.switchMap(/*Get data from Room Db.*/) {       pagedList -> MutableLiveData<PagedList<Content>>().apply {        this.value = pagedList      }    }    is Lce.Content -> Transformations.switchMap(lce.packet.pagedList!!) {       pagedList -> MutableLiveData<PagedList<Content>>().apply {        this.value = pagedList      }    }        is Lce.Error -> {       _viewEffect.value = Event(SnackBar(...))      Transformations.switchMap(/*Get data from Room Db.*/) {         pagedList -> MutableLiveData<PagedList<Content>>().apply {          this.value = pagedList         }    }}

Step 6 of 6 — Observe State Change!

Fragment.kt

contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->  viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->    adapter.submitList(contentList)  })  ...}