Jetpack Compose中的附带效应简介及使用
前言
附带效应是指LaunchedEffect、DisposableEffect、rememberCoroutineScope、rememberUpdatedState、produceState 、derivedStateOf的使用。附带效应这4个字在google官方文档上的表达与解释挺让人难以理解的。其实个人认为准确的描述应该是外部产生的数据向Compose状态作用域内传递。这里的外部数据是指网络请求数据、数据库数据、定时触发状态、子线程运算数据、Activity生命周期等等这些数据。
在学了Compose的状态管理后可以明白,Compose的重组需要依靠mutableStateOf 。那为什么还需要有LaunchedEffect、DisposableEffect等等这些函数呢?理由如下:
- 在Compose作用域里实现协程与异步操作,从而对一些资源的初始化与销毁进行异步操作
- 接收外部数据回调,但是建议尽量选择用协程取代接口回调方法
- 配合LifeCycle,让Compose对Activity的生命周期实现监听(这个其实与第二点其实是一样的,但是使用情况比较多这里单独拿出来强调)
- 进一步的控制Compose重组
个人的一些见解,这些见解你可以不认同。因为可能是我前期的粗浅认识。我认为Compose的附带效应各种方法很多,官方文档的举例也非常难以理解。其实根本原因是附带效应的各色方法使用场景其实有一部分相当重叠,容易让人一时无法判断我学习到的这些方法应该在什么情况下使用。有一些人会认为其实只要有DisposableEffect就能解决全部Compose里需要的协程异步、数据获取、资源初始化与释放完全不需要依靠其他方法,这个我也无法否认因为DisposableEffect的确可以满足以上全部功能。为什么附带效应会有这么多使用重叠的方法?我个人理解是jetpack的开发组,内部就有各色开发人员倾向不同的架构与思想。比如有人喜欢直接在Activity请求网络数据,有些人喜欢用MVP、MVC、MVVM这些架构请求网络数据、有一些人则倾向用RxJava、flow这种链式编程请求网络数据。他们为了满足这些干脆搞了一个大杂烩,让各种使用场景都能有各色附带效应方法使用。
所以,我这里建议是你需要全部学会附带效应的方法。但是使用的时候只需要挑两三个(这里推荐LaunchedEffect、rememberCoroutineScope、DisposableEffect)你习惯使用的就行了... 不要过度思考什么场景使用什么附带效应方法。有些东西你会有一天突然醒悟的. 在醒悟之前用你最习惯最稳妥的方式写是最安全的。
LaunchedEffect
LaunchedEffect的作用主要是让你在Compose作用域里实现协程工作,以完成异步、耗时数据处理、网络请求等等工作。
简单的延迟计数
@Preview()
@Composable
fun MyLaunchedEffect() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(true) {
while (true) {
delay(1000)
count.value++
}
}
Text(text = "${count.value}", fontSize = 100.sp)
}
效果图:
模拟请求网络数据
@Preview()
@Composable
fun MyLaunchedEffect() {
//是否请求数据
val isPostData = remember {
mutableStateOf(false)
}
//请求结果
val postResult = remember {
mutableStateOf("")
}
if (isPostData.value) {
LaunchedEffect(isPostData.value) {
//这里进行耗时操作处理异常,比如请求网络,从数据库获取数据
postResult.value = withContext(Dispatchers.Default){
delay(1000)
return@withContext "模拟请求结果>>成功 ${System.currentTimeMillis()}"
}
//请求完成后重新设置成false,这样就可以继续下一次请求
isPostData.value = false
}
}
Button(onClick = { isPostData.value = true }) {
Text(text = postResult.value)
}
}
效果图:
LaunchedEffect参数的作用
其实很简单,如果参数一样,那么LaunchedEffect只会触发一次,如果参数一直变化则会多次触发。但是如果你希望LaunchedEffect在指定情况下触发你还是需要向上面的例子里一样添加if。 否则LaunchedEffect在Compose创建的时候会立刻触发。
var countKey1 = 0
@Preview()
@Composable
fun MyLaunchedEffect() {
val countKey2 = remember {
mutableStateOf(0)
}
LaunchedEffect("A") {
Log.e("zh", "MyLaunchedEffect: A")
}
LaunchedEffect(countKey1) {
Log.e("zh", "MyLaunchedEffect: B")
}
LaunchedEffect(countKey2.value) {
Log.e("zh", "MyLaunchedEffect: C")
}
Button(onClick = {
countKey1++
countKey2.value++
}) {
}
}
连续点击后按钮效果如下,在下面的log里可以看到A触发了一次,B和C每次点击后都触发。这里需要使用mutableStateOf创建的countKey2来触发Compose的重组,从而验证普通Int值的countKey1也能触发LaunchedEffect的执行。
rememberCoroutineScope
rememberCoroutineScope 其实与 LaunchedEffect 功能上一致,但是LaunchedEffect 只能在compose的作用域中调用,无法在按键点击回调等等地方调用,rememberCoroutineScope可以在这些地方调用。此外,如果您需要手动控制一个或多个协程的生命周期,请使用 rememberCoroutineScope,例如在用户事件发生时取消动画。
下面的代码模拟了一个网络请求,其中引用了 implementation 'androidx.compose.runtime:runtime-livedata:1.2.1' ,把LiveData 转成 MutableState
代码:
ViewModel
class MainViewModel: ViewModel() {
private val _data = MutableLiveData<String>()
val data : LiveData<String> get() = _data
suspend fun postData(){
delay(500)
//模拟网络的异步请求
_data.postValue("当前时间戳 = ${System.currentTimeMillis()}")
}
}
activity
@Preview()
@Composable
fun MyCoroutineScope() {
val coroutineScope = rememberCoroutineScope()
val data = mViewModel.data.observeAsState()
Column {
Button(onClick = {
coroutineScope.launch {
mViewModel.postData()
}
}) {
}
Text(text = data.value ?: "")
}
}
rememberUpdatedState
rememberUpdatedState 只是辅助LaunchedEffect向外部(其他composeable方法)更新状态功能,用于在不同compose方法下LaunchedEffect的数据更新时提供最新数据。下面提供2个例子代码来验证效果。
不使用rememberUpdatedState
@Composable
fun MyTextField() {
val text = remember {
mutableStateOf("")
}
MyLaunchedEffect(content = text.value)
Column {
TextField(value = text.value, onValueChange = {
text.value = it
})
}
}
@Composable
fun MyLaunchedEffect(content:String){
Log.e("zh", "MyLaunchedEffect重组中 content = ${content}")
LaunchedEffect(Unit){
while (true){
delay(500)
Log.e("zh", "content = ${content}")
}
}
}
在输入框输入内容后,在日志里会反复打印代码。并没有新的 content 内容输出。
使用rememberUpdatedState
@Composable
fun MyTextField() {
val text = remember {
mutableStateOf("")
}
MyLaunchedEffect(content = text.value)
Column {
TextField(value = text.value, onValueChange = {
text.value = it
})
}
}
@Composable
fun MyLaunchedEffect(content:String){
val contentUpdatedState by rememberUpdatedState(content) //添加rememberUpdatedState
Log.e("zh", "MyLaunchedEffect重组中 content = ${contentUpdatedState}")
LaunchedEffect(Unit){
while (true){
delay(500)
Log.e("zh", "content = ${contentUpdatedState}")
}
}
}
在输入框输入内容后,日志里会打印输入的最新内容。
DisposableEffect
DisposableEffect 必须添加一个onDispose方法
,此方法一般用来处理资源清理与释放。 比如监听了生命周期、监听了广播等等情况。另外DisposableEffect 也带参数意义和LaunchedEffect的参数一样。
下面举例了官方文档上的监听了生命周期代码例子:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HomeScreen(LocalLifecycleOwner.current, {
//onStart
Log.e("zh", "触发了onStart生命周期" )
}, {
//onStop
Log.e("zh", "触发了onStop生命周期" )
})
}
}
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit,
onStop: () -> Unit
) {
//这里创建了2个更新状态,用于更新start生命周期与stop生命周期的回调
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
//创建一个Activity生命周期观察者
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
//添加监听
lifecycleOwner.lifecycle.addObserver(observer)
//在onDispose方法里移除了生命周期监听
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
}
SideEffect
SideEffect 其实就是不带参数的LaunchedEffect,并且SideEffect会在每一次重组的时候都触发。
代码:
@Preview
@Composable
fun MySideEffect() {
val count = remember { mutableStateOf(0) }
SideEffect {
Log.e("zh", "SideEffect触发${count.value}")
}
Text(text = count.value.toString(),fontSize = 36.sp, modifier = Modifier.clickable {
count.value++
})
}
produceState
produceState将非 Compose 状态转换为 Compose 状态,produceState的使用场景其实与LaunchedEffect多多少少有些重叠。google设计produceState的目的应该是使用在大量重复的网络请求情况下,比如加载网络图片、下载音频视频。这类情况有一个共同场景你可能不需要mutableStateOf 来保存数据。
下面的例子将网络请求的结果转换成Compose的状态,
代码:
@Composable
fun PostState() {
var updatePostData by remember {
mutableStateOf(true)
}
val state = produceState<Result<String>>(initialValue = Result(-1,"请求中", ""), updatePostData) {
value = if (true) { //模拟网络请求请求结果
Result(200,"请求成功", "{\"id\": \"1\",\"name\": \"小明\", \"time\":${System.currentTimeMillis()}}")
} else {
Result(400,"请求失败", "")
}
}
Text(text = "${state}", modifier = Modifier.clickable {
updatePostData = !updatePostData
})
}
附带效应重组后什么时候执行
重组触发顺序如下:
首先携带状态数据触发重组的View >> Compose的其他View重组 >> DisposableEffect >> SideEffect >> LaunchedEffect
代码:
@Preview
@Composable
fun MyRecombine() {
val count = remember { mutableStateOf(0) }
Log.e("zh", "外部重组1111")
LaunchedEffect(count.value){
Log.e("zh", "LaunchedEffect: 重组3333")
}
SideEffect {
Log.e("zh", "SideEffect: 重组2222")
}
DisposableEffect(count.value){
Log.e("zh", "DisposableEffect: 重组4444")
onDispose {
}
}
Log.e("zh", "外部重组5555")
Column {
Log.e("zh", "Column内部重组6666")
Text(text = "重组",fontSize = 36.sp)
Text(text = count.value.toString(),fontSize = 36.sp, modifier = Modifier.clickable {
count.value++
Log.e("zh", "Text: 重组7777")
})
}
}