如何使用新版本的 Compose 导航传递 parcelable 参数?

How pass parcelable argument with new version of compose navigation?

我有一个使用 jetpack compose 制作的应用程序,在我升级 compose 导航库之前它运行良好 从版本 2.4.0-alpha07 到版本 2.4.0-alpha08 在 alpha08 版本中,NavBackStackEntry class 的 arguments 属性在我看来是一个 val,所以它不能像我们在 2.4 中那样重新分配。 0-alpha07 版本。 2.4.0-alpha08?

版本如何解决这个问题

我的导航组件是这样的:

@Composable
private fun NavigationComponent(navController: NavHostController) {
    NavHost(navController = navController, startDestination = "home") {
        composable("home") { HomeScreen(navController) }
        composable("details") {
            val planet = navController
                .previousBackStackEntry
                ?.arguments
                ?.getParcelable<Planet>("planet")
            planet?.let {
                DetailsScreen(it, navController)
            }
        }
    }
}

我尝试让导航发生在详细信息页面的部分在这个函数中:

private fun navigateToPlanet(navController: NavHostController, planet: Planet) {
    navController.currentBackStackEntry?.arguments = Bundle().apply {
        putParcelable("planet", planet)
    }
    navController.navigate("details")
}

我已经尝试使用 apply 简单地应用于 navigateToPlanet 函数的循环 arguments,但它不起作用,屏幕打开时空白,没有任何信息。这是我失败尝试的 代码:

private fun navigateToPlanet(navController: NavHostController, planet: Planet) {
    navController.currentBackStackEntry?.arguments?.apply {
        putParcelable("planet", planet)
    }
    navController.navigate("details")
}

根据the Navigation documentation

Caution: Passing complex data structures over arguments is considered an anti-pattern. Each destination should be responsible for loading UI data based on the minimum necessary information, such as item IDs. This simplifies process recreation and avoids potential data inconsistencies.

您根本不应该将 Parcelables 作为参数传递,而且从来都不是推荐的模式:在 Navigation 2.4.0-alpha07 和 Navigation 2.4.0-alpha08 中都不是。相反,您应该从单一的真实来源读取数据。在您的情况下,这是您的 Planet.data 静态数组,但通常是 repository layer,负责为您的应用程序加载数据。

这意味着您应该传递给 DetailsScreen 的不是 Planet 本身,而是定义如何检索该 Planet 对象的唯一键。在您的简单情况下,这可能只是所选行星的索引。

按照 guide for navigating with arguments,这意味着您的图表将如下所示:

@Composable
private fun NavigationComponent(navController: NavHostController) {
    NavHost(navController = navController, startDestination = HOME) {
        composable(HOME) { HomeScreen(navController) }
        composable(
            "$DETAILS/{index}",
            arguments = listOf(navArgument("index") { type = NavType.IntType }
        ) { backStackEntry ->
            val index = backStackEntry.arguments?.getInt("index") ?: 0
            // Read from our single source of truth
            // This means if that data later becomes *not* static, you'll
            // be able to easily substitute this out for an observable
            // data source
            val planet = Planet.data[index]
            DetailsScreen(planet, navController)
        }
    }
}

根据 Testing guide for Navigation Compose,您不应该将 NavController 向下传递到您的层次结构 - 此代码不容易测试,您不能使用 @Preview 进行预览你的可组合物。相反,您应该:

  • Pass only parsed arguments into your composable
  • Pass lambdas that should be triggered by the composable to navigate, rather than the NavController itself.

所以您根本不应该将 NavController 传递给 HomeScreenDetailsScreen。您可能会开始这项工作,通过首先更改您在 PlanetCard 中对它的使用来使您的代码更易于测试,它应该采用 lambda,而不是 NavController:

@Composable
private fun PlanetCard(planet: Planet, onClick: () -> Unit) {
    Card(
        elevation = 4.dp,
        shape = RoundedCornerShape(15.dp),
        border = BorderStroke(
            width = 2.dp,
            color = Color(0x77f5f5f5),
        ),
        modifier = Modifier
            .fillMaxWidth()
            .padding(5.dp)
            .height(120.dp)
            .clickable { onClick() }
    ) {
       ...
    }
}

这意味着您的 PlanetList 可以写成:

@Composable
private fun PlanetList(navController: NavHostController) {
    LazyColumn {
        itemsIndexed(Planet.data) { index, planet ->
            PlanetCard(planet) {
                // Here we pass the index of the selected item as an argument
                navController.navigate("${MainActivity.DETAILS}/$index")
            }
        }
    }
}

您可以看到继续在层次结构中向上使用 lambda 将如何帮助将您的 MainActivity 常量单独封装在 class 中,而不是将它们散布在您的代码库中。

通过改用索引,您可以避免创建第二个事实来源(您的论点本身),而是让自己编写可测试的代码,以支持进一步扩展到静态数据集之外。

一个非常简单和基本的方法如下

1.First 创建要传递的可打包对象,例如

@Parcelize
data class User(
    val name: String,
    val phoneNumber:String
) : Parcelable

2.Then 在您所在的当前可组合项中,例如主屏幕

 val userDetails = UserDetails(
                            name = "emma",
                             phoneNumber = "1234"
                            )
                        )
navController.currentBackStackEntry?.arguments?.apply {
                            putParcelable("userDetails",userDetails)
                        }
                        navController.navigate(Destination.DetailsScreen.route)

3.Then 在 details composable 中,确保将 navcontroller 作为参数传递给它,例如

@Composable
fun Details (navController:NavController){
val data = remember {
        mutableStateOf(navController.previousBackStackEntry?.arguments?.getParcelable<UserDetails>("userDetails")!!)
    }
}

N.B: 如果 parcelable 没有被传递到状态,你会在返回时收到一个错误