1

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()
    }
}
krtkush
  • 1,378
  • 4
  • 23
  • 46

2 Answers2

2

You cannot inject directly the NavController with Hilt in that way, since you can only call Navigation.findNavController from your Activity/Fragment, but the NavigationUtil approach you are taking is a good one in order to have access to navigation from the ViewModel.

You only need to do a couple changes:

  • Instead of having NavController passed as a parameter of NavigationUtil, create two methods setNavController and clearNavController.
  • Make NavigationUtil scoped to the ViewModel annotating the provide method with @ViewModelScoped.
  • Inject your NavigationUtil into your Activity, and call to setNavController in your onCreate method and to clearNavController in your onDestroy method.

Now your NavigationUtil has access to NavController, it is updated when the Activity is recreated (and cleared when it is destroyed), and you can access it from your ViewModel.

You can check the answer I gave to a very similar question here: How to inject `rememberNavController` from jetpack compose into an activity using hilt?

DAA
  • 1,346
  • 2
  • 11
  • 19
  • How would the ViewModel have access to the `NavigationUtil`? Should I inject it in the `ViewModel` as well? Or pass it via a public function of the ViewModel? – krtkush Apr 18 '23 at 21:58
  • Also, when trying to inject `NavigationUtil` in my fragment, I get a `NoSuchMethodException` upon the initialization of the fragment. I am posting the updated code in my question above. – krtkush Apr 18 '23 at 22:00
  • 1
    You should inject it in both your Fragment/Activity and your ViewModel. The problem you are getting with the injection is due to using ViewModelScoped along with ActivityComponent. In Hilt, ViewModelScoped has to be used with ViewModelComponent. You could also use ApplicationComponent and Singleton. Check https://developer.android.com/training/dependency-injection/hilt-android#component-hierarchy – DAA Apr 20 '23 at 06:38
  • Thank you! I think you and @Peter are suggesting similar approaching wrt DI. – krtkush Apr 21 '23 at 07:58
1

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()
            }
        }
    }
}
Peter
  • 340
  • 4
  • 13