此文件會討論幾個 compose 程式的注意事項:
接下來會一一地討論這些注意事項。
假設有一個 composable 函式裡頭呼叫了其他多個 composable 函式,大家一定認為是按照程式碼的順序執行,實際上是按任何順序執行的。 Compose 可以選擇優先度較高的 UI 元件先繪製。下面程式碼有一個 composable 函式裡面有另外三個 composable 函式:StartScreen、MiddleScreen 和 EndScreen 這三個執行順序並不是一定依照 StartScreen -> MiddleScreen -> EndScreen 而是任何順序都是有可能的。
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
所以不能認為在 StartScreen 函式裡面改變 global 變數(是一個 Side-effect),然後期待 在 MiddleScreen 或 EndScreen 裏可以得到被改變後的 global 變數,可能會得到不是預期的數值,所以要盡可能保持每一個 composable 函式的獨立性。
Compose 在跑 composable 函式的時候可以平行/同時地最佳化 recomposition。這樣 Compose 可以利用多核處理器發揮最高效能,也可以較低優先度的 composable 函式。 composable 函式 可能在 background threads 裡執行,如果有一個 composable 函式呼叫了一個 ViewModel 的函式,Compose 可能同時從好幾個 thread 完成函式裏的工作。為了保證 application 可正常地運作,所有的 composable 函式應該盡量不要有 side-effect,盡可能在 UI Thread 執行。
我們來看這兩段程式碼:可以顯示一個清單以及清單裡面項目的數量
程式碼一:
這段程式案是安全,無 side-effect 的,並且可以把輸入的清單轉換為 UI,這段用來顯示不大的清單是非常好的 code。
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
但如果這個 composable 函式,加一個 local 變數 items,下面這段程式碼就會變得不 thread-safe 了。因為items 在每次的 recomposition (當動畫在播放或是list有異動的時候)都會被修改。所以 UI 顯示的 Count 有可能就不正確了。
@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")
}
}
Compose 不支持這樣的寫法,為了防止這樣 code 的寫法,framework 被允許改變 thread 來執行 composable lambdas。
Compose 會盡力只重組需要更新的部分。換句話說,它可以跳過某些內容以重新運行單個按鈕的 composable,而不執行 UI Tree 上面或下面的 composables。
下面這段程式碼展示了 recomposition 重繪 list 的時候跳過了一些元件的執行:
/**
* 有一個 Header 的可點擊的姓名清單 compose UI
*/
@Composable
fun NamePicker(
header: String,
names: List<String>,
onNameClicked: (String) -> Unit
) {
Column {
// 這個 Header Text 只有當字串 header 改變是會被 recompose,但當 names 清單改變時,不會
//被 recompose
Text(header, style = MaterialTheme.typography.h5)
Divider()
// LazyColumn 就是 compose 版的 RecycleView
// 它的 lambda 裡頭 items() 是類似 RecyclerView.ViewHolder 的作用
LazyColumn {
items(names) { name ->
// 當 item 的 name 異動時,NamePickerItem 就會被 recompose
// 但當 header 變化時,卻不會被 recompose
NamePickerItem(name, onNameClicked)
}
}
}
}
/**
* 使用者可以點擊的顯示姓名的項目
*/
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}
Recomposition 是樂觀的意思是,recomposition 當參數又改變的時候,是有可能放棄某些 recompose 動作的執行,也就是說如果參數改變的時候,但有些recomposition還沒有完成,Compose 可能會取消 recomposition 直到下次新的參數到來。
在某些情況下,可能會針對界面動畫的每一幀運行一個 composable 函式。如果這函式執行cost高的操作(從設備存儲空間讀取數據),可能會影響界面顯示,如卡頓的現象。
Composable 函式的資料數據應該相對應的參數。要養成把 cost 高的 task 搬到其他的 thread 上執行,多多利用 mutableStateOf 或 LiveData 將相應的數據傳遞給 Compose 。
竟然花了3天才把 Thinking in Compose 學習完,對 compose 也有較深入的瞭解了,期待之後的學習。