Blog Infos
Author
Published
Topics
,
Published

Highlighting the advantages of DI is not the purpose of this article, but almost all of the project needs it. In the official documentation, it’s easy to learn how to use Hilt with Jetpack Compose; however, in the real world, most of us have been using Dagger 2 for dependency injections.

Let’s start with how Hilt works under the hood with Jetpack Compose navigation. Thereafter, we will focus on the Dagger 2 solution. Before we begin, we must first define navigation.

Let’s define the navigation in Activity.

Add string constants for the navigation (i.e., for the advanced way you can store properties there).

sealed class NavigationDestination(val destination: String) {
object Screen1 : NavigationDestination("screen1")
object Screen2 : NavigationDestination("screen2")
}

In our Activity, we need to use NavController.
This class keeps the state of composables and tracks their back stack.
We must create the instance of this class in the composable hierarchy.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Next, we need NavHost. Using DSL inside NavHost, we define what our app will navigate to and what composables we should use for each screen.

setContent {
val navController = rememberNavController()
NavHost(navController, startDestination = NavigationDestination.Screen1.destination) {
composable(NavigationDestination.Screen1.destination) {
Screen1()
}
composable(NavigationDestination.Screen2.destination) {
Screen2()
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Suppose that for each screen, we need a ViewModel. If our ViewModel has an empty constructor with no dependencies, it’s easy to use.

setContent {
val navController = rememberNavController()
NavHost(navController, startDestination = NavigationDestination.Screen1.destination) {
composable(NavigationDestination.Screen1.destination) {
val viewModel: Screen1ViewModel = viewModel()
Screen1(viewModel = viewModel)
}
composable(NavigationDestination.Screen2.destination) {
val viewModel: Screen2ViewModel = viewModel()
Screen2(viewModel = viewModel)
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Again, in the real world, this is a pretty rare case. So, suppose that our ViewModel has some dependencies (e.g., Repository). Here, we are starting to use DI (as far as this article is concerned).

Using Hilt, we can inject our ViewModel in quite a straightforward manner.

composable(NavigationDestination.Screen1.destination) {
val viewModel: Screen1ViewModel = hiltNavGraphViewModel()
Screen1(viewModel = viewModel)
}
view raw MainActivity.kt hosted with ❤ by GitHub

Now, let’s see what is the inside the hiltNavGraphViewModel() extension.

  1. Hilt uses LocalViewModelStoreOwner to identify the ViewModel owner. It might be Activity, Fragment, or in our case, NavBackStackEntry (provided by composable()). So, our ViewModel will work in the scope of NavBackStackEntry and close accordingly.
  2. The new ViewModel instance will be created in Hilt’s ViewModel Factory provided by generated ViewModelComponent.

We now understand how it works with Hilt. So, let’s apply similar logic to Dagger 2. I would also like to mention that Dagger 2 offers a great advantage in that we can use various custom components for each composable screen.

Component, Model, and Scope
Here is all the same as usual.

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class Screen1Scope
view raw Screen1Scope.kt hosted with ❤ by GitHub
@Component(
modules = [Screen1Module::class]
)
@Screen1Scope
interface Screen1Component {
@Component.Builder
interface Builder {
fun build(): Screen1Component
}
fun getViewModel() : Screen1ViewModel
}

Job Offers

Job Offers


    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Mobile Engineer

    OLX Group
    Remote, Portugal, Spain, Romania, Poland
    • Full Time
    apply now

    Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

,

Branching out to Jetpack Compose

As one of the most widely used social media platforms, Twitter is always hunting for ways to better connect its users. In early 2021 the Client UI team at Twitter began the task of integrating…
Watch Video

Branching out to Jetpack Compose

Nacho López & Chris Banes
Staff Software Engineer
Twitter

Branching out to Jetpack Compose

Nacho López & Chr ...
Staff Software Engin ...
Twitter

Branching out to Jetpack Compose

Nacho López & C ...
Staff Software Engineer
Twitter

Jobs

The first important step is to create an extension similar to hiltNavGraphViewModel(). However, to make things clearer, let’s return lambda instead of complicated custom Hilt’s ViewModel Factory. Additionally, let’s also agree to name it daggerViewModel(). It’s just for naming, there is nothing related to Dagger inside.

@Composable
inline fun <reified T : ViewModel> daggerViewModel(
key: String? = null,
crossinline viewModelInstanceCreator: () -> T
): T =
androidx.lifecycle.viewmodel.compose.viewModel(
modelClass = T::class.java,
key = key,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return viewModelInstanceCreator() as T
}
}
)

What can we see in this extension? First, we simply create a new ViewModel instance using a custom factory.

Using this extension, we will create a Dagger Component (if needed, according to the ViewModel owner) and provide a ViewModel instance.

setContent {
val navController = rememberNavController()
NavHost(navController, startDestination = NavigationDestination.Screen1.destination) {
composable(NavigationDestination.Screen1.destination) {
// option #1 create a component inside NavBackStackEntry,
// which can be helpful if we need to provide more than one object from DI here
val component = DaggerScreen1Component.builder().build()
val viewModel: Screen1ViewModel = daggerViewModel {
component.getViewModel()
}
Screen1(viewModel = viewModel)
}
composable(NavigationDestination.Screen2.destination) {
val viewModel: Screen2ViewModel = daggerViewModel {
// option #2 create DI component and instantly get ViewModel instance
DaggerScreen2Component.builder().build().getViewModel()
}
Screen2(viewModel = viewModel)
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

It would be easier to understand this code and parts where we create DI components if we suppose that `composable(){}` block is something similar from a scope perspective as Activity or as Fragment. So, we have two options for how (and where) we create a DI Component.

  1. Create Dagger Component inside the composable(){} block; this solution gives us a possibility using the same instance of this Component a few times in this block, providing multiple objects.
  2. Create Dagger Component inside the daggerViewModel extension’s lambda. This way may be useful if we need to get only one object from the DI with resolved dependencies. In our case, we need just only ViewModel in here. It means that DaggerScreen2Component will be created only if the current owner (NavBackStackEntry) must create a new instance of ViewModel.

Here is a repository with the full code of this example, which you can compile and run:

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
blog
Yes! You heard it right. We’ll try to understand the complete OTP (one time…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu