1. 命令式 UI 和聲明式 UI
1.1 命令式 UI
在傳統(tǒng)的 XML UI 系統(tǒng)中饰豺,創(chuàng)建一個(gè) UI 的邏輯往往分為以下幾步:
- 通過 xml 控件完成 UI 布局
- 運(yùn)行期將 xml 中的各控件轉(zhuǎn)換為 java 對(duì)象濒憋,對(duì)象中的每個(gè)會(huì)直接或間接改變控件顯示效果的屬性堤尾,都被稱為控件的內(nèi)部狀態(tài)
- 通過
findViewById
拿到對(duì)應(yīng)的控件對(duì)象,并調(diào)用其getXXX
或setXXX
方法來手動(dòng)維護(hù)其內(nèi)部狀態(tài)的更新
這種由控件對(duì)象提供 setXXX
方法來由外部手動(dòng)維護(hù)控件內(nèi)部狀態(tài)更新的操作片林,就是命令式編程形葬。
1.2 聲明式 UI
在 Jetpack Compose 聲明式編程范式中切黔,每個(gè)控件都是無狀態(tài)的(控件內(nèi)部并不保存相應(yīng)的屬性),也不會(huì)提供對(duì)應(yīng)的 getXXX()
或 setXXX()
方法是偷,而是將控件的狀態(tài)抽象到了控件的外部拳氢,由專門的 State
狀態(tài)對(duì)象來維護(hù)其控件的屬性募逞,且控件與 State
對(duì)象的綁定是在聲明的過程中完成的。
運(yùn)行過程中馋评,只要 State
狀態(tài)的值發(fā)生了變化放接,與之綁定的控件就會(huì)被刷新。刷新過程完全是自動(dòng)完成的留特,不需要任何的手動(dòng)干預(yù)纠脾。這種只需聲明一次,就能自動(dòng)完成后續(xù)控件刷新操作的編程范式蜕青,就是聲明式編程苟蹈。
Jetpack Compose 應(yīng)用
只需要把界面聲明出來,而不需要手動(dòng)更新右核。界面的更新完全由數(shù)據(jù)驅(qū)動(dòng)慧脱。UI 會(huì)自動(dòng)根據(jù)數(shù)據(jù)的變化而更新。
2. Composition 和 Recomposition
2.1 Composition
Jetpack Compose 通過調(diào)用 composable 樹結(jié)構(gòu)來完成頁面 UI 顯示的過程被稱為一次 composition
贺喝。Composition
分為:initial composition 和 recomposition 兩個(gè)過程菱鸥。
Initial composition 指的是首次運(yùn)行 composable 樹結(jié)構(gòu)來完成頁面顯示的過程,控件與 state 狀態(tài)對(duì)象的關(guān)系綁定主要是在這一過程中完成的躏鱼。(部分控件并沒有在 initial composition 過程中得到執(zhí)行氮采,則其與 state 的綁定關(guān)系,是在 recomposition 過程中挠他,控件被第一次執(zhí)行的時(shí)候完成的)
2.2 Recomposition
Recomposition 是指當(dāng) Jetpack Compose 在執(zhí)行完 initial composition 過程并完成了絕大部分控件與 state 狀態(tài)對(duì)象的綁定之后扳抽,由于某一個(gè)或多個(gè) State
狀態(tài)對(duì)象發(fā)生變化后,Jetpack Compose 更新 UI 的方式殖侵。Recomposition 在執(zhí)行過程中贸呢,只會(huì)調(diào)用 State
狀態(tài)發(fā)生變化所對(duì)應(yīng)的 composable function 或 lambda 進(jìn)行 執(zhí)行,其它未發(fā)生變化的部分會(huì)盡可能的跳過拢军,通過這種方式來提高更新 UI 的執(zhí)行效率楞陷。
3. Compose 的執(zhí)行特點(diǎn)
- Composable function 可按任何順序執(zhí)行
- Composable function 可以并發(fā)執(zhí)行
- Recomposition 會(huì)跳過盡可能多的內(nèi)容
- Recomposition 是樂觀的操作
- Recomposition 可能執(zhí)行的非常頻繁
3.1 Composable function 可按任何順序執(zhí)行
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
這里 MyFancyNavigation 函數(shù)中調(diào)用的三個(gè) composable function 可以按照任何順序執(zhí)行。這三個(gè) composable function 中不應(yīng)該有任何的執(zhí)行依賴關(guān)系(如:在 StartScreen 中改變一個(gè)全局變量的值茉唉,而在 MiddleScreen 中使用這個(gè)改變后的全局變量的值)固蛾,并保證其相互獨(dú)立。
3.2 Composable function 可以并發(fā)執(zhí)行
Composable function 的執(zhí)行可能會(huì)在后臺(tái)線程執(zhí)行度陆。當(dāng)在 composable 方法之內(nèi)調(diào)用 Effect 附帶效應(yīng)(如:調(diào)用 viewModel 中的某個(gè)函數(shù))艾凯,可能會(huì)出現(xiàn)多線程并發(fā)問題。所以懂傀,Effect 附帶效應(yīng)應(yīng)該運(yùn)行在 composable 范圍之外執(zhí)行趾诗。
并發(fā)執(zhí)行的局部變量問題
@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 執(zhí)行的最小單位為 composable 或者 lambda 代碼塊,上面代碼中 Column 和 Text 可能會(huì)在不同的線程同時(shí)執(zhí)行,這樣恃泪,items 顯示的值就是錯(cuò)誤的郑兴。
3.3 Recomposition 會(huì)跳過盡可能多的內(nèi)容
Jetpack Compose 只會(huì)在某一個(gè)或者多個(gè) composeable 所綁定的 state 狀態(tài)發(fā)生變化的時(shí)候,進(jìn)行 recomposition 的更新操作贝乎。Recomposition 的過程中情连,以引用了 state 的 composable 為起點(diǎn),根據(jù)該 composable 調(diào)用的子 composable 參數(shù)是否變化览效,來判斷是否需要對(duì)該子 composable 進(jìn)行刷新却舀,并依此向下遞歸。以達(dá)到盡量只更新狀態(tài)發(fā)生改變所對(duì)應(yīng)的 composable 的目的朽肥。
@Composable
fun NamePicker(
header: String,
names: List<String>,
onNameClicked: (String) -> Unit
) {
Column {
// 當(dāng) header 值改變時(shí)禁筏,會(huì)引發(fā) Text 的 recompose,而 names 的改變不會(huì)引起 Text 的 recompose
Text(header, style = MaterialTheme.typography.h5)
Divider()
LazyColumn {
items(names) { name ->
// 當(dāng) names 中的某個(gè) name 的值發(fā)生改變時(shí)衡招,對(duì)應(yīng)的 NamePickerItem 執(zhí)行 recompose篱昔。header值的改變并不會(huì)引發(fā) NamePickerItem 的 recompose。
NamePickerItem(name, onNameClicked)
}
}
}
}
3.4 Recomposition 是樂觀的操作
當(dāng) recomposition 還未完成時(shí)始腾,由于新的狀態(tài)變化導(dǎo)致新的 recomposition 的發(fā)生州刽,舊的 recomposition 會(huì)被取消(也就是丟棄 recomposition 過程中所生成的界面樹),新的 recomposition 會(huì)得到執(zhí)行浪箭。
3.5 Recomposition 可能執(zhí)行的非常頻繁
Recomposition 的執(zhí)行可能會(huì)非常的頻繁穗椅,像一些 side-effect(附帶效應(yīng))的操作,推薦在其它線程執(zhí)行奶栖,并通過 state 對(duì)象將其結(jié)果通過 recomposition 的方式返回匹表。
4. State
4.1 State 是什么
對(duì)于應(yīng)用來說,state 就是會(huì)引起頁面或邏輯發(fā)生變化的值宣鄙。
對(duì)于控件來說袍镀,state 就是那些會(huì)直接或間接引起控件展示效果發(fā)生變化的值。比如:TextFild 的屬性 text 所對(duì)應(yīng)的值就是一個(gè) State冻晤。
在 Jetpack Compose 中 state 指的是實(shí)現(xiàn)了 state 接口的對(duì)象苇羡,它會(huì)與對(duì)應(yīng)的 composable 進(jìn)行綁定,并在值發(fā)生變化時(shí)鼻弧,通知對(duì)應(yīng)的 composable 進(jìn)行刷新设江。
interface MutableState<T> : State<T> {
override var value: T
}
4.2 Stateful(有狀態(tài))與 Stateless(無狀態(tài))
說明
Stateful 表示控件內(nèi)部持有外部設(shè)置的屬性值。只要用戶針對(duì)控件的某個(gè)屬性設(shè)置過一次值之后攘轩,接下去的頁面刷新導(dǎo)致控件的重新執(zhí)行叉存,對(duì)應(yīng)的值都是會(huì)顯示出來的,不需要再次設(shè)置度帮。
Stateful 的控件通常會(huì)返回控件對(duì)象本身給業(yè)務(wù)來進(jìn)行值的設(shè)置鹉胖,就像傳統(tǒng)的 XML 控件。
Stateless 表示控件內(nèi)部并不持有任何屬性對(duì)應(yīng)的值,每次控件被刷新了甫菠,都需要調(diào)用當(dāng)前控件并將對(duì)應(yīng)的屬性值通過參數(shù)的形式傳給控件來顯示,否則不會(huì)顯示對(duì)應(yīng)的屬性值冕屯。
Stateless 的控件通常不會(huì)返回控件對(duì)象本身寂诱,而是會(huì)提供參數(shù)讓業(yè)務(wù)來傳值,Composable 類型的控件就是這樣的控件安聘。
Composable 應(yīng)用及優(yōu)缺點(diǎn)
在某個(gè) composable 中痰洒,如果內(nèi)部創(chuàng)建并持有了一個(gè) state 狀態(tài)對(duì)象,那么這個(gè) composable 就是 stateful(有狀態(tài)的)浴韭,反之丘喻,則是 stateless 的。
Stateful composable 的好處:調(diào)用者無需管理狀態(tài)就可以直接使用念颈,使用起來比較方便泉粉。
Stateful composable 的壞處:由于其持有了一個(gè)特定的 state 對(duì)象,降低了可重用性和可測(cè)試性榴芳。
Stateless composable 的好處:降低了 composable 的復(fù)雜度的同時(shí)嗡靡,增加了其靈活性。
如果你是一個(gè)開發(fā)通用 composable 的開發(fā)者窟感,一般情況下讨彼,需要針對(duì)同一個(gè) composable 分別開發(fā) stateful 或者 stateless 的的版本,供調(diào)用者選擇使用柿祈。
為什么說 Composable 是無狀態(tài)的哈误?
這里的狀態(tài)主要指的是 composable 控件中的屬性。如:TextView 中的 text 屬性就是 TextView 中的一個(gè)狀態(tài)躏嚎,可以通過 TextView 實(shí)例拿到這個(gè)屬性(狀態(tài))的值蜜自。
而 Composable 中的無狀態(tài)所說的是:Composable 的 UI 控件是沒有屬性的,所有需要顯示的值都是被當(dāng)作 Composable 函數(shù)參數(shù)進(jìn)行執(zhí)行紧索,然后顯示出來袁辈,Composable UI 控件并沒有保存這些值,也就是我們無法再次通過 UI 控件實(shí)例獲取到設(shè)置的這些值珠漂,因?yàn)闊o法拿到 Composable UI 控件的實(shí)例晚缩。
無狀態(tài)不是一個(gè)功能或者優(yōu)點(diǎn),無狀態(tài)是 Compose 在實(shí)現(xiàn)聲明式 UI 控件過程中媳危,所自帶的特點(diǎn)荞彼。
Composables should be relatively Stateless — meaning their display state should be driven by arguments passed into the Composable function itself.
如果無法通過 Composable UI 控件獲取到其對(duì)應(yīng)的屬性,那么待笑,如果在實(shí)際開發(fā)過程中鸣皂,就是需要獲取某個(gè) Composable UI 控件所使用的值的話,那又該如何實(shí)現(xiàn)呢?
上面所說的狀態(tài)寞缝,其實(shí)是指的某個(gè)控件的內(nèi)部狀態(tài)癌压,而如果 Composable UI 控件內(nèi)部是無狀態(tài)的,所有的狀態(tài)(帶來控件改變的參數(shù))都是通過外部傳遞進(jìn)來的荆陆,那么我們就只需要將外部狀態(tài)滩届,也就是控件外部的值在兩個(gè) Composable UI 控件之間進(jìn)行共享,就相當(dāng)于是一個(gè) Composable UI 控件獲取到了另外一個(gè) Composable UI 控件的狀態(tài)(值)了被啼,只不過這里的狀態(tài)是外部狀態(tài)帜消,而非傳統(tǒng) View System 中,狀態(tài)是在控件的內(nèi)部存儲(chǔ)浓体,并通過控件提供的方法來進(jìn)行訪問的泡挺。
4.3 State hoisting(狀態(tài)提升)
State hoisting 的概念主要說的是將一個(gè) stateful 的 composable 通過將其 state 對(duì)象向上轉(zhuǎn)移來將其轉(zhuǎn)換為 stateless 狀態(tài)。
Stateful composable 轉(zhuǎn)換為 Stateless composable 的方法
一個(gè) State 對(duì)象需要使用 2 個(gè)函數(shù)參數(shù)來進(jìn)行替換:
-
value: T
:由原 state 對(duì)象所持有并需要被顯示的值命浴。 -
onValueChange: (T) -> Unit
:由原 stateful composable 中會(huì)改變?cè)?state 狀態(tài)變化的代碼娄猫,以回調(diào)方式將改變后的值,同步到持有 state 的 composable 去更新咳促。如果改變狀態(tài)的回調(diào)函數(shù)較多稚新,這里也可以接收一個(gè)帶多個(gè)函數(shù)的接口作為參數(shù)。
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(狀態(tài)提升)的好處
- 唯一性跪腹。當(dāng)多個(gè) composable 都需要持有同一個(gè) state 對(duì)象時(shí)褂删,將這個(gè) state 提升到共同最近一級(jí)的父類,可以減少維護(hù)的成本及降低出現(xiàn) bug 的概率冲茸。
- 封裝性屯阀。僅持有 state 對(duì)象的 composable 才需要維護(hù)其狀態(tài)。
- 共享性轴术。多個(gè) composable 可以共享同一個(gè) state 實(shí)例难衰。
- 可攔截性。在修改 state 對(duì)象前逗栽,可以對(duì)事件進(jìn)行忽略或者修改盖袭。
- 解藕性。將 composable 狀態(tài)與 composable 本身進(jìn)行解藕彼宠,增加了靈活性鳄虱、可重用性和可測(cè)試性。
State hoisting(狀態(tài)提升)原理 - 單向數(shù)據(jù)流
通過將 state 狀態(tài)提升后凭峡,持有 state 狀態(tài)的 composable 與 stateless composable 之間的關(guān)系就變成了單向數(shù)據(jù)流拙已,即 stateless composable 觸發(fā)了事件后向上傳遞給 stateful composable 對(duì)象,stateful composable 接收到 Event 事件后摧冀,改變其持有的 state 狀態(tài)對(duì)象倍踪,并將 state 狀態(tài)對(duì)象所持有的值向下傳遞個(gè) stateless composable 來完成 UI 上的展示系宫。
State Hoisting(狀態(tài)提升)原則
- 讀取 state 最低層級(jí)父類。state 狀態(tài)對(duì)象應(yīng)該被提升到離所有使用(讀冉ǔ怠)這個(gè) state 狀態(tài)對(duì)象最近的父類上扩借。
- 修改 state 最高層級(jí)。state 狀態(tài)對(duì)象應(yīng)該被提升到可能會(huì)修改此 state 的最高一級(jí)的 composable癞志。
- 合并 state 對(duì)象往枷。如果兩個(gè) state 狀態(tài)對(duì)象維護(hù)的是同一個(gè) Event 事件的話。應(yīng)該將兩個(gè) state 合并為同一個(gè)凄杯。
4.4 非 Composable 可觀察者對(duì)象轉(zhuǎn)換成 State 的方法
- LiveData
- Flow
- RxJava
上面 3 個(gè)常見的可觀察者對(duì)象都可以通過 xxx.observeAsState
來將其轉(zhuǎn)化為 state 對(duì)象。
4.5 Event 的類型
- 用戶主動(dòng)觸發(fā)有事件秉宿,主要是由人與應(yīng)用的交互中產(chǎn)生的戒突,如:點(diǎn)擊事件、觸摸事件等等描睦。
- 被動(dòng)觸發(fā)的事件膊存,如:登錄信息 token 過期后觸發(fā)的事件。
4.6 State 狀態(tài)類型
- 界面元素的狀態(tài)忱叭,即:界面元素的展示狀態(tài)隔崎。如:Snackbar 的 SnackbarHoststate 用來表示其本身顯示或者隱藏的狀態(tài)。
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 */
}
}
- 業(yè)務(wù)邏輯的狀態(tài)韵丑。比如:CartUiState 能同時(shí)包含 CartItem 內(nèi)容爵卒、加載失敗的內(nèi)容以及加載中需要顯示的內(nèi)容等。
// 業(yè)務(wù)邏輯狀態(tài)對(duì)象
data class ExampleUiState(
dataToDisplayOnScreen: List<Example> = emptyList(),
userMessages: List<Message> = emptyList(),
loading: Boolean = false
)
// 如何使用 viewModel 管理業(yè)務(wù)邏輯狀態(tài)
class ExampleViewModel(
private val repository: MyRepository,
private val savedState: SavedStateHandle
) : ViewModel() {
var uiState by mutableStateOf<ExampleUiState>(...)
private set
// Business logic
fun somethingRelatedToBusinessLogic() { ... }
}
// 如何在 Composable 中應(yīng)用業(yè)務(wù)邏輯狀態(tài)對(duì)象
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
...
Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
Text("Do something")
}
}
4.7 在 Compose 中如何存儲(chǔ) State
如果在 composable function 中直接創(chuàng)建 state 并將其綁定到 composable 控件的話撵彻,會(huì)存在一個(gè)問題:每次 recompositon 都會(huì)導(dǎo)致 state 被新建并將默認(rèn)值綁定到對(duì)應(yīng) composable 控件來展示钓株。這顯然達(dá)不到我們想要的效果。
remember
remember 也是一個(gè) @composable
對(duì)象陌僵,它的作用是在 composable 中保存單個(gè)對(duì)象到內(nèi)存中轴合。
保存時(shí)機(jī):默認(rèn)是當(dāng) composable function 初次 composition 的時(shí)候。同時(shí)會(huì)在每一次的 composition 將保存的值進(jìn)行返回(包括 initial composition)碗短。
移除時(shí)機(jī):當(dāng)調(diào)用 remember 的 composable 在 composition 過程中被移除的時(shí)候受葛。
重建時(shí)機(jī):當(dāng)應(yīng)用的配置發(fā)生改變(如:屏幕旋轉(zhuǎn))的時(shí)候,會(huì)導(dǎo)致 remember 所保存的對(duì)象被重建并重新保存偎谁。
多次保存:remember 支持傳遞一個(gè)或者多個(gè) key 來控制 remember 是否需要重新執(zhí)行保存操作总滩。如果傳遞的 key 值中有一個(gè)值發(fā)生了變化都會(huì)導(dǎo)致 remember 再次執(zhí)行保存的操作。
inline fun <T> remember(
key1: Any?,
key2: Any?,
key3: Any?,
calculation: @DisallowComposableCalls () -> T
): T
rememberSaveable
rememberSaveable 的作用也是保存對(duì)象值搭盾,只要能被 bundle 保存的值咳秉,都可以使用 rememberSaveable 來保存。與 remember 不同的是鸯隅,rememberSaveable 是將數(shù)據(jù)保存到 bundle 中并序列化到本地進(jìn)行持久化存儲(chǔ)澜建,所以向挖,當(dāng) activity 或者 process 銷毀并重建了之后,也是可以獲取到之前保存了的對(duì)象值炕舵。
4.8 持久化存儲(chǔ)非 Parcelizable 對(duì)象的方式
- 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"))
}
}
- 為對(duì)象中的每個(gè)值設(shè)置一個(gè) key
- 提供 save 和 restore 方法
本質(zhì)上就是將對(duì)象中的每個(gè)值都使用 key-value 的方式存儲(chǔ)何之,并在獲取的時(shí)候,將 key-value 值重新組織成相應(yīng)的對(duì)象進(jìn)行返回咽筋。mapSaver 同樣也是存儲(chǔ)在 bundle 中溶推。
- 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 是針對(duì) mapSaver 的簡(jiǎn)化,直接以 list 的下標(biāo)作為對(duì)象中值的 key 來存儲(chǔ)數(shù)據(jù)奸攻;并通過下標(biāo)取出對(duì)象相應(yīng)屬性的值來完成對(duì)象的組裝工作蒜危。最終數(shù)據(jù)同樣是存儲(chǔ)在 bundle 中。
4.9 正確聲明 State 的三種方式
- val mutableState = remember { mutableStateOf(default) }
- var value by remember { mutableStateOf(default) }
- val (value, setValue) = remember { mutableStateOf(default) }
4.10 如何管理 state
State Holders
當(dāng)業(yè)務(wù)邏輯越來越復(fù)雜睹耐,使用的 state 狀態(tài)對(duì)象越來越多的時(shí)候辐赞,state 狀態(tài)對(duì)象及其業(yè)務(wù)邏輯的的維護(hù)成本越來越高。此時(shí)硝训,可以使用一個(gè)或多個(gè)單獨(dú)的 state holder 對(duì)象來統(tǒng)一管理這些 state 狀態(tài)對(duì)象及其業(yè)務(wù)邏輯响委。
State Holder 例子:
// 通過 StateHolder 來管理 UI 邏輯及 State 狀態(tài)對(duì)象
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 對(duì)象
@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 的幾種方式
- Composable 管理。當(dāng) composable UI 較簡(jiǎn)單窖梁,state 狀態(tài)對(duì)象不多時(shí)赘风, 可以直接放在 composable 中進(jìn)行管理。
- State Holder 管理纵刘。當(dāng) composable UI 較復(fù)雜時(shí)邀窃,可以單獨(dú)創(chuàng)建一個(gè) state holder 來管理其 state 狀態(tài)對(duì)象及其邏輯。
- Viewmodel 管理彰导』壮幔可以直接使用 ViewModel 來管理其 state 狀態(tài)對(duì)象。
ViewModel 相比于 StateHolder 的好處
- ViewModel 不受屏幕配置變化的影響位谋。
- ViewModel 與 Navigation 集成山析,當(dāng)頁面位于回退棧中時(shí),Navigation 會(huì)緩存 ViewModel掏父,這樣做的好處是:可以在返回到當(dāng)前頁面時(shí)笋轨,立即顯示之前加載過的數(shù)據(jù)。而 StateHolder 由于屏幕旋轉(zhuǎn)等會(huì)導(dǎo)致 state 對(duì)象的重建而丟失之前的數(shù)據(jù)赊淑。同時(shí)爵政,當(dāng)頁面從返回棧退出時(shí),ViewModel 會(huì)自動(dòng)被清除陶缺,而對(duì)于 StateHodler 來說钾挟,state 狀態(tài)會(huì)被一直保存。
- ViewModel 與一些其它庫集成(如:LiveData饱岸、Hilt),擴(kuò)展性更強(qiáng)掺出。
ViewModel 與 StateHolder 協(xié)同工作
雖然 ViewModel 相比于 StateHodler 來說徽千,有諸多好處,但兩者的定位還是有一定的差距汤锨。
- StateHodler 主要是用于管理 UI 邏輯及界面元素的狀態(tài)双抽。
- ViewModel 主要用于處理業(yè)務(wù)邏輯及返回待展示的數(shù)據(jù)。
總體來說闲礼,在管理 state 狀態(tài)對(duì)象的時(shí)候牍汹,兩者都能勝任,而在處理 UI 邏輯時(shí)柬泽,StateHodler 更加適合慎菲;而在處理業(yè)務(wù)邏輯時(shí),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 處理業(yè)務(wù)邏輯狀態(tài)
val uiState = viewModel.uiState
// StateHodler 處理界面元素狀態(tài)
val exampleState = rememberExampleState()
LazyColumn(state = exampleState.lazyListState) {
items(uiState.dataToDisplayOnScreen) { item ->
if (exampleState.isExpandedItem(item) {
...
}
...
}
}
}
State 與 Reomposition 的關(guān)系
當(dāng) UI 完成 initial composition 的加載后钧嘶,Compose 也完成了對(duì) state 狀態(tài)的追蹤。接下來琳疏,UI 的 recompostion(重新組合) 通常是通過 state 的改變成觸發(fā)的。當(dāng)對(duì)應(yīng)的 state 發(fā)生變化后闸拿,會(huì)觸發(fā)引用了發(fā)生改變 state 狀態(tài)對(duì)象的 composable 及其被直接或間接調(diào)用的子 composable 的重新執(zhí)行斩芭。當(dāng)然龙亲,由于 Compose 對(duì)這種刷新做了優(yōu)化,只會(huì)對(duì)那些輸入發(fā)生改變的 composable 進(jìn)行更新。
如何理解單向數(shù)據(jù)流(undirectional data flow)
單向數(shù)據(jù)流描述的是事件與狀態(tài)(數(shù)據(jù))的一種邏輯關(guān)系丽惶,即事件觸發(fā)狀態(tài)的改變,狀態(tài)改變后啦撮,觸發(fā) UI 的更新顾稀,整個(gè)過程是單向的。
5. Composable 的生命周期
[圖片上傳失敗...(image-1d35d9-1641520488295)]
Composable 的生命周期與 Compose 的 composition 綁定在了一起痒芝。主要分為三個(gè)部分:
- 進(jìn)入 composition俐筋,也就是當(dāng)前 composable 得到了 Compose 的執(zhí)行;
- Recompose 0 到多次严衬,composable 在進(jìn)入 composition 后澄者,又被重新執(zhí)行了 0 到多次;
- 退出 composition请琳,composable 在 Compose 進(jìn)行 composition 過程中粱挡,沒有得到執(zhí)行(非 composable 由于其狀態(tài)未發(fā)生變化而跳過)。
當(dāng)然俄精,這個(gè) composable 的生命周期并沒有那么嚴(yán)格的執(zhí)行順序询筏,通常會(huì)多次進(jìn)入 composition 后,運(yùn)行 0 到多次后竖慧,又退出 composition嫌套。
5.1 Composable 實(shí)例及唯一性確認(rèn)
每一個(gè) composable 在初次被調(diào)用的時(shí)候會(huì)生成一個(gè) composable 實(shí)例逆屡,同一個(gè) composable 被不同的地方調(diào)用,會(huì)生成多個(gè)實(shí)例灌危。也就是同一個(gè) composable 在輸入(參數(shù))不變的情況下康二,是否會(huì)創(chuàng)建新的實(shí)例是以調(diào)用點(diǎn)來判斷的。如果同一 composable 在同一調(diào)用點(diǎn)(如:for 循環(huán)創(chuàng)建 list item 對(duì)象)被調(diào)用多次勇蝙,則以執(zhí)行順序來區(qū)分不同的 composable 實(shí)例(默認(rèn)情況下會(huì)將同一調(diào)用點(diǎn)不同的執(zhí)行順序與當(dāng)前 composable 進(jìn)行關(guān)聯(lián)并唯一的標(biāo)識(shí)當(dāng)前 composable 實(shí)例)沫勿。
調(diào)用點(diǎn)(源代碼調(diào)用處)所對(duì)應(yīng)的 composable 已經(jīng)被創(chuàng)建后,默認(rèn)情況下會(huì)將同一調(diào)用點(diǎn)不同的執(zhí)行順序與當(dāng)前 composable 進(jìn)行關(guān)聯(lián)并唯一的標(biāo)識(shí)當(dāng)前 composable 實(shí)例味混。重復(fù)調(diào)用产雹,如果其輸入沒有變化的話,不會(huì)重新去創(chuàng)建翁锡,而會(huì)重用之前創(chuàng)建的實(shí)例蔓挖;如果輸入(參數(shù))發(fā)生變化,則會(huì)重新執(zhí)行相應(yīng)的 composable function馆衔,并創(chuàng)建新的 composable 實(shí)例瘟判。
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
// 同一調(diào)用點(diǎn)多次調(diào)用同一 Composable 對(duì)象,以執(zhí)行順序來區(qū)分不同的 Composalbe 實(shí)例角溃。
MovieOverview(movie)
}
}
}
5.2 按執(zhí)行順序區(qū)分不同實(shí)例的幾種情況
- 列表隊(duì)尾添加新的 Composable 實(shí)例
由于 recomposition 之前已經(jīng)創(chuàng)建的 composable 實(shí)例的執(zhí)行順序與 recomposition 時(shí)的執(zhí)行順序及輸入都未發(fā)生變化拷获,所以,recomposition 之前就已經(jīng)創(chuàng)建的 composable 對(duì)象會(huì)被 recomposition 重用减细。
- 列表隊(duì)頭添加新的 Composable 實(shí)例
由于 recomposition 之前已經(jīng)創(chuàng)建的 composable 實(shí)例的執(zhí)行順序已經(jīng)與 recomposition 時(shí)的執(zhí)行順序不同匆瓜,所以,recomposition 會(huì)為對(duì)應(yīng)的 composable 創(chuàng)建新的實(shí)例未蝌,而不會(huì)重用 recomposition 之前已經(jīng)創(chuàng)建好的實(shí)例驮吱。
- 列表隊(duì)中添加新的 Composable 實(shí)例
插入點(diǎn)之上的 composable 實(shí)例在 recomposition 過程中會(huì)被重用;插入點(diǎn)之后的 composable 實(shí)例都會(huì)被重新創(chuàng)建萧吠。
5.3 Compose 默認(rèn)根據(jù)什么規(guī)則來跳過 Recomposition左冬,如何是用關(guān)鍵字(如:key @state)來避免不必要的 Recomposition
默認(rèn)情況下,當(dāng) composable 的輸入是穩(wěn)定的類型且沒有改變時(shí)怎憋,recomposition 會(huì)跳過這些輸入穩(wěn)定且沒有改變的 composable又碌。
這里的穩(wěn)定類型說的是輸入類型對(duì)象本身是否是穩(wěn)定類型,這個(gè)是前提绊袋,如果不是穩(wěn)定類型毕匀,就算對(duì)象本身沒有變化,也會(huì)被重建癌别,而不會(huì)復(fù)用之前的 composable皂岔。
Stable 類型需要滿足的條件
- 對(duì)于相同的兩個(gè)對(duì)象實(shí)例,其 equals 必須相等
- 如果一個(gè)類型中的公共屬性發(fā)生了改變展姐,composation 的時(shí)候需要被通知
- 所有公共屬性的類型都必須是穩(wěn)定的
默認(rèn)被認(rèn)為是 Stable 的類型
- 基礎(chǔ)數(shù)據(jù)類型
- 字符串類型
- 所有的 lambda 類型
- 被 State 狀態(tài)對(duì)象所持有的值
為什么是這些類型躁垛?
因?yàn)檫@些類型都是 immutable(不可變)的類型剖毯,這些類型本身是不可能發(fā)生改變的。如果發(fā)生了改變教馆,那都是不同的對(duì)象了逊谋,Compose 能識(shí)別這種變化而觸發(fā) recomposition 的更新。
State 狀態(tài)對(duì)象的實(shí)例是 MutableState土铺,它雖然是一個(gè) mutable 可變類型胶滋,但是由于其所持有的屬性 value 在發(fā)生變化后,會(huì)通知給 composition 進(jìn)行相應(yīng)的更新悲敷,所以究恤,state 狀態(tài)對(duì)象,也是穩(wěn)定的后德。
State 類型對(duì)象是否改變的默認(rèn)認(rèn)定方式
當(dāng)作為參數(shù)傳遞給 composable 的所有類型都是 stable 時(shí)部宿,Compose 會(huì)使用 equals
來對(duì)各參數(shù)進(jìn)行比對(duì),如果都相等瓢湃,則認(rèn)為數(shù)據(jù)沒有發(fā)生變化理张,會(huì)跳過當(dāng)次的 composition。
使用 @Stable 注解將非 Stable 類型對(duì)象改為 Stable 類型
如果手動(dòng)為某個(gè)類或接口使用 @Stable 修飾后绵患,其所有對(duì)象或?qū)崿F(xiàn)都會(huì)被 Compose 認(rèn)定為 stable 狀態(tài)的類型涯穷。如果某個(gè) composable 接收的參數(shù)類型使用了 @Stable 修飾,則會(huì)直接使用 equals 來判斷當(dāng)前參數(shù)是否改變藏雏,進(jìn)而判斷是否需要在 recomposition 時(shí),重建當(dāng)前的 composable作煌。
6. Side-effect
6.1 什么是 Side-effect(附帶效應(yīng))
Side-effect:指在可組合函數(shù)范圍之外發(fā)生的應(yīng)用狀態(tài)變化掘殴。
這里如何理解什么是可組合函數(shù)范圍?
可組合函數(shù)范圍指的就是 composable 中只要其接收的 lambda 是一個(gè) composable 對(duì)象粟誓,被其所包含的內(nèi)容被稱為 可組合函數(shù)范圍
奏寨,如果有多個(gè)可組合函數(shù)存在包含關(guān)系,那這里就是一個(gè)遞歸關(guān)系鹰服。只要 composable lambda 直接包含的區(qū)域中的代碼就被認(rèn)為是在 可組合函數(shù)范圍
之外病瞳。下面代碼中的 content 和 label 所對(duì)應(yīng)的 lambda 中直接包含的內(nèi)容就是在 可組合函數(shù)范圍
之內(nèi)。除此之外的其它部分(如:Text 中的 click 點(diǎn)擊事件所對(duì)應(yīng)的 lambda)悲酷,都被稱為 可組合函數(shù)范圍
之外的區(qū)域套菜。
簡(jiǎn)而言之,被 composable function 或 composable lambda 直接包含的區(qū)域就被稱為 可組合函數(shù)之內(nèi)
设易,其他地方都被稱為 可組合函數(shù)范圍之外
逗柴。
@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") })
})
}
可組合函數(shù)范圍的概念理解了,那么什么叫做可組合函數(shù)范圍之外發(fā)生的應(yīng)用狀態(tài)變化呢顿肺?
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 中啟動(dòng)協(xié)程并修改 snackbar 狀態(tài)的操作就被稱為 可組合函數(shù)范圍之外發(fā)生的應(yīng)用狀態(tài)變化
戏溺。
6.2 常見的 Side-effects
LaunchedEffect
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
// implementation
}
LaunchedEffect 是一個(gè) composable function渣蜗,其作用為:在 composable 作用域啟動(dòng)一個(gè)協(xié)程來執(zhí)行傳遞給它的包含相應(yīng)邏輯的掛起函數(shù)。每一次執(zhí)行掛起函數(shù)的時(shí)候旷祸,都會(huì)啟動(dòng)一個(gè)新的協(xié)程耕拷,并取消上一次啟動(dòng)的協(xié)程。同時(shí)托享,當(dāng) LaunchedEffect 所綁定的 composable 在某次 composition 過程中骚烧,沒有被包括在內(nèi)時(shí),之前啟動(dòng)的協(xié)程也會(huì)被取消嫌吠。
block 執(zhí)行的條件:
- 初次調(diào)用 LaunchedEffect 函數(shù)時(shí)止潘,block 會(huì)被執(zhí)行;
- 再次調(diào)用 LaunchedEffect 函數(shù)時(shí)辫诅,只有所接收的一到多個(gè) key 值中至少有一個(gè)值發(fā)生了變化后凭戴,block 都會(huì)執(zhí)行。
Note:每一次 block 的執(zhí)行都是在新的協(xié)程中運(yùn)行炕矮。
@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
只會(huì)被打印一次,而 launchedEffect launched, refresh = $refresh
在每一次 MyScreen 被調(diào)用時(shí)肤视,都會(huì)被打印档痪。
協(xié)程取消的時(shí)機(jī):
- 當(dāng) LaunchedEffect 在連續(xù)兩次 composition 過程中,其綁定的 composable 都被調(diào)用時(shí)邢滑,前一次啟動(dòng)的協(xié)程會(huì)被取消腐螟。
- 當(dāng) LaunchedEffect 被調(diào)用后的下一次 composition 過程中,其綁定的 composable 沒有再被調(diào)用時(shí)困后,會(huì)取消其啟動(dòng)的協(xié)程乐纸。
rememberCoroutineScope
rememberCoroutineScope 是一個(gè) composable 方法,其作用是:創(chuàng)建一個(gè)綁定了 composition 的協(xié)程作用域摇予,該協(xié)程作用域可以在可組合函數(shù)范圍之外啟動(dòng)一個(gè)綁定了 composable 生命周期的協(xié)程汽绢。當(dāng)創(chuàng)建該協(xié)程作用域的 composable 在 composition 過程從顯示中被移除時(shí),其通過 rememberCoroutineScope 啟動(dòng)的協(xié)程就會(huì)被取消侧戴。
@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")
}
}
協(xié)程被取消的時(shí)機(jī):
- 當(dāng)調(diào)用 rememberCoroutineScope 的 composable 在 composition 過程中宁昭,沒有被顯示到頁面上,會(huì)取消使用 rememberCoroutineScope 啟動(dòng)的所有協(xié)程酗宋。
rememberUpdatedState
作用:保存某個(gè)參數(shù)或者狀態(tài)的最新值积仗,當(dāng)被調(diào)用的時(shí)候,返回已保存的最新值蜕猫。
@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 作用:?jiǎn)?dòng)一個(gè)提供了回收方法的 LaunchedEffect(啟動(dòng)了一個(gè)協(xié)程)斥扛,當(dāng) DisposableEffect 在某次 composition 過程中沒有被執(zhí)行,則會(huì)取消之前啟動(dòng)的協(xié)程,并會(huì)在取消協(xié)程前調(diào)用其回收方法進(jìn)行資源回收相關(guān)的操作稀颁。
@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 綁定芬失,當(dāng)綁定的 composable 在 recomposition 過程中被更新時(shí),使用 SideEffect 來更新非 Compose 代碼匾灶。SideEffect 并未接收任何 key 值棱烂,所以,其只要被調(diào)用阶女,就會(huì)執(zhí)行其 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 狀態(tài)的對(duì)象,通過 produceState 的包裝后秃踩,轉(zhuǎn)化為 Compose state 狀態(tài)對(duì)象衬鱼,便于在 Compose 中直接與 composable 進(jìn)行綁定。
@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)
}
}
}
將網(wǎng)絡(luò)請(qǐng)求回來的 Result 普通對(duì)象轉(zhuǎn)換為 Compose state 對(duì)象憔杨。
derivedStateOf
將一個(gè)或多個(gè) Compose state 狀態(tài)對(duì)象轉(zhuǎn)化成一個(gè)新的 Compose state 對(duì)象鸟赫,并且當(dāng)舊的 state 狀態(tài)或者 derivedStateOf 所引用的變量發(fā)生改變時(shí),都會(huì)引起新 state 對(duì)象的聯(lián)動(dòng)更新消别。
@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 經(jīng)過 highPriorityKeywords 過濾后抛蚤,轉(zhuǎn)換成了新的 highPriorityTask state 對(duì)象。當(dāng) todo state 或者 highPriorityKeywords 的狀態(tài)發(fā)生變化時(shí)寻狂,都會(huì)引起 highPriorityTask state 的更新岁经。
snapshotFlow
在 Compose 中創(chuàng)建一個(gè) flow,并與 state 狀態(tài)對(duì)象綁定蛇券。當(dāng) state 對(duì)象發(fā)生改變時(shí)缀壤,會(huì)通過 flow 發(fā)送出去。
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
上面的代碼在 Effect 創(chuàng)建一個(gè) flow 并綁定了 listState 狀態(tài)對(duì)象纠亚,當(dāng) listState 狀態(tài)發(fā)生變化時(shí)诉位,都會(huì)通過該 flow 把變化后的值發(fā)送出去。