Android Jetpack Compose是谷歌推出的一種新的搭建UI的方式逞泄,使用Kotlin DSL的形式來組合UI組件焚挠。目前還在alpha階段。蘋果早也推出了類似的Swift UI,都是模仿前端的實現(xiàn)方式篮愉,目的都是UI更加輕量化和方便數(shù)據(jù)驅(qū)動拉鹃。
Compose使用感受
Compose的使用比較簡單考杉,官方有連續(xù)的課程矫付,還有比較完善的samples可以參考,網(wǎng)絡上也能找到好多相關(guān)的教程俯萌,就不再重復寫了果录。需要注意的是,Compose還在alpha階段咐熙,API還未穩(wěn)定弱恒,網(wǎng)上的教程好多是針對舊的API的,遇到問題還是盡量查看官方文檔棋恼。下面就簡單說說在使用Compose時的一些感受返弹。
好的地方:
- 相對于之前的xml方式,代碼編寫起來可能更加熟練爪飘,對開發(fā)可能更親切(個人感受)
- 一般的UI組件使用起來更加簡單义起,如Text:
Text(
"text",
style = MaterialTheme.typography.body,
modifier = Modifier.padding(start = 4.dp)
)
相對于xml中聲明TextView,代碼量會小很多师崎。
- 可能是由于目前Compose提供的組件比較少默终,布局實現(xiàn)比較單一,感覺實現(xiàn)比較復雜的布局比XML高效,簡潔很多齐蔽。而且性能也比XML高两疚,畢竟少了xml解析的過程。
- 可以多個UI組件一起預覽肴熏。傳統(tǒng)方式一次只能預覽一個xml布局鬼雀,使用Compose之后顷窒,只要在Compose上添加@preview注解蛙吏,在一個文件內(nèi)都可以預覽。在搭建UI時幾乎可以不再需要安裝到真機和模擬器上進行檢查鞋吉。
- 數(shù)據(jù)驅(qū)動UI更新更加直觀和高效鸦做,只需要聲明state即可,數(shù)據(jù)改變時會自動更新UI谓着,不再需要之前的Livedata等比較復雜的機制泼诱。
- Theme更加強大和自由,以前在XML中聲明Theme和自定義主題中某個組件的樣式赊锚,是比較麻煩的治筒,因為屬性太多了。現(xiàn)在就比較方便了可以直接使用Material Design主題中定義好的樣式舷蒲,更加規(guī)范高效耸袜,而且深色主題適配更加簡單。
- 動畫API更加簡單了牲平,不再需要復雜的寫法堤框,會自動根據(jù)之前的屬性生成對應的動畫,如更改組件的大小纵柿,只需要在modifier中添加:
modifier.animateContentSize(animSpec = TweenSpec(300)),
就可以了蜈抓,系統(tǒng)會自動監(jiān)控這個組件的大小的變化,生成動畫昂儒。
不好的地方
- 由于使用的是Kotlin DSL沟使,所以代碼排版縮進會比較多,相對于一般代碼結(jié)構(gòu)渊跋,直觀性比較差腊嗡。但是還是比Flutter的好一些。
- 由于要實現(xiàn)實時預覽刹枉,每次修改Compose都需要編譯叽唱,如果項目比較大,編譯時間很長微宝,那體驗就會很差了
- 組件的豐富度還比較欠缺棺亭,需要進一步完善
- 官方的教程,文檔包含的內(nèi)容有限蟋软,有很多之前的組件找不到在Compose中對應的組件镶摘,命名也發(fā)生了變化嗽桩。尋找起來比較麻煩。大部門只能看官方的文檔凄敢。而且對復雜的界面布局的實現(xiàn)沒有比較完善的指導文檔碌冶。
- jetpack組件還在推廣中,Compose已經(jīng)和好多AndroidX的組件沖突了涝缝,如果Compose推進的比較快扑庞,那還有必要學習使用AndroidX中的UI和管理UI組件嗎? 一般的項目也不會想兩套共存吧? 所以如何推廣Compose還是個問題, 好處是不像iOS的Swift UI那樣,和系統(tǒng)版本綁定拒逮。
Compose的實現(xiàn)
最初看到Compose的時候罐氨,以為就是對之前的View組件做了一次封裝,然后底層再做組裝滩援,渲染處理栅隐。后來看了下源碼,發(fā)現(xiàn)不是這樣的玩徊,是重新實現(xiàn)了一套租悄。如Text的具體實現(xiàn)是CoreText
,CoreText
的layout實現(xiàn):
Layout(
children = if (inlineComposables.isEmpty()) {
emptyContent()
} else {
{ InlineChildren(text, inlineComposables) }
},
modifier = modifier
.then(controller.modifiers)
.then(
if (selectionRegistrar != null) {
Modifier.longPressDragGestureFilter(
longPressDragObserver(
state = state,
selectionRegistrar = selectionRegistrar
)
)
} else {
Modifier
}
),
minIntrinsicWidthMeasureBlock = controller.minIntrinsicWidth,
minIntrinsicHeightMeasureBlock = controller.minIntrinsicHeight,
maxIntrinsicWidthMeasureBlock = controller.maxIntrinsicWidth,
maxIntrinsicHeightMeasureBlock = controller.maxIntrinsicHeight,
measureBlock = controller.measure
)
TextController
控制Text的layout恩袱、state泣棋、selection、measure和draw憎蛤。底層其實都是通過TextDelegate實現(xiàn)的
fun layout(
constraints: Constraints,
layoutDirection: LayoutDirection,
prevResult: TextLayoutResult? = null,
respectMinConstraints: Boolean = false
): TextLayoutResult {
val minWidth = if (respectMinConstraints || style.textAlign == TextAlign.Justify) {
constraints.minWidth.toFloat()
} else {
0f
}
val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
constraints.maxWidth.toFloat()
} else {
Float.POSITIVE_INFINITY
}
if (prevResult != null && prevResult.canReuse(
text, style, maxLines, softWrap, overflow, density, layoutDirection,
resourceLoader, constraints
)
) {
return with(prevResult) {
copy(
layoutInput = layoutInput.copy(
style = style,
constraints = constraints
),
size = computeLayoutSize(constraints, multiParagraph, respectMinConstraints)
)
}
}
val multiParagraph = layoutText(
minWidth,
maxWidth,
layoutDirection
)
val size = computeLayoutSize(constraints, multiParagraph, respectMinConstraints)
return TextLayoutResult(
TextLayoutInput(
text,
style,
placeholders,
maxLines,
softWrap,
overflow,
density,
layoutDirection,
resourceLoader,
constraints
),
multiParagraph,
size
)
}
layout最終返回一個TextLayoutResult
外傅,TextLayoutResult
在draw的時候使用:
fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
TextPainter.paint(canvas, textLayoutResult)
}
最終調(diào)用了TextPainter
,TextPainter
的paint方法:
fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
val needClipping = textLayoutResult.hasVisualOverflow &&
textLayoutResult.layoutInput.overflow == TextOverflow.Clip
if (needClipping) {
val width = textLayoutResult.size.width.toFloat()
val height = textLayoutResult.size.height.toFloat()
val bounds = Rect(Offset.Zero, Size(width, height))
canvas.save()
canvas.clipRect(bounds)
}
try {
textLayoutResult.multiParagraph.paint(
canvas,
textLayoutResult.layoutInput.style.color,
textLayoutResult.layoutInput.style.shadow,
textLayoutResult.layoutInput.style.textDecoration
)
} finally {
if (needClipping) {
canvas.restore()
}
}
}
設計思想還是View的那套俩檬,但是所有組件的實現(xiàn)更加扁平了萎胰。不再有View或Viewgroup的繼承關(guān)系,大部分組件都是直接自己直接實現(xiàn)layout棚辽,measure和draw技竟。所以看起來更加簡潔。
State
Compose的更新數(shù)據(jù)顯示是通過State來實現(xiàn)的屈藐,而且比之前的LiveData更加簡單榔组,如:
數(shù)據(jù):
val list = listOf(
"ListItem1“,
"ListItem2“,
"ListItem3“,
"ListItem4“,
"ListItem5“
)
val data by remember{ mutableStateOf(list) }
UI:
LazyColumnFor(
items = data,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(top = 8.dp))
{ text ->
Text(text = text)
}
添加數(shù)據(jù):
data.value = list += listOf("ListItem6")
添加之后列表會自動刷新數(shù)據(jù)和UI。不需要跟之前一樣去主動notify UI更新联逻。
其中remember{}
的表達的意思是跟字面意思一致搓扯,就是記住后面block中產(chǎn)生的value, 只有在UI組合時才會產(chǎn)生值。
mutableStateOf
產(chǎn)生一個SnapshotMutableState
包归,里面的value的讀和寫都是被監(jiān)控的锨推。在UI組合完成之后Composer會一直監(jiān)控state, 如果有值發(fā)生變化則會觸發(fā)Recomposition, 進行UI更新。
Recomposition
官方翻譯為重組换可,就是重新調(diào)用compose組合UI的過程椎椰,系統(tǒng)會根據(jù)需要使用新數(shù)據(jù)重新繪制函數(shù)發(fā)出的組件。因為UI是一直顯示的沾鳄,重組可能會很頻繁慨飘,為了保證流暢性,官方做了很多優(yōu)化译荞,如并行處理瓤的,自動跳過不需要重組的組件和使用樂觀算法優(yōu)化重組〈沤罚總之和之前的UI實現(xiàn)一樣堤瘤,不要在compose組合期間執(zhí)行比較耗時的邏輯玫芦。