If you want to go right to a library that enables easy View Model dependency injection, check out Lazy ViewModels on my GitHub
The Android team has been increasingly vocal about their support for Dependency Injection frameworks like Dagger, going so far as to develop and recommend Hilt - their Android DI framework built on top of Dagger - for modern Android development.
In their guide to manual dependency injection the Android team lays out approaches to manual DI for View Models. They offer both the basic approach to manual DI - just instantiating everything you need in onCreate
and using lateinit var
View Models - and the container approach using a custom AppContainer
to handle dependencies across all your Activities.
The alternative they give to this boiler-plate-heavy approach is to recommend Dagger or Hilt to handle this process for you. However, in many apps, pulling in a DI framework is overhead you really don't need. Instead, what if there was a way to manually inject dependencies into your Android Activity & Fragment View Models without all the boiler plate?
Lazy DI Using Android Lifecycle ViewModel Extensions
One of the ways the Android Fragment & Lifecycle teams have tried to make the View Model easier to use in Activities and Fragments is providing the Android Lifecycle ViewModel Kotlin Extensions library.
dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
}
This library lets you instantiate a View Model in a Fragment or Activity with a delegate method - making it easy to create a properly scoped View Model.
class MyActivity : AppCompatActivity() {
private val model: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
}
}
This is beautiful! However, what if our View Model needs to use a repository to make a call to Retrofit? We would like to inject that repository into our View Model when we construct it.
View Model Provider Factory
One way to enable this behavior is to use ViewModelProvider.Factory
, with which you can instantiate a View Model with it's needed dependencies. To do so, you need to create your own Factory that extends the ViewModelProvider.Factory
interface, or create a function that can return an object
that overrides the Factory.create
method.
If we wanted to stop here, we could create a Kotlin function to do this for us:
fun createWithFactory(
create: () -> ViewModel
): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")// Casting T as ViewModel
return create.invoke() as T
}
}
}
And use it in our Activity or Fragment like this:
private val model: MyViewModel by lazy {
ViewModelProvider(
this,
createWithFactory {
MyViewModel(repo = MyRepository())
}
).get(MyViewModel::class.java)
}
We can now create a View Model with the necessary dependencies! However, even with our Factory abstracted into a function, we still need to manage the boiler plate of ViewModelProvider
.
View Model Provider & Kotlin Extension Mashup
Remember, we could use by viewModels
or by activityViewModels
to delegate the creation of our View Models, but we weren't able to inject our required dependencies without a Factory.
Now that we have a handy way to instantiate a View Model with a Factory, we can let these Kotlin Extensions for View Model to hide this ViewModelProvider
boiler plate. Here's how it would look in a Fragment.
private val model: MyViewModel by activityViewModels {
createWithFactory {
MyViewModel(repo = MyRepository())
}
}
Almost perfect. All we have to do is provide a Factory and a lambda that returns our View Model. But, with a little help from the lifecycle-viewmodel-ktx
library and Kotlin extension functions, we can take it one step further.
Endgame: Easy View Model DI
Let's use the power of extension functions, our Factory knowledge, and the ViewModelLazy
class to delegate the creation of our View Model to functions on Activity and Fragment.
We provide the function to create our View Model as a lambda that returns type <VM : ViewModel>
, and the viewModelBuilder
and activityViewModelBuilder
extensions do the rest.
/**
* Get a [ViewModel] in an [ComponentActivity].
*/
@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.viewModelBuilder(
noinline viewModelInitializer: () -> VM
): Lazy<VM> {
return ViewModelLazy(
viewModelClass = VM::class,
storeProducer = { viewModelStore },
factoryProducer = {
return@ViewModelLazy object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")// Casting T as ViewModel
return viewModelInitializer.invoke() as T
}
}
}
)
}
/**
* Get a [ViewModel] in a [Fragment].
*/
@MainThread
inline fun <reified VM : ViewModel> Fragment.activityViewModelBuilder(
noinline viewModelInitializer: () -> VM
): Lazy<VM> {
return ViewModelLazy(
viewModelClass = VM::class,
storeProducer = { requireActivity().viewModelStore },
factoryProducer = {
object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")// Casting T as ViewModel
return viewModelInitializer.invoke() as T
}
}
}
)
}
Now, in our Activity:
private val model: MyViewModel by viewModelBuilder {
MyViewModel(repo = MyRepository())
}
Or Fragment:
private val model: MyViewModel by activityViewModelBuilder {
MyViewModel(repo = MyRepository())
}
We can easily build a ViewModel, along with all it's necessary dependencies, all without the annoying boilerplate!
If you're in a situation where you need complex Android View Models, want to follow SOLID principles of dependency injection, and you're not quite ready to adopt the complexity of a full Dependency Injection Framework - hopefully this helps present some other options that can make the task easier to manage.
If you have thoughts on the Android View Model or want to share your approaches to manual DI, don't hesitate to reach out @ajkueterman on Twitter, or follow me on DEV.to and leave a comment on this story.
This story was originally published on my blog, at ajkueterman.dev
Top comments (0)