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

2024-03-14

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

问题

The TopAppBar可组合项必须代表当前屏幕,例如显示其名称,实现一些特定于打开的屏幕的选项,如果屏幕是高级的则返回按钮。然而,Jetpack Compose 似乎没有现成的解决方案,开发人员必须自行实现。

因此,明显的想法伴随着明显的缺点,有些想法比其他想法更好。

跟踪导航的基线,如建议 https://developer.android.com/jetpack/compose/navigation#bottom-nav由 Google 提供(至少对于BottomNavigation), is a sealed类包含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)
}

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

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

我们为每个屏幕提供自己的TopAppBar并让它处理所有必要的逻辑。除了大量的代码重复之外,每个屏幕的TopAppBar将在打开屏幕时重新组合,并且如中所述this https://stackoverflow.com/questions/66638286/jetpack-compose-topappbar-flickers-after-setting-bottomnavview-with-navigation-c发帖,会闪烁。

解决方案 2 - 不太优雅

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

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

// 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)
}

然后我将其保存到state在顶级可组合项的范围内TopAppBar并通过currentScreenHandler as an 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())
        }
    }

Now the 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()

但并没有解决第二种方案的代码重复问题。首先,我必须通过这个viewModel到根可组合项MainActivity,因为似乎没有其他方法可以从可组合项内部访问它。所以现在,而不是通过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("")
    }
...

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Jetpack Compose 应用程序范围内的条件 TopAppBar 最佳实践 的相关文章

  • 应用程序在加载 xml 布局文件的主线程中做了太多工作

    我正在制作一个 9x9 数独网格 其中 81 个单元格本身就是一个 3x3 网格 单个细胞看起来像这样 1 2 3 4 5 6 7 8 9 每个数字代表该单元格的铅笔注释 我有一个名为 cell layout xml 的文件 表示这种 3x
  • 为什么赋值不是语句

    我有以下代码 class Presenter private var view View null fun attachView view View this view view error Assignment is not a stat
  • 如何从Firebase Firestore实时更新文档中获取修改后的字段或数据? [复制]

    这个问题在这里已经有答案了 我有多个文档 我的问题是我无法获取修改的特定数据 我正在获取完整的文档 db collection employees whereEqualTo OID OID addSnapshotListener new E
  • 如何通过我的活动在 Android 中设置铃声?

    我正在尝试找到一种方法来通过 Android 活动中的代码设置新的默认铃声 我已经将铃声下载到bytearray 最后 我设法将默认铃声设置为我下载的铃声 下面不包含下载代码 仅包含将其设置为默认铃声所需的代码 File k new Fil
  • 退出设备上的 system.img

    我正在为我们部署给客户的设备 LG p509 Optimus 1 开发自动应用程序更新解决方案 我们可以控制这些设备 并且目前在它们上安装自定义内核 但不是完整的自定义 ROM 由于我们试图在设备上自动更新我们的应用程序 因此我们需要由我们
  • ADB TCPIP 连接问题

    我有两台 Galaxy S3 其中一个已扎根 另一个则未扎根 因此 当我尝试通过本地网络连接它们时 计算机可以看到已root的计算机 但是正常的就卡在tcpip这一步了 所以 我写 adb tcpip 5555 It says restar
  • 华为手机“受保护的应用程序”设置及处理方法

    我有一台搭载 Android 5 0 的华为 P8 用于测试应用程序 该应用程序需要在后台运行 因为它跟踪 BLE 区域 我发现华为内置了一个名为 受保护的应用程序 的 功能 可以从手机设置 电池管理器 gt 受保护的应用程序 访问该功能
  • 需要 Android webview window.open() 和 window.close() 的信息

    我正在开发一个安卓应用程序 这是我网站的 WebView 该网站包含一个弹出按钮 单击该按钮后 将打开一个新窗口并显示内容 该链接可以来自外部站点 然而 当我实现此操作时 新选项卡正在打开 之后它会弹出以打开浏览器 尽管在 Web 视图中打
  • ExpandableListview OnGroupClickListener 未触发

    我正在关注这个 以编程方式折叠 ExpandableListView 中的组 https stackoverflow com questions 4314777 programmatically collapse a group in ex
  • 如何禁用操作栏上“向上”按钮的翻转?

    背景 我做了一个 应用程序管理器 https play google com store apps details id com lb app manager 替代应用程序 我希望添加 RTL 从右到左 语言的翻译 因为我知道在某些 And
  • Android 中 localTime 和 localDate 的替代类有哪些? [复制]

    这个问题在这里已经有答案了 我想使用从 android API 获得的长值 该值将日期返回为长值 表示为自纪元以来的毫秒数 我需要使用像 isBefore plusDays isAfter 这样的方法 Cursor managedCurso
  • 将 java 中的 byte[] 转换为 C++ 中的 unsigned char* 的正确方法,反之亦然?

    我是 C 和 JNI 的新手 我尝试找到一种正确的方法 通过使用 JNI 将 java 中的 byte 转换为 C 中的 unsigned char 反之亦然 我正在安卓上工作 在谷歌和SO中寻找解决方案后 我还没有找到将java中的byt
  • MIUI 权限被拒绝活动 KeyguardLocked

    当应用程序处于后台且屏幕被锁定时 我无法启动活动 没有异常或警告 只是不调用 onCreate 我一直在与这个问题作斗争 我想我终于找到了它的根源 日志中有一行 D com android server am ExtraActivityMa
  • 是否可以通过 Android 应用程序来录音?

    我是一名开发人员 希望创建一个 Android 应用程序来记录电话 这是出于我个人的需要 为了我自己的目的和记录而记录电话 是否有可能做到这一点 是否可以访问麦克风以及通过扬声器发出的声音 我对 Android 开发有点陌生 所以请耐心等待
  • 如何为我的 Android Market APK 创建证书?

    我想将我的第一个 APK 应用程序上传到 Android Market 但我收到了此错误 顺便说一下 在 stackoverflow 中搜索时并没有引导我找到正确的链接 市场不接受使用调试证书签名的 APK 创建有效期至少 50 年的新证书
  • 移动设备上的 TensorFlow(Android、iOS、Windows Phone)

    我目前正在寻找不同的深度学习框架 特别是用于训练和部署卷积神经网络 要求是 它可以在带有 GPU 的普通 PC 上进行训练 但训练后的模型必须部署在三个主要的移动操作系统上 即 Android iOS 和 Windows Phone Ten
  • 安卓。 CalendarView...一次仅显示一个月的日历

    我正在使用 CalendarView 其中我想一次仅查看一个月的日历并滚动查看下个月 但 CalendarView 一次显示所有月份 下面是我的代码
  • 如何通过 Android 按钮单击运行单独的应用程序

    我尝试在 Android 应用程序中添加两个按钮 以从单独的两个应用程序订单系统和库存系统中选择一个应用程序 如图所示 我已将这两个应用程序实现为两个单独的 Android 项目 当我尝试运行此应用程序时 它会出现直到正确选择窗口 但是当按
  • 离子初始加载时间

    我正在使用 Ionic 构建一个简单的应用程序 但我的应用程序在冷启动时的初始加载时间方面存在性能问题 这是我所做的 collection repeat 代替带有 track by 的 ng repeat 原生滚动 overflow scr
  • LifeCycleAware Fragment 中的片段生命周期事件

    我有一个生命周期感知片段和一个LifecycleObserver class public class MyFragment extends Fragment Override public void onCreate Nullable B

随机推荐