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.
Compose 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).
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() | |
} | |
} |
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() | |
} | |
} | |
} |
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) | |
} | |
} | |
} |
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).
Hilt
Using Hilt, we can inject our ViewModel in quite a straightforward manner.
composable(NavigationDestination.Screen1.destination) { | |
val viewModel: Screen1ViewModel = hiltNavGraphViewModel() | |
Screen1(viewModel = viewModel) | |
} |
Now, let’s see what is the inside the hiltNavGraphViewModel() extension.
- 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.
- The new ViewModel instance will be created in Hilt’s ViewModel Factory provided by generated ViewModelComponent.
Dagger 2
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 |
@Component( | |
modules = [Screen1Module::class] | |
) | |
@Screen1Scope | |
interface Screen1Component { | |
@Component.Builder | |
interface Builder { | |
fun build(): Screen1Component | |
} | |
fun getViewModel() : Screen1ViewModel | |
} |
Job Offers
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) | |
} | |
} | |
} |
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.
- 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.
- 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: