ViewModel and LiveData with Dagger Android
I was given the task to make a small Android application as part of an interview process. I used the opportunity to write some Kotlin and learn about the Android Architecture Components, which had just reached their first stable release. I had already used Dagger for Android in the past and I wanted to find out how they fit together. I will share my findings with you in this tutorial.
Let’s begin by creating a new application using Android Studio’s wizard, making sure we select Empty Activity
as the template for the project.
Next, let’s create a custom Application class:
class MyApplication : Application()
And then add a reference to it in our AndroidManifest.xml
:
<application android:name=".MyApplication" ... />
Now we need to add Dagger as a dependency in our app module’s build.config
file:
...
apply plugin: 'kotlin-kapt'
...
dependencies {
...
implementation 'com.google.dagger:dagger:2.13'
kapt 'com.google.dagger:dagger-compiler:2.13'
implementation 'com.google.dagger:dagger-android:2.13'
implementation 'com.google.dagger:dagger-android-support:2.13'
kapt 'com.google.dagger:dagger-android-processor:2.13'
}
We can now create a Component for our Application:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
DaggerMyApplication_Component
.builder()
.create(this)
.inject(this)
} @dagger.Component
internal interface Component : AndroidInjector<MyApplication> {
@dagger.Component.Builder
abstract class Builder
: AndroidInjector.Builder<MyApplication>()
}
}
I decided to include the Component as an internal interface of MyApplication
for convenience, but you may want to extract it into its own file. All we are doing here is declaring a Component that is able to inject our Application class. However, we still have not added any Module so there is nothing to inject it with.
It is time to add a Module to our application which will provide an Injector for our Activity:
class MyApplication : Application(), HasActivityInjector {
@Inject
lateinit var dispatchingActivityInjector :
DispatchingAndroidInjector<Activity> override fun activityInjector() :
AndroidInjector<Activity> = dispatchingActivityInjector ... @dagger.Module
internal abstract class Module {
@ContributesAndroidInjector
abstract fun mainActivity(): MainActivity
} @dagger.Component(modules = arrayOf(Module::class))
internal interface Component : AndroidInjector<MyApplication> {
...
}
}
We have just added a module to our Component, which will provide an Activity Injector, and implemented the HasActivityInjector
interface to let Dagger know the Application exposes it. With all this in place, we can now inject MainActivity
:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_main)
}
}
All we have done so far is create a Component for our Application and let Dagger generate a Subcomponent (which is the Activity Injector) for our Activity. We are ready to introduce the Architecture Components library. Let’s add the required dependencies:
dependencies {
...
implementation "android.arch.lifecycle:extensions:1.0.0"
}
Next, we are going to create a ViewModel for our Activity and a Module to provide it:
class MainActivity : AppCompatActivity() {
@Inject
internal lateinit var viewModel : ViewModel ...
internal class ViewModel :
android.arch.lifecycle.ViewModel() @dagger.Module
internal object Module {
@JvmStatic
@Provides
fun viewModel(
activity: MainActivity
) = ViewModelProviders
.of(activity)
.get(ViewModel::class.java)
}
}
All that is left is to bind the new Module to the Activity Injector. For that, we need to update the Application Component:
class MyApplication : Application(), HasActivityInjector {
...
@dagger.Module
internal abstract class Module {
@ContributesAndroidInjector(modules = arrayOf(
MainActivity.Module::class
))
abstract fun mainActivity(): MainActivity
}
...
}
Congratulations! Your Activity now has its own ViewModel. It doesn’t do much at the moment but it means we are ready to introduce LiveData into the mix:
class MainActivity : AppCompatActivity() {
...
internal class ViewModel(
greeting: MutableLiveData<String>
) : android.arch.lifecycle.ViewModel() {
val greeting: LiveData<String> = greeting
init {
greeting.value = "Hello world!"
} class Factory @Inject constructor(
private val greeting: MutableLiveData<String>
) : ViewModelProvider.Factory {
override fun <T : android.arch.lifecycle.ViewModel?>
create(modelClass: Class<T>) =
ViewModel(greeting) as T
}
} @dagger.Module
internal object Module {
@JvmStatic
@Provides
fun greeting() = MutableLiveData<String>() @JvmStatic
@Provides
fun viewModel(
activity: MainActivity
) = ViewModelProviders
.of(activity, factory)
.get(ViewModel::class.java)
}
}
There are a few things to consider here. As an example, we assign an initial value to our LiveData object when the ViewModel is initialized. The Activity Module now provides LiveData of String. Also, in order to let the Architecture Components library inject the LiveData into the ViewModel through its constructor, we need to add a Factory class and pass it as a parameter to theViewModelProviders.of()
method.
Finally, we are going to bind the LiveData from our ViewModel to the view in our Activity:
class MainActivity : AppCompatActivity() {
...
private lateinit var greetingView: TextView override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_main)
greetingView = findViewById(R.id.greeting)
viewModel.greeting.observe(this, Observer { greeting ->
greetingView.text = greeting
})
}
...
}
We can now say we have a working Hello World app using Dagger, ViewModel and LiveData. But there is one more thing I want to mention. Every time we call AndroidInjeciton.inject()
we are actually recreating the object graph for the Activity. This means every time we rotate the screen and the activity is created again, we get a new instance of the LiveData object which is injected into yet another new Factory object.
The fact is the ViewModel outlives the Activity. Therefore, the second time ViewModelProvide.of(activity, factory)
is called, the whole Factory object is ignored. That is a lot of wasteful memory allocation we would like to avoid. I came up with the following solution:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
viewModel = ViewModelProviders
.of(this)
.get(ViewModel::class.java)
} catch (e: Throwable) {
AndroidInjection.inject(this)
}
...
}
...
}
Basically, attempting to retrieve the ViewModel without providing a Factory will throw an error the first time, in which case we will execute the injection. After that, we will be able to retrieve the ViewModel without the need of a Factory. Of course this approach can only be taken if the Activity is kept as dumb as possible and no other objects need to be injected to it.