I am working on an Android App using the MVVM pattern along LiveData (possibly Transformations) and DataBinding between View and ViewModel. Since the app is "growing", now ViewModels contain lots of data, and most of the latter are kept as LiveData to have Views subscribe to them (of course, this data is needed for the UI, be it a Two-Way Binding as per EditTexts or a One-Way Binding). I heard (and googled) about keeping data that represents the UI state in the ViewModel. However, the results I found were just simple and generic. I would like to know if anyone has hints or could share some knowledge on best practices for this case. In simple words, What could be the best way to store the state of an UI (View) in a ViewModel considering LiveData and DataBinding available? Thanks in advance for any answer!
UI state is usually stored or referenced in ViewModel objects and not activities, so using onSaveInstanceState() requires some boilerplate that the saved state module can handle for you. When using this module, ViewModel objects receive a SavedStateHandle object through its constructor.
The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.
Anything that is important to the logical behavior of the application should go into the view model. Code to retrieve or manipulate data items that are to be displayed in the view through data binding should reside in the view model.
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.
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 ) } })
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) }) ...
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>
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With