DEV Community

bright inventions
bright inventions

Posted on • Updated on • Originally published at brightinventions.pl

Android ViewModel injections revisited

In one of our previous posts we 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.

Vaccine

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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]

        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

Originally published at brightinventions.pl

By Andrzej Zabost, Android Developer at Bright Inventions
Blog, Email

Top comments (1)

Collapse
 
architectak profile image
Ankit Kumar

Nice article.