Button 的 "进化之旅" | 我们是如何设计 Compose API 的 (上篇)
本文由 Jetpack Compose 团队的 Louis Pullen-Freilich (软件工程师)、Matvei Malkov (软件工程师) 和 Preethi Srinivas (UX 研究员) 共同撰写。
API 指南 https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md
绘制可点击的矩形
@Composable
fun Button(
text: String,
onClick: (() -> Unit)? = null,
enabled: Boolean = true,
shape: ShapeBorder? = null,
color: Color? = null,
elevation: Dp = 0.dp
) {
// 下面是具体实现
}
public commit
https://github.com/androidx/androidx/commit/d4f91b3a79ced7473e21c7c000edd469d24c318b
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) {
// 下面是具体实现
}
△ 1.0 版本的 Button API
获得开发者反馈
Material Button 类型
https://material.io/components/buttons
ButtonStyle https://github.com/androidx/androidx/commit/401f755476bfb330bcf4580709a86b170f1c9442
为了验证我们的假设和设计方法,我们邀请开发者参与编程活动,并使用 Button API 完成简单的编程练习。编程练习中包括实现下图的界面:
△ 开发者所需开发的 Rally Material Study 的界面
Rally
https://material.io/design/material-studies/rally.html
Button(text = "Refresh"){
}
△ 使用 Button API
认知维度框架 (Cognitive Dimensions Framework) https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.222.7327&rep=rep1&type=pdf 可用性 https://cacm.acm.org/magazines/2016/6/202645-improving-api-usability/fulltext
// 这里我们有 Padding 可组合函数,但是没有修饰符
Padding(padding = 12.dp) {
Column {
Text(text = "Refresh", style = +themeTextStyle { body1 })
}
}
当时使用样式 API,比如 themeShape 或 themeTextStyle,需要添加 + 操作符前缀。这是因为当时的 Compose Runtime 的特定限制造成的。开发者调查表明: 开发者发现很难理解此操作符的工作原理。从该现象中我们得到的启示是,不受设计者直接控制的 API 样式会影响开发者对 API 的认知。比如,我们了解到某位开发者对这里的操作符的评论是:
就我目前的理解,它是在复用一个已有的样式,或者基于该样式进行扩展。
我感觉只是在这里随意堆叠了一些东西,没有信心能够使其发挥作用。
Button{
text = "Refresh",
textStyle = +themeStyle {caption},
color = rallyGreen,
shape = RoundedRectangleBorder(borderRadius = BorderRadius.circular(5.dp.value))
}
△ 正确自定义 Button 的文字样式、颜色和形状
保持 API 的一致性
在我们的编程活动中,样式给开发人员带来了很多问题。要洞悉其中的原因,我们先回溯一下为什么样式的概念存在于 Android 框架和其他工具包中。
"样式" 本质上是与 UI 相关的属性的集合,可被应用于组件 (如 Button)。样式包含两大主要优点:
1. 将 UI 配置与业务逻辑相剥离
在命令式工具包中,独立定义样式有助于分离关注点并且使代码更易于阅读: UI 可以在一个地方定义,比如 XML 文件中;而回调和业务逻辑可以在另外的地方定义和关联。
在类似 Compose 的声明式工具包中,会通过设计减少业务逻辑和 UI 的耦合。像 Button 这样的组件,大多是无状态的,它仅仅显示您所传递的数据。当数据更新时,您无需更新它的内部状态。由于组件也都是函数,可以通过向 Button 函数传参实现自定义,如其他函数的操作一样。但是这会增加将 UI 配置从功能配置中剥离的难度。比如,设置 Button 的 enabled = false ,不仅控制 Button 的功能,还会控制 Button 是否显示。
这就引出一个问题: enabled 应该是一个顶层的参数呢,还是应该在样式中作为一个属性进行传递?而对于可用于 Button 的其他样式呢,比如 elevation,或者当 Button 被点按时,它的颜色变化呢?设计可用 API 的一个核心原则是保持一致性。我们发现在不同的 UI 组件中,保证 API 的一致性是非常重要的。
2. 自定义一个组件的多个实例
在典型的 Android View 系统中,样式非常有优势,因为创建一个新的组件的成本很高: 您需要创建一个子类,实现构造方法,并且启用自定义属性。样式允许以一种更加简洁的方式,来表达一系列共享的属性。比如,创建一个 LoginButtonStyle,来定义应用中全部用于登录按钮的外观。在 Compose 中,实现如下所示:
val LoginButtonStyle = ButtonStyle(
backgroundColor = Color.Blue,
contentColor = Color.White,
elevation = 5.dp,
shape = RectangleShape
)
Button(style = LoginButtonStyle) {
Text(text = "LOGIN")
}
△ 为登录按钮定义样式
@Composable
fun LoginButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(onClick = onClick, modifier = modifier, style = LoginButtonStyle) {
Text(text = "LOGIN")
}
}
@Composable
fun LoginButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier,
shape = RectangleShape,
elevation = ButtonDefaults.elevation(defaultElevation = 5.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue, contentColor = Color.White)
) {
Text(text = "LOGIN")
}
}
△ 最终的 LoginButton 实现
@Composable
inline fun OutlinedButton(
modifier: Modifier = Modifier.None,
noinline onClick: (() -> Unit)? = null,
backgroundColor: Color = MaterialTheme.colors().surface,
contentColor: Color = MaterialTheme.colors().primary,
shape: Shape = MaterialTheme.shapes().button,
border: Border? =
Border(1.dp, MaterialTheme.colors().onSurface.copy(alpha = OutlinedStrokeOpacity)),
elevation: Dp = 0.dp,
paddings: EdgeInsets = ButtonPaddings,
noinline children: @Composable() () -> Unit
) = Button(
modifier = modifier,
onClick = onClick,
backgroundColor = backgroundColor,
contentColor = contentColor,
shape = shape,
border = border,
elevation = elevation,
paddings = paddings,
children = children
)
△ 1.0 版本中的 OutlinedButton
去掉样式
https://github.com/androidx/androidx/commit/e386f97dd769da18d9f3103958714e43d797219f
提高 API 的可发现性或可见性
使用默认值创建一个简单的 Button 从 MaterialTheme.kt 源文件中参考关于形状的主题设置相关的内容 再回看 MaterialButtonShapeTheme 函数 找到 RoundedCornerShape,并且使用类似的方法创建一个带有切角的 shape
将 CutCornerShape 迁移 https://github.com/androidx/androidx/commit/468c797c109cf24a561dad6a496310964d2a4a2b
小结
本文以 Jetpack Compose 中 Button API 作为切入点,从原型设计出发,为大家展示了研发团队如何基于开发者反馈、API 一致性和 API 可发现性三个角度,对公共 API 接口进行不断迭代和优化,以及在过程中所遇到的问题。
问题提出和建议反馈
https://issuetracker.google.com/issues/new?component=612128&template=1253476
Google 用户体验调研
https://google.qualtrics.com/jfe/form/SV_3NMIMtX0F2zkakR?reserved=1&utm_source=Survey&Q_Language=en&utm_medium=own_evt&utm_campaign=Q1&productTag=adstu&campaignDate=February2021&referral_code=UXSf298725
推荐阅读