如何使用新版本的撰写导航传递可打包参数?

Pie*_*ira 18 android android-jetpack-navigation android-jetpack-compose

我有一个用 jetpack compose 制作的应用程序,它工作得很好,直到我将 compose 导航库从版本2.4.0-alpha07升级到版本2.4.0-alpha08 在 alpha08 版本中,在我看来,该类arguments的属性NavBackStackEntry是 a 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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

ian*_*ake 33

根据导航文档

注意:通过参数传递复杂的数据结构被认为是反模式。每个目的地应负责根据最少的必要信息(例如项目 ID)加载 UI 数据。这简化了流程重新创建并避免了潜在的数据不一致。

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

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

通过遵循参数导航指南,这意味着您的图表将如下所示:

@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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

根据Navigation Compose 的测试指南,您不应该NavController在层次结构中向下传递 - 此代码无法轻松测试,并且您无法用于@Preview预览可组合项。相反,您应该:

  • 仅将解析后的参数传递到可组合项中
  • 传递应由可组合项触发的 lambda 进行导航,而不是 NavController 本身。

所以你不应该把你的东西传递NavControllerHomeScreenDetailsScreen根本不应该。您可以首先在 中更改代码的用法,以提高代码的可测试性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() }
    ) {
       ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这意味着你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")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 虽然文件是官方的,但是真的合理吗?经过十几年的发展,页面之间传递对象已经成为一种习惯。突然间,我只传递唯一标识符,这会增加开发人员的大量工作。我的对象暂时传递到下一页。当用户退出该页面时,该对象就没有意义。我必须将其存储在本地吗?Android的设计真的很有趣 (55认同)
  • 抱歉直言不讳,但我认为禁止传递物体是一个非常糟糕的决定。在很多情况下,数据并非来自存储库。例如,如果我需要跨多个屏幕收集分析数据怎么办?在我能够逐步收集数据并作为对象从一个屏幕传递到另一个屏幕之前,现在导航迫使我们引入以前不需要的可变状态,并确保它得到正确的管理。怎样才是好的设计呢?我们不能使用那种迫使我们创建容易出错的架构的方法来使用导航。 (6认同)
  • 就我而言,我想传递一个 Ids 数据类,它允许我检索多个源中的数据,由于施加了此限制,人们正在传递 JSON 字符串,这甚至更糟 (5认同)
  • 而且,有时会引入很多我作为开发人员不会做的工作。例如,当前该库无法使用文件路径作为参数,它只会使应用程序崩溃。因此我必须传递一个 id,进行查询等等,这增加了复杂性并使用户体验变得更糟。 (3认同)
  • 但实际上 Fragments 的导航组件支持传递可打包和序列化对象,没有任何问题,而 Compose 的导航组件不再可能了,您需要创建自定义 NavType,这非常难看。所以我想唯一的方法是仅使用原始参数、id 等,以便稍后从存储库获取对象,如本答案中所述 (3认同)
  • Okhttp,内存缓存......这似乎有点矫枉过正,而且非常容易出错。是否可以通过组合导航传递数据类(非序列化,通过引用)? (2认同)

Mah*_*eei 11

你可以传递这样的参数

val data = DestinationScreenArgument(title = "Hello")

navController.currentBackStackEntry?.savedStateHandle?.apply {
   set("detailArgument", data)
}

navController.navigate(Screen.DetailScreen.route)
Run Code Online (Sandbox Code Playgroud)

并像这样在目的地中获取参数

val detailArgument = navController.previousBackStackEntry?.savedStateHandle?.get<DestinationScreenArgument>(
    "detailArgument"
)
Run Code Online (Sandbox Code Playgroud)