今天大概會聊到的範圍
- rememberUpdateState
上一篇聊到,SideEffect
周邊還有一堆和 state & effect 相關的 API。今天想要聊聊 rememberUpdatedState
。
當今天我們要把某個資料/事件從 composable 傳出來的時候,我們可以用 callback 的形式將資料往外傳:
@Composable
fun MyComposable(callback: () -> Unit ) {
// childBtn
Button( onClick = { callback() }) {
Text("click")
}
}
在某些狀況,callback 可能會改變。但是 callback 在還沒被呼叫之前,其實都和這個 composable 無關:
@Composable
fun MyComposable(callback: () -> Unit ) {
Button( onClick = { callback() }) {
Text("click")
}
}
@Compoable
fun ParentComposable() {
Column {
var count by remember { mutableStateOf(0) }
var callback by remember { mutableStateOf({
Log.d(TAG, "MyComposable: callback init")
Unit
}) }
MyComposable(callback)
// parentBtn
Button(onClick = { count++ }) {
Text("Change")
callback = { Log.d(TAG, "MyComposable: callback $count") }
}
}
}
舉個例,假設有兩層的 Button,內層的 Button 點完之後會 log 一段文字。外層的 Button 每次點擊的時候都會更新內層 Button 點擊後 log 的文字。但因為 callback 是透過參數傳入,這個參數又有被 Button 用到。因此,每次 parentBtn
點擊一次,childBtn
都會觸發 recomposition,即便 MyComposable
其實根本沒有改變。
為了讓 MyComposable
不要觸發不必要的 recomposition,我們可以將 callback 給 remember
起來。remember
會在第一次 composition 執行時去運算 lambda 內的資料一次並存放起來,後面因為 Button 不是 reference 到 callback,而是 remember
後的結果,所以 callback 改變不會觸發 recomposition。
@Composable
fun MyComposable(callback: () -> Unit) {
val callbackRemembered by remember { mtableStateOf(callback) }
Button(onClick = { callbackRemembered() }) {
Text("click")
}
}
不對啊,remember 記起來了就不會再改了。這樣怎麼呼叫 callback 都會呼叫到同一個 callback 啊?這時候就是 rememberUpdatedState
發揮效果的時候了!
@Composable
fun MyComposable(callback: () -> Unit) {
val callbackRemembered by rememberUpdatedState(callback)
Button(onClick = { callbackRemembered() }) {
Text("click")
}
}
rememberUpdatedState
會永遠記錄最新的資料,但是透過 rememberUpdatedState
包裝後的資料 ( callbackRemembered
) 卻不會被視為修改,因此不會觸發 recomposition 。
其實前面舉 Button 的例子不太好,因為 rememberUpdateState
最好被使用的時機點是當這個資料需要用在 SideEffect
之中的情況。舉個例:
@Composable
fun MyComposable(onDisposeCallback: () -> Unit) {
val onDisposeRem by rememberUpdatedState(newValue = onDisposeCallback)
DisposableEffect(Unit) {
// do something
onDispose {
onDisposeRem()
}
}
}
假設我們要在 dispose
的時候做某件事情,但是這件事情需要外部的人提供參數(這裡是舉 callback 的例子,其實不一定是 callbck 的情境),這時我們就可以透過 rememberUpdatedState
將資料記住。
那如果我們不用 remember
呢?不能單純將參數傳入並且使用嗎?
@Composable
fun MyComposable(onDisposeCallback: () -> Unit) {
DisposableEffect(Unit) {
// do something
onDispose {
onDisposeCallback()
}
}
}
因為 Effect 會在首次 composition 時建立,並且在 key 值沒有修改之前不會重新建立 ( 此處的 key = Unit ),因此在 onDispose
時只會觸發第一次拿到的 onDisposeCallback
。若要利用 key 值改變會重建 DisposeEffect
的機制,又會讓 Dispose lambda 和 onDispose lambda 沒必要的重複觸發。SideEffect
、LaunchedEffect
等其他的 side-effect API 也都有一樣的情況。
其實仔細看看 rememberUpdatedState
其實也就是 remember
和 mutableStateOf
的組合:
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
差別在最後一行的 apply
,當參數改變並觸發 recompose 時,rememberUpdatedState
會透過 apply
將 value 寫進 state 中,達到修改 state 的效果。
今天主要是研究了 rememberUpdatedState
這個工具。有點慶幸這次我是因為看了文件發現有這個東西,要是我是直接在遇到問題時需要這個東西,感覺一定會摸不著頭緒也不知道該從何查起。Side effect 和 state mangement 是 compose 中重要的大主題,感覺還有很多東西可以挖,希望之後可以多挖一點來和大家分享
Reference:
一年後補充:其實這邊的用詞不太精準
SideEffect, LaunchedEffect 與 DisposableEffect 都是 Compose Effect API 的一員。文章中有時會把 "Effect API" 和 "SideEffect" 兩者混用