Skip to main content

Jetpack Compose: 何時該用 derivedStateOf?

官方導讀


核心摘要 (一句話總結)

derivedStateOf 是一個效能優化工具,它像一個智慧守門員,專門用來防止因「來源狀態」變化過於頻繁而導致的**「不必要的 UI 重組 (Recomposition)」。只有當計算出的最終結果**確實改變時,它才會通知 UI 更新。


1. 狀態衍生的效能陷阱

一個常見的範例開場:一個可滾動的列表,以及一個「回到頂部」的按鈕。

  • 需求: 這個按鈕只有在使用者向下滾動後才需要顯示。
  • 狀態關係: 按鈕的顯示狀態 (showButton: Boolean),是衍生自列表的滾動狀態 (listState.firstVisibleItemIndex > 0)。

一個最直覺、但錯誤的寫法是:

Kotlin

// 錯誤的示範 (Naive Solution)
@Composable
fun MyList(listState: LazyListState) {
    // ...
    val showButton = listState.firstVisibleItemIndex > 0 // 直接計算

    // ...
    if (showButton) {
        ScrollToTopButton() // 任何用到 showButton 的地方都會受影響
    }
}

問題在哪?

listState.firstVisibleItemIndex (第一個可見項目的索引) 這個狀態在使用者滾動列表時,會以極高的頻率改變(每滾動一個像素都可能觸發)。這導致 showButton 這個布林值被頻繁地重新計算。

雖然 showButton 的布林結果可能一直都是 true,但因為它的「來源」listState 一直在變,任何讀取 showButton 的 Composable 都會被不斷地重組,造成嚴重的效能浪費,這就是文章所說的「效能殺手」。


2. derivedStateOf 的運作原理:聰明的守門員

derivedStateOf 正是為了解決這個問題而生。

Kotlin

// 推薦的正確解法 (Recommended Solution)
@Composable
fun MyList(listState: LazyListState) {
    // ...
    // 使用 derivedStateOf
    val showButton by remember {
        derivedStateOf {
            listState.firstVisibleItemIndex > 0
        }
    }

    // ...
    if (showButton) {
        ScrollToTopButton()
    }
}

它的運作方式如下:

derivedStateOf 會觀察其 {…} 區塊內的計算。它只關心這個計算的最終結果是否與上一次相同。

  • 初始狀態: listState.firstVisibleItemIndex 是 0。derivedStateOf 計算出的結果是 false。
  • 開始向下滾動:
    • firstVisibleItemIndex 變成 1。計算結果從 false 變為 true。→ derivedStateOf 通知 UI 重組,按鈕出現。
    • firstVisibleItemIndex 繼續變成 2, 3, 4… 100。雖然來源狀態一直在變,但計算結果始終是 true→ derivedStateOf 發現結果沒變,因此「攔截」了更新,不會觸發 UI 重組。
  • 滾動回頂部:
    • firstVisibleItemIndex 變回 0。計算結果從 true 變為 false。→ derivedStateOf

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *