Jetpack Compose 导航 - 传递参数给 startDestination

26

我正在构建的应用程序使用具有路由的Compose导航。挑战在于起始点是动态的。

这里是一个简单的示例:

class MainActivity : ComponentActivity()
{
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)

        setContent {
            val navController = rememberNavController()

            NavHost(
                navController = navController,
                startDestination = "dynamic/1", // doesn't work
                // startDestination = "static", // workaround
            ) {
                composable(
                    route = "dynamic/{$ARG_ID}",
                    arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType }),
                ) {
                    val id = it.arguments?.getString(ARG_ID)
                    Text("dynamic route, received argument: $id!")
                }
                // part of the workaround
                // composable(
                //  route = "static",
                // ) {
                //  LaunchedEffect(this) {
                //      navController.navigate("dynamic/1")
                //  }
                // }
            }
        }
    }

    companion object
    {
        const val ARG_ID = "id"
    }
}

应用程序崩溃并显示以下信息:
java.lang.IllegalArgumentException: navigation destination route/1 is not a direct child of this NavGraph

只有在使用“动态”路由作为起始目的地时才存在问题。可以通过使用startDestination =“ static”来验证。
虽然“静态”路线的解决方法有效,但我正在寻找一种无需它的解决方案,因为它会使代码变得混乱不清,并且还会在后向堆栈中创建一个额外的条目。
-> 完整的代码示例以重现该问题
相关SO问题
- Navigation Architecture Component-向startDestination传递参数数据 - 回答似乎不适用于Compose Navigation。 - 在Jetpack Compose中将参数传递给嵌套的导航图 - 没有给出答案。 - Compose Navigation-导航目标...不是此NavGraph的直接子代 - 接受的答案无法解决问题。 编辑: 我要强调原始示例并未包含“静态”组合。我只添加了“静态”组合以获得可工作的startDestination并证明可以导航到“动态”组合。 更新: 即使切换到可选参数的查询参数语法,提供默认值并设置没有任何参数的起始目的地也不起作用。
以下变化
NavHost(
    navController = navController,
    startDestination = "dynamic",
) {
    composable(
        route = "dynamic?$ARG_ID={$ARG_ID}",
        arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType; defaultValue = "1" }),
    ) {
        val id = it.arguments?.getString(ARG_ID)
        Text("dynamic route, received argument: $id!")
    }
}

导致异常。
java.lang.IllegalArgumentException: navigation destination dynamic is not a direct child of this NavGraph

1
不要使用 static 调用 navController.navigate("dynamic/1"),而是直接调用动态场景下的真实内容组合,将 "1" 作为硬编码值传递进去,而不是从参数 Bundle 中获取。这样可以避免在后退栈中出现额外的条目,并且看起来并不特别复杂。 - CommonsWare
@CommonsWare 谢谢!您能否详细说明何时调用 navController.navigate("dynamic/1") 以及您将设置为 startDestination 的内容? - Peter F
1
请问您能详细说明在哪里调用navController.navigate("dynamic/1")吗?我建议不要这样做。保留“静态”路由,但让它调用与您的“动态”路由相同的函数,只是使用硬编码的“1”参数,而不是从参数包中获取值。 - CommonsWare
4
您在 startDestination 中使用的字符串 "dynamic" 需要与您在可组合的 route 中使用的确切字符串一一匹配,即 "dynamic?$ARG_ID={$ARG_ID}" - ianhanniballake
1
非常感谢@ianhanniballake!我一直以为 startDestination 需要是一个具体的路由(包含参数的值)。如果那是可能的,那不是一个很酷的功能吗?那么,参数就不需要默认值,可以被声明为真正必需的。 - Peter F
显示剩余6条评论
4个回答

39

感谢ianhanniballake在评论中向我解释了解决方案。以下是我代码示例的工作版本。

对我来说最重要的发现是:

startDestination不能与可组合的route匹配,意味着必须完全相同。

这意味着参数不能直接通过startDestination设置,而必须通过参数的defaultValue进行设置。

以下是工作示例:

class MainActivity : ComponentActivity()
{
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)

        setContent {
            val navController = rememberNavController()

            NavHost(
                navController = navController,
                // 1st change: Set startDestination to the exact string of route
                startDestination = "dynamic/{$ARG_ID}", // NOT "dynamic/1", provide arguments via defaultValue
            ) {
                composable(
                    route = "dynamic/{$ARG_ID}",
                    // 2nd change: Set startDestination argument via defaultValue
                    arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType; defaultValue = "1" }),
                ) {
                    val id = it.arguments?.getString(ARG_ID)
                    Text("dynamic route, received argument: $id!")
                }
            }
        }
    }

    companion object
    {
        const val ARG_ID = "id"
    }
}

这种方法同样适用于以查询参数的形式提供的参数。

defaultValue必须是什么。我可能想设置不同的defaultValue,或者根本不设置。然而,在大多数情况下,这应该是最优雅的解决方案。


5
@ianhanniballake很希望在官方文档中得到这些信息。 - M Tomczynski
3
谢谢你的回答,你的假设也是我的直觉感觉;如果Compose Navigation应该像URL导航一样操作,我认为它会以那种方式工作。我同意@MTomczynski的看法,这个边缘案例应该被纳入官方文档。 - whoisthemachine
3
在一天结束时,我们无法动态更改默认值。对于__startDestination__。 - Mohd Qasim

2
如果你的第一个屏幕需要从API或类似的地方获取动态数据,那么默认值就不够用了。
这并不是最简洁的解决方案。但你可以先创建一个空的虚拟屏幕,在它加载完毕后再导航到实际的主屏幕。

    var isPlaced by remember { mutableStateOf(false) }

    LaunchedEffect(isPlaced) {
        if (isPlaced) {
            navController.navigate("home/some-argument") {
                launchSingleTop = true
            }
        }
    }


    NavHost(
        navController = navController,
        startDestination = "start"
        ) {

            composable("start") {
                Box(
                    modifier = Modifier.onGloballyPositioned {
                        isPlaced = true
                    }
                ) {}
            }

            composable(
                "home/{argument}",
            ) { backStackEntry ->
                backStackEntry.arguments?.getString(argument)?.let {
                HomeScreen(it)
                }
            }
        }

1

所有的功劳归功于ianhanniballakePeter。在我的情况下,我没有为参数数据在路由中添加任何额外的(强制/可选)键。我保持了以下干净的路由:

导航图:

 navigation(route = Route.Root.route, startDestination = Route.SubmitForm.route) {

    composable(
        route = Route.SubmitForm.route,
        arguments = listOf(
            navArgument(ARG_KEY) {
                type = NavType.StringType
                defaultValue = JsonConverter.toJson(user, User::class.java)
            },
        )
    )
}

路由密封类:

sealed class Route(val route: String) {
    object MyRoute : Route("$ROOT/submit-form")
}

而在视图模型中,只需像这样获取数据:

@HiltViewModel
class MyViewModel @Inject constructor(
    stateHandle: SavedStateHandle,
) : ViewModel {

    lateinit var user

    init {
        user = stateHandle.get<String>(ARG_NAME) // Supported data types
    }
}

对我有效。


0

在“startDestination”NavHost中不应使用动态路由值 --> navController.navigate(<动态路由‌>)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接