如何在jetpack compose中从屏幕向AppBar做出贡献

问题描述 投票:0回答:1

我想实现一个简单的用户流程,用户可以看到多个屏幕来输入数据。该流程应该共享一个通用的导航栏,每个屏幕都可以在其处于活动状态时贡献其菜单项(例如添加“搜索”或“下一步”按钮)。导航栏还具有概念上属于用户流程而不属于单个屏幕的按钮(如后退按钮和关闭按钮)。屏幕应该在其他上下文中可重用,因此屏幕不应该知道它们运行的流程。

从技术上讲,用户流程是作为定义导航栏并使用组合导航的组合功能来实现的。每个屏幕都作为单独的撰写功能实现。 在基于片段/视图的 Android 中,这种场景通过

onCreateOptionsMenu
和相关函数得到开箱即用的支持。但我该如何在撰写中做到这一点?我找不到有关该主题的任何指导。

用代码来说明问题:

@Composable
fun PaymentCoordinator(
    navController: NavHostController = rememberNavController()
) {
    AppTheme {
        Scaffold(
            bottomBar = {
                BottomAppBar(backgroundColor = Color.Red) {
                    IconButton(onClick =  navController::popBackStack) {
                        Icon(Icons.Filled.ArrowBack, "Back")
                    }
                    Spacer(modifier = Modifier.weight(1f))

                    // 0..n IconButtons provided by the active Screen
                    // should be inserted here
                    // How can we do that, because state should never
                    // go up from child to parent


                    // this button (or at least its text and onClick action) should
                    // be defined by the currently visible Screen as well
                    Button(
                        onClick = {  /* How to call function of screen? */ }
                    ) {
                        Text("Next"))
                    }
                }
            }
        ) { padding ->
            Box(modifier = Modifier.padding(padding)) {
                NavHost(
                    navController = navController,
                    startDestination = "selectAccount"
                ) {
                    // screens that can contribute items to the menu
                    composable("selectAccount") {
                        AccountSelectionRoute(
                            onAccountSelected = {
                                navController.navigate("nextScreen")
                            }
                        )
                    }
                    composable("...") {
                        // ...
                    }
                }
            }
        }
    }
}
android android-jetpack-compose android-jetpack-navigation
1个回答
3
投票

我想出了一种利用副作用和生命周期监听器来实现我的目标的方法。基本上,每当屏幕变为活动状态(ON_START)时,它都会通知父级(协调器)有关其菜单配置的信息。协调器评估配置并相应地更新导航栏。

该方法基于 Google 关于副作用的文档 (https://developer.android.com/jetpack/compose/side-effects#disposableeffect) 这种方法感觉复杂且尴尬,我认为 compose 框架缺少一些功能来实现这一点。但是,我的实现在我的测试用例中似乎运行良好。

助手类

// currently I only need to configure a single button, however the approach 
// can be easily extended now (you can put anything inside rightButton)
data class MenuConfiguration(
    val rightButton: @Composable () -> Unit
)

@Composable
fun SimpleMenuConfiguration(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
    onUnregisterMenuConfiguration: () -> Unit,
    rightButton: @Composable () -> Unit
) {
    val currentOnRegisterMenuConfiguration by rememberUpdatedState(onRegisterMenuConfiguration)
    val currentOnUnregisterMenuConfiguration by rememberUpdatedState(onUnregisterMenuConfiguration)
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnRegisterMenuConfiguration(
                    MenuConfiguration(
                        rightButton = rightButton
                    )
                )
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnUnregisterMenuConfiguration()
            }
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

协调员级别

@Composable
fun PaymentCoordinator(
    navController: NavHostController = rememberNavController()
) {
    var menuConfiguration by remember { mutableStateOf<MenuConfiguration?>(null) }
    AppTheme {
        Scaffold(
            bottomBar = {
                BottomAppBar(backgroundColor = Color.Red) {
                    IconButton(onClick =  navController::popBackStack) {
                        Icon(Icons.Filled.ArrowBack, "Back")
                    }
                    Spacer(modifier = Modifier.weight(1f))
                    menuConfiguration?.rightButton?.invoke()
                }
            }
        ) { padding ->
            Box(modifier = Modifier.padding(padding)) {
                PaymentNavHost(
                    navController = navController,
                    finishedHandler = finishedHandler,
                    onRegisterMenuConfiguration = { menuConfiguration = it },
                    onUnregisterMenuConfiguration = { menuConfiguration = null }
                )
            }
        }
    }
}

@Composable
fun PaymentNavHost(
    navController: NavHostController = rememberNavController(),
    onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
    onUnregisterMenuConfiguration:() -> Unit
) {
    NavHost(
        navController = navController,
        startDestination = "selectAccount"
    ) {
        composable("selectAccount") {
            DemoAccountSelectionRoute(
                onAccountSelected = {
                    navController.navigate("amountInput")
                },
                onRegisterMenuConfiguration = onRegisterMenuConfiguration,
                onUnregisterMenuConfiguration = onUnregisterMenuConfiguration
            )
        }
        composable("amountInput") {
            AmountInputRoute(
                onRegisterMenuConfiguration = onRegisterMenuConfiguration,
                onUnregisterMenuConfiguration = onUnregisterMenuConfiguration,
                onFinished = {
                    ...
                }
            )
        }
    }
}

屏幕级别

@Composable
internal fun AmountInputRoute(
    onRegisterMenuConfiguration: (MenuConfiguration?) -> Unit,
    onUnregisterMenuConfiguration:() -> Unit,
    onFinished: (Amount?) -> Unit
) {

    SimpleMenuConfiguration(
        onRegisterMenuConfiguration = onRegisterMenuConfiguration,
        onUnregisterMenuConfiguration = onUnregisterMenuConfiguration,
        rightButton = {
            Button(
                onClick = {
                    ...
                }
            ) {
                Text(text = stringResource(id = R.string.next))
            }
        }
    )

** 替代方法 ** 在这个 git repo 中,还有另一种稍微不同的方法来解决问题 主要区别在于,不需要通过 onDispose 删除屏幕特定的菜单项。相反,屏幕特定菜单将通过 navGraph 的目标路线注册到菜单管理器,并根据当前活动的路线,将加载正确的屏幕特定路线。

© www.soinside.com 2019 - 2024. All rights reserved.