[英]Jetpack Compose application-wide conditional TopAppBar best practice
我有一个 Android Jetpack Compose 应用程序,它使用BottomNavigation
和TopAppBar
可组合项。 从通过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,但是它如何知道是否从给定选项卡中打开了屏幕?
我们为每个屏幕提供自己的TopAppBar
并让它处理所有必要的逻辑。 除了大量代码重复外,每个屏幕的TopAppBar
都会在打开屏幕时重新组合,并且如本文所述,会闪烁。
从现在开始,我决定在我的项目的顶级可组合项中有一个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
知道更高级别的屏幕并可以实现必要的逻辑。 但是对应用程序的每个子屏幕都这样做吗? 大量代码重复,以及此应用栏与其代表的可组合项之间的通信架构(可组合项如何对在应用栏上执行的操作做出反应)尚未组合(双关语意)。
我实现了一个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("")
}
...
可能不是完美的方法,但没有重复的代码。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.