Jetpack Compose 是一個適用于 Android 的新式聲明性界面工具包。Compose 提供聲明性 API,可在不以命令方式改變前端視圖的情況下呈現(xiàn)應(yīng)用界面,從而使編寫和維護(hù)應(yīng)用界面變得更加容易。它的含義對應(yīng)用設(shè)計非常重要。
1. 聲明性編程范式
長期以來,Android 視圖層次結(jié)構(gòu)一直可以表示為界面View樹。由于應(yīng)用的狀態(tài)會因用戶交互等因素而發(fā)生變化,因此界面層次結(jié)構(gòu)需要進(jìn)行更新以顯示當(dāng)前數(shù)據(jù)。最常見的界面更新方式是使用 findViewById()
等函數(shù)遍歷樹,并通過調(diào)用 button.setText(String)
、container.addChild(View)
或 img.setImageBitmap(Bitmap)
等方法更改節(jié)點。這些方法會改變View的內(nèi)部狀態(tài)。
手動操縱視圖會提高出錯的可能性。如果一條數(shù)據(jù)在多個位置呈現(xiàn),很容易忘記更新顯示它的某個視圖。此外,當(dāng)兩項更新以意外的方式發(fā)生沖突時,也很容易造成異常狀態(tài)。例如,某項更新可能會嘗試設(shè)置剛剛從界面中移除的節(jié)點的值。
一般來說,軟件維護(hù)復(fù)雜性會隨著需要更新的視圖數(shù)量而增長。
在過去的幾年中,整個行業(yè)已開始轉(zhuǎn)向聲明性界面模型,該模型大大簡化了與構(gòu)建和更新界面關(guān)聯(lián)的工程設(shè)計。該技術(shù)的工作原理是在概念上從頭開始重新生成整個屏幕,然后僅執(zhí)行必要的更改。此方法可避免手動更新有狀態(tài)視圖層次結(jié)構(gòu)的復(fù)雜性。Compose 是一個聲明性界面框架
。
重新生成整個屏幕所面臨的一個難題是,在時間、計算能力和電池用量方面可能成本高昂。為了減輕這一成本,Compose 會智能地選擇在任何給定時間需要重新繪制界面的哪些部分。這會對您設(shè)計界面組件的方式有一定影響,如下面重組
中所述。
2. 簡單的可組合函數(shù)
在Compose中我們稱View 為 微件,但是和View 不完全一致。
一個簡單的可組合函數(shù),系統(tǒng)向它傳遞了數(shù)據(jù),它使用該數(shù)據(jù)在屏幕上呈現(xiàn)文本微件。
此函數(shù)帶有
@Composable
注釋。所有可組合函數(shù)都必須帶有此注釋;此注釋可告知 Compose 編譯器:此函數(shù)旨在將數(shù)據(jù)轉(zhuǎn)換為界面。此函數(shù)接受數(shù)據(jù)。可組合函數(shù)可以接受一些參數(shù),這些參數(shù)可讓應(yīng)用邏輯描述界面。在本例中,我們的微件接受一個
String
,因此它可以按名稱問候用戶。此函數(shù)可以在界面中顯示文本。為此,它會調(diào)用
Text()
可組合函數(shù),該函數(shù)實際上會創(chuàng)建文本界面元素。可組合函數(shù)通過調(diào)用其他可組合函數(shù)來發(fā)出界面層次結(jié)構(gòu)。此函數(shù)不會返回任何內(nèi)容。發(fā)出界面的 Compose 函數(shù)不需要返回任何內(nèi)容,因為它們描述所需的屏幕狀態(tài),而不是構(gòu)造界面微件。
此函數(shù)快速、
冪等
且沒有副作用。
冪等,在我的kotlin文章也有介紹,關(guān)于副作用等
在某二元運算下,冪等元素是指被自己重復(fù)運算(或?qū)τ诤瘮?shù)是為復(fù)合)的結(jié)果等于它自己的元素。例如,乘法下僅有兩個冪等實數(shù),為0和1。
某一元運算為冪等的時,其作用在任一元素兩次后會和其作用一次的結(jié)果相同。例如,高斯符號便是冪等的。
一元運算的定義是二元運算定義的特例
簡單理解f(f(x)) = f(x)
可以自行查詢 維基百科 或者 百度百科
- 使用同一參數(shù)多次調(diào)用此函數(shù)時,它的行為方式相同,并且它不使用其他值,如全局變量或?qū)?
random()
的調(diào)用。 - 此函數(shù)描述界面而沒有任何副作用,如修改屬性或全局變量。
一般來說,出于重組
部分所述的原因,所有可組合函數(shù)都應(yīng)使用這些屬性來編寫。
3. 聲明性范式轉(zhuǎn)變
在許多面向?qū)ο蟮拿钍浇缑婀ぞ甙?,您可以通過實例化View樹來初始化界面。您通常通過膨脹 XML 布局文件來實現(xiàn)此目的。每個View都維護(hù)自己的內(nèi)部狀態(tài),并且提供 getter 和 setter 方法,允許應(yīng)用邏輯與View進(jìn)行交互。
在 Compose 的聲明性方法中,微件相對無狀態(tài),并且不提供 setter 或 getter 函數(shù)。實際上,微件不會以對象形式提供。您可以通過調(diào)用帶有不同參數(shù)的同一可組合函數(shù)來更新界面。這使得向架構(gòu)模式(如 ViewModel
)提供狀態(tài)變得很容易,如應(yīng)用架構(gòu)指南
中所述。然后,可組合項負(fù)責(zé)在每次可觀察數(shù)據(jù)更新時將當(dāng)前應(yīng)用狀態(tài)轉(zhuǎn)換為界面。
PS: 這里我們想到,F(xiàn)lutter中的有狀態(tài)組件和無狀態(tài)組件。還有Kotlin 底層默認(rèn)實現(xiàn)setter和getter。
上圖說明:應(yīng)用邏輯為頂級可組合函數(shù)提供數(shù)據(jù)。該函數(shù)通過調(diào)用其他可組合函數(shù)來使用這些數(shù)據(jù)描述界面,將適當(dāng)?shù)臄?shù)據(jù)傳遞給這些可組合函數(shù),并沿層次結(jié)構(gòu)向下傳遞數(shù)據(jù)。
當(dāng)用戶與界面交互時,界面會發(fā)起 onClick 等事件。這些事件應(yīng)通知應(yīng)用邏輯,應(yīng)用邏輯隨后可以改變應(yīng)用的狀態(tài)。當(dāng)狀態(tài)發(fā)生變化時,系統(tǒng)會使用新數(shù)據(jù)再次調(diào)用可組合函數(shù)。這會導(dǎo)致重新繪制界面元素,此過程稱為“重組”。
PS: 這里我猜測,Viewmodel,LifeCycle 等狀態(tài)機(jī)模式類似,根據(jù)狀態(tài)動態(tài)更新,也是MVVM,F(xiàn)Lux等類似的機(jī)制,沒有看底層源碼,先這樣理解,以后深入研究在驗證猜想。
上圖說明:用戶與界面元素進(jìn)行了交互,導(dǎo)致觸發(fā)一個事件。應(yīng)用邏輯響應(yīng)該事件,然后系統(tǒng)根據(jù)需要使用新參數(shù)自動再次調(diào)用可組合函數(shù)。
動態(tài)內(nèi)容
由于可組合函數(shù)是用 Kotlin 而不是 XML 編寫的,因此它們可以像其他任何 Kotlin 代碼一樣動態(tài)。例如,假設(shè)您想要構(gòu)建一個界面,用來問候一些用戶:
@Composable
fun Greeting(names: List<String>) {
for (name in names) {
Text("Hello $name")
}
}
此函數(shù)接受名稱的列表,并為每個用戶生成一句問候語??山M合函數(shù)可能非常復(fù)雜。可以使用 if 語句來確定是否要顯示特定的界面元素。可以使用循環(huán)??梢哉{(diào)用輔助函數(shù)。使用底層語言的全部靈活性。這種功能和靈活性是 Jetpack Compose 的主要優(yōu)勢之一。
PS:其實就是用代碼控制,去除模板xml的做法,以前需要自己寫邏輯實現(xiàn),現(xiàn)在已經(jīng)實現(xiàn)好了,我們只關(guān)心自己的邏輯就好,所以才會強(qiáng)大好用。最主要是靈活了。
4. 重組
終于學(xué)習(xí)到這個概念。
在命令式界面模型中,如需更改某個View,您可以在該View上調(diào)用 setter 以更改其內(nèi)部狀態(tài)。在 Compose 中,您可以使用新數(shù)據(jù)再次調(diào)用可組合函數(shù)。這樣做會導(dǎo)致函數(shù)進(jìn)行重組 -- 系統(tǒng)會根據(jù)需要使用新數(shù)據(jù)重新繪制函數(shù)發(fā)出的微件。Compose 框架可以智能地僅重組已更改的組件。
例如,假設(shè)有以下可組合函數(shù),它用于顯示一個按鈕:
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
每次點擊該按鈕時,調(diào)用方都會更新 clicks 的值。Compose 會再次調(diào)用 lambda 與 Text 函數(shù)以顯示新值;此過程稱為“重組”。不依賴于該值的其他函數(shù)不會進(jìn)行重組。
如前文所述,重組整個界面樹在計算上成本高昂,因為會消耗計算能力并縮短電池續(xù)航時間。Compose 使用智能重組來解決此問題。
重組是指在輸入更改時再次調(diào)用可組合函數(shù)的過程。當(dāng)函數(shù)的輸入更改時,會發(fā)生這種情況。當(dāng) Compose 根據(jù)新輸入重組時,它僅調(diào)用可能已更改的函數(shù)或 lambda,而跳過其余函數(shù)或 lambda。通過跳過所有未更改參數(shù)的函數(shù)或 lambda,Compose 可以高效地重組。
切勿依賴于執(zhí)行可組合函數(shù)所產(chǎn)生的附帶效應(yīng),因為可能會跳過函數(shù)的重組。如果您這樣做,用戶可能會在您的應(yīng)用中遇到奇怪且不可預(yù)測的行為。附帶效應(yīng)是指對應(yīng)用的其余部分可見的任何更改。例如,以下操作全部都是危險的附帶效應(yīng):
- 寫入共享對象的屬性
- 更新 ViewModel 中的可觀察項
- 更新共享偏好設(shè)置
PS: 這里更像U3D或者其他腳本周期的update周期函數(shù)刷新機(jī)制,不能隨便亂用。
可組合函數(shù)可能會像每一幀一樣頻繁地重新執(zhí)行,例如在呈現(xiàn)動畫時。可組合函數(shù)應(yīng)快速執(zhí)行,以避免在播放動畫期間出現(xiàn)卡頓。如果您需要執(zhí)行成本高昂的操作(例如從共享偏好設(shè)置讀取數(shù)據(jù)),請在后臺協(xié)程中執(zhí)行,并將值結(jié)果作為參數(shù)傳遞給可組合函數(shù)。
PS: 也就是說,把耗時的操作交給協(xié)程處理(理解成子線程)把最終結(jié)果交給作為參數(shù)給函數(shù),那么函數(shù)就會監(jiān)聽結(jié)果的變化動態(tài)更新。
例如,以下代碼會創(chuàng)建一個可組合項以更新 SharedPreferences 中的值。該可組合項不應(yīng)從共享偏好設(shè)置本身讀取或?qū)懭耄谑谴舜a將讀取和寫入操作移至后臺協(xié)程中的 ViewModel。應(yīng)用邏輯會使用回調(diào)傳遞當(dāng)前值以觸發(fā)更新。
@Composable
fun SharedPrefsToggle(
text: String,
value: Boolean,
onValueChanged: (Boolean) -> Unit
) {
Row {
Text(text)
Checkbox(checked = value, onCheckedChange = onValueChanged)
}
}
在 Compose 中編程時,有許多事項需要注意:
- 可組合函數(shù)可以按任何順序執(zhí)行。
- 可組合函數(shù)可以并行執(zhí)行。
- 重組會跳過盡可能多的可組合函數(shù)和 lambda。
- 重組是樂觀的操作,可能會被取消。
- 可組合函數(shù)可能會像動畫的每一幀一樣非常頻繁地運行。
如何構(gòu)建可組合函數(shù)以支持重組。在每種情況下,最佳做法都是使可組合函數(shù)保持快速、冪等且沒有附帶效應(yīng)。
可組合函數(shù)可以按任何順序執(zhí)行
Compose 可以通過并行運行可組合函數(shù)來優(yōu)化重組。這樣一來,Compose 就可以利用多個核心,并以較低的優(yōu)先級運行可組合函數(shù)(不在屏幕上)。
這種優(yōu)化意味著,可組合函數(shù)可能會在后臺線程池中執(zhí)行。如果某個可組合函數(shù)對 ViewModel 調(diào)用一個函數(shù),則 Compose 可能會同時從多個線程調(diào)用該函數(shù)。
為了確保應(yīng)用正常運行,所有可組合函數(shù)都不應(yīng)有附帶效應(yīng),而應(yīng)通過始終在界面線程上執(zhí)行的 onClick 等回調(diào)觸發(fā)附帶效應(yīng)。
調(diào)用某個可組合函數(shù)時,調(diào)用可能發(fā)生在與調(diào)用方不同的線程上。這意味著,應(yīng)避免使用修改可組合 lambda 中的變量的代碼,既因為此類代碼并非線程安全代碼,又因為它是可組合 lambda 不允許的附帶效應(yīng)。
以下示例展示了一個可組合項,它顯示一個列表及其項數(shù):
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
此代碼沒有附帶效應(yīng),它會將輸入列表轉(zhuǎn)換為界面。此代碼非常適合顯示小列表。不過,如果函數(shù)寫入局部變量,則這并非線程安全或正確的代碼:
@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
var items = 0
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
items++ // Avoid! Side-effect of the column recomposing.
}
}
Text("Count: $items")
}
}
PS: 這個錯誤不安全代碼,在重組中賦值操作局部變量,那么導(dǎo)致計數(shù)錯誤出現(xiàn)。
重組會跳過盡可能多的內(nèi)容
如果界面的某些部分無效,Compose 會盡力只重組需要更新的部分。這意味著,它可以跳過某些內(nèi)容以重新運行單個按鈕的可組合項,而不執(zhí)行界面樹中在其上面或下面的任何可組合項。
每個可組合函數(shù)和 lambda 都可以自行重組。以下示例演示了在呈現(xiàn)列表時重組如何跳過某些元素:
/**
* Display a list of names the user can click with a header
*/
@Composable
fun NamePicker(
header: String,
names: List<String>,
onNameClicked: (String) -> Unit
) {
Column {
// this will recompose when [header] changes, but not when [names] changes
Text(header, style = MaterialTheme.typography.h5)
Divider()
// LazyColumn is the Compose version of a RecyclerView.
// The lambda passed to items() is similar to a RecyclerView.ViewHolder.
LazyColumn {
items(names) { name ->
// When an item's [name] updates, the adapter for that item
// will recompose. This will not recompose when [header] changes
NamePickerItem(name, onNameClicked)
}
}
}
}
/**
* Display a single name the user can click.
*/
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}
PS: 我的理解就是局部刷新,[header]更改時,它將重新組合,但[names]更改時,則不會重新組合
LazyColumn 是Compose的一個RecyclerView版本。
傳遞給items() 的lambda類似傳統(tǒng)RecyclerView的ViewHolder
當(dāng)items的name發(fā)生改變,只會重組item,不影響header,header也不會重組
這些作用域中的每一個都可能是在重組期間執(zhí)行的唯一一個作用域。當(dāng) header 發(fā)生更改時,Compose 可能會跳至 Column lambda,而不執(zhí)行它的任何父項。此外,執(zhí)行 Column 時,如果 names 未更改,Compose 可能會選擇跳過 LazyColumnItems。
同樣,執(zhí)行所有可組合函數(shù)或 lambda 都應(yīng)該沒有附帶效應(yīng)。當(dāng)您需要執(zhí)行附帶效應(yīng)時,應(yīng)通過回調(diào)觸發(fā)。
PS: 可以理解為原子操作,一個lambda只做一件事,沒有關(guān)聯(lián)。
重組是樂觀的操作
只要 Compose 認(rèn)為某個可組合項的參數(shù)可能已更改,就會開始重組。(可能就是發(fā)生改變)Compose 預(yù)計會在參數(shù)再次更改之前完成重組。如果某個參數(shù)在重組完成之前發(fā)生更改,Compose 可能會取消重組,并使用新參數(shù)重新開始。
PS:就是當(dāng)參數(shù)第一次更改,Compose監(jiān)聽到 第二次更改到來,那么會停止第一次刷新,使用第二次的新值進(jìn)行刷新,保持最新。
取消重組后,Compose 會從重組中舍棄界面樹。如有任何附帶效應(yīng)依賴于顯示的界面,則即使取消了組成操作,也會應(yīng)用該附帶效應(yīng)。這可能會導(dǎo)致應(yīng)用狀態(tài)不一致。
確保所有可組合函數(shù)和 lambda 都冪等且沒有附帶效應(yīng),以處理樂觀的重組。
可組合函數(shù)可能會非常頻繁地運行
在某些情況下,可能會針對界面動畫的每一幀運行一個可組合函數(shù)。如果該函數(shù)執(zhí)行成本高昂的操作(例如從設(shè)備存儲空間讀取數(shù)據(jù)),可能會導(dǎo)致界面卡頓。
例如,如果您的微件嘗試讀取設(shè)備設(shè)置,它可能會在一秒內(nèi)讀取這些設(shè)置數(shù)百次,這會對應(yīng)用的性能造成災(zāi)難性的影響。
如果您的可組合函數(shù)需要數(shù)據(jù),它應(yīng)為相應(yīng)的數(shù)據(jù)定義參數(shù)。然后,您可以將成本高昂的工作移至組成操作線程之外的其他線程,并使用 mutableStateOf 或 LiveData 將相應(yīng)的數(shù)據(jù)傳遞給 Compose。