ViewModel Overview
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.
If you are building an app of considerable complexity, it is recommended to stick to the MVVM architecture. This is being pushed even more now with the release of Android Architecture Components which includes a great abstraction for creating a ViewModel
. There are several pros to using this approach, the major ones being that you abstract “logic” and “model interactions” inside the view model to keep your activities and fragments lightweight. It also makes it tremendously easy to test your logic because ViewModels can be tested with JUnit tests rather than using slow instrumentation tests on the emulator.
Full description about ViewModel
s would probably warrant a complete post of its own, so I will leave it at this: They are great and very easy to use and I would recommend them, even for a small sized application because they make it so much easier to manage the code.
Obtaining an instance of ViewModel
For those who are familiar with ViewModel
, we already know how to get an instance of the model inside an Activity or a Fragment. It is as simple as adding the following code in onCreate
, onViewCreated
or any of the other lifecycle methods as appropriate for your app.
val model = ViewModelProviders.of(this).get(MyViewModel::class.java)
Another very common use case is when sharing the same instance of a ViewModel
between two fragments embedded in the same activity. This is as simple as passing the reference to the activity to obtain the ViewModel
inside your Fragment
’s lifecycle method.
activity?.let {
val sharedViewModel = ViewModelProviders.of(it).get(SharedViewModel::class.java)
}
Obtaining ViewModel inside a View
While it is easy enough to obtain the ViewModels inside an Activity or a Fragment, it is not straightforward to obtain this instance inside a View. The main reason behind this is because Views are supposed to be independent of all processing and even if all your logic is inside a ViewModel, the fact that you are accessing that ViewModel inside the View makes it reliant on something that it shouldn’t. The recommended way of controlling a View is to pass parameters to it based on the state of the ViewModel from the Fragment or the Activity.
But sometimes, this could be difficult because you want to split your code into multiple small Views that can be reused at different places. Or you would like to separate the logic into several small views rather than having the Fragment or the Activity coordinate everything. The fact that we can use ViewModelProviders to obtain a shared instance of a ViewModel comes to our rescue in this scenario.
Here is how to achieve this:
override val activity: FragmentActivity by lazy {
try {
context as FragmentActivity
} catch (exception: ClassCastException) {
throw ClassCastException("Please ensure that the provided Context is a valid FragmentActivity")
}
}
override var viewModel = ViewModelProviders.of(activity).get(SharedViewModel::class.java)
Just adding this to our View gives us access to the view model inside. But, as we said before, our aim is to access the ViewModel inside multiple small views. What is the best way to add the same code to all our Views? The most straightforward approach is to create a BaseView
that all our View
classes can extend from. But this isn’t ideal as the custom views can inherit from a variety of different classes and it isn’t feasible to create a base class for all such super classes. Kotlin “Mixins” are the simplest way of handling this. Although a lesser known concept, the mixins are a powerful feature of Kotlin. Java allows classes to implement several Interface
s, but what’s great about Kotlin is that it allows providing a default implementation for these Interface
s using Delegation. This is what is also called a Kotlin Mixin. Here’s how we can achieve the injection with the mixin.
interface MyViewModelAccessor {
var viewModel: MyViewModel
val activity: FragmentActivity
}
class MyViewModelInjector(val context: Context) : MyViewModelAccessor {
override val activity: FragmentActivity by lazy {
try {
context as FragmentActivity
} catch (exception: ClassCastException) {
throw ClassCastException("Please ensure that the provided Context is a valid FragmentActivity")
}
}
override var viewModel = ViewModelProviders.of(activity).get(MyViewModel::class.java)
}
This can now be injected inside any custom View like this.
class MyCustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr),
MyViewModelAccessor by MyViewModelInjector(context) {
init {
View.inflate(context, R.layout.view_custom, this)
subscribe()
}
private fun subscribe() {
viewModel.property.observe(activity, Observer {
Timber.d("Property udpated from ViewModel: %s", it)
})
}
}