Jetpack Compose - Effect與協程 (十五)

語言: CN / TW / HK

SideEffect

大家都知道在Compose中有一個重組的概念,也就是Recompose, 一般是因為資料來源發生了變化,介面跟隨要發生變化的場景, 但是有時候我們要考慮兩種場景:

1.某個Composable函式 在執行的過程中,因為資料來源發生了變化,所以執行到一半 又重新執行了 但是在這個Composable函式中,我們還有其他的一些程式碼,跟ui無關的,這樣這些程式碼會執行多次,有時候這個執行多次的程式碼 也許並不符合我們的需求

2.在某個Composable函式 中,我們有一段程式碼,這個程式碼我就是僅僅想讓他在生命週期內 只執行一次,不想他因為Recompose的緣故 會執行多次

這個時候 我們就可以使用SideEffect

``` Row() { var count=0 Column() { SideEffect { println("hello side Effect") }

    var names= arrayOf("111","222","333","444")
    for (name in names){
        Text(text = name)
        count++
    }
    Text(text = "count:$count")

}

} ```

他的作用就是 有2點: 1. 被SideEffect包裹起來的 程式碼 只會執行一次 2. 在重組的過程中,SideEffect 只會在重組結束之後 被執行

DisposableEffect

這個effect的作用 主要就是可以監聽元件的 是否展示中,也就是元件 在介面內展示出來了,還是在介面外沒有展示

``` setContent {

var flag = remember {
    mutableStateOf(true)
}


Column {
    if (flag.value){
        Text(text = "hello", modifier = Modifier.clickable {
            flag.value = !flag.value
        })
        DisposableEffect(Unit) {
            Log.v("wuyue", " coming ")
            onDispose {
                Log.v("wuyue", "leave")
            }
        }
    }


    Text(text = "change flag", modifier = Modifier.clickable {
        flag.value = !flag.value
    })
}

} ```

可以執行一下程式碼看一下 ,hello的這個text 每次展示 都會列印coming,同樣的每次不展示消失的時候 也會列印leave

其實到這裡也能猜到了,這些SideEffect,以及DisposableEffect中的onDispose函式 本質上都是回撥函式 在重組的生命週期的各個階段會走這些回撥函式,僅此而已

另外要注意的是,如果可見性沒有發生變化,那麼Disposable 也是不會有變化的 比如下面的程式碼

``` setContent {

var flag by remember {
    mutableStateOf("hello")
}


Column {
    Log.v("wuyue", " compose ")
    Text(text = flag, modifier = Modifier.clickable {
            flag = "$flag:${Math.random()}"
        })
        DisposableEffect(Unit) {
            Log.v("wuyue", " coming ")
            onDispose {
                Log.v("wuyue", "leave")
            }
        }
    }

} ```

這裡就是隻會改變text的內容,text元件雖然改變了,觸發了Column這個元件的recompose, 但是因為text元件的可見性沒有發生變化,所以DisposableEffect 只會執行coming這行程式碼,而且只執行一次 leave 是不會執行的

同樣的 我們也可以看到,這個effect是有一個key引數的,這個引數的作用就是 當key發生變化的時候 DisposableEffect 也會得到執行 ,不管可見性有沒有發生變化

image.png

還是上面的例子,我們稍微改一下:

``` setContent {

var flag by remember {
    mutableStateOf("hello")
}


Column {
    Log.v("wuyue", " compose ")
    Text(text = flag, modifier = Modifier.clickable {
            flag = "$flag:${Math.random()}"
        })
        DisposableEffect(flag) {
            Log.v("wuyue", " coming ")
            onDispose {
                Log.v("wuyue", "leave")
            }
        }
    }

} ```

這個時候你就會發現,每次點選的時候,先觸發了重組,然後觸發了leave回撥,再觸發了coming回撥

image.png

LaunchedEffect

這個東西和上面2個小節的effect 作用就不太一樣了,這個effect主要的作用主要是在Compose中啟動一個協程 而且具有2個特點 1. 在重組過程完成以後 才會啟動協程 2. key 發生變化的時候 也會啟動協程

同樣的 ,這個Effect的引數和DisposableEffect 其實是一樣的

image.png

這裡就不演示具體的程式碼了,因為和上一個小節的內容是差不多的,唯一的區別其實就是這個effect是專門為協程準備的,僅此而已

首先看下 下面這段程式碼

``` setContent { Column { Log.v("wuyue"," Recompose ") var text by remember { mutableStateOf("custom") } Text(text = "hello", Modifier.clickable { text = "${Math.random()}" }) LaunchedEffect(Unit) { delay(3000) Log.v("wuyue"," text: $text ") } }

} ```

這個3s之後的列印 應該能猜到 列印的值應該是隨機數了, 但是我如果稍微改一下

@Composable fun printlnCompose(text:String){ LaunchedEffect(Unit) { delay(3000) Log.v("wuyue"," text: $text ") } }

``` setContent { Column { Log.v("wuyue"," Recompose ") var text by remember { mutableStateOf("custom") } Text(text = "hello", Modifier.clickable { text = "${Math.random()}" }) printlnCompose(text) }

} ```

這個時候你就會發現,3s之後的列印 還是custom,而不會是隨機數了。這是為啥?

其實問題出在這個函式引數這裡:

image.png

這個函式引數是一個普通型別的String,這會導致 我們的LanunchedEffect 感知不到我們的 text發生了變化

所以要改一下:

``` @Composable fun printlnCompose(text: String) { var rememeberText by remember { mutableStateOf(text) } rememeberText = text

LaunchedEffect(Unit) {
    delay(3000)
    Log.v("wuyue", " text: $rememeberText ")
}

} ```

我們只要稍微的手動進行轉換一下 即可,將這個text 手動轉換成 一個remember型別的變數即可

上述的寫法 也可以用一個簡便的寫法:

@Composable fun printlnCompose(text: String) { var rememeberText = rememberUpdatedState(newValue = text) LaunchedEffect(Unit) { delay(3000) Log.v("wuyue", " text: $rememeberText ") } }

看下原始碼,其實底層和我們的程式碼是以一樣的 美

image.png

為什麼不能在Compose中 隨意啟動一個協程?

有人要問了,為啥在compose中啟動一個協程這麼麻煩?可以看下面截圖的報錯資訊

image.png 她告訴你 ,這個協程 必須要在Lanunedeffect中使用,直接用是不行的,為什麼? 因為Kotlin中 所有的協程都需要一個Scope,這個Scope主要的作用就是在 某一個時刻將你的協程取消我們的lifeCycleScope 是和activity的生命週期繫結在一起的,並沒有和compose繫結在一起,所以 如果Compose 不加這個限制,那麼協程執行在compose中就會出錯了

所以我們在Compose中使用協程的前提條件是必須得有一個和Compose生命週期繫結在一起的scope

image.png

一看圖,還是錯了,還是不給我們用嗎,為嘛?

因為這裡compose中有重組的概念,所以你必須要用一個remember去包裹一下,否則每次重組你的協程都要執行一次 那不是亂套了嘛

所以其實你看LanunchedEffect 也是類似的封裝思路

image.png

image.png

有人可能會奇怪,既然如此,為啥還要對外暴露這個rememberScope的回撥?,直接private 這個remember不行嗎 ,反正單獨呼叫她也沒用

可以看一下 下面這行程式碼:

``` val scope = rememberCoroutineScope() Text(text = "hello", Modifier.clickable { scope.launch {

} }) ``` 在這個點選事件裡面,他其實並不屬於Compose的環境了,所以我們只需要一個scope 即可完成協程的啟動, 你甚至可以在這裡直接啟動一個lifeCycleScope的協程

唯一的區別僅僅在於 你到底希望你的協程在哪個scope的生命週期裡 被結束掉。

Compose狀態轉換

在Compose中,一個介面元件要響應一個變數的變化,這個變數必須是一個state型別的物件

image.png

通常而言,我們會使用MutableState這個state的子介面,因為state是可讀,而MutableState是可讀可寫在有時候,我們變化的資料來源 如果不是一個MutableState 那怎麼辦? 怎麼讓Compose的頁面來感知我們的資料來源變化?

例如,我們要感知使用者的地理位置,這個不斷變化的地理位置 怎麼讓Compose的介面可以感知到?因為地址位置的變更顯然返回的是一個座標,而不是一個state

可以參考如下程式碼

``` var address by remember { mutableStateOf(Point(0, 0)) }

Text(text = address.toString())

DisposableEffect(Unit){ val callBack = object : GetAddressInfo { override fun getAddressInfo(p: Point) { address = p }

}
// register callback
onDispose {
    //unregister callback
}

} ``` 同樣的 對於livedata來說 我們想感知介面的變化,那也是需要轉成state的,好在runtime庫 幫我們把這個操作做了

注意要引入新的依賴

implementation "androidx.compose.runtime:runtime:$compose_version" implementation "androidx.compose.runtime:runtime-livedata:$compose_version" implementation "androidx.compose.runtime:runtime-rxjava2:$compose_version"

image.png

對於flow來說 也有對應的轉換方式

val flow: StateFlow<Point> = MutableStateFlow(Point(0, 0)) val flowState = produceState(Point(0, 0)) { flow.collect { value = it } } 同樣的 也有更加簡便的寫法:

flow.collectAsState()

image.png

snapshotflow

前面一個小節 我們介紹了各種資料型別向Compose的state轉換的方法,這一小節來簡單介紹一下 state如何向flow去轉換,簡單來說 就是把state的變化, 利用flow 通知出去

``` Column { var text by remember { mutableStateOf("hello") } var flow = snapshotFlow { text } LaunchedEffect(key1 = Unit){ flow.collect{ Log.v("wuyue","it:$it") } } Text(text = text, Modifier.clickable { text = "${Math.random()}" })

} ``` 注意了 snapshotFlow 是可以感知多個state的變化的

``` Column { var text by remember { mutableStateOf("hello") }

var age by remember {
    mutableStateOf(18)
}
var flow = snapshotFlow {
    "$text $age"
}
LaunchedEffect(key1 = Unit){
   flow.collect{
       Log.v("wuyue","it:$it")
   }
}
Text(text = text, Modifier.clickable {
    text = "${Math.random()}"
})

Text(text = "age:$age", Modifier.clickable {
    age = (Math.random() * 100).toInt()
})

} ```