iT邦幫忙

2022 iThome 鐵人賽

DAY 9
0

此文件會討論幾個 compose 程式的注意事項:

  • Composable 函式可以按任意順序執行
  • Composable 函式可以同時/平行地被執行
  • Recomposition 會盡可能地忽略沒有相關性的 composable 函式和 lambdas
  • Recomposition是樂觀的,也可以被取消動作
  • 一個 Composable 函式時可以像播放一個動畫那樣頻繁地被更新

接下來會一一地討論這些注意事項。

Composable 函式可以按任意順序執行

假設有一個 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 函式的獨立性。

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。

Recomposition 會盡可能地忽略沒有相關性的內容(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 是樂觀的意思是,recomposition 當參數又改變的時候,是有可能放棄某些 recompose 動作的執行,也就是說如果參數改變的時候,但有些recomposition還沒有完成,Compose 可能會取消 recomposition 直到下次新的參數到來。

一個 Composable 函式時可以像播放一個動畫那樣頻繁地被更新

在某些情況下,可能會針對界面動畫的每一幀運行一個 composable 函式。如果這函式執行cost高的操作(從設備存儲空間讀取數據),可能會影響界面顯示,如卡頓的現象。

Composable 函式的資料數據應該相對應的參數。要養成把 cost 高的 task 搬到其他的 thread 上執行,多多利用 mutableStateOf 或 LiveData 將相應的數據傳遞給 Compose 。

竟然花了3天才把 Thinking in Compose 學習完,對 compose 也有較深入的瞭解了,期待之後的學習。


上一篇
[Day8] Thinking in Compose (二)
下一篇
[Day10] Compose 的狀態管理 (一)
系列文
30天 Android Jetpack Compose 奇幻旅程13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言