Implementing a ViewModel in our App with MVVM & DI in Android: (Day 08)
Up until now, we have been working on injecting and implementing retrofit, RoomDB, and then our repository layer. The repository layer essentially helped us abstract the source from where data was generated or sent and worked as a single source which would be used for data integrity.
In today's section, we'll try to implement the next most important part of the MVVM architecture i.e. the ViewModel.
But before we dive into the code, we first need to understand what exactly is a ViewModel.
ViewModel
A ViewModel is part of the Android Architecture Components library, which 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.
Definition of ViewModel:
ViewModel is a class that is responsible for preparing and managing the data for an Android Activity or Fragment.
It serves as a communication center between the Repository (data source) and the UI.
The ViewModel does not contain any logic related to the UI and does not hold any reference to the Activity or Fragment.
Now that we have some basic understanding of the ViewModel, let's go ahead and start implementing it.
The code to this blog post can be found here
Step 1
Let's start by adding the necessary dependencies for ViewModel.
dependencies {
//Other Dependencies ...
//viewmodels
def lifecycle_version = "2.6.2" // check for the latest version
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}
Step 2
Let's start by creating a class named SingUpViewModel
and injecting UserRepository
into it.
class SignUpViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel(){
val userDataResponse: LiveData<UserDataResponse> get() = repository.userDataResponse
fun fetchAllUserData(){
repository.fetchAllUserData()
}
fun storeUserData(userData: UserData){
repository.storeUserData(userData = userData)
}
}
This is a very simple implementation where we have only exposed the repository methods to our viewmodel in a clean and concise manner.
Step 3
Now, as usual, our next task would be injecting this ViewModel into our dagger graph.
BUT!
Injecting a ViewModel is somewhat different from injecting other types of objects due to the way ViewModels are constructed and managed by the Android system.
1. ViewModel Lifecycle:
ViewModels have a different lifecycle compared to other objects. They survive configuration changes (like screen rotations) and are scoped to either an Activity or Fragment lifecycle.
This unique lifecycle requires a special way to create and manage ViewModel instances, which is why we use
ViewModelProvider
.
2. ViewModelProvider:
ViewModelProvider
is a factory for ViewModels. It creates new ViewModels or returns existing ones, ensuring they survive configuration changes.
However, this default implementation requires that all ViewModel classes have a zero-argument constructor, which doesn't allow for constructor injection of dependencies.
So the question arises, how do we let Dagger create a ViewModel that is otherwise created by a factory class and let dagger inject other dependencies to it?
Enters Custom ViewModelFactory:
- To bridge the gap between Dagger and
ViewModelProvider
, we create a customViewModelFactory
that Dagger can inject dependencies into.
@Singleton
class ViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
return creator.get() as T
}
}
The custom
ViewModelFactory
contains a map where the keys areClass
objects representing ViewModel types, and the values areProvider
objects generated by Dagger 2 that can create instances of those ViewModel types.This map allows the
ViewModelFactory
to create instances of any ViewModel type, with all of the necessary dependencies injected.
Important
The use of Provider
objects in the custom ViewModelFactory
is a design choice that leverages the Provider
interface to allow for more flexible and dynamic creation of objects, in this case, ViewModel
instances, more details about it can be found in this article - Lazy and Provider Injection in Dagger2 : Day 13
Now, that we have our viewmodelfactory implemented, we can go ahead and start injecting viewmodels as follows.
Step 4
An important point to remember here is that we might have multiple implementations of ViewModel
. To differentiate between these sub-classes, we will use a concept in Dagger2 i.e. Multibinding.
If you want to brush up on your concepts, here's a deep dive into multi binding: Multibinding with Dagger2: (Day 15)
From a high-level perspective, what we need to do is to annotate our viewmodel provider method with a unique key that will tell Dagger2 which version of the ViewModel sub-class needs to be injected in the dagger graph. To do this, we'll create a ViewModelKey as follows.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
Step 5
Now, let's create a new ViewModelModule
class, and provide our view model as follows.
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(SignUpViewModel::class)
abstract fun bindUserViewModel(userViewModel: SignUpViewModel): ViewModel
}
Also, don't forget to add this module to our AppComponent
class
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, DatabaseModule::class, RepositoryModule::class, ViewModelModule::class, FragmentModule::class])
interface AppComponent : AndroidInjector<MyApp> {
fun viewModelFactory(): ViewModelFactory //used to inject our cusstom viewmodel factory
@Component.Factory
interface Factory {
fun create(@BindsInstance application: Application): AppComponent
}
}
Step 6
Now that we've set everything up, we are ready to inject our SignUpViewModel
into our MainActivity
class MainActivity : DaggerAppCompatActivity() {
lateinit var binding: ActivityMainBinding
@Inject
lateinit var viewModelFactory: ViewModelFactory
private lateinit var viewModel: SignUpViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = this.viewModelFactory.create(SignUpViewModel::class.java)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
viewModel.fetchAllUserData()
viewModel.let{
it.userDataResponse.observe(this, Observer {userDataResponse ->
Log.e("MainActivity", "Some data received in observer ${Gson().toJson(userDataResponse)}")
})
}
}
}
Result
And that's all, if we run our application again, we'd still be able to see the same response returned from the server as earlier, the only difference is that now this data is being provided to the UI layer of our app via a ViewModel.
While the implementation process of the ViewModel might suggest that it as a complex solution for a relatively simple solution, as we move forward and create more complex UI and data flow patterns, you'll realize that creating an intermediate ViewModel actually makes the overall code much more readable, easy to handle and at the same time retains data during orientation changes.
Please note, that we are still not done with the MVVM architecture, while we have implemented all the basic elements of MVVM, we still need to understand how databinding and a few similar concepts makes the overall implementation of MVVM even more streamlined (which we'll be discussing in our next blog post). Till then, Happy Coding!
Subscribe to my newsletter
Read articles from Abou Zuhayr directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Abou Zuhayr
Abou Zuhayr
Hey, I’m Abou Zuhayr, Android developer by day, wannabe rockstar by night. With 6 years of Android wizardry under my belt and currently working at Gather.ai, I've successfully convinced multiple devices to do my bidding while pretending I'm not secretly just turning them off and on again. I’m also deeply embedded (pun intended) in systems that make you question if they’re alive. When I'm not fixing bugs that aren’t my fault (I swear!), I’m serenading my code with guitar riffs or drumming away the frustration of yet another NullPointerException. Oh, and I write sometimes – mostly about how Android development feels like extreme sport mixed with jazz improvisation. Looking for new challenges in tech that don’t involve my devices plotting against me!