Android ViewModel injections revisited

In one of my previous posts I have described how to implement a ViewModel factory that was able to provide ViewModels with their dependencies injected, e.g. an API client, and it was good enough for me at that time. Later on, thanks to Piotr, we’ve found out even better and simpler approach with an additional possibility of injecting Activity- or Fragment-dependant data into ViewModels.

Simpler factory

Previously, we’ve created a singleton factory that was supplied with a map of ViewModel-based classes and their respective Providers. It required us to create a custom ViewModelKey annotation and use Dagger to generate the map using IntoMap bindings. It didn’t require a lot of boilerplate code compared to some other solutions I saw at that time, but it wasn’t perfect either.

On the contrary, the new solution is based on a generic ViewModel factory class of which instances are created for each Activity or Fragment instance.

import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider
import dagger.Lazy
import javax.inject.Inject

class ViewModelFactory<VM : ViewModel> @Inject constructor(
    private val viewModel: Lazy<VM>
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return viewModel.get() as T
    }
}

For example (see the full code here):

class MainViewModel @Inject constructor(
    private val apiClient: ApiClient
) : ViewModel() {
    // ...
}

class MainActivity : BaseActivity() {

    @Inject
    lateinit var vmFactory: ViewModelFactory<MainViewModel>

    lateinit var vm: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        vm = ViewModelProviders.of(this, vmFactory)[MainViewModel::class.java]

        // ...
    }
}

As you can see, there is much less code and personally I think it’s also easier to understand. To make it even more concise, we can add an extension function in the BaseActivity class like this:

abstract class BaseActivity : AppCompatActivity() {
    // ...

    inline fun <reified T : ViewModel> ViewModelFactory<T>.get(): T =
        ViewModelProviders.of(this@BaseActivity, this)[T::class.java]
}

Then, we can get a ViewModel with just: vm = vmFactory.get()

Analogically, we can add a similar function for Fragments.

More possibilities

One of the issues we’ve had was that the singleton factory holding a map of ViewModel providers was widely scoped, therefore it wouldn’t let us inject anything coming from a more narrow scope, e.g. Activity’s extras or Fragment’s arguments.

Creating a new factory each time makes it possible. In order to achieve this, we need an additional module that knows how to obtain the dependencies. For example:

import com.azabost.simplemvvm.net.response.RepoResponse
import dagger.Module
import dagger.Provides

@Module
class RepoActivityIntentModule {
    @Provides
    fun providesRepoResponse(activity: RepoActivity): RepoResponse {
        return activity.intent.getSerializableExtra(RepoActivity.REPO_RESPONSE_EXTRA) as RepoResponse
    }
}

This module must then be added to the respective RepoActivity subcomponent generated by the ContributesAndroidInjector annotation:

import com.azabost.simplemvvm.ui.main.MainActivity
import com.azabost.simplemvvm.ui.repo.RepoActivity
import com.azabost.simplemvvm.ui.repo.RepoActivityIntentModule
import dagger.Module
import dagger.android.ContributesAndroidInjector

@Module
abstract class AndroidInjectorsModule {
    @ContributesAndroidInjector
    abstract fun contributeMainActivity(): MainActivity

    @ContributesAndroidInjector(modules = [RepoActivityIntentModule::class])
    abstract fun contributeRepoActivity(): RepoActivity
}

Finally, when we get our RepoViewModel in the RepoActivity, it has the data coming from the intent already injected:

class RepoViewModel @Inject constructor(
    val repoResponse: RepoResponse
) : ViewModel()

class RepoActivity : BaseActivity() {

    @Inject
    lateinit var vmFactory: ViewModelFactory<RepoViewModel>

    lateinit var vm: RepoViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_repo)

        vm = ViewModelProviders.of(this, vmFactory)[RepoViewModel::class.java]

        repoData.text = vm.repoResponse.id.toString()
    }

    companion object {
        const val REPO_RESPONSE_EXTRA = "REPO_RESPONSE_EXTRA"
    }
}

This article was originally published on Bright Inventions blog.

0 comments on “Android ViewModel injections revisitedAdd yours →

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.