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()
}
}
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()
}
}
}
}