Jetpack Compose 应用程序范围的条件 TopAppBar 最佳实践
Posted
技术标签:
【中文标题】Jetpack Compose 应用程序范围的条件 TopAppBar 最佳实践【英文标题】:Jetpack Compose application-wide conditional TopAppBar best practice 【发布时间】:2021-11-23 23:57:14 【问题描述】:我有一个使用 BottomNavigation
和 TopAppBar
可组合组件的 android Jetpack Compose 应用程序。从通过BottomNavigation
打开的选项卡中,用户可以更深入地导航到导航图。
问题
TopAppBar
可组合对象必须代表当前屏幕,例如显示它的名字,实现一些特定于打开的屏幕的选项,如果屏幕是高级的,则返回按钮。但是,Jetpack Compose 似乎没有开箱即用的解决方案,开发者必须自己实现。
因此,显而易见的想法伴随着明显的缺点,有些想法比其他想法更好。
跟踪导航的基线,如 Google 的 suggested(至少对于 BottomNavigation
),是一个 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)
现在TopAppBar
可以知道打开了哪个选项卡,前提是我们remember
AppTab
对象,但是它如何知道屏幕是否从给定选项卡中打开?
解决方案 1 - 明显且明显错误
我们为每个屏幕提供自己的TopAppBar
并让它处理所有必要的逻辑。除了大量的代码重复之外,每个屏幕的TopAppBar
将在打开屏幕时重新组合,并且如this 帖子中所述,会闪烁。
解决方案 2 - 不太优雅
从现在开始,我决定在我的项目的***可组合项中添加一个 TopAppBar
,这将取决于保存当前屏幕的 state
。现在我们可以轻松实现 Tabs 的逻辑了。
为了解决从 Tab 中打开屏幕的问题,我扩展了 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)
然后,我将其保存到 TopAppBar
范围内***可组合项中的 state
并将 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()
但是并没有解决第二种方案的代码重复问题。首先,我必须将这个viewModel
从MainActivity
传递给可组合的根,因为似乎没有其他方法可以从可组合内部访问它。所以现在,我没有将currentScreenHandler
传递给Tab 组合,而是将viewModel
传递给它们,而不是在导航事件上调用处理程序,而是调用viewModel.onNavigate(AppScreen)
,所以还有更多代码!至少,我也许可以实现上一个解决方案中提到的通信机制。
问题
目前,就代码量而言,第二种解决方案似乎是最好的,但第三种解决方案可以为一些尚未被请求的功能提供通信和更大的灵活性。我可能会遗漏一些明显而优雅的东西。你认为我的哪个实现最好,如果没有,你会怎么解决这个问题?
谢谢。
【问题讨论】:
【参考方案1】:我在 Scaffold 中使用单个 TopAppBar,并通过从 Composables 引发事件来使用不同的标题、下拉菜单、图标等。这样,我可以只使用一个具有不同值的 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("")
...
这可能不是完美的方法,但没有重复的代码。
【讨论】:
我可以看到您正在使用解决方案 2(传递事件处理程序)和解决方案 3(也实现通信的视图模型)的某种组合。我通常不喜欢解决方案 2,因为将处理程序传递给可组合对象,以便再次传递,依此类推。但我现在知道实现通信的视图模型方式并不令人畏惧并且也被使用。谢谢! 您可以将视图模型、导航等传递到屏幕的第一级,但无需再向下传递。改为使用状态提升并从顶层处理较低级别屏幕的操作。这样,您就不需要到处传递资源,这是推荐的方式。以上是关于Jetpack Compose 应用程序范围的条件 TopAppBar 最佳实践的主要内容,如果未能解决你的问题,请参考以下文章
Jetpack Compose Navigation - 底部导航多个返回堆栈 - 查看模型范围问题
Jetpack Compose 和 Compose Navigation 如何处理 Android 活动?