这是我的相关代码,用 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)
}
}
}
有两个问题:
您的路线实际上并未使用实际的字符串 - 您使用
R.string.____.toString()
意味着您正在打印出一个整数。路由是常量,因此它们无论如何都不应该出现在字符串资源中。
您正在使用
val routeWithId: String = "$route/${PLAN_ID}"
,它在错误的位置有第二个 $
- 如果您打印该字符串,您会看到 edit_plan/planId
,因为 ${planId}
是您转义值的方式(它相当于$planId
,因为它不是一个复杂的表达式)。您真正想要的是 $route/{$planId}
生成实际的最终字符串 edit_plan/{planId}
,这实际上允许导航了解 /
之后的内容是 planId
变量的占位符。