How to Implement MVVM in Android
MVVM
- A structure that uses the observer pattern to implement a single source of truth for data callbacks.
- Core structure (this diagram is drawn based on my understanding using ProcessOn; everyone’s understanding may differ. Feel free to adapt it according to your own business logic)
Creating a ViewModel is straightforward. Following the official recommendation, you can inject it according to the corresponding lifecycle. This also involves the factory pattern; for details, refer to ViewModelProvider.Factory
:
- ViewModel tied to its own lifecycle
kotlin
private val viewModel: MyViewModel by lazy {
ViewModelProvider(
this,
MyViewModelFactory()
)[MyViewModel::class.java]
}
- ViewModel tied to a parent container’s lifecycle
kotlin
private val parentViewModel: RouteViewModel? by lazy {
parentFragment?.let {
ViewModelProvider(
it,
MyViewModelFactory()
)[RouteViewModel::class.java]
}
}
private val activityViewModel: ActivityViewModel? by lazy {
activity?.let {
ViewModelProvider(
it,
MyViewModelFactory()
)[ActivityViewModel::class.java]
}
}
Writing ViewModel and Using LiveData
kotlin
class MyViewModel(
private val repository: Repository,
): ViewModel() {
private val _data: MutableLiveData<String> = MutableLiveData()
val data: LiveData<String> = _data
private val _errorMessage: MutableLiveData<Throwable> = MutableLiveData()
val errorMessage: LiveData<Throwable> = _errorMessage
private val _isRefresh: MutableLiveData<Boolean> = MutableLiveData()
val isRefresh: LiveData<Boolean> = _isRefresh
fun fetchData() {
_isRefresh.value = true
viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable -> {
_errorMessage.value = throwable
_isRefresh.value = false
}}) {
// Call repository.fetchData() in a background thread to fetch data.
// Note: when posting to LiveData, you must be on the UI thread.
_data.value = repository.fetchData()
_isRefresh.value = false
}
}
}
class MyFragment: Fragment() {
private val viewModel: MyViewModel by lazy {
ViewModelProvider(this)[MyViewModel::class.java]
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.isRefresh.observe(this) {
showHideSpinner(it)
}
viewModel.errorMessage.observe(this) {
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
}
viewModel.data.observe(this) {
updateUI(it)
}
viewModel.fetchData()
}
private fun updateUI(data: String) {
// update UI
println(data)
}
}
- The difference between MVVM and MVP is that MVVM does not necessarily rely on two-way binding. Because DataBinding’s two-way binding capability is quite limited, when using MVVM, I personally prefer a single source of truth. This is also the approach most recommended by Google. With LiveData, a lifecycle-aware observer tool, data in the ViewModel can be easily returned to the View.
- A notable point: in a common single-Activity, multiple-Fragment setup, using the
parentFragment
lifecycle is a good way to handle routing. - One unavoidable aspect of using LiveData is that traditionally, each LiveData corresponds to a single state. In the example above, there are three states: refreshing, data successfully fetched, and fetch failed. This can lead to a LiveData explosion in the ViewModel, where the number of LiveData objects becomes very large.