
本文由 Jetpack Compose 團(tuán)隊(duì)的 Louis Pullen-Freilich (軟件工程師)、Matvei Malkov (軟件工程師) 和 Preethi Srinivas (UX 研究員) 共同撰寫。
近期 Jetpack Compose 發(fā)布了 1.0 版本,帶來了一系列用于構(gòu)建 UI 的穩(wěn)定 API。今年早些時(shí)候,我們發(fā)布了 API 指南,介紹了編寫 Jetpack Compose API 的最佳實(shí)踐和 API 設(shè)計(jì)模式。經(jīng)過多次迭代公共 API 接口 (API surface) 之后形成的指南,其實(shí)沒有展示出這些設(shè)計(jì)模式的形成過程和我們?cè)诘^程中決策背后的故事。
本文將帶您了解一個(gè) "簡(jiǎn)單" 的 Button 的 "進(jìn)化之旅",來深入了解我們是如何迭代設(shè)計(jì) API,使其簡(jiǎn)單易用又不失靈活性。這個(gè)過程需要基于開發(fā)者的反饋,對(duì) API 的可用性進(jìn)行多次的適配和改進(jìn)。
繪制可點(diǎn)擊的矩形
Google 的 Android Toolkit 團(tuán)隊(duì)中有一個(gè)調(diào)侃: 我們所做的就是在屏幕上畫一個(gè)帶著顏色的矩形,并且讓它可以被點(diǎn)擊。事實(shí)證明,這是 UI toolkit 中最難實(shí)現(xiàn)的事情之一。
也許有人會(huì)認(rèn)為,按鈕是一個(gè)簡(jiǎn)單的組件: 只是一個(gè)有顏色的矩形,帶有一個(gè)點(diǎn)擊監(jiān)聽器。造成 Button API 設(shè)計(jì)復(fù)雜的原因有很多方面: 可發(fā)現(xiàn)性、參數(shù)的順序和命名等等。另一個(gè)約束是靈活性: Button 提供了很多參數(shù),可供開發(fā)者隨意自定義各個(gè)元素。其中一些參數(shù)默認(rèn)使用主題的配置,而一些參數(shù)可以基于其他參數(shù)的值。這樣的搭配使得 Button API 的設(shè)計(jì)成為了一個(gè)很有意思的挑戰(zhàn)。
我們針對(duì) Button API 的第一個(gè)迭代版本,由兩年前的一個(gè) public commit 開始。當(dāng)時(shí)的 API 就像下面這樣:
@Composable
fun Button(
text: String,
onClick: (() -> Unit)? = null,
enabled: Boolean = true,
shape: ShapeBorder? = null,
color: Color? = null,
elevation: Dp = 0.dp
) {
// 下面是具體實(shí)現(xiàn)
}
△ 最初的 Button API
除了名字外,最初的 Button API 與最終版本的代碼相去甚遠(yuǎn)。它經(jīng)歷了多次迭代,我們將為大家展示這一過程:
@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
) {
// 下面是具體實(shí)現(xiàn)
}
△ 1.0 版本的 Button API
獲得開發(fā)者反饋
在 Compose 的研究和實(shí)驗(yàn)階段的早期,我們的 Button 組件可以接收一個(gè) ButtonStyle 類型的參數(shù)。ButtonStyle 為 Button 定義了視覺相關(guān)的配置,比如顏色和形狀。這使得我們可以展現(xiàn)三種不同的 Material Button 類型: 內(nèi)含型 (Contained)、輪廓型 (Outlined) 和純文本型 (Text);我們直接暴露頂層的構(gòu)建函數(shù),它會(huì)返回一個(gè) ButtonStyle 實(shí)例,該實(shí)例對(duì)應(yīng) Material 規(guī)范中對(duì)應(yīng)的按鈕類型。開發(fā)者可以復(fù)制這些內(nèi)置的按鈕樣式并微調(diào),或者從頭開始創(chuàng)建新的 ButtonStyle,從而完全重新設(shè)計(jì)自定義 Button。我們對(duì)于最初的 Button API 是比較滿意的,這個(gè) API 是可復(fù)用的,而且包含了易用的樣式。
為了驗(yàn)證我們的假設(shè)和設(shè)計(jì)方法,我們邀請(qǐng)開發(fā)者參與編程活動(dòng),并使用 Button API 完成簡(jiǎn)單的編程練習(xí)。編程練習(xí)中包括實(shí)現(xiàn)下圖的界面:

△ 開發(fā)者所需開發(fā)的 Rally Material Study 的界面
對(duì)這些代碼開發(fā)的觀察結(jié)果使用了 認(rèn)知維度框架 (Cognitive Dimensions Framework) 進(jìn)行復(fù)盤,以評(píng)估 Button API 的 可用性。
很快,我們觀察到一個(gè)有趣的現(xiàn)象: 一些開發(fā)者一開始這樣使用 Button API:
Button(text = "Refresh"){
}
△ 使用 Button API
也有開發(fā)者嘗試創(chuàng)建一個(gè) Text 組件,然后使用圓角矩形圍在文本的外圍:
// 這里我們有 Padding 可組合函數(shù),但是沒有修飾符
Padding(padding = 12.dp) {
Column {
Text(text = "Refresh", style = +themeTextStyle { body1 })
}
}
△ 在 Text 上添加 Padding 來模擬一個(gè) Button
當(dāng)時(shí)使用樣式 API,比如 themeShape 或 themeTextStyle,需要添加 + 操作符前綴。這是因?yàn)楫?dāng)時(shí)的 Compose Runtime 的特定限制造成的。開發(fā)者調(diào)查表明: 開發(fā)者發(fā)現(xiàn)很難理解此操作符的工作原理。從該現(xiàn)象中我們得到的啟示是,不受設(shè)計(jì)者直接控制的 API 樣式會(huì)影響開發(fā)者對(duì) API 的認(rèn)知。比如,我們了解到某位開發(fā)者對(duì)這里的操作符的評(píng)論是:
就我目前的理解,它是在復(fù)用一個(gè)已有的樣式,或者基于該樣式進(jìn)行擴(kuò)展。
大多數(shù)開發(fā)者認(rèn)為 Compose API 之間出現(xiàn)了不一致性 —— 比如,對(duì) Button 添加樣式的方式與 Text 組件添加樣式的方式不同*。
*大多數(shù)開發(fā)者希望在樣式前加上 "加號(hào)",使用 +themeButtonStyle 或者 +buttonStyle,類似他們對(duì) Text 組件使用 +themeTextStyle 一樣的方式。
此外,我們發(fā)現(xiàn)大多數(shù)開發(fā)者在 Button 上實(shí)現(xiàn)圓角邊緣時(shí),都經(jīng)歷了痛苦的過程,但是本來的預(yù)期是非常簡(jiǎn)單。通常,他們需要瀏覽多個(gè)層次的實(shí)現(xiàn)代碼,來理解 API 的結(jié)構(gòu)。
我感覺只是在這里隨意堆疊了一些東西,沒有信心能夠使其發(fā)揮作用。
Button{
text = "Refresh",
textStyle = +themeStyle {caption},
color = rallyGreen,
shape = RoundedRectangleBorder(borderRadius = BorderRadius.circular(5.dp.value))
}
△ 正確自定義 Button 的文字樣式、顏色和形狀
這就影響了開發(fā)者對(duì) Button 設(shè)置樣式的方式。比如,當(dāng)為 Android 應(yīng)用添加 Button時(shí),ContainedButtonStyle 是無法對(duì)應(yīng)到開發(fā)者所已知的樣式的。點(diǎn)擊這里 查看來自開發(fā)者研究的早期的感悟視頻。
通過舉辦的這些編程活動(dòng),我們體會(huì)到需要簡(jiǎn)化 Button API,來使其能夠?qū)崿F(xiàn)簡(jiǎn)單的自定義操作,同時(shí)支持復(fù)雜的應(yīng)用場(chǎng)景。我們開始在可發(fā)現(xiàn)性和個(gè)性化上下功夫,而這兩點(diǎn)為我們帶來了接下來的一系列挑戰(zhàn): 樣式和命名。
保持 API 的一致性
在我們的編程活動(dòng)中,樣式給開發(fā)人員帶來了很多問題。要洞悉其中的原因,我們先回溯一下為什么樣式的概念存在于 Android 框架和其他工具包中。
"樣式" 本質(zhì)上是與 UI 相關(guān)的屬性的集合,可被應(yīng)用于組件 (如 Button)。樣式包含兩大主要優(yōu)點(diǎn):
1. 將 UI 配置與業(yè)務(wù)邏輯相剝離
在命令式工具包中,獨(dú)立定義樣式有助于分離關(guān)注點(diǎn)并且使代碼更易于閱讀: UI 可以在一個(gè)地方定義,比如 XML 文件中;而回調(diào)和業(yè)務(wù)邏輯可以在另外的地方定義和關(guān)聯(lián)。
在類似 Compose 的聲明式工具包中,會(huì)通過設(shè)計(jì)減少業(yè)務(wù)邏輯和 UI 的耦合。像 Button 這樣的組件,大多是無狀態(tài)的,它僅僅顯示您所傳遞的數(shù)據(jù)。當(dāng)數(shù)據(jù)更新時(shí),您無需更新它的內(nèi)部狀態(tài)。由于組件也都是函數(shù),可以通過向 Button 函數(shù)傳參實(shí)現(xiàn)自定義,如其他函數(shù)的操作一樣。但是這會(huì)增加將 UI 配置從功能配置中剝離的難度。比如,設(shè)置 Button 的 enabled = false ,不僅控制 Button 的功能,還會(huì)控制 Button 是否顯示。
這就引出一個(gè)問題: enabled 應(yīng)該是一個(gè)頂層的參數(shù)呢,還是應(yīng)該在樣式中作為一個(gè)屬性進(jìn)行傳遞?而對(duì)于可用于 Button 的其他樣式呢,比如 elevation,或者當(dāng) Button 被點(diǎn)按時(shí),它的顏色變化呢?設(shè)計(jì)可用 API 的一個(gè)核心原則是保持一致性。我們發(fā)現(xiàn)在不同的 UI 組件中,保證 API 的一致性是非常重要的。
2. 自定義一個(gè)組件的多個(gè)實(shí)例
在典型的 Android View 系統(tǒng)中,樣式非常有優(yōu)勢(shì),因?yàn)閯?chuàng)建一個(gè)新的組件的成本很高: 您需要?jiǎng)?chuàng)建一個(gè)子類,實(shí)現(xiàn)構(gòu)造方法,并且啟用自定義屬性。樣式允許以一種更加簡(jiǎn)潔的方式,來表達(dá)一系列共享的屬性。比如,創(chuàng)建一個(gè) LoginButtonStyle,來定義應(yīng)用中全部用于登錄按鈕的外觀。在 Compose 中,實(shí)現(xiàn)如下所示:
val LoginButtonStyle = ButtonStyle(
backgroundColor = Color.Blue,
contentColor = Color.White,
elevation = 5.dp,
shape = RectangleShape
)
Button(style = LoginButtonStyle) {
Text(text = "LOGIN")
}
△ 為登錄按鈕定義樣式
現(xiàn)在可以在 UI 中的各種 Button 上使用 LoginButtonStyle,而無需在每個(gè) Button 上顯式設(shè)置這些參數(shù)。然而,如果您也希望提取文本,讓所有的登錄按鈕都顯示相同的文本: "LOGIN",該怎么辦呢?
在 Compose 中,每個(gè)組件都是一個(gè)函數(shù),所以常規(guī)的解決方法是定義一個(gè)函數(shù),其中調(diào)用 Button,并且為 Button 提供正確的文本:
@Composable
fun LoginButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(onClick = onClick, modifier = modifier, style = LoginButtonStyle) {
Text(text = "LOGIN")
}
}
△ 創(chuàng)建一個(gè)在語義上表達(dá)了其含義的 LoginButton 函數(shù)
由于組件先天的無狀態(tài)特性,以這樣的方式提煉函數(shù)的成本是很低的: 參數(shù)可以直接從封裝的函數(shù),傳遞給內(nèi)部的按鈕。由于您并不是繼承一個(gè)類,所以僅暴露需要的參數(shù);剩下的可以留在 LoginButton 的內(nèi)部實(shí)現(xiàn)體中,從而避免顏色和文本被覆蓋。這樣的方式適用于很多自定義場(chǎng)景,超過樣式所涵蓋的范圍。
此外,相比在 Button 上設(shè)置 LoginButtonStyle,創(chuàng)建一個(gè) LoginButton 函數(shù),可以具有更多的語義上的含義。我們也在研究過程中發(fā)現(xiàn): 相比樣式,獨(dú)立的函數(shù)更具有可發(fā)現(xiàn)性。
沒有了樣式,LoginButton 現(xiàn)在可以重構(gòu)為直接向其中的 Button 傳參,而無需使用樣式對(duì)象,這樣就能與其他自定義操作保持一致:
@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 實(shí)現(xiàn)
最終我們 去掉樣式,并且將參數(shù)扁平化到組件中 —— 一方面是為了整體 Compose 設(shè)計(jì)的一致性,另一方面是鼓勵(lì)開發(fā)者創(chuàng)建更具語義特征的 "封裝" 函數(shù):
@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
提高 API 的可發(fā)現(xiàn)性或可見性
我們還在研究中發(fā)現(xiàn),在如何設(shè)置按鈕形狀方面存在一個(gè)重大缺陷。要自定義 Button 的形狀,開發(fā)者可以使用 shape 參數(shù),它可接受一個(gè) Shape 對(duì)象。當(dāng)開發(fā)者需要新建一個(gè)帶有切角的按鈕時(shí),通常可通過如下方式實(shí)現(xiàn):
- 使用默認(rèn)值創(chuàng)建一個(gè)簡(jiǎn)單的
Button - 從
MaterialTheme.kt源文件中參考關(guān)于形狀的主題設(shè)置相關(guān)的內(nèi)容 - 再回看
MaterialButtonShapeTheme函數(shù) - 找到
RoundedCornerShape,并且使用類似的方法創(chuàng)建一個(gè)帶有切角的 shape
大多數(shù)開發(fā)者在這里會(huì)感到迷惑,在瀏覽大量 API 和源代碼時(shí),常常會(huì)不知所措。我們發(fā)現(xiàn)開發(fā)者不易發(fā)現(xiàn) CutCornerShape,這是因?yàn)樗菑呐c其他的 shape API 不同的包里所暴露出來的。
可見性用于衡量開發(fā)者達(dá)到其目標(biāo)時(shí),定位函數(shù)或者參數(shù)的難易程度。它和編寫代碼所需的認(rèn)知過程所付出的精力直接相關(guān);用于探索發(fā)現(xiàn)和使用一個(gè)方法的路徑越深,API 的可見性越差。最終,這會(huì)導(dǎo)致較低的效率和較差的開發(fā)者體驗(yàn)。基于這樣的認(rèn)知,我們 將 CutCornerShape 遷移 到與其他 shape API 相同的包中,來支持便捷的可發(fā)現(xiàn)性。
映射開發(fā)者的工作框架
接下來是更多的反饋 —— 我們?cè)谝幌盗懈M(jìn)一步的編程活動(dòng)中,重新評(píng)估了 Button API 的可用性。在這些活動(dòng)中,我們使用 Material Design 中對(duì)于按鈕的定義來進(jìn)行命名: Button 變?yōu)?ContainedButton 以符合它在 Material Design 中的特性。然后,我們測(cè)試新的命名,以及當(dāng)時(shí)已有的整個(gè) Button API,并且評(píng)估了兩個(gè)主要的開發(fā)者目標(biāo):
- 創(chuàng)建
Button并且處理點(diǎn)擊事件 - 使用預(yù)定義的 Material 主題為
Button添加樣式

我們從開發(fā)者活動(dòng)中得到了一個(gè)關(guān)鍵啟示 —— 大多數(shù)開發(fā)者不太熟悉 Material Button 中的命名習(xí)慣。比如,很多開發(fā)者無法區(qū)分 ContainedButton 和 OutlinedButton:
ContainedButton 是什么意思呢?
我們發(fā)現(xiàn)當(dāng)輸入 Button,并且看到自動(dòng)補(bǔ)全建議的三個(gè) Button 組件時(shí),開發(fā)者花費(fèi)了相當(dāng)?shù)木聿聹y(cè)哪個(gè)才是自己需要的。大多數(shù)開發(fā)者希望默認(rèn)的按鈕就是 ContainedButton,因?yàn)檫@是最常用的一個(gè),并且也是最像 "按鈕" 的一個(gè)。所以就明確了我們需要一個(gè)默認(rèn)設(shè)置,使開發(fā)者可以直接使用而無需閱讀 Material Design 的指南。此外,基于視圖的 MDC-Android Button 默認(rèn)就是填充式按鈕,這也是將其作為默認(rèn)按鈕的先例。
更清楚地描述角色
研究發(fā)現(xiàn),另外一個(gè)令人困惑的點(diǎn)是兩個(gè)已存在的 Button 的版本: 一個(gè) Button 可接受一個(gè) String類型的參數(shù)作為文本,而一個(gè) Button 可接受一個(gè)可修改的 lambda 參數(shù),表示通用內(nèi)容。這么設(shè)計(jì)的本意是從兩個(gè)不同的層次來提供 API:
- 帶有文本的
Button更簡(jiǎn)單一些,更加易于實(shí)現(xiàn) - 更高級(jí)的
Button,它其中的內(nèi)容更具開放性
我們發(fā)現(xiàn)開發(fā)者在兩者之間進(jìn)行選擇時(shí),會(huì)有一定困難: 但是當(dāng)從 String 重載轉(zhuǎn)移到 lambda 重載時(shí),自定義 "懸崖" 的存在,使得增量自定義 Button 變得具有挑戰(zhàn)性。我們常常聽到開發(fā)者要求在 String 重載中為 Button 增加 TextStyle 參數(shù)。
它允許自定義內(nèi)部的 TextStyle 而無需使用 lambda 重載的版本。
我們提供 String 的本意是希望能夠簡(jiǎn)化那些最簡(jiǎn)單用例的實(shí)現(xiàn),但是這樣卻阻礙了開發(fā)者使用帶有可組合的 lambda 的重載,轉(zhuǎn)而要求 String 重載增加額外功能。這兩個(gè)單獨(dú) API 的存在,不僅造成了開發(fā)者的困惑,也表明了帶有原始類型的重載的確存在一些根本的問題: 他們接受了原始類型,比如 String,而不是可組合的 lambda 類型。
單步代碼
原始類型的 Button 重載直接將文本作為參數(shù),減少了開發(fā)者在創(chuàng)建文本式 Button 時(shí)所需要寫的代碼。我們最初使用簡(jiǎn)單的 String 類型作為文本參數(shù),但是后來發(fā)現(xiàn) String 類型很難對(duì)其中的部分文本添加樣式。
對(duì)于這樣的需求,Compose 提供了 AnnotatedString API,來對(duì)文本的不同部分添加自定義樣式。然而,它對(duì)于簡(jiǎn)單的應(yīng)用場(chǎng)景增加了一定成本,因?yàn)殚_發(fā)者首先需要將 String 轉(zhuǎn)換為 AnnotatedString。這也使我們?cè)诳紤]是否應(yīng)該提供新的 Button 重載,既可以接受 String 作為參數(shù),也可以接受 AnnotatedString 作為參數(shù),來支持簡(jiǎn)單和更加進(jìn)階的需求。
我們的 API 設(shè)計(jì)討論在圖片和圖標(biāo)方面更加的復(fù)雜,比如當(dāng) FloatingActionButton 需要用到圖片或者圖標(biāo)的時(shí)候。icon 參數(shù)的類型應(yīng)該是 Vector 還是 Bitmap?如何支持帶有動(dòng)畫的圖標(biāo)?即使我們竭盡了全力,最終發(fā)現(xiàn)我們也只能支持 Compose 中可用的類型 —— 任何第三方圖片類型都需要開發(fā)者實(shí)現(xiàn)他們自己的重載以提供支持。
緊耦合的副作用
Compose 最大的優(yōu)勢(shì)之一是可組合性。創(chuàng)建可組合的函數(shù)以較小成本分離關(guān)注點(diǎn),構(gòu)建可復(fù)用的和相對(duì)獨(dú)立的組件。通過可組合的 lambda 重載,可以直觀地看到這樣的思路: Button 是可點(diǎn)擊內(nèi)容的容器,但是它無需關(guān)心其中的內(nèi)容是什么。
但是對(duì)于原始類型的重載,情況就變復(fù)雜了: 直接接受文本參數(shù)的 Button,現(xiàn)在既需要負(fù)責(zé)作為可點(diǎn)擊的容器,又需要將 Text 組件傳遞到內(nèi)部。這意味著它現(xiàn)在需要管理兩者的公共 API 接口,這也引發(fā)了另一個(gè)重要的問題: Button 該對(duì)外暴露什么樣的文本相關(guān)參數(shù)呢?這也將 Button 和 Text的公共 API 接口綁定到了一起: 如果未來 Text 增加了新的參數(shù)和功能,那是不是意味著 Button 也需要增加對(duì)這些新增內(nèi)容的支持?緊耦合是 Compose 試圖避免的問題之一,而且很難以統(tǒng)一的方式在所有組件上回答該問題,這也導(dǎo)致了公共 API 接口的不一致性。
支持工作框架
原始類型的重載使開發(fā)者可以避免使用可組合的 lambda 重載,而以較少的自定義空間作為代價(jià)。但是當(dāng)開發(fā)者需要在原始類型的重載上,實(shí)現(xiàn)原本無法實(shí)現(xiàn)的自定義呢?唯一的選擇,就是使用可組合的 lambda 重載,然后,將內(nèi)部的實(shí)現(xiàn)代碼從原始類型重載中復(fù)制過來,并做相應(yīng)的修改。我們?cè)谘芯恐邪l(fā)現(xiàn),自定義操作的 "懸崖" 阻礙了開發(fā)者使用更加靈活、可組合的 API,因?yàn)樵趯蛹?jí)之間的操作顯得比之前更具挑戰(zhàn)。
使用 "slot API" 解決問題
列舉上述問題后,我們決定去掉 Button 的原始類型重載,為每種 Button 僅留下包含針對(duì)內(nèi)容的可組合 lambda 參數(shù)的 API。我們開始將這個(gè)通用的 API 形式叫做 "slot API",現(xiàn)已經(jīng)廣泛應(yīng)用于各個(gè)組件。

Button(backgroundColor = Color.Purple) {
// 任何可組合內(nèi)容都可以寫在這里
}
△ 帶有空白 "slot" 的 Button

Button(backgroundColor = Color.Purple) {
Row {
MyImage()
Spacer(4.dp)
Text("Button")
}
}
△ 帶有橫向排列的圖片和文本的 Button
一個(gè) "slot" 代表一個(gè)可組合的 lambda 參數(shù),它代表組件中的任意內(nèi)容,比如 Text 或者 Icon。Slot API 增加了可組合性,使組件更加簡(jiǎn)單,減少了組件之間的獨(dú)立概念數(shù)量,使開發(fā)者可以快速上手創(chuàng)建一個(gè)新的組件,或者在不同的組件之間切換。

展望未來
我們對(duì) Button API 所做的修改數(shù)量之多,在討論 Button 的會(huì)議中所付出的時(shí)間之多,以及收集開發(fā)者的反饋所投入的精力之巨大,足以驚人。話雖如此,我們對(duì) API 整體的效果非常滿意。事后看來,我們看到在 Compose 中 Button 變得更具可發(fā)現(xiàn)性、可定制性,最重要的是它促進(jìn)了組合式思維。
@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
) {
// 實(shí)現(xiàn)體代碼
}
重要的是認(rèn)識(shí)到,我們的設(shè)計(jì)決策都基于下面這句口號(hào):
讓簡(jiǎn)單的開發(fā)變得簡(jiǎn)單,讓困難的開發(fā)變得可能。*
*這里出自著名的技術(shù)類書籍: 英文版:《Learning Perl: Making Easy Things Easy and Hard Things Possible》(Randal L. Schwartz、Brian D Foy 和 Tom Phoenix 著),中文版:《Perl 語言入門》(盛春譯)
我們嘗試通過減少重載,并將 "樣式" 扁平化處理,使開發(fā)變得更加簡(jiǎn)單。與此同時(shí),我們改進(jìn)了 Android Studio 的自動(dòng)補(bǔ)全功能,來幫助開發(fā)者提高效率。
這里我們希望特別提出在整個(gè) API 設(shè)計(jì)過程中的兩個(gè)要點(diǎn):
- API 的設(shè)計(jì)是一個(gè)迭代的過程。在 API 最初的迭代中就達(dá)到完美的狀態(tài)是幾乎不可能的。有一些需求容易被忽視。作為一個(gè) API 的作者,您需要做出一些假設(shè)。這其中包括開發(fā)者背景的不同,所帶來的不同思維方式1 ,最終影響了開發(fā)者探索和使用 API 的方式。適配調(diào)整是無法避免的,這是好事,不斷迭代可以得到可用性更高并且更加直觀的 API。
- 在迭代一個(gè) API 設(shè)計(jì)時(shí),您最有價(jià)值的工具之一是開發(fā)者使用 API 體驗(yàn)的反饋循環(huán)。對(duì)我們的團(tuán)隊(duì)來說,最關(guān)鍵的是去理解開發(fā)者所說的 "這個(gè) API 太復(fù)雜了" 意味著什么。當(dāng)錯(cuò)誤調(diào)用 API 時(shí),通常會(huì)降低開發(fā)者的成功率和效率,從中所獲得感悟,會(huì)幫助我們更深入理解 "復(fù)雜 API" 的意思。我們不斷迭代的關(guān)鍵驅(qū)動(dòng)力是我們要設(shè)計(jì)易用且出色的 API。為此,創(chuàng)建開發(fā)者反饋循環(huán),我們使用了多種研究路徑 —— 現(xiàn)場(chǎng)編程活動(dòng)2 ,和需要開發(fā)者提供體驗(yàn)日記3 的遠(yuǎn)程途徑。我們已經(jīng)可以理解開發(fā)者是如何處理 API,以及他們?yōu)榇蛩銓?shí)現(xiàn)的功能,找到正確方法所采取的路徑。諸如工程師思維方式 (Programmer Thinking Styles) 和認(rèn)知緯度 (Cognitive Dimensions) 這類框架中的支柱,有助于我們跨職能團(tuán)隊(duì)保持語言思維上的一致,不僅表現(xiàn)在審核、溝通開發(fā)者反饋中,也涉及到 API 設(shè)計(jì)討論。尤其是,當(dāng)評(píng)估用戶體驗(yàn)和功能性之間的關(guān)系時(shí),這個(gè)框架幫助我們塑造了為選擇和權(quán)衡所做的討論。
來自 Android Developer UX 團(tuán)隊(duì)的 Meital Tagor Sbero 受到 角色模型和思維方式 (personas & Thinking Styles) 的設(shè)計(jì)和 認(rèn)知維度框架 (Cognitive Dimensions Framework) 的啟發(fā),開發(fā)了工程師思維方式框架 (Programmer Thinking Styles Framework)。該框架使用開發(fā)者在限定時(shí)間內(nèi)所需 "解決方案的類型"的動(dòng)機(jī)和態(tài)度,幫助開發(fā)者確定 API 可用性的設(shè)計(jì)思路。它兼顧了普通工程師的工作方式,并且針對(duì)高強(qiáng)度開發(fā)任務(wù)優(yōu)化了可用性。
我們通常使用這種方式評(píng)估 API 特定方面的可用性。比如,每個(gè)活動(dòng)會(huì)邀請(qǐng)一組開發(fā)者使用 Button API 來完成一系列開發(fā)任務(wù),這些任務(wù)會(huì)特意暴露一些 API 的特征,而這些特征是我們希望收集反饋的目標(biāo)。我們通過放聲思考法,來獲得更多關(guān)于開發(fā)者所追求的和開發(fā)者所設(shè)想的信息。這些活動(dòng)中還包含研究者通過一些隨訪的問題,來進(jìn)一步了解開發(fā)者的需求。我們會(huì)回顧這些活動(dòng),從而確定開發(fā)者在編程任務(wù)中促成成功或者導(dǎo)致失敗的行為模式。
我們通常使用這種方式來評(píng)估 API 在一段時(shí)間內(nèi)的可用性和易學(xué)習(xí)性。這種方式可以通過傾聽開發(fā)者在常規(guī)工作中的反饋,來捕捉遇到困難的瞬間和受到啟發(fā)的瞬間。在這個(gè)過程中,我們會(huì)有一組開發(fā)者開發(fā)由他們自選的特定項(xiàng)目,同時(shí)也確保他們會(huì)使用我們希望評(píng)估的 API。我們會(huì)結(jié)合開發(fā)者通過自行提交的日記,和由研究人員基于認(rèn)知維度框架 (Cognitive Dimensions Framework) (示例) 所組織的深度調(diào)查,以及專訪活動(dòng)來幫助我們確定 API 的可用性。
我們承認(rèn)雖然我們對(duì)現(xiàn)有版本的 Button API 很滿意,但是我們也知道它并不是完美的。開發(fā)者的思維方式有很多,加上不同的應(yīng)用場(chǎng)景,以及層出不窮的需求,要求我們要不斷迎接新的挑戰(zhàn)。這都不是問題!Button 的整個(gè)進(jìn)化過程,對(duì)于我們和開發(fā)者社區(qū)的意義都很大。所有這些都是為 Compose 設(shè)計(jì)和塑造了一個(gè)可用的 Button API —— 一個(gè)可以在屏幕上點(diǎn)擊的簡(jiǎn)單矩形。
希望這篇文章能夠幫助大家清楚了解到您的反饋如何幫助我們改進(jìn) Compose 中 Button API。如果您在使用 Compose 時(shí)遇到任何問題,或者對(duì)新 API 的體驗(yàn)提升有任何 建議和想法,請(qǐng)告訴我們。歡迎廣大開發(fā)者參與到我們接下來的 用戶調(diào)研活動(dòng) 中,期待您的注冊(cè)報(bào)名。
歡迎您 點(diǎn)擊這里 向我們提交反饋,或分享您喜歡的內(nèi)容、發(fā)現(xiàn)的問題。您的反饋對(duì)我們非常重要,感謝您的支持!