I am getting this error when trying to navigate to another screen from the view model,
kotlin.UninitializedPropertyAccessException: lateinit property _navController has not been initialized
This is my activity code,
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var navigator: Navigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AssessmentAppTheme {
// A surface container using the 'background' color from the theme
Column(modifier = Modifier
.fillMaxWidth(1f)
.padding(vertical = 10.dp, horizontal = 10.dp), horizontalAlignment = Alignment.CenterHorizontally) {
AssessmentApp(modifier = Modifier.padding(bottom = 40.dp))
NavigationGraph(navigator)
}
}
}
}
}
This is my navigation module,
@Module
@InstallIn(ActivityRetainedComponent::class)
class AppModule {
@Provides
fun providesNavigation() = Navigator()
}
This is my navigation class,
@ActivityRetainedScoped
class Navigator {
private lateinit var _navController: NavHostController
fun navigate(destination: NavigationDestination) {
_navController.navigate(destination.route)
}
fun setController(controller: NavHostController) {
_navController = controller
}
}
this is the navigation graph where I am remembering the navController,
@Composable
fun NavigationGraph(
navigator: Navigator
) {
val navController = rememberNavController()
navigator.setController(navController)
NavHost(navController = navController, startDestination = Routes.CLIENTS_ROUTE ) {
composable(Routes.CLIENTS_ROUTE) {
val viewModel = hiltViewModel<ClientViewModel>()
ClientScreen(viewModel = viewModel)
}
composable(Routes.ASSESSMENT_OPTIONS_ROUTE, arguments = listOf(navArgument(RouteArgs.CLIENT_ID) {type = NavType.StringType})) { backStackEntry ->
val viewModel = hiltViewModel<ClientViewModel>()
ClientAssessmentOptionScreen(viewModel = viewModel)
}
}
finally, this is one of view models trying to navigate to different screen,
@HiltViewModel
class ClientViewModel @Inject constructor(
private val repository: IClientRepository,
private val navigator: Navigator,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
// Some code here //
fun onEvent(event: ClientEvent) {
viewModelScope.launch {
when(event) {
is ClientEvent.OnClientClicked -> {
event.client.clientName?.let {
navigator.navigate(
NavigationDestination(Routes.generateAssessmentOptionsRoute(clientId = it))
)
}
}
}
}
}
}
What am I doing wrong here? and is the approach to make view models handle navigation the right one for jetpack compose applications?
Just answering it here in case someone else also stumbles upon this. I have modified my navigator class and added a shared flow
. Which would be used sort of as an event emitter. Whenever we would want to navigate to another screen we can use the navigate
method which would emit the route destination.
@Singleton
class Navigator {
private val _sharedFlow =
MutableSharedFlow<NavigationDestination>(extraBufferCapacity = 1)
val sharedFlow = _sharedFlow.asSharedFlow()
fun navigate(destination: NavigationDestination) {
_sharedFlow.tryEmit(destination)
}
}
Now in the NavigationGraph
, I have remembered the NavController
and have also added a launchedEffect
coroutine, which would be listening to the navigate events from the flow. For each flow event, we will trigger the NavController
to navigate to that emitted destination.
@Composable
fun NavigationGraph(
navController: NavHostController = rememberNavController(),
navigator: Navigator
) {
LaunchedEffect("navigation") {
navigator.sharedFlow.onEach {
navController.navigate(it.route)
}.launchIn(this)
}
NavHost(navController = navController, startDestination = Routes.CLIENTS_ROUTE ) {
// some code here... //
}
}