Search code examples
androidandroid-jetpack-composedagger-hiltandroid-jetpack-navigation

How to inject `rememberNavController` from jetpack compose into an activity using hilt?


I'm trying to inject my navHostController into my MainActivity using hilt. But I'm getting the following error when trying to compile the code:

> Task :app:kaptDebugKotlin
C:\Users\pierr\AndroidStudioProjects\AndroidApps\Compose\Udemy\course01\crud\app\build\generated\source\kapt\debug\com\example\crud\CrudApplication_HiltComponents.java:129: error: [Dagger/MissingBinding] androidx.navigation.NavHostController cannot be provided without an @Inject constructor or an @Provides-annotated method.
  public abstract static class SingletonC implements CrudApplication_GeneratedInjector,
                         ^
      androidx.navigation.NavHostController is injected at
          com.example.crud.ui.MainActivity.navHostController
      com.example.crud.ui.MainActivity is injected at
          com.example.crud.ui.MainActivity_GeneratedInjector.injectMainActivity(com.example.crud.ui.MainActivity) [com.example.crud.CrudApplication_HiltComponents.SingletonC ? com.example.crud.CrudApplication_HiltComponents.ActivityRetainedC ? com.example.crud.CrudApplication_HiltComponents.ActivityC]

> Task :app:kaptDebugKotlin FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:kaptDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask$KaptExecutionWorkAction
   > java.lang.reflect.InvocationTargetException (no error message)

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/7.0.2/userguide/command_line_interface.html#sec:command_line_warnings

BUILD FAILED in 5s
24 actionable tasks: 2 executed, 22 up-to-date

This is my MainActivity code:

package com.example.crud.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.navigation.NavHostController
import com.example.crud.navigation.NavigationComponent
import com.example.crud.ui.theme.CRUDTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    @Inject lateinit var navHostController: NavHostController

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

        setContent {
            CRUDTheme {
                Surface(color = MaterialTheme.colors.background) {
                    NavigationComponent(navHostController)
                }
            }
        }
    }
}

This is my NavigationModule code:

package com.example.crud.di

import androidx.compose.runtime.Composable
import androidx.navigation.compose.rememberNavController
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent

@Module
@InstallIn(ActivityComponent::class)
object NavigationModule {
    @Composable
    fun provideNavHostController() = rememberNavController()
}

This is the code for the NavigationComponent (it takes as a parameter the navHostController I'm trying to inject into MainActivity):

package com.example.crud.navigation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.example.crud.ui.screens.crud.details.DetailScreen
import com.example.crud.ui.screens.crud.register.RegisterScreen
import com.example.crud.ui.screens.home.HomeScreen
import com.example.crud.ui.screens.home.HomeViewModel

@Composable
fun NavigationComponent(navController: NavHostController) {
    NavHost(navController = navController, startDestination = Routes.HOME) {
        composable(Routes.HOME) {
            val homeViewModel: HomeViewModel = hiltViewModel()
            val cities = homeViewModel.cities.observeAsState(listOf())
            HomeScreen(
                cities = cities,
                navigateToDetailsAction = { navController.navigate(Routes.REGISTER) }
            ) { cityId ->
                navController.navigate(Routes.getDetailsDynamicRoute(cityId))
            }
        }
        composable(Routes.REGISTER) { RegisterScreen { navController.popBackStack() } }
        composable(
            route = Routes.DETAILS,
            arguments = listOf(navArgument(Routes.CITY_ID_KEY) { type = NavType.IntType })
        ) { backStackEntry ->
            val cityId = backStackEntry.arguments?.getInt(Routes.CITY_ID_KEY)
            cityId?.let {
                DetailScreen(cityId = it, popNavigation = { navController.popBackStack() })
            }
        }
    }
}

I believe this is irrelevant to solving the problem, but the code in the Routes.kt file follows:

package com.example.crud.navigation

object Routes {
    private const val DETAILS_BASE_ROUTE = "details/"
    const val HOME = "home"
    const val REGISTER = "register"
    const val CITY_ID_KEY = "cityId"
    const val DETAILS = "$DETAILS_BASE_ROUTE{$CITY_ID_KEY}"

    fun getDetailsDynamicRoute(cityId: Int) = "$DETAILS_BASE_ROUTE${cityId}"
}

What am I doing wrong?


Solution

  • You can achieve this behaviour wrapping the NavHostController inside another class. Scope this Navigator class to your MainActivity (ActivityRetainedComponent and @ActivityRetainedScoped) and set the NavHostController after remembering it. Then inject it too in your ViewModels, and you can navigate from there.

    /**
     * Class to handle navigation. It should be injected into the screens' ViewModel
     */
    class Navigator {
    
        private var navController: NavHostController? = null
    
        fun setController(controller: NavHostController) {
            navController = controller
        }
    
        fun clear() {
            navController = null
        }
    
        fun navigate() {
            // TODO handle navigation with the navController
        }
    }
    

    Also, remember to inject an interface implemented by Navigator, so you can create a MockNavigator class to be able to test your ViewModels.