繁体   English   中英

Jetpack Compose 应用程序范围的条件 TopAppBar 最佳实践

[英]Jetpack Compose application-wide conditional TopAppBar best practice

我有一个 Android Jetpack Compose 应用程序,它使用BottomNavigationTopAppBar可组合项。 从通过BottomNavigation打开的选项卡,用户可以更深入地导航到导航图中。

问题

TopAppBar可组合项必须代表当前屏幕,例如显示其名称,实现一些特定于打开的屏幕的选项,如果屏幕是高级屏幕,则为后退按钮。 然而,Jetpack Compose 似乎没有开箱即用的解决方案,开发者必须自己实现。

所以,显而易见的想法伴随着明显的缺点,有些想法比其他想法更好。

跟踪导航的基线,正如谷歌所建议的(至少对于BottomNavigation ),是一个sealed的 class 包含代表当前活动屏幕的object s。 具体我的项目是这样的:

sealed class AppTab(val route: String, @StringRes val resourceId: Int, val icon: ImageVector) {
    object Events: AppTab("events_tab", R.string.events, Icons.Default.EventNote)
    object Projects: AppTab("projects_tab", R.string.projects, Icons.Default.Widgets)
    object Devices: AppTab("devices_tab", R.string.devices, Icons.Default.DevicesOther)
    object Employees: AppTab("employees_tab", R.string.employees, Icons.Default.People)
    object Profile: AppTab("profile_tab", R.string.profile, Icons.Default.AccountCircle)
}

现在TopAppBar可以知道打开了哪个选项卡,前提是我们remember AppTab object,但是它如何知道是否从给定选项卡中打开了屏幕?

解决方案 1 - 明显且明显错误

我们为每个屏幕提供自己的TopAppBar并让它处理所有必要的逻辑。 除了大量代码重复外,每个屏幕的TopAppBar都会在打开屏幕时重新组合,并且如本文所述,会闪烁。

解决方案 2 - 不太优雅

从现在开始,我决定在我的项目的顶级可组合项中有一个TopAppBar ,这将取决于保存当前屏幕的state 现在我们可以轻松实现 Tabs 的逻辑。

为了解决从 Tab 内打开屏幕的问题,我扩展了 Google 的想法并实现了一个通用的AppScreen class 代表每个可以打开的屏幕:

// This class represents any screen - tabs and their subscreens.
// It is needed to appropriately change top app bar behavior
sealed class AppScreen(@StringRes val screenNameResource: Int) {
    // Employee-related
    object Employees: AppScreen(R.string.employees)
    object EmployeeDetails: AppScreen(R.string.profile)

    // Events-related
    object Events: AppScreen(R.string.events)
    object EventDetails: AppScreen(R.string.event)
    object EventNew: AppScreen(R.string.event_new)

    // Projects-related
    object Projects: AppScreen(R.string.projects)

    // Devices-related
    object Devices: AppScreen(R.string.devices)

    // Profile-related
    object Profile: AppScreen(R.string.profile)
}

然后,我将它保存到 TopAppBar 的state中顶级可组合项中的TopAppBar ,并将currentScreenHandler作为onNavigate参数传递给我的 Tab 可组合项:

    var currentScreen by remember { mutableStateOf(defaultTab.asScreen()) }

    val currentScreenHandler: (AppScreen) -> Unit = {navigatedScreen -> currentScreen = navigatedScreen}
// Somewhere in the bodyContent of a Scaffold
                when (currentTab) {
                    AppTab.Employees -> EmployeesTab(currentScreenHandler)
                // And other tabs
                // ...
                }

从 Tab 可组合项内部:

    val navController = rememberNavController()

    NavHost(navController, startDestination = "employees") {
        composable("employees") {
            onNavigate(AppScreen.Employees)
            Employees(it.hiltViewModel(), navController)
        }
        composable("employee/{userId}") {
            onNavigate(AppScreen.EmployeeDetails)
            Employee(it.hiltViewModel())
        }
    }

现在,根可组合项中的TopAppBar知道更高级别的屏幕并可以实现必要的逻辑。 但是对应用程序的每个子屏幕都这样做吗? 大量代码重复,以及此应用栏与其代表的可组合项之间的通信架构(可组合项如何对在应用栏上执行的操作做出反应)尚未组合(双关语意)。

解决方案 3 - 最好的?

我实现了一个viewModel来处理所需的逻辑,因为它看起来是最优雅的解决方案:

@HiltViewModel
class AppBarViewModel @Inject constructor() : ViewModel() {
    private val defaultTab = AppTab.Events
    private val _currentScreen = MutableStateFlow(defaultTab.asScreen())
    val currentScreen: StateFlow<AppScreen> = _currentScreen

    fun onNavigate(screen: AppScreen) {
        _currentScreen.value = screen
    }
}

根可组合:

    val currentScreen by appBarViewModel.currentScreen.collectAsState()

但是并没有解决方案二的代码重复问题。 首先,我必须将此viewModelMainActivity传递到根可组合项,因为似乎没有其他方法可以从可组合项内部访问它。 所以现在,我没有将currentScreenHandler传递给 Tab 可组合项,而是将viewModel传递给它们,而不是在导航事件上调用处理程序,而是调用viewModel.onNavigate(AppScreen) ,所以至少还有更多代码。 我也许可以实现上一个解决方案中提到的通信机制。

问题

目前,就代码量而言,第二种解决方案似乎是最好的,但第三种解决方案允许进行通信,并为一些尚未请求的功能提供更大的灵活性。 我可能会遗漏一些明显而优雅的东西。 您认为我的哪个实现是最好的,如果没有,您将如何解决这个问题?

谢谢你。

我在脚手架中使用单个 TopAppBar,并通过从可组合项引发事件来使用不同的标题、下拉菜单、图标等。 这样,我就可以只使用一个具有不同值的 TopAppBar。 这是一个例子:

    val navController = rememberNavController()
    var canPop by remember { mutableStateOf(false) }

    var appTitle by remember { mutableStateOf("") }
    var showFab by remember { mutableStateOf(false) }

    var showDropdownMenu by remember { mutableStateOf(false) }
    var dropdownMenuExpanded by remember { mutableStateOf(false) }
    var dropdownMenuName by remember { mutableStateOf("") }
    var topAppBarIconsName by remember { mutableStateOf("") }

    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()

    val tourViewModel: TourViewModel = viewModel()
    val clientViewModel: ClientViewModel = viewModel()

    navController.addOnDestinationChangedListener { controller, _, _ ->
        canPop = controller.previousBackStackEntry != null
    }

    val navigationIcon: (@Composable () -> Unit)? =
        if (canPop) {
            {
                IconButton(onClick = { navController.popBackStack() }) {
                    Icon(
                        imageVector = Icons.Filled.ArrowBack,
                        contentDescription = "Back Arrow"
                    )
                }
            }
        } else {
            {
                IconButton(onClick = {
                    scope.launch {
                        scaffoldState.drawerState.apply {
                            if (isClosed) open() else close()
                        }
                    }
                }) {
                    Icon(Icons.Filled.Menu, contentDescription = null)
                }
            }
        }

    Scaffold(
        scaffoldState = scaffoldState,
        drawerContent = {
            DrawerContents(
                navController,
                onMenuItemClick = { scope.launch { scaffoldState.drawerState.close() } })
        },
        topBar = {
            TopAppBar(
                title = { Text(appTitle) },
                navigationIcon = navigationIcon,
                elevation = 8.dp,
                actions = {
                    when (topAppBarIconsName) {
                        "ClientDirectoryScreenIcons" -> {
                            // search icon on client directory screen
                            IconButton(onClick = {
                                clientViewModel.toggleSearchBar()
                            }) {
                                Icon(
                                    imageVector = Icons.Filled.Search,
                                    contentDescription = "Search Contacts"
                                )
                            }
                        }
                    }

                    if (showDropdownMenu) {
                        IconButton(onClick = { dropdownMenuExpanded = true }) {
                            Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null)

                            DropdownMenu(
                                expanded = dropdownMenuExpanded,
                                onDismissRequest = { dropdownMenuExpanded = false }
                            ) {

                                // show different dropdowns based on different screens
                                when (dropdownMenuName) {
                                    "ClientDirectoryScreenDropdown" -> ClientDirectoryScreenDropdown(
                                        onDropdownMenuExpanded = { dropdownMenuExpanded = it })
                                }
                            }
                        }
                    }
                }
            )
        },
...
   ) { paddingValues ->

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)

        ) {
            NavHost(
                navController = navController,
                startDestination = Screen.Tours.route
            ) {
                composable(Screen.Tours.route) {
                    TourScreen(
                        tourViewModel = tourViewModel,
                        onSetAppTitle = { appTitle = it },
                        onShowDropdownMenu = { showDropdownMenu = it },
                        onTopAppBarIconsName = { topAppBarIconsName = it }
                    )
                }

然后像这样从不同的屏幕设置 TopAppBar 值:

@Composable
fun TourScreen(
    tourViewModel: TourViewModel,
    onSetAppTitle: (String) -> Unit,
    onShowDropdownMenu: (Boolean) -> Unit,
    onTopAppBarIconsName: (String) -> Unit
) {
    LaunchedEffect(Unit) {
        onSetAppTitle("Tours")
        onShowDropdownMenu(false)
        onTopAppBarIconsName("")
    }
...

可能不是完美的方法,但没有重复的代码。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM