我有一个 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)
,所以还有更多的代码!至少,我也许可以实现之前解决方案中提到的通信机制。
问题
目前,就代码量而言,第二种解决方案似乎是最好的,但第三种解决方案允许针对一些尚未请求的功能进行通信和更大的灵活性。我可能会错过一些明显而优雅的东西。您认为我的哪种实现最好,如果没有,您会采取什么措施来解决这个问题?
谢谢。