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 最佳实践 的相关文章

  • Sqlite 查询检查 - 小于和大于

    return mDb query DATABASE TABLE new String KEY ROWID KEY LEVEL KEY LEVEL gt 3 lt 5 null null null null 我究竟做错了什么 它返回的值全部高
  • 任务“:app:checkReleaseDuplicateClasses”执行失败

    我的 React Native Android 构建中突然出现构建问题 令人惊讶的是 它是早上建好的 没有做任何改变 但突然就失败了 这就是我得到的错误 知道为什么会发生这种情况吗 在 stack 和 GitHub 中也看到了一些类似的问题
  • android - 过度绘制布局允许通过 LinearLayout 进行触摸

    在下面的 UI 中 我将下面的 drabable 覆盖了整个屏幕 LinearLayout 是透明的 并允许其下方的控件可单击或可触摸 基本上我可以滚动此 LinearLayout 下面的列表以及单击控件 我如何禁用它 See attach
  • 注销时Firebase facebook按钮android身份验证

    我在我的 Android 应用程序中使用 firebase 并在 facebook SDK 中使用登录 我面临的唯一问题是 当我使用 facebook 登录然后注销时 facebook 登录按钮处于 注销 状态 当我单击它时 它会询问我是否
  • 使用 POST 将数据从 Android 发送到 AppEngine Datastore

    抱歉 如果这是一个简单的问题 但我只是不知道我应该做什么 而且我认为我有点超出了我的深度 我想将数据从 Android 应用程序发送到在 Google App Engine 上运行的应用程序 数据必须从那里写入数据存储区 我的数据主要采用对
  • Twitter 集成期间获取访问令牌函数行为不当

    我只是想使用 twitter4j 从我的 Android 应用程序向 Twitter 分享文本 首先 我尝试的是我创建了一个新项目 并且这个特定的代码运行成功 然后我在我的应用程序中混合了该特定代码 登录 Twitter 成功完成 之后我发
  • 使用 Guice + Kotlin 绑定对象列表

    我正在 Kotlin 中使用以下控制器定义编写 JavaFX 应用程序 class MainController Inject private lateinit var componentDescriptors List
  • 如何在Firebase Android应用程序中分离两个不同的用户?

    我有一个应用程序 有两种不同类型的用户 一种是教师 第二种是普通用户 如果普通会员登录 他会去normal memberActivity如果他是教师会员 他会去Teacher memberActivity 我如何在登录活动中执行此操作 我的
  • 如何在代码中设置TextView的文字颜色?

    在 XML 中 我们可以通过以下方式设置文本颜色textColor属性 比如android textColor FF0000 但如何通过编码来改变它呢 我尝试过类似的东西 holder text setTextColor R color R
  • 如何知道点击的widget id?

    我已经实施了一个widget与ImageButton and a TextView That ImageButton启动一个activity当它被点击时 这activity使用用户在活动上写入的内容更新小部件文本EditText 现在的问题
  • NoClassDefFoundError:com.google.firebase.FirebaseOptions

    我继续得到NoClassDefFoundError在我正在使用的其他测试设备 4 4 2 上 但在我的测试设备 Android 5 1 上运行良好 我尝试了用谷歌搜索的解决方案 但似乎没有任何效果 我正在使用 Firebase 实时数据库
  • 如何在Android中隐藏应用程序标题? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我想隐藏应用程序标题栏 您可以通过编程来完成 import android app Activity import android os
  • 改造Android基本且简单的问题

    我的服务器返回简单的 Json 结果 如下所示 message Upload Success 我正在尝试将结果放入改造模型类中 public class MyResponse SerializedName message String me
  • EditText 的高度不会扩展到其父级的高度

    我在滚动视图中放置了编辑文本 高度 match parent并期望它的高度等于滚动视图 但事实并非如此 它的高度就像wrap content这意味着如果 EditText 中没有文本 我必须将光标指向要弹出的软键盘的第一 行 我想要的是我可
  • MPAndroidChart:组合图表

    我在用MPAndroidChart 库 https github com PhilJay MPAndroidChart 我想用CombinedChart创建这样的图表 那可能吗 我尝试了一下 但似乎不起作用 因为 这些条目没有按我的预期工作
  • 如何使用asynctask显示倒计时的进度条?

    在我的应用程序中 我希望用户按下按钮 然后等待 5 分钟 我知道这听起来很糟糕 但就这样吧 5 分钟等待期间的剩余时间应显示在进度条中 我使用带有文本视图的 CountDownTimer 来倒计时 但我的老板想要看起来更好的东西 这就是进度
  • 使用 eclipse 配置mockito 时出现问题。给出错误:java.lang.verifyError

    当我将我的mockito库添加到类路径中 并使用一个简单的mockito示例进行测试时 我尝试使用模拟对象为函数add返回错误的值 我得到java lang verifyerror 以下是用于测试的代码 后面是 logcat Test pu
  • 在 Android SDK 中通过单击按钮更改背景颜色不起作用

    我有一个简单的程序 可以在单击按钮后更改背景颜色 但它不起作用 public class ChangeBackgroundActivity extends Activity Called when the activity is first
  • 在 TextView onTextChanged 上设置文本

    我有一个定义为类属性的文本视图 以便我可以在整个类中访问它 在 onCreate 方法中我执行以下操作 chars TextView findViewById R id chars chars setText 300 之后 public v
  • 将数据放入短信发送意图中?

    我想发送短信 如果文字太长 我会将其分成多条消息 我试图将一些额外的信息放入 已发送 意图中 以了解哪个部分已发送 以及所有部分何时完成 ArrayList

随机推荐