Jetpack Compose 核心概念(一)

1. 命令式 UI 和聲明式 UI

1.1 命令式 UI

在傳統的 XML UI 系統中,創建一個 UI 的邏輯往往分為以下幾步:

  1. 通過 xml 控件完成 UI 布局
  2. 運行期將 xml 中的各控件轉換為 java 對象,對象中的每個會直接或間接改變控件顯示效果的屬性,都被稱為控件的內部狀態
  3. 通過 findViewById 拿到對應的控件對象,并調用其 getXXXsetXXX 方法來手動維護其內部狀態的更新

這種由控件對象提供 setXXX 方法來由外部手動維護控件內部狀態更新的操作,就是命令式編程。

1.2 聲明式 UI

在 Jetpack Compose 聲明式編程范式中,每個控件都是無狀態的(控件內部并不保存相應的屬性),也不會提供對應的 getXXX()setXXX() 方法,而是將控件的狀態抽象到了控件的外部,由專門的 State 狀態對象來維護其控件的屬性,且控件與 State 對象的綁定是在聲明的過程中完成的。

運行過程中,只要 State 狀態的值發生了變化,與之綁定的控件就會被刷新。刷新過程完全是自動完成的,不需要任何的手動干預。這種只需聲明一次,就能自動完成后續控件刷新操作的編程范式,就是聲明式編程。

Jetpack Compose 應用

只需要把界面聲明出來,而不需要手動更新。界面的更新完全由數據驅動。UI 會自動根據數據的變化而更新。

2. Composition 和 Recomposition

2.1 Composition

Jetpack Compose 通過調用 composable 樹結構來完成頁面 UI 顯示的過程被稱為一次 compositionComposition 分為:initial composition 和 recomposition 兩個過程。

Initial composition 指的是首次運行 composable 樹結構來完成頁面顯示的過程,控件與 state 狀態對象的關系綁定主要是在這一過程中完成的。(部分控件并沒有在 initial composition 過程中得到執行,則其與 state 的綁定關系,是在 recomposition 過程中,控件被第一次執行的時候完成的)

2.2 Recomposition

Recomposition 是指當 Jetpack Compose 在執行完 initial composition 過程并完成了絕大部分控件與 state 狀態對象的綁定之后,由于某一個或多個 State 狀態對象發生變化后,Jetpack Compose 更新 UI 的方式。Recomposition 在執行過程中,只會調用 State 狀態發生變化所對應的 composable function 或 lambda 進行 執行,其它未發生變化的部分會盡可能的跳過,通過這種方式來提高更新 UI 的執行效率。

3. Compose 的執行特點

  1. Composable function 可按任何順序執行
  2. Composable function 可以并發執行
  3. Recomposition 會跳過盡可能多的內容
  4. Recomposition 是樂觀的操作
  5. Recomposition 可能執行的非常頻繁

3.1 Composable function 可按任何順序執行

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

這里 MyFancyNavigation 函數中調用的三個 composable function 可以按照任何順序執行。這三個 composable function 中不應該有任何的執行依賴關系(如:在 StartScreen 中改變一個全局變量的值,而在 MiddleScreen 中使用這個改變后的全局變量的值),并保證其相互獨立。

3.2 Composable function 可以并發執行

Composable function 的執行可能會在后臺線程執行。當在 composable 方法之內調用 Effect 附帶效應(如:調用 viewModel 中的某個函數),可能會出現多線程并發問題。所以,Effect 附帶效應應該運行在 composable 范圍之外執行。

并發執行的局部變量問題

@Composable
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")
    }
}

由于 composable 執行的最小單位為 composable 或者 lambda 代碼塊,上面代碼中 Column 和 Text 可能會在不同的線程同時執行,這樣,items 顯示的值就是錯誤的。

3.3 Recomposition 會跳過盡可能多的內容

Jetpack Compose 只會在某一個或者多個 composeable 所綁定的 state 狀態發生變化的時候,進行 recomposition 的更新操作。Recomposition 的過程中,以引用了 state 的 composable 為起點,根據該 composable 調用的子 composable 參數是否變化,來判斷是否需要對該子 composable 進行刷新,并依此向下遞歸。以達到盡量只更新狀態發生改變所對應的 composable 的目的。

@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // 當 header 值改變時,會引發 Text 的 recompose,而 names 的改變不會引起 Text 的 recompose
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        LazyColumn {
            items(names) { name ->
                // 當 names 中的某個 name 的值發生改變時,對應的 NamePickerItem 執行 recompose。header值的改變并不會引發 NamePickerItem 的 recompose。
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

3.4 Recomposition 是樂觀的操作

當 recomposition 還未完成時,由于新的狀態變化導致新的 recomposition 的發生,舊的 recomposition 會被取消(也就是丟棄 recomposition 過程中所生成的界面樹),新的 recomposition 會得到執行。

3.5 Recomposition 可能執行的非常頻繁

Recomposition 的執行可能會非常的頻繁,像一些 side-effect(附帶效應)的操作,推薦在其它線程執行,并通過 state 對象將其結果通過 recomposition 的方式返回。

4. State

4.1 State 是什么

對于應用來說,state 就是會引起頁面或邏輯發生變化的值。

對于控件來說,state 就是那些會直接或間接引起控件展示效果發生變化的值。比如:TextFild 的屬性 text 所對應的值就是一個 State。

在 Jetpack Compose 中 state 指的是實現了 state 接口的對象,它會與對應的 composable 進行綁定,并在值發生變化時,通知對應的 composable 進行刷新。

interface MutableState<T> : State<T> {
    override var value: T
}

4.2 Stateful(有狀態)與 Stateless(無狀態)

說明

Stateful 表示控件內部持有外部設置的屬性值。只要用戶針對控件的某個屬性設置過一次值之后,接下去的頁面刷新導致控件的重新執行,對應的值都是會顯示出來的,不需要再次設置。

Stateful 的控件通常會返回控件對象本身給業務來進行值的設置,就像傳統的 XML 控件。

Stateless 表示控件內部并不持有任何屬性對應的值,每次控件被刷新了,都需要調用當前控件并將對應的屬性值通過參數的形式傳給控件來顯示,否則不會顯示對應的屬性值。

Stateless 的控件通常不會返回控件對象本身,而是會提供參數讓業務來傳值,Composable 類型的控件就是這樣的控件。

Composable 應用及優缺點

在某個 composable 中,如果內部創建并持有了一個 state 狀態對象,那么這個 composable 就是 stateful(有狀態的),反之,則是 stateless 的。

Stateful composable 的好處:調用者無需管理狀態就可以直接使用,使用起來比較方便。

Stateful composable 的壞處:由于其持有了一個特定的 state 對象,降低了可重用性和可測試性。

Stateless composable 的好處:降低了 composable 的復雜度的同時,增加了其靈活性。

如果你是一個開發通用 composable 的開發者,一般情況下,需要針對同一個 composable 分別開發 stateful 或者 stateless 的的版本,供調用者選擇使用。

為什么說 Composable 是無狀態的?

這里的狀態主要指的是 composable 控件中的屬性。如:TextView 中的 text 屬性就是 TextView 中的一個狀態,可以通過 TextView 實例拿到這個屬性(狀態)的值。

而 Composable 中的無狀態所說的是:Composable 的 UI 控件是沒有屬性的,所有需要顯示的值都是被當作 Composable 函數參數進行執行,然后顯示出來,Composable UI 控件并沒有保存這些值,也就是我們無法再次通過 UI 控件實例獲取到設置的這些值,因為無法拿到 Composable UI 控件的實例。

無狀態不是一個功能或者優點,無狀態是 Compose 在實現聲明式 UI 控件過程中,所自帶的特點。

Composables should be relatively Stateless — meaning their display state should be driven by arguments passed into the Composable function itself.

如果無法通過 Composable UI 控件獲取到其對應的屬性,那么,如果在實際開發過程中,就是需要獲取某個 Composable UI 控件所使用的值的話,那又該如何實現呢?

上面所說的狀態,其實是指的某個控件的內部狀態,而如果 Composable UI 控件內部是無狀態的,所有的狀態(帶來控件改變的參數)都是通過外部傳遞進來的,那么我們就只需要將外部狀態,也就是控件外部的值在兩個 Composable UI 控件之間進行共享,就相當于是一個 Composable UI 控件獲取到了另外一個 Composable UI 控件的狀態(值)了,只不過這里的狀態是外部狀態,而非傳統 View System 中,狀態是在控件的內部存儲,并通過控件提供的方法來進行訪問的。

4.3 State hoisting(狀態提升)

State hoisting 的概念主要說的是將一個 stateful 的 composable 通過將其 state 對象向上轉移來將其轉換為 stateless 狀態。

Stateful composable 轉換為 Stateless composable 的方法

一個 State 對象需要使用 2 個函數參數來進行替換:

  1. value: T:由原 state 對象所持有并需要被顯示的值。
  2. onValueChange: (T) -> Unit:由原 stateful composable 中會改變原 state 狀態變化的代碼,以回調方式將改變后的值,同步到持有 state 的 composable 去更新。如果改變狀態的回調函數較多,這里也可以接收一個帶多個函數的接口作為參數。

Stateful composable:

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       var name by remember { mutableStateOf("") }
       if (name.isNotEmpty()) {
           Text(
               text = "Hello, $name!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
       }
       OutlinedTextField(
           value = name,
           onValueChange = { name = it },
           label = { Text("Name") }
       )
   }
}

Stateless composable:

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

State hoisting(狀態提升)的好處

  1. 唯一性。當多個 composable 都需要持有同一個 state 對象時,將這個 state 提升到共同最近一級的父類,可以減少維護的成本及降低出現 bug 的概率。
  2. 封裝性。僅持有 state 對象的 composable 才需要維護其狀態。
  3. 共享性。多個 composable 可以共享同一個 state 實例。
  4. 可攔截性。在修改 state 對象前,可以對事件進行忽略或者修改。
  5. 解藕性。將 composable 狀態與 composable 本身進行解藕,增加了靈活性、可重用性和可測試性。

State hoisting(狀態提升)原理 - 單向數據流

通過將 state 狀態提升后,持有 state 狀態的 composable 與 stateless composable 之間的關系就變成了單向數據流,即 stateless composable 觸發了事件后向上傳遞給 stateful composable 對象,stateful composable 接收到 Event 事件后,改變其持有的 state 狀態對象,并將 state 狀態對象所持有的值向下傳遞個 stateless composable 來完成 UI 上的展示。

image

State Hoisting(狀態提升)原則

  1. 讀取 state 最低層級父類。state 狀態對象應該被提升到離所有使用(讀取)這個 state 狀態對象最近的父類上。
  2. 修改 state 最高層級。state 狀態對象應該被提升到可能會修改此 state 的最高一級的 composable。
  3. 合并 state 對象。如果兩個 state 狀態對象維護的是同一個 Event 事件的話。應該將兩個 state 合并為同一個。

4.4 非 Composable 可觀察者對象轉換成 State 的方法

  1. LiveData
  2. Flow
  3. RxJava

上面 3 個常見的可觀察者對象都可以通過 xxx.observeAsState 來將其轉化為 state 對象。

4.5 Event 的類型

  1. 用戶主動觸發有事件,主要是由人與應用的交互中產生的,如:點擊事件、觸摸事件等等。
  2. 被動觸發的事件,如:登錄信息 token 過期后觸發的事件。

4.6 State 狀態類型

  1. 界面元素的狀態,即:界面元素的展示狀態。如:Snackbar 的 SnackbarHoststate 用來表示其本身顯示或者隱藏的狀態。
val snackbarHostState = remember { SnackbarHostState() }

val result = snackbarHostState.showSnackbar(
            message = "Snackbar # $index",
            actionLabel = "Action on $index"
        )
        when (result) {
            SnackbarResult.ActionPerformed -> {
                /* action has been performed */
            }
            SnackbarResult.Dismissed -> {
                /* dismissed, no action needed */
            }
        }
  1. 業務邏輯的狀態。比如:CartUiState 能同時包含 CartItem 內容、加載失敗的內容以及加載中需要顯示的內容等。
// 業務邏輯狀態對象
data class ExampleUiState(
    dataToDisplayOnScreen: List<Example> = emptyList(),
    userMessages: List<Message> = emptyList(),
    loading: Boolean = false
)

// 如何使用 viewModel 管理業務邏輯狀態
class ExampleViewModel(
    private val repository: MyRepository,
    private val savedState: SavedStateHandle
) : ViewModel() {

    var uiState by mutableStateOf<ExampleUiState>(...)
        private set

    // Business logic
    fun somethingRelatedToBusinessLogic() { ... }
}

// 如何在 Composable 中應用業務邏輯狀態對象
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    ...

    Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
        Text("Do something")
    }
}

4.7 在 Compose 中如何存儲 State

如果在 composable function 中直接創建 state 并將其綁定到 composable 控件的話,會存在一個問題:每次 recompositon 都會導致 state 被新建并將默認值綁定到對應 composable 控件來展示。這顯然達不到我們想要的效果。

remember

remember 也是一個 @composable 對象,它的作用是在 composable 中保存單個對象到內存中。

保存時機:默認是當 composable function 初次 composition 的時候。同時會在每一次的 composition 將保存的值進行返回(包括 initial composition)。

移除時機:當調用 remember 的 composable 在 composition 過程中被移除的時候。

重建時機:當應用的配置發生改變(如:屏幕旋轉)的時候,會導致 remember 所保存的對象被重建并重新保存。

多次保存:remember 支持傳遞一個或者多個 key 來控制 remember 是否需要重新執行保存操作。如果傳遞的 key 值中有一個值發生了變化都會導致 remember 再次執行保存的操作。

inline fun <T> remember(
    key1: Any?,
    key2: Any?,
    key3: Any?,
    calculation: @DisallowComposableCalls () -> T
): T

rememberSaveable

rememberSaveable 的作用也是保存對象值,只要能被 bundle 保存的值,都可以使用 rememberSaveable 來保存。與 remember 不同的是,rememberSaveable 是將數據保存到 bundle 中并序列化到本地進行持久化存儲,所以,當 activity 或者 process 銷毀并重建了之后,也是可以獲取到之前保存了的對象值。

4.8 持久化存儲非 Parcelizable 對象的方式

  1. MapSaver
data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}
  1. 為對象中的每個值設置一個 key
  2. 提供 save 和 restore 方法

本質上就是將對象中的每個值都使用 key-value 的方式存儲,并在獲取的時候,將 key-value 值重新組織成相應的對象進行返回。mapSaver 同樣也是存儲在 bundle 中。

  1. ListSaver
data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

listSaver 是針對 mapSaver 的簡化,直接以 list 的下標作為對象中值的 key 來存儲數據;并通過下標取出對象相應屬性的值來完成對象的組裝工作。最終數據同樣是存儲在 bundle 中。

4.9 正確聲明 State 的三種方式

  1. val mutableState = remember { mutableStateOf(default) }
  2. var value by remember { mutableStateOf(default) }
  3. val (value, setValue) = remember { mutableStateOf(default) }

4.10 如何管理 state

State Holders

當業務邏輯越來越復雜,使用的 state 狀態對象越來越多的時候,state 狀態對象及其業務邏輯的的維護成本越來越高。此時,可以使用一個或多個單獨的 state holder 對象來統一管理這些 state 狀態對象及其業務邏輯。

State Holder 例子:

// 通過 StateHolder 來管理 UI 邏輯及 State 狀態對象
class MyAppState(
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val resources: Resources,
    /* ... */
) {
    val bottomBarTabs = /* State */

    // Logic to decide when to show the bottom bar
    val shouldShowBottomBar: Boolean
        get() = /* ... */

    // Navigation logic, which is a type of UI logic
    fun navigateToBottomBarRoute(route: String) { /* ... */ }

    // Show snackbar using Resources
    fun showSnackbar(message: String) { /* ... */ }
}

@Composable
fun rememberMyAppState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources,
    /* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
    MyAppState(scaffoldState, navController, resources, /* ... */)
}

// 使用 StateHodler 對象
@Composable
fun MyApp() {
    MyTheme {
        val myAppState = rememberMyAppState()
        Scaffold(
            scaffoldState = myAppState.scaffoldState,
            bottomBar = {
                if (myAppState.shouldShowBottomBar) {
                    BottomBar(
                        tabs = myAppState.bottomBarTabs,
                        navigateToRoute = {
                            myAppState.navigateToBottomBarRoute(it)
                        }
                    )
                }
            }
        ) {
            NavHost(navController = myAppState.navController, "initial") { /* ... */ }
        }
    }
}

管理 State 的幾種方式

  1. Composable 管理。當 composable UI 較簡單,state 狀態對象不多時, 可以直接放在 composable 中進行管理。
  2. State Holder 管理。當 composable UI 較復雜時,可以單獨創建一個 state holder 來管理其 state 狀態對象及其邏輯。
  3. Viewmodel 管理。可以直接使用 ViewModel 來管理其 state 狀態對象。

ViewModel 相比于 StateHolder 的好處

  1. ViewModel 不受屏幕配置變化的影響。
  2. ViewModel 與 Navigation 集成,當頁面位于回退棧中時,Navigation 會緩存 ViewModel,這樣做的好處是:可以在返回到當前頁面時,立即顯示之前加載過的數據。而 StateHolder 由于屏幕旋轉等會導致 state 對象的重建而丟失之前的數據。同時,當頁面從返回棧退出時,ViewModel 會自動被清除,而對于 StateHodler 來說,state 狀態會被一直保存。
  3. ViewModel 與一些其它庫集成(如:LiveData、Hilt),擴展性更強。

ViewModel 與 StateHolder 協同工作

雖然 ViewModel 相比于 StateHodler 來說,有諸多好處,但兩者的定位還是有一定的差距。

  1. StateHodler 主要是用于管理 UI 邏輯及界面元素的狀態。
  2. ViewModel 主要用于處理業務邏輯及返回待展示的數據。

總體來說,在管理 state 狀態對象的時候,兩者都能勝任,而在處理 UI 邏輯時,StateHodler 更加適合;而在處理業務邏輯時,ViewModel 更加適合。

private class ExampleState(
    val lazyListState: LazyListState,
    private val resources: Resources,
    private val expandedItems: List<Item> = emptyList()
) { ... }

@Composable
private fun rememberExampleState(...) { ... }

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    // ViewModel 處理業務邏輯狀態
    val uiState = viewModel.uiState
    // StateHodler 處理界面元素狀態
    val exampleState = rememberExampleState()

    LazyColumn(state = exampleState.lazyListState) {
        items(uiState.dataToDisplayOnScreen) { item ->
            if (exampleState.isExpandedItem(item) {
                ...
            }
            ...
        }
    }
}

State 與 Reomposition 的關系

當 UI 完成 initial composition 的加載后,Compose 也完成了對 state 狀態的追蹤。接下來,UI 的 recompostion(重新組合) 通常是通過 state 的改變成觸發的。當對應的 state 發生變化后,會觸發引用了發生改變 state 狀態對象的 composable 及其被直接或間接調用的子 composable 的重新執行。當然,由于 Compose 對這種刷新做了優化,只會對那些輸入發生改變的 composable 進行更新。

如何理解單向數據流(undirectional data flow)

單向數據流描述的是事件與狀態(數據)的一種邏輯關系,即事件觸發狀態的改變,狀態改變后,觸發 UI 的更新,整個過程是單向的。

5. Composable 的生命周期

[圖片上傳失敗...(image-1d35d9-1641520488295)]

Composable 的生命周期與 Compose 的 composition 綁定在了一起。主要分為三個部分:

  1. 進入 composition,也就是當前 composable 得到了 Compose 的執行;
  2. Recompose 0 到多次,composable 在進入 composition 后,又被重新執行了 0 到多次;
  3. 退出 composition,composable 在 Compose 進行 composition 過程中,沒有得到執行(非 composable 由于其狀態未發生變化而跳過)。

當然,這個 composable 的生命周期并沒有那么嚴格的執行順序,通常會多次進入 composition 后,運行 0 到多次后,又退出 composition。

5.1 Composable 實例及唯一性確認

每一個 composable 在初次被調用的時候會生成一個 composable 實例,同一個 composable 被不同的地方調用,會生成多個實例。也就是同一個 composable 在輸入(參數)不變的情況下,是否會創建新的實例是以調用點來判斷的。如果同一 composable 在同一調用點(如:for 循環創建 list item 對象)被調用多次,則以執行順序來區分不同的 composable 實例(默認情況下會將同一調用點不同的執行順序與當前 composable 進行關聯并唯一的標識當前 composable 實例)。

調用點(源代碼調用處)所對應的 composable 已經被創建后,默認情況下會將同一調用點不同的執行順序與當前 composable 進行關聯并唯一的標識當前 composable 實例。重復調用,如果其輸入沒有變化的話,不會重新去創建,而會重用之前創建的實例;如果輸入(參數)發生變化,則會重新執行相應的 composable function,并創建新的 composable 實例。

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // 同一調用點多次調用同一 Composable 對象,以執行順序來區分不同的 Composalbe 實例。
            MovieOverview(movie)
        }
    }
}

5.2 按執行順序區分不同實例的幾種情況

  1. 列表隊尾添加新的 Composable 實例
image

由于 recomposition 之前已經創建的 composable 實例的執行順序與 recomposition 時的執行順序及輸入都未發生變化,所以,recomposition 之前就已經創建的 composable 對象會被 recomposition 重用。

  1. 列表隊頭添加新的 Composable 實例
image

由于 recomposition 之前已經創建的 composable 實例的執行順序已經與 recomposition 時的執行順序不同,所以,recomposition 會為對應的 composable 創建新的實例,而不會重用 recomposition 之前已經創建好的實例。

  1. 列表隊中添加新的 Composable 實例

插入點之上的 composable 實例在 recomposition 過程中會被重用;插入點之后的 composable 實例都會被重新創建。

5.3 Compose 默認根據什么規則來跳過 Recomposition,如何是用關鍵字(如:key @state)來避免不必要的 Recomposition

默認情況下,當 composable 的輸入是穩定的類型且沒有改變時,recomposition 會跳過這些輸入穩定且沒有改變的 composable。

這里的穩定類型說的是輸入類型對象本身是否是穩定類型,這個是前提,如果不是穩定類型,就算對象本身沒有變化,也會被重建,而不會復用之前的 composable。

Stable 類型需要滿足的條件

  1. 對于相同的兩個對象實例,其 equals 必須相等
  2. 如果一個類型中的公共屬性發生了改變,composation 的時候需要被通知
  3. 所有公共屬性的類型都必須是穩定的

默認被認為是 Stable 的類型

  1. 基礎數據類型
  2. 字符串類型
  3. 所有的 lambda 類型
  4. 被 State 狀態對象所持有的值

為什么是這些類型?

因為這些類型都是 immutable(不可變)的類型,這些類型本身是不可能發生改變的。如果發生了改變,那都是不同的對象了,Compose 能識別這種變化而觸發 recomposition 的更新。

State 狀態對象的實例是 MutableState,它雖然是一個 mutable 可變類型,但是由于其所持有的屬性 value 在發生變化后,會通知給 composition 進行相應的更新,所以,state 狀態對象,也是穩定的。

State 類型對象是否改變的默認認定方式

當作為參數傳遞給 composable 的所有類型都是 stable 時,Compose 會使用 equals 來對各參數進行比對,如果都相等,則認為數據沒有發生變化,會跳過當次的 composition。

使用 @Stable 注解將非 Stable 類型對象改為 Stable 類型

如果手動為某個類或接口使用 @Stable 修飾后,其所有對象或實現都會被 Compose 認定為 stable 狀態的類型。如果某個 composable 接收的參數類型使用了 @Stable 修飾,則會直接使用 equals 來判斷當前參數是否改變,進而判斷是否需要在 recomposition 時,重建當前的 composable。

6. Side-effect

6.1 什么是 Side-effect(附帶效應)

Side-effect:指在可組合函數范圍之外發生的應用狀態變化。

這里如何理解什么是可組合函數范圍?

可組合函數范圍指的就是 composable 中只要其接收的 lambda 是一個 composable 對象,被其所包含的內容被稱為 可組合函數范圍,如果有多個可組合函數存在包含關系,那這里就是一個遞歸關系。只要 composable lambda 直接包含的區域中的代碼就被認為是在 可組合函數范圍 之外。下面代碼中的 content 和 label 所對應的 lambda 中直接包含的內容就是在 可組合函數范圍 之內。除此之外的其它部分(如:Text 中的 click 點擊事件所對應的 lambda),都被稱為 可組合函數范圍 之外的區域。

簡而言之,被 composable function 或 composable lambda 直接包含的區域就被稱為 可組合函數之內,其他地方都被稱為 可組合函數范圍之外

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(Modifier.padding(15.dp), content = {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp)
                .clickable { Log.d("TAG", "onClick") },
            style = MaterialTheme.typography.h5
        )

        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text(text = "Name") })
    })
}

可組合函數范圍的概念理解了,那么什么叫做可組合函數范圍之外發生的應用狀態變化呢?

Button(
    onClick = {
            // Create a new coroutine in the event handler
            // to show a snackbar
            scope.launch {
                scaffoldState.snackbarHostState
                    .showSnackbar("Something happened!")
            }
        }
    ) {
        Text("Press me")
    }
}

這里的 onclick 中啟動協程并修改 snackbar 狀態的操作就被稱為 可組合函數范圍之外發生的應用狀態變化

6.2 常見的 Side-effects

LaunchedEffect

fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    // implementation
}

LaunchedEffect 是一個 composable function,其作用為:在 composable 作用域啟動一個協程來執行傳遞給它的包含相應邏輯的掛起函數。每一次執行掛起函數的時候,都會啟動一個新的協程,并取消上一次啟動的協程。同時,當 LaunchedEffect 所綁定的 composable 在某次 composition 過程中,沒有被包括在內時,之前啟動的協程也會被取消。

block 執行的條件:

  1. 初次調用 LaunchedEffect 函數時,block 會被執行;
  2. 再次調用 LaunchedEffect 函數時,只有所接收的一到多個 key 值中至少有一個值發生了變化后,block 都會執行。

Note:每一次 block 的執行都是在新的協程中運行。

@Composable
fun c() {
    var refreshState by remember { mutableStateOf(false) }

    MyScreen(refresh = refreshState) {
        refreshState = !refreshState
    }
}

@Composable
fun MyScreen(refresh: Boolean, value: () -> Unit) {
    Button(onClick = { value.invoke() }) {
        Text(text = "refresh the page")
    }

    LaunchedEffect(key1 = refresh) {
        Log.d("launchedEffect", "launchedEffect launched, refresh = $refresh")
    }

    LaunchedEffect(true) {
        Log.d("launchedEffect", "launchedEffect launched, key never changed")
    }
}

上面的代碼中,launchedEffect launched, key never changed 只會被打印一次,而 launchedEffect launched, refresh = $refresh 在每一次 MyScreen 被調用時,都會被打印。

協程取消的時機:

  1. 當 LaunchedEffect 在連續兩次 composition 過程中,其綁定的 composable 都被調用時,前一次啟動的協程會被取消。
  2. 當 LaunchedEffect 被調用后的下一次 composition 過程中,其綁定的 composable 沒有再被調用時,會取消其啟動的協程。

rememberCoroutineScope

rememberCoroutineScope 是一個 composable 方法,其作用是:創建一個綁定了 composition 的協程作用域,該協程作用域可以在可組合函數范圍之外啟動一個綁定了 composable 生命周期的協程。當創建該協程作用域的 composable 在 composition 過程從顯示中被移除時,其通過 rememberCoroutineScope 啟動的協程就會被取消。

@Composable
@Composable
fun rememberCoroutineScopeClick(scaffoldState: ScaffoldState = rememberScaffoldState()) {
    var showState by remember { mutableStateOf(true) }

    Scaffold(scaffoldState = scaffoldState) {
        Column {

            Button(onClick = {
                showState = !showState
            }) {
                Text("show or not show")
            }

            if (showState) {
                Log.d("sideEffect", "showState = $showState")
                rememberCoroutineScopeExample()
            }
        }
    }
}

@Composable
fun rememberCoroutineScopeExample() {
    val scope = rememberCoroutineScope()


    Button(onClick = {
        scope.launch {
            delay(6000)
            Log.d("sideEffect", "coroutine launch")
        }
    }) {
        Text("Press me")
    }

}

協程被取消的時機:

  1. 當調用 rememberCoroutineScope 的 composable 在 composition 過程中,沒有被顯示到頁面上,會取消使用 rememberCoroutineScope 啟動的所有協程。

rememberUpdatedState

作用:保存某個參數或者狀態的最新值,當被調用的時候,返回已保存的最新值。

@Composable
fun rememberUpdateStateExample() {
    var count by remember { mutableStateOf(0) }

    Column {

        Button(onClick = {
            count++
        }) {
            Text("Change the onTime $count")
        }

        LandingScreen {
            Log.d("sideEffect", "count = $count")
        }
    }
}

@Composable
fun LandingScreen(onTime: () -> Unit) {
    Log.d("sideEffect", "LandingScreen")
    val currentOnTimeout by rememberUpdatedState(onTime)
    var executeState by remember { mutableStateOf(false) }


    if (executeState) {
        LaunchedEffect(true) {
            delay(2000)
            currentOnTimeout()
        }
    }

    Button(onClick = { executeState = !executeState }) {
        Text(text = "loading updated state")
    }
}

DisposableEffect

DisposableEffect 作用:啟動一個提供了回收方法的 LaunchedEffect(啟動了一個協程),當 DisposableEffect 在某次 composition 過程中沒有被執行,則會取消之前啟動的協程,并會在取消協程前調用其回收方法進行資源回收相關的操作。

@Composable
fun DisposableEffectExample() {
    var showState by remember { mutableStateOf(false) }

    Column {
        Button(onClick = { showState = !showState }) {
            Text(text = "showing or not Showing")
        }

        if (showState) {
            HomeScreen(
                onStart = { Log.d("sideEffect", "onStart") },
                onStop = { Log.d("sideEffect", "onStop") })
        }
    }
}

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // Safely update the current lambdas when a new one is provided
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // If `lifecycleOwner` changes, dispose and reset the effect
    DisposableEffect(lifecycleOwner) {
        // Create an observer that triggers our remembered callbacks
        // for sending analytics events
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            Log.d("sideEffect", "onDispose")
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

SideEffect

通過將非 Compose 代碼與 composable 綁定,當綁定的 composable 在 recomposition 過程中被更新時,使用 SideEffect 來更新非 Compose 代碼。SideEffect 并未接收任何 key 值,所以,其只要被調用,就會執行其 block。

@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

produceState

將非 Compose 狀態的對象,通過 produceState 的包裝后,轉化為 Compose state 狀態對象,便于在 Compose 中直接與 composable 進行綁定。

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

將網絡請求回來的 Result 普通對象轉換為 Compose state 對象。

derivedStateOf

將一個或多個 Compose state 狀態對象轉化成一個新的 Compose state 對象,并且當舊的 state 狀態或者 derivedStateOf 所引用的變量發生改變時,都會引起新 state 對象的聯動更新。

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {

    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or highPriorityKeywords
    // change, not on every recomposition
    val highPriorityTasks by remember {
        derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

上面的代碼將 todoTask state 經過 highPriorityKeywords 過濾后,轉換成了新的 highPriorityTask state 對象。當 todo state 或者 highPriorityKeywords 的狀態發生變化時,都會引起 highPriorityTask state 的更新。

snapshotFlow

在 Compose 中創建一個 flow,并與 state 狀態對象綁定。當 state 對象發生改變時,會通過 flow 發送出去。

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

上面的代碼在 Effect 創建一個 flow 并綁定了 listState 狀態對象,當 listState 狀態發生變化時,都會通過該 flow 把變化后的值發送出去。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容