Jetpack Compose - Effect与协程 (十五)

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 也会得到执行 ,不管可见性有没有发生变化

还是上面的例子,我们稍微改一下:

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回调

LaunchedEffect

这个东西和上面2个小节的effect 作用就不太一样了,这个effect主要的作用主要是在Compose中启动一个协程 而且具有2个特点

  1. 在重组过程完成以后 才会启动协程
  2. key 发生变化的时候 也会启动协程

同样的 ,这个Effect的参数和DisposableEffect 其实是一样的

这里就不演示具体的代码了,因为和上一个小节的内容是差不多的,唯一的区别其实就是这个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,而不会是随机数了。这是为啥?

其实问题出在这个函数参数这里:

这个函数参数是一个普通类型的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 ")
    }
}

看下源码,其实底层和我们的代码是以一样的 美

为什么不能在Compose中 随意启动一个协程?

有人要问了,为啥在compose中启动一个协程这么麻烦?可以看下面截图的报错信息

她告诉你 ,这个协程 必须要在Lanunedeffect中使用,直接用是不行的,为什么? 因为Kotlin中 所有的协程都需要一个Scope,这个Scope主要的作用就是在 某一个时刻将你的协程取消我们的lifeCycleScope 是和activity的生命周期绑定在一起的,并没有和compose绑定在一起,所以 如果Compose 不加这个限制,那么协程运行在compose中就会出错了

所以我们在Compose中使用协程的前提条件是必须得有一个和Compose生命周期绑定在一起的scope

一看图,还是错了,还是不给我们用吗,为嘛?

因为这里compose中有重组的概念,所以你必须要用一个remember去包裹一下,否则每次重组你的协程都要执行一次 那不是乱套了嘛

所以其实你看LanunchedEffect 也是类似的封装思路

有人可能会奇怪,既然如此,为啥还要对外暴露这个rememberScope的回调?,直接private 这个remember不行吗 ,反正单独调用她也没用

可以看一下 下面这行代码:

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

   }
})

在这个点击事件里面,他其实并不属于Compose的环境了,所以我们只需要一个scope 即可完成协程的启动, 你甚至可以在这里直接启动一个lifeCycleScope的协程

唯一的区别仅仅在于 你到底希望你的协程在哪个scope的生命周期里 被结束掉。

Compose状态转换

在Compose中,一个界面组件要响应一个变量的变化,这个变量必须是一个state类型的对象

通常而言,我们会使用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"

对于flow来说 也有对应的转换方式

val flow: StateFlow<Point> = MutableStateFlow(Point(0, 0))
val flowState = produceState(Point(0, 0)) {
    flow.collect {
        value = it
    }
}

同样的 也有更加简便的写法:

flow.collectAsState()

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()
    })

}

作者:vivo祁同伟
链接:https://juejin.cn/post/7181375063449403450

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容