【发布时间】:2021-11-23 23:57:14
【问题描述】:
我有一个使用 BottomNavigation 和 TopAppBar 可组合组件的 Android Jetpack Compose 应用程序。从通过BottomNavigation 打开的选项卡中,用户可以更深入地导航到导航图。
问题
TopAppBar 可组合对象必须代表当前屏幕,例如显示它的名字,实现一些特定于打开的屏幕的选项,如果屏幕是高级的,则返回按钮。但是,Jetpack Compose 似乎没有开箱即用的解决方案,开发者必须自己实现。
因此,显而易见的想法伴随着明显的缺点,有些想法比其他想法更好。
跟踪导航的基线,如 Google 的 suggested(至少对于 BottomNavigation),是一个 sealed 类,其中包含代表当前活动屏幕的 objects。专门针对我的项目,是这样的:
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),所以还有更多代码!至少,我也许可以实现上一个解决方案中提到的通信机制。
问题
目前,就代码量而言,第二种解决方案似乎是最好的,但第三种解决方案可以为一些尚未被请求的功能提供通信和更大的灵活性。我可能会遗漏一些明显而优雅的东西。你认为我的哪个实现最好,如果没有,你会怎么解决这个问题?
谢谢。
【问题讨论】:
标签: android kotlin android-jetpack-compose android-jetpack-navigation