6

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?

Pierre Vieira
  • 2,252
  • 4
  • 21
  • 41
  • 3
    You can't "inject" a remembered variable. Those need to be created as part of your Composable function (i.e., within `setContent`). Why are you trying inject your NavController in the first place? – ianhanniballake Sep 22 '21 at 02:15
  • 1
    It's just a matter of "design pattern". I believe that it shouldn't be the responsibility of `MainActivity` to create the `NavHostController`, although that doesn't interfere with anything currently in my code. But apparently it's impossible to make an injection in this case, thanks! – Pierre Vieira Sep 22 '21 at 14:13

2 Answers2

5

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.

DAA
  • 1,346
  • 2
  • 11
  • 19
  • Sorry I am very new to DI but I think what you said is what I want to achieve, can you provide more code for like how can I do it in Activity and ViewModel please? I want to be able to inject NavHostController into ViewModel.. – Arst Aug 08 '22 at 09:31
  • That looks more like a generic DI question, which is not the same as the NavHostController one (and can vary depending on the library you are using). Take a look at the Hilt guide https://developer.android.com/training/dependency-injection/hilt-android – DAA Aug 09 '22 at 10:19
  • if lets say I have 2 navController for 2 different navHost, how to scope each of them? so if the second navHost is binded, also the navController binded to the particular navhost @DAA – Yehezkiel L Jan 23 '23 at 15:14
  • You can have 2 different Navigator instances, each one with a different NavController, and inject them in different places. If you are using Hilt, check this link: https://developer.android.com/training/dependency-injection/hilt-android#multiple-bindings – DAA Jan 24 '23 at 16:39
  • but they both will retain in the application right? Because we can only scope the Navigator in application or the activity. I want the second one to destroyed after the second screen is pop @DAA – Yehezkiel L Jan 26 '23 at 14:21
  • You should have a clear() method to call when you no longer want it. Check the edited response – DAA Jan 30 '23 at 15:40
3

but I think that you don't need to create the NavController on the main class, since this part of the la class NavigationComponent I thought this class is the one in charge to handle this one.

I am new to the android world but it makes sense to me that the NavigationComponent controls the NavController and all its interactions with it.

Normally I use it this way

@Composable fun NavigationHost() {

val navController = rememberNavController()

MyTheme {
    NavHost(
        navController = navController,
        NavItem.Main.route
    )
    {

        composable(route = NavItem.Main.route) {
            FeedScreen { productOnClick ->
                navController.navigate(NavItem.Detail.createNavRoute(productOnClick.id))
            }
        }

        composable(route = NavItem.Main.route) {
            ConfirmationScreen {
                navController.navigate(NavItem.Main.route)
            }
        }
    }

}

}

Jesr2104
  • 31
  • 5