Search code examples
androidkotlindagger-hiltandroid-jetpack-navigation

NavController in ViewModel: androidx.navigation.NavController cannot be provided without an @Inject constructor or an @Provides-annotated method


I am trying to inject navController into my ViewModel -

ViewModel -

@HiltViewModel
class DeviceHolderListViewModelImpl @Inject constructor(
    private val fetchUsersUseCase: FetchUsersUseCase,
    private val navigationUtil: NavigationUtil
    ) : DeviceHolderListViewModel, ViewModel() {
      

      // Trying to access navigationUtil here

}

NavigationUtil -

class NavigationUtil @Inject constructor(private val navController: NavController) {

    fun navigateTo(destination: String, bundle: Bundle) {

        when(destination) {
            DEVICE_HOLDER_LIST -> navController.navigate(R.id.action_global_goto_deviceHolderListFragment, bundle)
            DEVICE_HOLDER_DETAILS -> navController.navigate(R.id.action_global_goto_deviceHolderDetailsFragment, bundle)
        }
    }

    fun navigateBack() {
        navController.popBackStack()
    }
}

NavigationModule -

@Module
@InstallIn(ActivityComponent::class)
object NavigationModule {

    @Provides
    fun provideNavController(activity: AppCompatActivity): NavController {
        return Navigation.findNavController(activity, R.id.nav_host_fragment)
    }

    @Provides
    fun provideNavigationUtil(navController: NavController): NavigationUtil {
        return NavigationUtil(navController)
    }
}

Upon trying to build the code, I get the following error -

error: [Dagger/MissingBinding] androidx.navigation.NavController cannot be provided without an @Inject constructor or an @Provides-annotated method.

IS it because I am trying to access the navController from ViewModel while it should be accessed from a Fragment or Activity?

My aim is to initiate navigation from the ViewModel. How do I ideally do that?


EDIT: Changes according to @DAA's answer -

NavigationUtil

class NavigationUtil {

    private var navController: NavController? = null

    fun setController(controller: NavController) {
        navController = controller
    }

    fun clear() {
        navController = null
    }

    fun navigateTo(destination: String, bundle: Bundle) {

        when(destination) {
            DEVICE_HOLDER_LIST -> navController?.navigate(R.id.action_global_goto_deviceHolderListFragment, bundle)
            DEVICE_HOLDER_DETAILS -> navController?.navigate(R.id.action_global_goto_deviceHolderDetailsFragment, bundle)
        }
    }

    fun navigateBack() {
        navController?.popBackStack()
    }
}

NavigationModule

@Module
@InstallIn(ActivityComponent::class)
object NavigationModule {

    @Provides
    @ViewModelScoped
    fun provideNavigationUtil(): NavigationUtil {
        return NavigationUtil()
    }
}

NavigationUtil

class NavigationUtil {

    private var navController: NavController? = null

    fun setController(controller: NavController) {
        navController = controller
    }

    fun clear() {
        navController = null
    }

    fun navigateTo(destination: String, bundle: Bundle) {

        when(destination) {
            DEVICE_HOLDER_LIST -> navController?.navigate(R.id.action_global_goto_deviceHolderListFragment, bundle)
            DEVICE_HOLDER_DETAILS -> navController?.navigate(R.id.action_global_goto_deviceHolderDetailsFragment, bundle)
        }
    }

    fun navigateBack() {
        navController?.popBackStack()
    }
}

Solution

  • I will suggest a different approach.

    I usually make a Navigatior class, that is injected in both Activity and your ViewModel. ViewModel calls navigator methods and activity subscribes to them.

    Disclaimer: Following code is written based on my memory and may not be syntactically correct. Feel free to improve it.

    sealed interface NavEvent {
       data class Navigate(val directions: NavDirections): NavEvent
       object Pop(): NavEvent
       data class PopForResult(val requestKey: String, val result: Bundle): NavEvent
    }
    
    @Singleton
    class Navigator @Inject constructor() {
    
       private val _navigateFlow = MutableSharedFlow<NavEvent>
       val navigateFlow: SharedFlow = _navigateFlow
    
       suspend fun navigate(nav: NavDirections) {
           _navigateFlow.emit(Navigate(directions))
       }
       
       suspend fun pop() {
           _navigateFlow.emit(Pop)
       }
    }
    
    @HiltViewModel
    class MyVm @Inject constructor(
        private val navigator: Navigator
    ) : ViewModel() {
    
        fun onClickSmth() {
            viewModelScope.launch {
                navigator.navigate(MyFragmentDirections.actionToSomewhere())
            }
        }
    }
    
    @AndroidEntryPoint
    class MainActivity : Activity {
        
        @Inject lateinit var navigator: Navigator
    
        private lateinit var navController: NavController = TODO()
    
        fun onCreate() {
            lifecycleScope.launch {
                navigator.navigateFlow.collect(this::onNavEvent)
            }
        }
        
        private fun onNavEvent(event: NavEvent) {
            when (navEvent) {
                Navigate -> navController.navigate(navEvent.directions)
                Pop -> navController.popBackStack()
                PopForResult -> {
                    navController.previousBackStackEntry
                        ?.savedStateHandle
                        ?.set(event.requestKey, event.result)
                    navController.popBackStack()
                }
            }
        }
    }