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)
}
}
}
R.string.route_plan_edit.toString()means you are taking the integerR.string.route_plan_editand 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?R.string.route_plan_edit.toString()to "edit_plan" doesn't change the outcome of the runtime errorjava.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}