Jetpack Compose recently got released to production and its release was accompanied with great anticipation. This new toolkit helps developers accelerate UI development in a much simpler way, eliminating the traditional XML-based UI definitions.
There are two ways to integrate Jetpack Compose — in a fresh app (which is probably the simplest) and integrating it into an existing app. After spending some time deep-diving into Jetpack Compose, I have begun slowly integrating it into one of my existing apps.
The app in question is an MVVM (Model-View-ViewModel) application that makes use of Hilt for dependency injection as well as the Jetpack Navigation library for easy navigation between the different fragments.
The idea in Compose is to have what are referred to as Composables. These composables are annotated functions and are the fundamental building blocks in Compose applications. A composable, like a fragment or activity, can have a viewModel linked to it, this is especially important for having the viewModel handle state and not the composable itself.
To add the compose-based navigation dependency, the following is added to the app-level build.gradle file:
implementation "androidx.navigation:navigation-compose:$compose_navigation_version"
The setup of a navigation graph in Jetpack Compose is slightly different from the traditional setup found within a non-Compose app, for example:
@AndroidEntryPoint class MainActivity: AppCompatActivity() { ... @Composable fun MyApp() { // setup the navController this way val navController = rememberNavController() NavHost( navController, // the navController created above startDestination = "dashboard" // start destination variable needs to match one of the composable screen routes below ) { composable("dashboard") { DashboardScreen(navController) } composable("library") { LibraryScreen(navController) } } }
where the NavHost expects two main parameters — the NavController and the start destination to be defined; and all the app’s composables (or screens) to be added to the navigation graph through the use of NavGraphBuilder.composable:
composable("dashboard") { DashboardScreen(navController) }
where “dashboard” is the route and “DashboardScreen(navController)” is the name of my composable function with the navController passed to it as an argument.
This was all fine until I had to define my viewModel annotated with @HiltViewModel inside the composable function. The standard way of defining a viewModel inside a composable function is by doing the following:
@Composable fun DashboardScreen(navController: NavController) { val viewModel: DashboardViewModel = viewModel() }
Job Offers
Unfortunately, because my viewModel is annotated with @HiltViewModel it could not be added to the navigation graph through the standard way as I encountered the following crash:
java.lang.RuntimeException: Cannot create an instance of class com.exampleapp.ui.dashboard.DashboardViewModel at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:221) at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:278) at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.java:112) at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:185) at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150) at androidx.lifecycle.viewmodel.compose.ViewModelKt.get(ViewModel.kt:87) at androidx.lifecycle.viewmodel.compose.ViewModelKt.viewModel(ViewModel.kt:72) at com.exampleapp.ui.dashboard.DashboardScreenKt.DashboardScreen(DashboardScreen.kt:17) at com.exampleapp.MainActivity$ExampleApp$2$1$2.invoke(MainActivity.kt:156) at com.exampleapp.MainActivity$ExampleApp$2$1$2.invoke(MainActivity.kt:156)
Basically, to be able to add my HiltViewModel to the navigation graph involves a different syntax. First, the hilt-navigation dependency needs to be added:
implementation "androidx.hilt:hilt-navigation-compose:$compose_hilt_navigation_version"
With this dependency added, the viewModel can be added using hiltViewModel whose documentation states:
[hiltViewModel] Returns an existing HiltViewModel -annotated ViewModel or creates a new one scoped to the current navigation graph present on the {@link NavController} back stack.
If no navigation graph is currently present then the current scope will be used, usually, a fragment or an activity.
My composable function DashboardScreen was then updated to be:
@Composable fun DashboardScreen(navController: NavController) { val dashboardViewModel = hiltViewModel<DashboardViewModel>() }
and the aforementioned crash was resolved.