这张图片来自 https://stock.adobe.com/br/search?k=magical+tree&asset_id=820824773 (来自Adobe Stock)
在之前的文章中,我们深入介绍了围绕Kotlin Decompose的概念及其如何用于构建整个屏幕流程中的导航,同时拥有某些称为组件的具有应用程序生命周期感知能力的组件,这些组件应确保应用程序交互的一致性和完整性。在这篇文章里,我们将演示一个非常常见的使用Decompose的导航模式,并进一步解释Children
Compose组件的工作原理。我们将再次使用之前提到的项目,但现在我们将在登录、引导页、主枢纽和个人资料这四个屏幕之间建立标签式导航。
首先,先创建一个组件来表示整个应用顶部的标签条,该标签条将放置在我们的RootContent
之上,并与Children
组件一起分割UI空间为不同的部分。
首先,我们需要一个表示每个可选标签按钮的密封类。
sealed class TabBarItem(
val image: ImageVector,
val title: String
) {
object Login : TabBarItem(
image = Icons.Default.Add,
title = "登录"
)
object Onboarding : TabBarItem(
image = Icons.Default.Create,
title = "引导页"
)
object MainHub : TabBarItem(
image = Icons.Default.Home,
title = "主页"
)
object Profile : TabBarItem(
image = Icons.Default.AccountBox,
title = "个人主页"
)
}
这可以看作是我们适配器的又一层,因为它是一个需要转换成根 Config
的数据模型,我们以后再细说。
现在我们来创建一个简单的组件作为我们标签项组件的部分:
@Composable
private fun TabComponent(
modifier: Modifier = Modifier,
item: TabBarItem,
isSelected: Boolean
) {
// 下面的 Column 是一个垂直排列的容器,水平居中对齐
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
// 根据isSelected的值来决定图标颜色,选中时为蓝色,未选中时为灰色
Icon(
tint = if (isSelected) Color.Blue else Color.Gray,
contentDescription = null,
imageVector = item.image
)
// 一个高度为4dp的空白区域,用于分隔图标和标题
Spacer(Modifier.height(4.dp))
// 显示标签项的标题,颜色根据isSelected的值决定,选中时为蓝色,未选中时为灰色
Text(item.title, color = if (isSelected) Color.Blue else Color.Gray)
}
}
此组件将接收一个选项卡项,并根据其内容来填充图像和文本。我们还将传递一个布尔值,用以决定选项卡项的颜色。
@Composable
fun 底部标签栏组件(
modifier: Modifier = Modifier,
项: List<TabBarItem>,
选择项: (TabBarItem) -> Unit
) {
var 当前选中项: TabBarItem? by remember {
mutableStateOf(TabBarItem.Login)
}
Surface(
modifier = modifier
.fillMaxWidth(),
shape = RoundedCornerShape(
topStart = 8.dp,
topEnd = 8.dp,
bottomEnd = 0.dp,
bottomStart = 0.dp
),
color = Color.White
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
项.forEach {
TabComponent(
modifier = Modifier
.clickable {
当前选中项 = it
选择项(it)
},
item = it,
isSelected = 当前选中项 == it
)
}
}
}
}
最后,我们有一个 BottomTabBar
组件,它将包含选项卡项列表。我们还有一个名为 selectedItem
的状态变量,它将定义选中的选项卡,并在每次选择新选项卡时进行更新。
我们现在稍微修改一下导航器的协议。每次我们切换到一个新的标签时,导航器应该用对应的配置替换当前屏幕。
class RootNavigator(
private val navigation: StackNavigation<RootComponent.Config>,
private val screenStack: Value<ChildStack<*, RootComponent.Child>>
) {
fun showTabItem(item: TabBarItem) {
when(item) {
is TabBarItem.Login -> navigation.bringToFront(RootComponent.Config.Login)
is TabBarItem.Onboarding -> navigation.bringToFront(RootComponent.Config.Onboarding)
is TabBarItem.MainHub -> navigation.bringToFront(RootComponent.Config.MainHub)
is TabBarItem.Profile -> navigation.bringToFront(RootComponent.Config.Profile)
}
}
``
实际上,我们将 `TabBarItem` 映射到一个表示新选中标签的 `Config`。
将标签栏(Tab栏)包含到根屏幕上
我们现在将在`RootContent`可组合项中放置新的`BottomTabBar`,它将使用`Children`来划分屏幕空间。我们将使用一个`Scaffold`来创建新的布局结构。
![](https://imgapi.imooc.com/670f2358097a6aba14000995.jpg)
@Composable
fun RootContent(rootComponent: RootComponent) {
声明一个navigator变量,它是一个remember函数返回的值,该值由rootComponent的getNavigator方法获取。
框架(
修改器 = Modifier.fillMaxSize(),
底部栏 = {
底部标签栏(
项目 = listOf(
TabBarItem.Login,
TabBarItem.Onboarding,
TabBarItem.MainHub,
TabBarItem.Profile
),
当选择 = { navigator.showTabItem(it) }
)
}) {
子组件(rootComponent.stack) {
根据子组件的实例 {
如果子组件的实例是登录组件 -> LoginScreen(navigator)
如果子组件的实例是引导组件 -> OnboardingScreen(navigator)
如果子组件的实例是主页组件 -> MainHubScreen(navigator)
如果子组件的实例是个人资料组件 -> ProfileScreen(navigator)
}
}
}
}
我们把 `BottomTabBar` 作为底部栏插入,并注意到 `Children` 是作为 `Scaffold` 的内容。每次我们切换标签时,导航器会相应地展示新的内容。同时,我们还从 `Children` 中去掉了动画参数,以实现最直观的标签切换效果。
这是我们得到的结果。
![](https://imgapi.imooc.com/670f235a0a41b57002960640.jpg)
# 结论部分
标签导航在 Decompose 中的实现与其他前端(如 UIKit、SwiftUI 和 Voyager)有所不同。我们手动创建一个新的底部栏,并添加标签项,实现了一些逻辑来将选中的屏幕置于前端。在我看来,这种实现方式甚至比其他前端框架更好,因为它为我们提供了更多的自由,我们可以自定义标签栏,以及标签项的展示方式。希望你喜欢这篇文章,并准备好利用标签栏创建你自己的流程。