Android ViewModel: Manual Dependency Injection Made Easy

robotsquidward

A.J. Kueterman

Posted on September 28, 2020

Android ViewModel: Manual Dependency Injection Made Easy

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

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

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

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

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

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

Now, in our Activity:

private val model: MyViewModel by viewModelBuilder {
    MyViewModel(repo = MyRepository())  
}
Enter fullscreen mode Exit fullscreen mode

Or Fragment:

private val model: MyViewModel by activityViewModelBuilder {
    MyViewModel(repo = MyRepository())  
}
Enter fullscreen mode Exit fullscreen mode

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

πŸ’– πŸ’ͺ πŸ™… 🚩
robotsquidward
A.J. Kueterman

Posted on September 28, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related