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 Provider
s. 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"
}
}