Implementing a ViewModel in our App with MVVM & DI in Android: (Day 08)

Photo by Kaleidico on Unsplash

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 custom ViewModelFactory 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 are Class objects representing ViewModel types, and the values are Provider 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!