3

Here's my relevant code, built in 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() },
            )
        }
    }
}

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

These are pulled from my app project Outlook Planner on GitHub Under the GPL license, you can git clone it to your computers and inspect it yourself.

My goal on the NavHost is to pull the Plan.id from the Home composable function, and then transport that value to DestinationPlanEdit.routeWithId to edit that Plan object and save that to the Room database.

Only problem is it returns the following error:

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)

I'm at lost as to how to solve this. I've looked into various video tutorials online (much of which are not helpful as they teach XML and Fragments, which isn't what I'm learning in Android Basics with Compose

I ask for help as to what's causing this issue, as I'm tired and cannot fix this myself. But I need this functionality to work.

Thanks in advance.

EDIT Few more relevant files, in case cloning GitHub projects isn't your thing:

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

4
  • R.string.route_plan_edit.toString() means you are taking the integer R.string.route_plan_edit and making that integer into a String. That's not going to work. The only reason to ever put strings as resources is to translate them and routes are never something you translate - they are just constants in your code. Does it work if you actually put the constants in your code? Commented Apr 19, 2024 at 4:26
  • @ianhanniballake Just tried, and changing the route from R.string.route_plan_edit.toString() to "edit_plan" doesn't change the outcome of the runtime error Commented Apr 19, 2024 at 4:32
  • What's the new runtime error? Commented Apr 19, 2024 at 4:40
  • @ianhanniballake same error, but much clearer as to what's happening: java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/edit_plan/1 } cannot be found in the navigation graph ComposeNavGraph(0x0) startDestination={Destination(0xf08bc5ce) route=2131689704} Commented Apr 19, 2024 at 4:47

1 Answer 1

2

There's two problems:

  1. Your routes aren't actually using actual strings - your usages of R.string.____.toString() means you are printing out an integer. Routes are constants, so they shouldn't be in string resources anyways.

  2. You are using val routeWithId: String = "$route/${PLAN_ID}", which has the second $ in the wrong spot - if you print that string, you'll see edit_plan/planId since ${planId} is how you escape a value (it is the equivalent of $planId since it isn't a complicated expression). What you actually want is $route/{$planId} to generate the actual final string edit_plan/{planId}, which is what actually allows Navigation to understand that what is after the / is a placeholder for your planId variable.

Sign up to request clarification or add additional context in comments.

4 Comments

Doing the following returns the that outcome: Log.d("DebuggerTracker", "${DestinationPlanEdit.route}/{$planId}") Outputs -> plan_edit/{1} However, doing this still results in the runtime error: java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/plan_edit/{1} } cannot be found in the navigation graph ComposeNavGraph(0x0) startDestination={Destination(0xf08bc5ce) route=2131689704} Any more ideas, may I ask? Just starting out in Android app dev and can't figure out the issue despite searching
I think you need to stop using variables and just write actual strings, following the type safe guide. Your route = DestinationPlanEdit.routeWithId needs to be edit_plan/{planId}. When you navigate, you need to navigate to edit_plan/1.
But why? Why is it that the Inventory App in Unit 6 at Android Basics with Compose uses variables to pass entity ID's at InventoryNavGraph AND IT JUST WORKS but not mine? I'm following the model of one of the juggernaut's beginner training apps, and yet mine fails. It doesn't make sense to me at all.
You're on the right track with debugging what the strings are. You might try avoiding string interpolation (using the $ in a string) and just use regular string concatenation until you are comfortable that the string you are creating is in the right format.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.