loading...

Dagger-Dot-Android Part 4: Late-binding values

autonomousapps profile image Tony Robalik ・2 min read
  1. Part 1 — Basic Setup
  2. Part 2 — ViewModels and View Model Factories
  3. Part 3 — Fragments
  4. This part

If you haven't read the other parts of this tutorial, please consider checking them out.

In this part, I'm going to briefly describe a neat trick for late-binding objects into your graph.

Use-case

Let's say you have a pair of activities, and clicking on a button in one will launch the second. There are an array of buttons on the first screen, each representing data you have stored in a database. How does the second activity know which button you've pressed, and which data to display? Well, that's easy, right? You just pass an argument in your Intent. But what if you're a modern developer, you care about separation of concerns and the single responsibility principle, and you can't just hand off that button ID to some DAO directly inside your Activity?

In this post, we're going to imagine that your Activity class has a ViewModel that mediates access to the database layer. You want to get that ID into the viewmodel, but you'd prefer not to have to call a setter on it. What you really want is for the VM to just "know" which data it needs to populate the View correctly, and as early as possible. Let's see how it can be done.

ClientActivity

class ClientActivity : AppCompatActivity {

  companion object {
    fun getStartIntent(context: Context, id: Long): Intent {
      return Intent(context, ClientActivity::class.java).apply {
        putExtra(EXTRA_ID, id)
      }
    }
  }

  internal val id by lazy { intent.extras.getLong(EXTRA_ID) }

  @Inject lateinit var factory: ClientActivityViewModelFactory

  // Our goal is for this view model to know about `id` at construction time
  private val viewModel by lazy { 
    ViewModelProviders.of(this, factory).get(ClientActivityViewModel::class.java)
  }

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

    viewModel.stuff().observe(this, Observer {
      // presumably, `stuff()` returns LiveData that has something to do with our ID
    })
  }
}

@Module object ClientActivityModule {
  // Waaaaat. Now our ID, which is only know at activity.onCreate, is bound
  @JvmStatic @Provides @Id fun provideId(activity: ClientActivity): Long = activity.id
}

// This is only necessary if you bind other Longs in your Dagger graph
@Qualifier
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Id

ViewModel and Factory

class ClientActivityViewModel(
  private val id: Long // our late-bound ID!
) : ViewModel() { 

  // interact with persistence layer using the injected ID
}


class ClientActivityViewModelFactory @Inject constructor(
  private val id: Long
) : ViewModelProvider.Factory {

  @Suppress("UNCHECKED_CAST")
  override fun <T : ViewModel> create(modelClass: Class<T>): T = when {
    modelClass.isAssignableFrom(ClientActivityViewModel::class.java) -> {
      ClientActivityViewModel(id) as T
    }
    else -> {
      throw IllegalArgumentException("${modelClass.simpleName} is an unknown type")
    }
  }
}

There's no magic here. We're simply doing something a bit non-standard and exposing a property of an activity publicly to our dependency graph. The alternative would be something like viewModel.setId(id), which is not bad exactly, but also not nearly as fun ;-)

Discussion

pic
Editor guide