查看原文
其他

Button 的 "进化之旅" | 我们是如何设计 Compose API 的 (下篇)

Android Android 开发者 2021-10-19
本文由 Jetpack Compose 团队的 Louis Pullen-Freilich (软件工程师)、Matvei Malkov (软件工程师) 和 Preethi Srinivas (UX 研究员) 共同撰写。

近期 Jetpack Compose 发布了 1.0 版本,带来了一系列用于构建 UI 的稳定 API。今年早些时候,我们发布了 API 指南,介绍了编写 Jetpack Compose API 的最佳实践和 API 设计模式。经过多次迭代公共 API 接口 (API surface) 之后形成的指南,其实没有展示出这些设计模式的形成过程和我们在迭代过程中决策背后的故事。


  • API 指南
    https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md

本文将继续带您了解一个 "简单" 的 Button 的 "进化之旅",来深入了解我们是如何迭代设计 API,使其简单易用又不失灵活性。这个过程需要基于开发者的反馈,对 API 的可用性进行多次的适配和改进。

 

上一篇文章中,我们通过开发者反馈、API 一致性和可发现性三个方面,和大家探讨了 Button API 的迭代过程。本文将进一步为大家展示 Button API 是如何在原有设计上逐步优化演变成为今天的设计。



映射开发者的工作框架



接下来是更多的反馈 —— 我们在一系列更进一步的编程活动中,重新评估了 Button API 的可用性。在这些活动中,我们使用 Material Design 中对于按钮的定义来进行命名: Button 变为 ContainedButton 以符合它在 Material Design 中的特性。然后,我们测试新的命名,以及当时已有的整个 Button API,并且评估了两个主要的开发者目标:
  • 创建 Button 并且处理点击事件
  • 使用预定义的 Material 主题为 Button 添加样式

△ material.io 中的 Material Button

我们从开发者活动中得到了一个关键启示 —— 大多数开发者不太熟悉 Material Button 中的命名习惯。比如,很多开发者无法区分 ContainedButtonOutlinedButton:        


ContainedButton 是什么意思呢?

我们发现当输入 Button,并且看到自动补全建议的三个 Button 组件时,开发者花费了相当的精力来猜测哪个才是自己需要的。大多数开发者希望默认的按钮就是 ContainedButton,因为这是最常用的一个,并且也是最像 "按钮" 的一个。所以就明确了我们需要一个默认设置,使开发者可以直接使用而无需阅读 Material Design 的指南。此外,基于视图的 MDC-Android Button 默认就是填充式按钮,这也是将其作为默认按钮的先例。

  • MDC-Android
    https://github.com/material-components/material-components-android


更清楚地描述角色



研究发现,另外一个令人困惑的点是两个已存在的 Button 的版本: 一个 Button 可接受一个 String 类型的参数作为文本,而一个 Button 可接受一个可修改的 lambda 参数,表示通用内容。这么设计的本意是从两个不同的层次来提供 API:

  • 带有文本的 Button 更简单一些,更加易于实现

  • 更高级的 Button,它其中的内容更具开放性


我们发现开发者在两者之间进行选择时,会有一定困难: 但是当从 String 重载转移到 lambda 重载时,自定义 "悬崖" 的存在,使得增量自定义 Button 变得具有挑战性。我们常常听到开发者要求在 String 重载中为 Button 增加 TextStyle 参数。


它允许自定义内部的 TextStyle 而无需使用 lambda 重载的版本。

 

我们提供 String 的本意是希望能够简化那些最简单用例的实现,但是这样却阻碍了开发者使用带有可组合的 lambda 的重载,转而要求 String 重载增加额外功能。这两个单独 API 的存在,不仅造成了开发者的困惑,也表明了带有原始类型的重载的确存在一些根本的问题: 他们接受了原始类型,比如 String,而不是可组合的 lambda 类型。

单步代码


原始类型的 Button 重载直接将文本作为参数,减少了开发者在创建文本式 Button 时所需要写的代码。我们最初使用简单的 String 类型作为文本参数,但是后来发现 String 类型很难对其中的部分文本添加样式。

对于这样的需求,Compose 提供了 AnnotatedString API,来对文本的不同部分添加自定义样式。然而,它对于简单的应用场景增加了一定成本,因为开发者首先需要将 String 转换为 AnnotatedString。这也使我们在考虑是否应该提供新的 Button 重载,既可以接受 String 作为参数,也可以接受 AnnotatedString 作为参数,来支持简单和更加进阶的需求。

我们的 API 设计讨论在图片和图标方面更加的复杂,比如当 FloatingActionButton 需要用到图片或者图标的时候。icon 参数的类型应该是 Vector 还是 Bitmap?如何支持带有动画的图标?即使我们竭尽了全力,最终发现我们也只能支持 Compose 中可用的类型 —— 任何第三方图片类型都需要开发者实现他们自己的重载以提供支持。

紧耦合的副作用


Compose 最大的优势之一是可组合性。创建可组合的函数以较小成本分离关注点,构建可复用的和相对独立的组件。通过可组合的 lambda 重载,可以直观地看到这样的思路: Button 是可点击内容的容器,但是它无需关心其中的内容是什么。
 
但是对于原始类型的重载,情况就变复杂了: 直接接受文本参数的 Button,现在既需要负责作为可点击的容器,又需要将 Text 组件传递到内部。这意味着它现在需要管理两者的公共 API 接口,这也引发了另一个重要的问题: Button 该对外暴露什么样的文本相关参数呢?这也将 ButtonText 的公共 API 接口绑定到了一起: 如果未来 Text 增加了新的参数和功能,那是不是意味着 Button 也需要增加对这些新增内容的支持?紧耦合是 Compose 试图避免的问题之一,而且很难以统一的方式在所有组件上回答该问题,这也导致了公共 API 接口的不一致性。

支持工作框架


原始类型的重载使开发者可以避免使用可组合的 lambda 重载,而以较少的自定义空间作为代价。但是当开发者需要在原始类型的重载上,实现原本无法实现的自定义呢?唯一的选择,就是使用可组合的 lambda 重载,然后,将内部的实现代码从原始类型重载中复制过来,并做相应的修改。我们在研究中发现,自定义操作的 "悬崖" 阻碍了开发者使用更加灵活、可组合的 API,因为在层级之间的操作显得比之前更具挑战。

使用 "slot API" 解决问题


列举上述问题后,我们决定去掉 Button 的原始类型重载,为每种 Button 仅留下包含针对内容的可组合 lambda 参数的 API。我们开始将这个通用的 API 形式叫做 "slot API",现已经广泛应用于各个组件。

Button(backgroundColor = Color.Purple) { // 任何可组合内容都可以写在这里}
带有空白 "slot" 的 Button

Button(backgroundColor = Color.Purple) { Row { MyImage() Spacer(4.dp) Text("Button") }}

△ 带有横向排列的图片和文本的 Button

一个 "slot" 代表一个可组合的 lambda 参数,它代表组件中的任意内容,比如 Text 或者 Icon。Slot API 增加了可组合性,使组件更加简单,减少了组件之间的独立概念数量,使开发者可以快速上手创建一个新的组件,或者在不同的组件之间切换。
移除原始类型重载的 CL


展望未来



我们对 Button API 所做的修改数量之多,在讨论 Button 的会议中所付出的时间之多,以及收集开发者的反馈所投入的精力之巨大,足以惊人。话虽如此,我们对 API 整体的效果非常满意。事后看来,我们看到在 Compose 中 Button 变得更具可发现性、可定制性,最重要的是它促进了组合式思维。

@Composablefun 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 AP

  • 1.0 Button AP

    https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt;l=96?q=Button.kt


重要的是认识到,我们的设计决策都基于下面这句口号:


让简单的开发变得简单,让困难的开发变得可能。*

*这里出自著名的技术类书籍: 英文版:《Learning Perl: Making Easy Things Easy and Hard Things Possible》(Randal L. Schwartz、Brian D Foy 和 Tom Phoenix 著),中文版:《Perl 语言入门》(盛春译)

 

我们尝试通过减少重载,并将 "样式" 扁平化处理,使开发变得更加简单。与此同时,我们改进了 Android Studio 的自动补全功能,来帮助开发者提高效率。


这里我们希望特别提出在整个 API 设计过程中的两个要点:

  1. API 的设计是一个迭代的过程。在 API 最初的迭代中就达到完美的状态是几乎不可能的。有一些需求容易被忽视。作为一个 API 的作者,您需要做出一些假设。这其中包括开发者背景的不同,所带来的不同思维方式¹,最终影响了开发者探索和使用 API 的方式。适配调整是无法避免的,这是好事,不断迭代可以得到可用性更高并且更加直观的 API。

  2. 在迭代一个 API 设计时,您最有价值的工具之一是开发者使用 API 体验的反馈循环。对我们的团队来说,最关键的是去理解开发者所说的 "这个 API 太复杂了" 意味着什么。当错误调用 API 时,通常会降低开发者的成功率和效率,从中所获得感悟,会帮助我们更深入理解 "复杂 API" 的意思。我们不断迭代的关键驱动力是我们要设计易用且出色的 API。为此,创建开发者反馈循环,我们使用了多种研究路径 —— 现场编程活动²,和需要开发者提供体验日记³ 的远程途径。我们已经可以理解开发者是如何处理 API,以及他们为打算实现的功能,找到正确方法所采取的路径。诸如工程师思维方式 (Programmer Thinking Styles) 和认知纬度 (Cognitive Dimensions) 这类框架中的支柱,有助于我们跨职能团队保持语言思维上的一致,不仅表现在审核、沟通开发者反馈中,也涉及到 API 设计讨论。尤其是,当评估用户体验和功能性之间的关系时,这个框架帮助我们塑造了为选择和权衡所做的讨论。


  1. 来自 Android Developer UX 团队的 Meital Tagor Sbero 受到角色模型和思维方式 (personas & Thinking Styles) 的设计和认知维度框架 (Cognitive Dimensions Framework) 的启发,开发了工程师思维方式框架 (Programmer Thinking Styles Framework)。该框架使用开发者在限定时间内所需 "解决方案的类型"的动机和态度,帮助开发者确定 API 可用性的设计思路。它兼顾了普通工程师的工作方式,并且针对高强度开发任务优化了可用性。


  2. 我们通常使用这种方式评估 API 特定方面的可用性。比如,每个活动会邀请一组开发者使用 Button API 来完成一系列开发任务,这些任务会特意暴露一些 API 的特征,而这些特征是我们希望收集反馈的目标。我们通过放声思考法,来获得更多关于开发者所追求的和开发者所设想的信息。这些活动中还包含研究者通过一些随访的问题,来进一步了解开发者的需求。我们会回顾这些活动,从而确定开发者在编程任务中促成成功或者导致失败的行为模式。


  3. 我们通常使用这种方式来评估 API 在一段时间内的可用性和易学习性。这种方式可以通过倾听开发者在常规工作中的反馈,来捕捉遇到困难的瞬间和受到启发的瞬间。在这个过程中,我们会有一组开发者开发由他们自选的特定项目,同时也确保他们会使用我们希望评估的 API。我们会结合开发者通过自行提交的日记,和由研究人员基于认知维度框架 (Cognitive Dimensions Framework) (示例) 所组织的深度调查,以及专访活动来帮助我们确定 API 的可用性。


  • Meital Tagor Sbero

    https://www.linkedin.com/in/meitaltagor/

  • 角色模型和思维方式 (personas & Thinking Styles)

    https://medium.com/inclusive-software/tagged/thinking-styles

  • 认知维度框架 (Cognitive Dimensions Framework)

    https://www.researchgate.net/profile/Marian-Petre-4/publication/200085937_Usability_Analysis_of_Visual_Programming_Environments_A_%27Cognitive_Dimensions%27_Framework/links/02bfe50fbf23476730000000/Usability-Analysis-of-Visual-Programming-Environments-A-Cognitive-Dimensions-Framework.pdf

  • 示例

    https://arxiv.org/pdf/1703.09846.pdf


我们承认虽然我们对现有版本的 Button API 很满意,但是我们也知道它并不是完美的。开发者的思维方式有很多,加上不同的应用场景,以及层出不穷的需求,要求我们要不断迎接新的挑战。这都不是问题!Button 的整个进化过程,对于我们和开发者社区的意义都很大。所有这些都是为 Compose 设计和塑造了一个可用的 Button API —— 一个可以在屏幕上点击的简单矩形。

希望这篇文章能够帮助大家清楚了解到您的反馈如何帮助我们改进 Compose 中 Button API。如果您在使用 Compose 时遇到任何问题,或者对新 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

也欢迎您通过下方二维码向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!



推荐阅读

如页面未加载,请刷新重试

 点击屏末 | 阅读原文 | 即刻了解使用 Jetpack Compose 打造更出色的应用




视频 小程序 ,轻点两下取消赞 在看 ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存