Граф навигации не был установлен для NavController compose.

Вопрос или проблема

Это код. На первый взгляд, все настроено правильно, но я не могу понять, почему это не работает. Я навигация с одного главного экрана на экран деталей после нажатия на элемент списка. Я могу предоставить больше информации, если это необходимо. navGraph равен null.


@Composable
fun Navigation(navController: NavHostController, viewModel: GamesViewModel){
    NavHost(navController = navController, startDestination = Screen.MainScreen.route){
        composable(route = Screen.MainScreen.route) {
            OverallStanding(viewModel, navController)
        }

        navigation(startDestination = Screen.MainScreen.route, route = Screen.DetailsScreen.route){
            composable(route = Screen.DetailsScreen.route + "/{teamName}",
                arguments = listOf(navArgument("teamName"){
                    NavType.StringType
                    nullable = false
                })
            ) { entry ->
                GamesDetailScreen(viewModel, navController, teamName = entry.arguments?.getString("teamName"))
            }
        }
    }
}

Это композируемая функция, где я вызываю функцию

@Composable
fun TeamStatsItem(teamStatsItem: TeamStats, isMainScreen: Boolean) {
    val navController = rememberNavController()
    val lastColumnValue =
        if (isMainScreen) teamStatsItem.winPercentage.toString() + "%" else teamStatsItem.totalGamesPlayed.toString()
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(16.dp))
            .padding(12.dp)
            .height(55.dp)
            .background(Color.White)
            .clickable(
                true,
                onClick = { navController.navigate(Screen.DetailsScreen.withArguments(teamStatsItem.teamName)) }),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(text = teamStatsItem.teamName, modifier = Modifier.width(175.dp))
        Text(text = teamStatsItem.wins.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = teamStatsItem.losses.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = teamStatsItem.draws.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = lastColumnValue)
    }
}

Это ошибка, которую я получил полностью. Она вызвана этим вызовом функции для навигации в вышеупомянутой композируемой функции.

FATAL EXCEPTION: main
                                                                                                    Process: com.dushanesmith.yahoocodingexercise, PID: 29195
                                                                                                    java.lang.IllegalArgumentException: Cannot navigate to details_screen/Olympiacos. Navigation graph has not been set for NavController androidx.navigation.NavHostController@79d6b61.
                                                                                                        at androidx.navigation.NavController.navigate(NavController.kt:2375)
                                                                                                        at androidx.navigation.NavController.navigate$default(NavController.kt:2370)
                                                                                                        at com.dushanesmith.yahoocodingexercise.TeamStatsItemKt.TeamStatsItem$lambda$0(TeamStatsItem.kt:35)
                                                                                                        at com.dushanesmith.yahoocodingexercise.TeamStatsItemKt.$r8$lambda$TBskDp7ealVso3TrsxH8j1ss2Nw(Unknown Source:0)
                                                                                                        at com.dushanesmith.yahoocodingexercise.TeamStatsItemKt$$ExternalSyntheticLambda0.invoke(D8$$SyntheticClass:0)
                                                                                                        at androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke-k-4lQ0M(Clickable.kt:639)
                                                                                                        at androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke(Clickable.kt:633)
                                                                                                        at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)
                                                                                                        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
                                                                                                        at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:179)
                                                                                                        at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:168)
                                                                                                        at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
                                                                                                        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
                                                                                                        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
                                                                                                        at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368)
                                                                                                        at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:719)
                                                                                                        at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.dispatchPointerEvent(SuspendingPointerInputFilter.kt:598)
                                                                                                        at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:620)
                                                                                                        at androidx.compose.foundation.AbstractClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:1044)
                                                                                                        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:387)
                                                                                                        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:373)
                                                                                                        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:373)
                                                                                                        at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:229)
                                                                                                        at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:144)
                                                                                                        at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:120)
                                                                                                        at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1999)
                                                                                                        at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1950)
                                                                                                        at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1834)
                                                                                                        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
                                                                                                        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
                                                                                                        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
                                                                                                        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
                                                                                                        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
                                                                                                        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
                                                                                                        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
                                                                                                        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
                                                                                                        at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:490)
                                                                                                        at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1904)
                                                                                                        at android.app.Activity.dispatchTouchEvent(Activity.java:4377)
                                                                                                        at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:448)
2024-10-17 23:53:04.058 29195-29195 AndroidRuntime          com...hanesmith.yahoocodingexercise  E      at android.view.View.dispatchPointerEvent(View.java:15919)
                                                                                                        at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:7021)
                                                                                                        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:6815)
                                                                                                        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6229)
                                                                                                        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6286)
                                                                                                        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6252)
                                                                                                        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:6417)
                                                                                                        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6260)
                                                                                                        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:6474)
                                                                                                        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6233)
                                                                                                        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6286)
                                                                                                        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6252)
                                                                                                        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6260)
                                                                                                        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6233)
                                                                                                        at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:9211)
                                                                                                        at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:9162)
                                                                                                        at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:9131)
                                                                                                        at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:9337)
                                                                                                        at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:267)
                                                                                                        at android.os.MessageQueue.nativePollOnce(Native Method)
                                                                                                        at android.os.MessageQueue.next(MessageQueue.java:335)
                                                                                                        at android.os.Looper.loopOnce(Looper.java:162)
                                                                                                        at android.os.Looper.loop(Looper.java:294)
                                                                                                        at android.app.ActivityThread.main(ActivityThread.java:8177)
                                                                                                        at java.lang.reflect.Method.invoke(Native Method)
                                                                                                        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
                                                                                                        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
                                                                                                        Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.MotionDurationScaleImpl@41684e3, androidx.compose.runtime.BroadcastFrameClock@4a5eee0, StandaloneCoroutine{Cancelling}@aaf2599, AndroidUiDispatcher@75dd05e]

Я пробовал изменить методы определения навигационной графики.

Ваш TeamStatsItem использует:

val navController = rememberNavController()

Это создает новый NavController, который вы никогда не ассоциировали с NavHost, поэтому этот NavController не имеет установленного навигационного графика, как и говорит сообщение об ошибке.

Вам нужно использовать точно тот NavController, который вы передали в свой NavHost.

Хотя вы можете передать его через ваши композируемые функции, как вы сделали для ваших OverallStanding и GamesDetailScreen, это не лучший подход. Согласно руководству по тестированию навигации Compose:

Разъедините код навигации от ваших композируемых назначений, чтобы позволить тестировать каждую композируемую функцию в изоляции, отдельно от композируемой функции NavHost.

Это означает, что вы не должны передавать navController напрямую в любую композируемую функцию, а вместо этого передавать обратные вызовы навигации как параметры. Это позволяет всем вашим композируемым функциям быть индивидуально тестируемыми, так как они не требуют экземпляра navController в тестах.

Уровень абстракции, предоставляемый лямбдой композируемой функции, позволяет вам отделить ваш код навигации от самой композируемой функции. Это работает в двух направлениях:

  1. Передавайте только разобранные аргументы в свою композируемую функцию
  2. Передавайте лямбды, которые должны быть инициированы композируемой функцией для навигации, а не сам NavController.

Таким образом, вы могли бы написать вашу композируемую функцию так:

@Composable
fun TeamStatsItem(
    teamStatsItem: TeamStats,
    isMainScreen: Boolean,
    onTeamSelected: (teamName: String) -> Unit
) {
    val lastColumnValue =
        if (isMainScreen) teamStatsItem.winPercentage.toString() + "%" else teamStatsItem.totalGamesPlayed.toString()
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(16.dp))
            .padding(12.dp)
            .height(55.dp)
            .background(Color.White)
            .clickable(
                true,
                onClick = { onTeamSelected(teamStatsItem.teamName) }),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(text = teamStatsItem.teamName, modifier = Modifier.width(175.dp))
        Text(text = teamStatsItem.wins.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = teamStatsItem.losses.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = teamStatsItem.draws.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = lastColumnValue)
    }
}

Это позволит вам легко написать @Preview для этой композируемой функции, а также написать юнит-тесты для нее в изоляции.

TeamStatsItem( teamStatsItem, isMainScreen ) { teamName -> navController.navigate(Screen.DetailsScreen.withArguments(teamName)) }

Или передайте лямбду на другой уровень, желательно прямо до уровня вашего NavHost (который должен быть единственным уровнем, который действительно имеет доступ к вашему NavController).

Ответ или решение

Существует распространенная ошибка при работе с библиотекой Navigation в Jetpack Compose, когда NavController не ассоциирован с NavHost, что приводит к возникновению исключения "Navigation graph has not been set for NavController". Чтобы понять причины этой проблемы и способы её решения, давайте обратим внимание на ваш код и описание ситуации.

Проблема

Ошибка, с которой вы столкнулись, возникает из-за того, что в вашем компоненте TeamStatsItem создаётся новый экземпляр NavController с помощью функции rememberNavController(). Этот новый NavController не связан с NavHost, который вы определили в функции Navigation.

При попытке вызвать navController.navigate(...) у этого нового экземпляра не будет установлен граф навигации, что и приводит к ошибке.

Решение

Для решения этой проблемы нужно использовать тот же экземпляр NavController, который вы передаете в NavHost. Один из способов сделать это — передать navController через параметры в ваш TeamStatsItem. Следующим образом можно модифицировать решение:

  1. Передача navController через параметры: откажитесь от создания нового экземпляра NavController в TeamStatsItem и вместо этого передавайте уже существующий navController.
@Composable
fun TeamStatsItem(
    teamStatsItem: TeamStats,
    isMainScreen: Boolean,
    onTeamSelected: (teamName: String) -> Unit
) {
    val lastColumnValue =
        if (isMainScreen) "${teamStatsItem.winPercentage}%" else teamStatsItem.totalGamesPlayed.toString()
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(16.dp))
            .padding(12.dp)
            .height(55.dp)
            .background(Color.White)
            .clickable(
                true,
                onClick = { onTeamSelected(teamStatsItem.teamName) }
            ),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(text = teamStatsItem.teamName, modifier = Modifier.width(175.dp))
        Text(text = teamStatsItem.wins.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = teamStatsItem.losses.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = teamStatsItem.draws.toString(), modifier = Modifier.padding(end = 42.dp))
        Text(text = lastColumnValue)
    }
}
  1. Модификация вызова TeamStatsItem: Вызов данного компонента должен выглядеть следующим образом:
TeamStatsItem(teamStatsItem, isMainScreen) { teamName ->
    navController.navigate(Screen.DetailsScreen.withArguments(teamName))
}
  1. De-coupling логики навигации от представления: Это решение следует принципам, изложенным в документации по навигации в Jetpack Compose, где рекомендуется разъединить логику навигации и представления. Это позволяет вам более гибко тестировать каждый компонент в отдельности, не нуждаясь в экземпляре NavController для каждого из них.

Заключение

Следуя этим практике, вы сможете избежать упомянутых проблем с навигацией, обеспечив встроенную связь между вашими экранами и навигацией в приложении. Не забудьте также протестировать изменения, чтобы убедиться, что навигация теперь работает как задумано.

Если у вас остались дополнительные вопросы или вам нужна помощь с другими аспектами Jetpack Compose, не стесняйтесь обращаться!

Оцените материал
Добавить комментарий

Капча загружается...