Android NavHost:“在导航图中找不到与请求 NavDeepLinkRequest 匹配的导航目的地”

问题描述 投票:0回答:1

这是我的相关代码,用 Kotlin + Jetpack Compose 构建:

OutlookPlannerNavHost.kt

/**
 * Provides Navigation graph for the application.
 */
@Composable
fun OutlookPlannerNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = DestinationHome.route,
        modifier = modifier
    ) {
        /**
         * Home page
         */
        composable(route = DestinationHome.route) {
            Home(
                modifier = modifier,
                pageCurrent = DestinationHome.route,
                navigateToPlanMake = { navController.navigate(route = DestinationPlanMake.route) },
                navigateToPlanEdit = { planId -> navController.navigate(route = "${DestinationPlanEdit.route}/${planId}") }
            )
        }
        /**
         * Make Plan page
         */
        composable(route = DestinationPlanMake.route) {
            PlanMake(
                modifier = modifier,
                pageCurrent = DestinationPlanEdit.route,
                navigateBack = {
                    navController.popBackStack()
                },
            )
        }
        /**
         * Edit Plan page
         * (AKA Make Plan page with a Plan object passed)
         */
        composable(
            route = DestinationPlanEdit.routeWithId,
            arguments = listOf(navArgument(name = DestinationPlanEdit.PLAN_ID) {
                type = NavType.IntType
            })
        ) {
            Log.d("Args", it.arguments?.getInt(DestinationPlanEdit.PLAN_ID).toString())
            PlanMake(
                modifier = modifier,
                pageCurrent = DestinationPlanEdit.route,
                navigateBack = { navController.popBackStack() },
            )
        }
    }
}

首页.kt

@Composable
fun Home(
    modifier: Modifier = Modifier,
    navigateToPlanMake: () -> Unit,
    navigateToPlanEdit: (Int) -> Unit,
    pageCurrent: String,
    viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
    /**
     * Immutable variables
     */
    val homeUiState by viewModel.homeUiState.collectAsState()
    val planList = homeUiState.planList

    Scaffold (
        floatingActionButton = {
            AppFAB(
                pageCurrent = pageCurrent,
                onClick = navigateToPlanMake
            )
        },
        modifier = modifier,
    ) {
        LazyColumn (modifier = modifier.padding(it)) {
            item {
                ViewingArea()
            }
            items(items = planList) { planEntity ->
                PlanCard(
                    planEntity = planEntity,
                    modifier = modifier
                        .padding(16.dp)
                        .clickable { navigateToPlanEdit(planEntity.id) }
                )
            }
        }
    }
}

NavigationDestination.kt

/**
 * Interface to describe the navigation destinations for the app
 */
interface NavigationDestination {
    /**
     * Unique name to define the path for a composable
     */
    val route: String

    /**
     * String resource id to that contains title to be displayed for the screen.
     */
    val titleRes: Int
}

DestinationPlanEdit.kt

object DestinationPlanEdit: NavigationDestination {
    override val route = R.string.route_plan_edit.toString()
    override val titleRes = R.string.name_plan_edit
    /**
     * Additional values for routing
     */
    const val PLAN_ID: String = "planId"
    val routeWithId: String = "$route/${PLAN_ID}"
}

PlanMakeViewModel.kt

class PlanMakeViewModel(
    savedStateHandle: SavedStateHandle,
    private val planRepository: PlanRepository,
): ViewModel() {
    /**
     * Make Plan UI state
     */
    var planMakeUiState by mutableStateOf(PlanMakeUiState())
        private set

    /**
     * Initialize private values in presence of an existing Plan object
     */
    init {
        try {
            val planId: Int = checkNotNull(savedStateHandle[DestinationPlanEdit.PLAN_ID])
            viewModelScope.launch {
                planMakeUiState = planRepository
                    .getPlanOne(planId)
                    .filterNotNull()
                    .first()
                    .toMakePlanUiState()
            }
        } catch(_: Exception) {
            /**
             * No Plan ID supplied
             *
             * Assume user is making plan, or plan is missing in database
             */
        }
    }

    /**
     * Check that no fields are empty
     */
    private fun validateInput(planCheck: Plan = planMakeUiState.plan): Boolean {
        return with(planCheck) {
            note.isNotBlank()
        }
    }

    /**
     * Updates the [planMakeUiState] with the value provided in the argument. This method also triggers
     * a validation for input values.
     */
    fun updateUiState(planUpdated: Plan) {
        planMakeUiState = PlanMakeUiState(
            plan = planUpdated,
            fieldNotEmptyAll = validateInput(planUpdated)
        )
    }

    /**
     * Insert + Update current plan
     */
    suspend fun planUpsert() {
        if (validateInput()) planRepository.planUpsert(planMakeUiState.plan.toEntity())
    }
}

这些是从我的应用程序项目中提取的 GitHub 上的 Outlook Planner 在 GPL 许可证下,您可以将其 git 克隆到您的计算机上并自行检查。

我在 NavHost 上的目标是从 Home 可组合函数中提取 Plan.id,然后将该值传输到 DestinationPlanEdit.routeWithId 以编辑该 Plan 对象并将其保存到 Room 数据库。

唯一的问题是它返回以下错误:

FATAL EXCEPTION: main
                                                                                                    Process: com.outlook.planner, PID: 19658
                                                                                                    java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/2131689725/3 } cannot be found in the navigation graph ComposeNavGraph(0x0) startDestination={Destination(0xf08bc5ce) route=2131689704}
                                                                                                        at androidx.navigation.NavController.navigate(NavController.kt:1819)
                                                                                                        at androidx.navigation.NavController.navigate(NavController.kt:2225)
                                                                                                        at androidx.navigation.NavController.navigate$default(NavController.kt:2220)
                                                                                                        at com.outlook.planner.ui.navigation.OutlookPlannerNavHostKt$OutlookPlannerNavHost$1$1$2.invoke(OutlookPlannerNavHost.kt:38)
                                                                                                        at com.outlook.planner.ui.navigation.OutlookPlannerNavHostKt$OutlookPlannerNavHost$1$1$2.invoke(OutlookPlannerNavHost.kt:34)
                                                                                                        at com.outlook.planner.ui.pages.home.HomeKt$Home$2$1$1$1$1.invoke(Home.kt:52)
                                                                                                        at com.outlook.planner.ui.pages.home.HomeKt$Home$2$1$1$1$1.invoke(Home.kt:52)
                                                                                                        at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:987)
                                                                                                        at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:981)

我不知道如何解决这个问题。我在线查看了各种视频教程(其中大部分都没有帮助,因为它们教授 XML 和片段,这不是我在 Android Basics with Compose

中学习的内容)

我请求帮助了解导致此问题的原因,因为我很累并且无法自己解决此问题。但我需要这个功能才能工作。

提前致谢。

编辑 还有一些相关文件,以防您不喜欢克隆 GitHub 项目:

PlanMakeUiState.kt

/**
 * Data class that represents the plan UI state
 *
 * Uses:
 * - Hold current plan
 * - Check if all fields are not empty
 * - To show MaterialTimePicker or not
 * - To show MaterialDatePicker or not
 */
data class PlanMakeUiState(
    val plan: Plan = Plan(
        note = "",
        year = LocalDateTime.now().year,
        month = LocalDateTime.now().monthValue,
        date = LocalDateTime.now().dayOfMonth,
        hour = LocalDateTime.now().hour,
        minute = LocalDateTime.now().minute,
        ),
    val fieldNotEmptyAll: Boolean = false
)

MakePlan.kt

@Composable
fun PlanMake(
    modifier: Modifier = Modifier,
    pageCurrent: String,
    navigateBack: () -> Unit,
    context: Context = LocalContext.current,
    viewModel: PlanMakeViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
    /**
     * Immutable values
     */
    val coroutineScope = rememberCoroutineScope()

    Scaffold(
        floatingActionButton = {
            AppFAB(
                pageCurrent = pageCurrent,
                onClick = {
                    coroutineScope.launch {
                        viewModel.planUpsert()
                        navigateBack()
                    }
                }
            )
        },
        modifier = modifier
    ) { innerPadding ->
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier
                .padding(innerPadding)
                .fillMaxWidth()
        ) {
            ViewingArea(pageCurrent = pageCurrent)

            /**
             * Note of plan
             */
            MakePlanBody(
                modifier = modifier,
                planMakeUiState = viewModel.planMakeUiState,
                onPlanValueChange = viewModel::updateUiState,
                context = context
            )
        }
    }
}


/**
 * References:
 * https://codingwithrashid.com/how-to-add-underlined-text-in-android-jetpack-compose/
 */
@Composable
fun MakePlanBody(
    modifier: Modifier = Modifier,
    planMakeUiState: PlanMakeUiState,
    onPlanValueChange: (Plan) -> Unit,
    context: Context
) {
    /**
     * Encompass the Plan in the UI state to a concise pointer variable
     */
    val plan: Plan = planMakeUiState.plan

    /**
     * Embellishment values
     */
    val heightSpacer: Dp = 32.dp
    val heightSpacerBetweenTextAndButton: Dp = 8.dp
    val textStyle = TextStyle(textAlign = TextAlign.Left)
    val sizeFontOfHeader = 16.sp
    val sizeFontOfDateTimeValue = 24.sp

    /**
     * Logic variables
     */
    var showPickerTime: Boolean by remember { mutableStateOf(false) }
    var showPickerDate: Boolean by remember { mutableStateOf(false) }

    /**
     * Display variables
     */
    var displayTime: LocalTime by remember { mutableStateOf(LocalTime.of(plan.hour, plan.minute)) }
    var displayDate: LocalDate by remember { mutableStateOf(LocalDate.of(plan.year, plan.month, plan.date)) }

    /**
     * Field to insert a note
     */
    Text(
        text = "Note",
        fontSize = sizeFontOfHeader
    )
    TextField(
        value = planMakeUiState.plan.note,
        onValueChange = { noteNew -> onPlanValueChange(plan.copy(note = noteNew)) },
        textStyle = textStyle,
        placeholder = {
            Text(
                text = "Your note here",
                textAlign = TextAlign.Center,
            )
        }
    )
    Spacer(modifier = Modifier.height(heightSpacer))

    /**
     * Field to pick a time
     */
    Text(
        text = stringResource(id = R.string.ask_time),
        fontSize = sizeFontOfHeader
    )
    Spacer(modifier = Modifier.height(heightSpacerBetweenTextAndButton))
    Text(
        text = buildAnnotatedString {
            append("On")
            append(" ")
            withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
                append(displayTime.toString())
            }
        },
        fontSize = sizeFontOfDateTimeValue,
    )
    Spacer(modifier = Modifier.height(heightSpacerBetweenTextAndButton))
    Button(
        onClick = { showPickerTime = !showPickerTime },
    ) {
        Text(
            text = stringResource(id = R.string.set_time)
        )
        if(showPickerTime) {
            ShowMaterialDateTimePicker(
                context = context,
                typeReturn = TYPE_TIME,
                onDateTimeSet = {
                    newTime -> onPlanValueChange(plan.copy(hour = newTime.hour, minute = newTime.minute))
                }
            )
            showPickerTime = !showPickerTime
        }
        // Update the time to show on UI screen
        displayTime = LocalTime.of(plan.hour, plan.minute)
    }
    Spacer(modifier = Modifier.height(heightSpacer))

    /**
     * Field to pick a date
     */
    Text(
        text = stringResource(id = R.string.ask_date),
        fontSize = sizeFontOfHeader
    )
    Spacer(modifier = Modifier.height(heightSpacerBetweenTextAndButton))
    Text(
        text = when(displayDate) {
            LocalDate.now() -> "Today!"
            LocalDate.now().plusDays(1) -> "Tomorrow!"
            else -> "${displayDate.dayOfMonth} ${displayDate.month} ${displayDate.year}"
        },
        fontSize = sizeFontOfDateTimeValue,
    )
    Spacer(modifier = Modifier.height(heightSpacerBetweenTextAndButton))
    Button(
        onClick = { showPickerDate = !showPickerDate },
    ) {
        Text(
            text = stringResource(id = R.string.set_date)
        )
        if(showPickerDate) {
            ShowMaterialDateTimePicker(
                typeReturn = TYPE_DATE,
                onDateTimeSet = {newDate -> onPlanValueChange(plan.copy(date = newDate.dayOfMonth, month = newDate.monthValue, year = newDate.year))
                }
            )
            showPickerDate = !showPickerDate
        }
        // Update the [displayDate] to show on UI screen display
        displayDate = LocalDate.of(plan.year, plan.month, plan.date)
    }
}


@Composable
fun ShowMaterialDateTimePicker(
    context: Context? = null,
    typeReturn: String,
    onDateTimeSet: (LocalDateTime) -> Unit
) {
    /**
     * Shared variables among the dialogs
     */
    var pickedDateTime: LocalDateTime = LocalDateTime.now()

    /**
     * Check if user wants date or time
     */
    when(typeReturn) {
        /**
         * RETURN:
         * Time in Hours & Minutes
         *
         * Build the MaterialTimePicker Dialog
         */
        TYPE_TIME -> {
            val pickerTime = PickerTime(
                modeInput = MaterialTimePicker.INPUT_MODE_CLOCK,
                title = "Set a Time",
                setClockFormat = is24HourFormat(context)
            ).dialog

            /**
             * Show it
             */
            pickerTime.show(
                getActivity().supportFragmentManager,
                DestinationPlanMake.route
            )
            /**
             * Save its values
             */
            pickerTime.addOnPositiveButtonClickListener {
                /**
                 * Convert the chosen time to Java's new API called "LocalDateTime"
                 * then pass two arguments to it to be made:
                 * - date = LocalDateTime.now().toLocalDate()
                 * - time = Picked time of user
                 */
                pickedDateTime = LocalDateTime.of(
                    LocalDateTime.now().toLocalDate(),
                    LocalTime.of(pickerTime.hour, pickerTime.minute)
                )
                /**
                 * And then we return that value
                 */
                Log.d("ADebug", "Picked time is now ${pickedDateTime.toLocalTime()}")
                onDateTimeSet(pickedDateTime)
            }
        }
        /**
         * RETURN:
         * Date in Year, Month, Date
         *
         * Build the MaterialDatePicker Dialog
         */
        TYPE_DATE -> {
            val pickerDate = PickerDate(title = stringResource(id = R.string.set_date)).dialog

            /**
             * Show it
             */
            pickerDate.show(
                getActivity().supportFragmentManager,
                DestinationPlanMake.route
            )
            /**
             * Save its values
             */
            pickerDate.addOnPositiveButtonClickListener { dateInLong ->
                /**
                 * Convert the chosen date to Java's new API called "LocalDateTime"
                 * then pass two arguments to it to be made:
                 * - date = Conversion of user's picked date from long (default type) to a date
                 * - time = Picked time of user
                 *
                 * NOTE:
                 * By default, this returns the date yesterday, so
                 * use plusDays() or UTC timezone to correct that
                 *
                 * - https://stackoverflow.com/a/7672633
                 */
//                pickedDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(dateInLong), TimeZone.getDefault().toZoneId())
                pickedDateTime = Instant.ofEpochMilli(dateInLong).atZone(ZoneId.of("UTC")).toLocalDateTime()


                /**
                 * And then we return that value
                 */
                Log.d("ADebug", "What is ${pickedDateTime.dayOfMonth} ${pickedDateTime.monthValue} ${pickedDateTime.year} to you?")
                Log.d("ADebug", "Correct date should be ${LocalDateTime.now().toLocalDate()} to you?")
                Log.d("ADebug", "Timezone is ${TimeZone.getDefault()}\n vs. ${TimeZone.getTimeZone("UTC")} to you?")
//                Log.d("ADebug", "What is $selectedDate to you?")
                onDateTimeSet(pickedDateTime)
            }
        }
        else -> {
            /**
             * Neither was specified,
             * so return a generic answer: Today
             */
            onDateTimeSet(pickedDateTime)
        }
    }
}

android kotlin android-jetpack-compose runtime-error mobile-development
1个回答
0
投票

有两个问题:

  1. 您的路线实际上并未使用实际的字符串 - 您使用

    R.string.____.toString()
    意味着您正在打印出一个整数。路由是常量,因此它们无论如何都不应该出现在字符串资源中。

  2. 您正在使用

    val routeWithId: String = "$route/${PLAN_ID}"
    ,它在错误的位置有第二个
    $
    - 如果您打印该字符串,您会看到
    edit_plan/planId
    ,因为
    ${planId}
    是您转义值的方式(它相当于
    $planId
    ,因为它不是一个复杂的表达式)。您真正想要的是
    $route/{$planId}
    生成实际的最终字符串
    edit_plan/{planId}
    ,这实际上允许导航了解
    /
    之后的内容是
    planId
    变量的占位符。

© www.soinside.com 2019 - 2024. All rights reserved.