插槽API模式(Slots API Pattern)帮助我们通过提供通用的可组合插槽参数来创建灵活的组件。
然而,在没有一些限制的情况下,保持设计和行为的一致性几乎是不可能的,特别是在设计像Toolbar或Button这样的通用系统组件(例如工具栏或按钮)时……
在接下来的这篇博客中,我们将结合灵活性和限制,打造出完美的组件 👌。
我们试着用工具栏来试试吧。
保持“Toolbar”为“工具栏”。
让我们从创建一些基本组件开始吧。我们需要一个工具栏
,它的actions
只接受一组预定义的子组件,而body
插槽则非常灵活,
@Composable
fun Toolbar(
modifier: Modifier = Modifier,
actions: (@Composable () -> Unit)? = null,
/* 其他可选参数... */
body: @Composable () -> Unit,
){
/*...*/
}
// 子组件...
@Composable
fun ToolbarIcon(
imageVector: ImageVector,
onClick: () -> Unit,
tint: Color = LocalContentColor.current,
) {
/*...*/
}
@Composable
fun ToolbarAnimatedVisibility(
visible: Boolean,
content: @Composable AnimatedVisibilityScope.() -> Unit,
) {
/*...*/
}
@Composable
fun ToolbarButton(
onClick: () -> Unit,
text: String,
){
/*...*/
}
// 其他任何子组件,比如自定义弹出窗口...
注意: 我省略了子组件中的
modifier
参数,以保持完全掌控,因此没有给这些组件添加任何额外样式。
目前,导航栏接受任何可组合组件。为了限制这种做法,我们可以使用 layoutId
修饰符为每个子组件分配一个独特的 ID,然后在导航栏中验证这些唯一标识。
@Composable
fun ToolbarIcon(/*...*/) {
Icon(
modifier = Modifier.layoutId(工具栏图标布局ID)
/*...*/
)
}
private object 工具栏图标布局ID
注意:关键是要限制其他开发人员访问
layoutId
,从而使开发人员不能将其赋值给其他组件,进而绕过限制。
接下来,在测量阶段中,我们检查 layoutId
以验证组件的 ID。
@Composable
fun RequireLayoutId(
layoutIds: List<Any>,
content: @Composable () -> Unit,
errorMessage: () -> Any,
) {
Layout(content) { measurables, _ ->
// 检查所有子元素的布局ID
确保(measurables.all { it.layoutId in layoutIds }, errorMessage)
// 实际上不测量或布局任何子元素
layout(0, 0) {}
}
}
在这个功能中,我们读取组件列表的内容并验证这些ID是否有效。
例如,将三个 ToolbarIcons
和一个 ToolbarButton
传给工具栏的 action
槽位是可以接受的。任何其他类型的组件都会引发运行时异常。
最后一步,我们应该在工具栏的最开始使用这个功能。
@Composable
fun Toolbar(/*...*/) {
if (actions != null) {
RequireLayoutId(
layoutIds = listOf(
ToolbarIconLayoutId,
ToolbarButton,
),
content = { actions() },
errorMessage = { "操作仅限于工具栏组件" }
)
}
// ...
}
// 示例如下
@Composable
fun HomeScreen() {
Toolbar(
actions = {
// 可接受 ✅
ToolbarIcon()
// 可接受 ✅
ToolbarButton()
// 会引发错误 ❌
Box {
ToolbarIcon()
}
},
body = { /*...*/ }
)
}
现在看起来一切都很好,但所有子组件在任何地方都能访问到!
要解决这个问题,我们可以自定义一个作用域,将我们的所有子组件都包含进去。
@file:Suppress("ABSTRACT_COMPOSABLE_DEFAULT_PARAMETER_VALUE")
interface ToolbarScope {
@Composable
fun ToolbarIcon(/*...*/) {}
companion object {
private val instance = object : ToolbarScope {}
internal operator fun invoke() = instance
// 如果您的 UI 工具包没有单独打包...
internal object ToolbarIconLayoutId
}
}
@Composable
fun Toolbar(
actions: @Composable (ToolbarScope.() -> Unit)? = null,
) {
if (actions != null) {
// 需要布局ID
/*...*/
content = { ToolbarScope().actions() },
}
with(ToolbarScope()) {
// 例如...
Row {
body()
actions()
}
}
// ...
}
这样做让这些组件从外部更难访问。如果有人试图在不合适的地方使用它们,审查时很容易发现。
另一个选择是将所有子组件放入一个 ToolbarDefaults
对象。这样就稍微难以不小心访问。
object ToolbarDefaults {
@Composable
fun ToolbarIcon(/*注释...*/) {}
}
或者更详细翻译:
object 工具栏默认设置 {
@组合式注解
fun 工具栏图标(/*注释...*/) {}
}
实际中的组件限制措施 💙
在Getcontact中,屏幕数量超过700后,我们发现灵活性与限制相结合带来了很大的好处。
- 一致的UI/UX: 应用看起来更清晰,行为和动画在应用中保持一致。
- 减少UI错误: 限制减少了间距、波纹和动画错误,减少了测试人员和开发人员的来回沟通。
- 防止设计师偏离正轨: 使用高度灵活的组件,我们可以实现任何设计——即使它不符合设计系统。但是有了这些限制,我们可以停下来问,“为什么这个组件与其他组件的不同之处如此显著?” 并且只有在符合设计系统的情况下才继续前进。
实施限制并不容易,而且只有在确实需要的时候才应该使用。然而,当正确实施,它可以帮所有团队省去很多麻烦。
哦,顺便说一句,订阅或者随便做点什么吧,随便 :)
回头见,你一会儿再聊吧…
共同学习,写下你的评论
评论加载中...
作者其他优质文章