Dagger-Dot-Android Part 2: ViewModels and ViewModel Factories

autonomousapps

Tony Robalik

Posted on April 30, 2018

Dagger-Dot-Android Part 2: ViewModels and ViewModel Factories

(no, not those kind of factories)

  1. Part 1 — Basic Setup
  2. This part
  3. Part 3 — Fragments

If you read the first part of this tutorial, then you already know the basics of how to set up a project that uses the dagger.android package, Google's (relatively) new take on Dagger and Android. You know the basics of injecting an Activity, and you know how to replace your app's production objects with test doubles to make instrumentation testing a breeze (relatively speaking...).

Now in Part 2, we're going to learn how to use ViewModels and LiveData, from the Android Architecture Components, to manage the lifecycle of our Dagger-provided objects.

Setup

Add the following to your app/build.gradle file:

dependencies {
  // ...all the libs...
  implementation "android.arch.lifecycle:extensions:1.1.1"
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we're just adding a single dependency. This one gives us access to both ViewModel and LiveData (and friends). Now, let's use these classes for managing the state of our "View", represented by our activity.

MainActivity

This is very similar to what we saw in Part 1. New section are called out with comments.

class MainActivity : AppCompatActivity() {
  // New!
  @Inject lateinit var viewModelFactory: MainActivityViewModelFactory
  private val viewModel by lazy(mode = LazyThreadSafetyMode.NONE) {
    ViewModelProviders.of(this, viewModelFactory)
      .get(MainActivityViewModel::class.java)
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    AndroidInjection.inject(this)
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // New!
    viewModel.counter().observe(this, Observer {
      // Assuming the existence of a TextView with the id "counterText"
      counterText.text = it.toString()
    })
    // Assuming the existence of a Button with the id "counterButton"
    counterButton.setOnClickListener {
      viewModel.onClickCounter()
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Here's what we've changed from last time:

  1. We're injecting a "view model factory", which is literally a ViewModelProvider.Factory. This is only necessary if your ViewModel has a non-default constructor. In other words, it's absolutely necessary as soon as you want to do anything interesting.
  2. We use our view model factory to provide our actual ViewModel, which is the thing we really care about.
  3. We're using by lazy because we really like immutability, and it's the only way to have a val view model. (See the note at the bottom of this article for why I'm setting the mode.)
  4. We then use our ViewModel in concert with a couple of widgets.

The eagle-eyed among you will have noticed that we're injecting a new object (MainActivityViewModelFactory), but I haven't shown any code for how to inject this new object. Doesn't it need a @Provides or @Binds inside a @Module? Not so much. Dagger supports two kinds of bindings, which I will refer to as "explicit" and "implicit."

Explicit bindings

@Module abstract class MyModule {
  @Binds abstract fun bindThing(impl: ThingImpl): IThing
  @Provides @JvmStatic fun provideThingama(bob: Bob): Thingama = Thingama(bob)
}
Enter fullscreen mode Exit fullscreen mode

In this case, we are explicitly telling Dagger how it can create instances of IThings and Thingamas. In the first case, we bind the IThing interface to its concrete implementation ThingImpl, and therefore we use @Bind. In the second case, the Thingama class (tragically) doesn't implement an interface, so we have to provide instances of it directly, and therefore we use @Provides.

But even here we see that I've cheated. Where does ThingImpl come from? What about Bob?

Implicit bindings

class ThingImpl @Inject constructor()

class Bob @Inject constructor()
Enter fullscreen mode Exit fullscreen mode

These are implicit bindings, and they simply require we annotate one of our constructors (even a default one!) with @Inject.

Of course, we can specify non-default constructors with an arbitrary number of parameters, and still rely upon Dagger to instantiate these objects — just so long as our Directed Acyclic Graph of dependencies contains the information DAGger needs to provide each of the required objects.

MainActivityViewModelFactory

For trivial ViewModels, we could follow the basic sample code in the docs and just do this:

// `this` can be a variety of things, but in our example, it is an `Activity`
private val viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
Enter fullscreen mode Exit fullscreen mode

and, as you can see, nary a factory to be seen.

But if our view model is at all interesting, it will certainly require collaborators; that is, a non-default constructor. For example:

class MutableObject(var counter: Int = 0)

class MainActivityViewModel(
  private val mutableObject: MutableObject
) : ViewModel() {
  // We need our counter to be mutable...
  private val counterLiveData = MutableLiveData<Int>()
  // ...but we only want to expose it to observers as an immutable object
  fun counter(): LiveData<Int> = counterLiveData

  init {
    // initialize it to something, else `value` will be `null`, which is annoying
    counterLiveData.value = mutableObject.counter
  }

  // Clicking increments the counter. Nothing simpler!
  fun onClickCounter() {
    counterLiveData.value = ++mutableObject.counter
  }
}
Enter fullscreen mode Exit fullscreen mode

but if we tried to get a reference to such a view model with the above example, it would fail! There is a default ViewModelProvider.Factory that knows how to create ViewModels with default (no-arg) constructors, but it can't possibly know how to create our custom view model.

ViewModelProvider.Factory

You will certainly have noticed the following:

ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
Enter fullscreen mode Exit fullscreen mode

This is an overload on the ViewModelProviders.of() method that takes a custom factory. What does one of those look like? Here's one way to do it:

class MutableObjectFactory @Inject constructor() {
  fun newMutableObject() = MutableObject()
}

class MainActivityViewModelFactory @Inject constructor(
  private val mutableObjectFactory: MutableObjectFactory
) : ViewModelProvider.Factory {

  @Suppress("UNCHECKED_CAST")
  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return when {
      modelClass.isAssignableFrom(MainActivityViewModel::class.java) -> {
        MainActivityViewModel(mutableObjectFactory.newMutableObject()) as T
      }
    else -> throw IllegalArgumentException(
        "${modelClass.simpleName} is an unknown type of view model"
      )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We have defined two new classes here. Let's talk about the view model factory, first.

ViewModelProvider.Factory, defined

This is an interface that declares a single method, create(). It takes a a reference to the class it must create, and returns an instance of that class. If we strip it down, this is what we have:

class MyViewModelFactory : ViewModelProvider.Factory {
  @Suppress("UNCHECKED_CAST")
  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return MyViewModel() as T
  }
}
Enter fullscreen mode Exit fullscreen mode

and this is essentially what the default factory does.

The other class, MutableObjectFactory, is trivial. I've declared it to showcase an important point.

Why do we care about ViewModels at all?

We care about ViewModels because they

  1. Live outside the Activity/Fragment lifecycle
  2. Know about the Activity/Fragment lifecycle.

This is amazingly useful. Because view models live outside of the lifecycle, they don't get destroyed every time your activity gets destroyed (haha, rotation, go away). But because they also know about the lifecycle, you can start observing a LiveData object in Activity.onCreate(), and you will only receive updates on that object while the the activity is in the "started" state (between onStart and onStop).

In the context of Dagger, this is a bit like having a custom @Scope, without having to declare or manage one. We create our objects in Activity.onCreate(), mutate those objects, rotate the screen, get references to those same objects again, and keep on going, no problem at all.

There is a small gotcha, though. Here's that activity code, one more time:

class MainActivity : AppCompatActivity() {

  // Hmmm
  @Inject lateinit var viewModelFactory: MainActivityViewModelFactory

  private val viewModel by lazy {
    ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    // Hmmmmm
    AndroidInjection.inject(this)

    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    viewModel.counter().observe(this, Observer {
      counterText.text = it.toString()
    })
    counterButton.setOnClickListener {
      viewModel.onClickCounter()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Every time we rotate our device, the activity goes through onDestroy/onCreate. Therefore, every time we rotate our device, we inject a new view model factory! However, even though the factory is new, the call to ViewModelProviders.of(this, factory).get(MyViewModel::class.java) will return the same view model! The Android Architecture Component framework maintains a separate map of class-to-view model that is independent from Dagger. So, each time you try to get a reference to a view model that already exists, it returns the extant instance.

Heavy objects and MutableObjectFactory

This brings us back to MutableObjectFactory. If, instead of providing that factory to our view model factory, we provided an instance of MutableObject itself, we'd be instantiating a new MutableObject each time we instantiated a view model factory which, incidentally, is every time we inject a view model factory in our activity — and that's every time we rotate. If MutableObject is very heavy, then we'd like to avoid creating new instances that will never get used. So, we provide a factory instead. Illustrating in code:

// New instances of this class are created each time we inject our activity
class MainActivityViewModelFactory @Inject constructor(
  // that means new instances of this factory are created each time
  private val mutableObjectFactory: MutableObjectFactory
) : ViewModelProvider.Factory {
  @Suppress("UNCHECKED_CAST")
  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return when {
      // HOWEVER, this code will ONLY BE CALLED ONCE
      modelClass.isAssignableFrom(MainActivityViewModel::class.java) -> {
        MainActivityViewModel(mutableObjectFactory.newMutableObject()) as T
      }
      else -> throw IllegalArgumentException(
        "${modelClass.simpleName} is an unknown type of view model"
      )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You may be thinking (like me) "ew, gross. Creating a factory is better than creating the heavy object itself, but can I avoid even creating that factory?" Well... yes, but. You can declare and manage a custom Dagger @Scope to handle that. You can use static instances, somehow. You can get really clever with programming techniques. Or you can profile this code and decide for yourself if it's worth the effort. (A future post will discuss custom scopes.)

Is that it for view models? Not quite. We still have yet to discuss them in the context of fragments, and believe me, there are some interesting gotchas there. But... until next time.

Notes

  1. Synchronization mode for Kotlin's by lazy. We use mode = LazyThreadSafetyMode.NONE because the default mode is LazyThreadSafetyMode.SYNCHRONIZED, which uses the double-checked locking pattern to ensure only one instance of this object is ever created. This adds overhead we don't want, because we want to minimize the work done on the main thread. We achieve the goal of "just one instance" but only accessing our ViewModel on the main thread; because our programming model is single-threaded, there's no chance of our by lazy being called in a concurrent way, and therefore no chance of accidentally creating multiple instances of our ViewModel.
💖 💪 🙅 🚩
autonomousapps
Tony Robalik

Posted on April 30, 2018

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

Sign up to receive the latest update from our blog.

Related