Compose 中的附带效应

附带效应是指发生在可组合函数作用域之外的应用状态的变化。由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无附带效应的。凡是回影响外界的操作都属于副作用,比如弹toast,保存本地文件,访问远程或本地数据等。在Compose中常用的以及介绍的一共有8种,本篇将详细介绍其使用。

LaunchedEffect:在某个可组合项的作用域内运行挂起函数

常用的构造函数如下

fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

用于处理异步任务,在Composable进入组件树时或者key发生变化时启动协程执行block中的内容,可以在其中启动子协程或者调用挂起函数。如果key发生变化,当前协程会自动结束并开启新的协程,Composable进入onDispose(离开组件树)时,协程会自动取消。使用时的demo如下

@Composable
fun ScaffoldSample(
    state: MutableState<Boolean>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {

    //这种写法第一次也会弹出,每次更新state值时(改变key值)
    //都会重新调用LaunchedEffect中的block函数
    /*   LaunchedEffect(state.value) {
           scaffoldState.snackbarHostState.showSnackbar(
               message = "Error msg",
               actionLabel = "Retry message"
           )
       }*/

    //这种写法是组件添加或从组件树上移除
    if(state.value){
        LaunchedEffect(Unit) {
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error msg",
                actionLabel = "Retry message"
            )
        }
    }

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = { Text(text = "脚手架示例") })
        },
        content = {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Button(onClick = {
                    state.value = !state.value
                }) {
                    Text(text = "Error occurs")
                }
            }
        }
    )
}

@Composable
fun LaunchedEffectSample() {
    val state = remember { mutableStateOf(false) }
    ScaffoldSample(state)
}

rememberCoroutineScope:获取组合感知作用域,以便在可组合项外启动协程

在非Composable环境中使用协程,比如在Button的onClick中使用协程显示SnackBar,并希望在onDispose(离开组件树)时自动取消,此时可以使用rememberCoroutineScope。它可以在当前Composable进入onDispose时自动取消,简单的示例如下

@Composable
fun RememberCoroutineScopeSample() {
    val scaffoldState = rememberScaffoldState()
    //创建协程作用域,在多个地方使用(floatingActionButton,IconButton中的onClick)
    val scope = rememberCoroutineScope()

    Scaffold(
        scaffoldState = scaffoldState,
        //标题栏区域
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = "脚手架示例"
                    )
                },
                navigationIcon = {
                    IconButton(onClick = {
                        scope.launch {
                            scaffoldState.drawerState.open()
                        }
                    }) {
                        Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
                    }
                })
        },
        //屏幕内容区域
        content = {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Text(text = "屏幕内容区域")
            }
        },

        //左侧抽屉
        drawerContent = {

            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Text(text = "抽屉中的内容")
            }
        },

        //悬浮按钮
        floatingActionButton = {
            ExtendedFloatingActionButton(
                text = { Text(text = "悬浮按钮") },
                onClick = {
                    scope.launch { scaffoldState.snackbarHostState.showSnackbar("我是msg") }
                })
        },
        floatingActionButtonPosition = FabPosition.End,
        snackbarHost = {
            SnackbarHost(hostState = it) { data ->
                Snackbar(
                    snackbarData = data,
                    backgroundColor = MaterialTheme.colors.surface,
                    contentColor = MaterialTheme.colors.onSurface,
                    shape = CutCornerShape(10.dp)
                )
            }
        }
    )
}

rememberUpdatedState:在效应中引用某个值,该效应在值改变时不应重启

如果key值有更新,那么LaunchedEffect在重组时就会被重新启动,但是有时候需要在LaunchedEffect中使用最新的参数值,但是又不想重新启动LaunchedEffect,此时就需要使用rememberUpdateState。它的作用就是给某个参数创建一个引用,并保证其值被使用时是最新值,而且参数改变时不重启effect。比如对生命周期的监听

@Composable
fun LifeAwareScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit,
    onStop: () -> Unit
) {
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            //回调onStart或者onStop
            when (event) {
                Lifecycle.Event.ON_START -> {
                    currentOnStart()
                }
                Lifecycle.Event.ON_STOP -> {
                    currentOnStop()
                }
                else -> {}
            }


        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

在上面的代码中,当LifecycleOwner变化时,需要终止对当前LifecycleOwner的监听,并重新注册Observer,因此必须将其添加为观察参数。而currentOnStart和currentOnStop只要保证在回调它们的时候可以获取最新值即可,所以应该通过rememberUpdateState包装后在副作用中使用,不应该因为他们的变动终止副作用。

DisposableEffect:需要清理的效应

它可以感知Composable的onActive(进入组件树)和onDispose(离开组件树),允许通过副作用完成一些预处理和收尾处理,比如注册监听和注销系统返回键。

@Composable
fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) {

    val backCallback=object :OnBackPressedCallback(true){
        override fun handleOnBackPressed() {
            onBack()
        }
    }
    DisposableEffect(backDispatcher) {
        backDispatcher.addCallback(backCallback)
        onDispose {
            Log.e("tag", "onDispose")
            backCallback.remove()
        }
    }
}

@Composable
fun DisposableEffectSample(backDispatcher: OnBackPressedDispatcher) {
    var addBackCallback by remember {
        mutableStateOf(false)
    }
    Row {
        Switch(checked = addBackCallback, onCheckedChange = { addBackCallback = !addBackCallback })
        Text(text = if (addBackCallback) "Add Back Callback" else "Not Add Back Callback")

    }

    if (addBackCallback) {
        BackHandler(backDispatcher = backDispatcher) {
            Log.e("tag", "onBack")
        }
    }
}

DisposableEffect的key不能为空

  • 如果key为Unit或者true这样的常量,则block只在onActive时执行一次
  • 如果key为其他变量,则block在onActive以及参数变化时的onUpdate中执行,比如这里的backDispatcher变化时,block会再次被执行,也就是注册新的backCallback。当有新的副作用到来时,前一次的副作用会执行onDispose,此外当Composable进入onDispose时也会执行。

SideEffect:将 Compose 状态发布为非 Compose 代码

SideEffect是简化版的DisposableEffect,sideEffect并未接收任何key,所以每次重组都会执行其block。因而它不能用来处理耗时或者异步的副作用逻辑。示例如下

@Composable
private fun SideEffectDemo() {
    val requestCount = remember {
        mutableStateOf(0)
    }
    SideEffect {
        requestCount.value++
    }
}

produceState:将非 Compose 状态转换为 Compose 状态

它可以将非Compose(如Flow、LiveData或RxJava)状态转化为Compose状态,它接收一个λ表达式作为函数体,能将这些入参经过一系列操作后生成一个State类型的变量返回,

@Composable
fun ProduceStateSample() {

    val imagesList = listOf(
        "https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=%E5%9B%BE%E7%89%87&hs=0&pn=3&spn=0&di=7117150749552803841&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=1640548213%2C2648418637&os=1565653820%2C2209507028&simid=1640548213%2C2648418637&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=%E5%9B%BE%E7%89%87&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F1113%2F052420110515%2F200524110515-1-1200.jpg%26refer%3Dhttp%3A%2F%2Fimg.jj20.com%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1665726682%26t%3D89dfc4d6812ed1e2223be4848d5e3225&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3B33da_z%26e3Bv54AzdH3FkzAzdH3Fz6u2AzdH3Fxfx3AzdH3Fd9camm_z%26e3Bip4s&gsm=4&islist=&querylist=&dyTabStr=MCwzLDEsNiw0LDUsMiw3LDgsOQ%3D%3D",
        "https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=%E5%9B%BE%E7%89%87&hs=0&pn=7&spn=0&di=7117150749552803841&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=4091970494%2C846758848&os=2320783045%2C207549810&simid=4091970494%2C846758848&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=%E5%9B%BE%E7%89%87&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2Ftp09%2F210F2130512J47-0-lp.jpg%26refer%3Dhttp%3A%2F%2Fimg.jj20.com%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1665726682%26t%3D20d9ada72306354f601e4a3fd5428f0e&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3B33da_z%26e3Bv54AzdH3FprAzdH3Fnnc0nm_z%26e3Bip4s&gsm=4&islist=&querylist=&dyTabStr=MCwzLDEsNiw0LDUsMiw3LDgsOQ%3D%3D",
        "https://xxximage.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=%E5%9B%BE%E7%89%87&hs=0&pn=10&spn=0&di=7117150749552803841&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=392156243%2C1688163758&os=3522723729%2C1775037553&simid=3419262904%2C298030006&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=%E5%9B%BE%E7%89%87&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%3A%2F%2Fwww.pptbz.com%2Fd%2Ffile%2Fp%2F201708%2Fb92908f5427aaa3dc10aea19c06e013d.jpg%26refer%3Dhttp%3A%2F%2Fwww.pptbz.com%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1665726682%26t%3D2bc65a633c82cd88b77a329cfbe0d2fc&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3Brrpkz_z%26e3Bv54AzdH3Frrp15ogAzdH3F8cm8l0_z%26e3Bip4s&gsm=4&islist=&querylist=&dyTabStr=MCwzLDEsNiw0LDUsMiw3LDgsOQ%3D%3D"
    )
    var index by remember { mutableStateOf(0) }
    val imageRepository = ImageRepository(LocalContext.current)
    val result = loadNetworkImg(url = imagesList[index], imageRepository = imageRepository)
    Column {
        Button(
            onClick = {
                index %= imagesList.size
                if (++index == imagesList.size) index = 0
            }) {
            Text(text = "选择第 $index 张图片")
        }
        when (result.value) {
            is Result.Success -> {
                Image(
                    bitmap = (result.value as Result.Success).image.imageBitmap,
                    contentDescription = "image load success"
                )
            }
            is Result.Error -> {
                Image(
                    imageVector = Icons.Rounded.Warning,
                    contentDescription = "image load error",
                    modifier = Modifier.size(200.dp,200.dp)
                )
            }
            is Result.Loading -> {
                CircularProgressIndicator()
            }
        }

    }
}


@Composable
fun loadNetworkImg(url: String, imageRepository: ImageRepository): State<Result<Image>> {
    return produceState(
        initialValue = Result.Loading as Result<Image>,
        url,
        imageRepository
    ) {
        val image = imageRepository.load(url)
        value = if (image == null)
            Result.Error
        else Result.Success(image)
    }
}

derivedStateOf:将一个或多个状态对象转换为其他状态

derivedStateOf用来将一个或多个State转换成另一个state。derivedStateOf{...}的block中可以依赖其他State创建并返回一个DerivedState,当block中依赖的State发生变化时,会更新此DerivedState,依赖此DerivedState的所有Composable会因其变化而重组。比如实现本地数据检索,在输入框中的内容发生变化时,列表数据会自动刷新。

//ViewModel的定义
class MainVM : ViewModel() {
    val state = mutableStateOf(0)

    val list = mutableListOf<String>()
    val keyword = mutableStateOf("")

    init {

        for (i in 0 until 10) {
            list.add("test:$i")
        }
    }
    fun onValueChanged(text:String){
        keyword.value=text
    }
}

//页面逻辑
@Composable
fun DerivedStateOfDemo() {
    val vm: MainVM = viewModel()
    val result by remember {
        derivedStateOf {
            vm.list.filter { it.contains(vm.keyword.value, false) }
        }
    }
    //调用方处理监听逻辑
    val onTextChanged: (String) -> Unit = { vm.onValueChanged(it) }
    //创建状态容器
    val editableUserInputState = rememberEditableUserInputState(initialText = "")
    //保证重组时使用最新的onTextChanged方法
    val currentOnDestinationChanged by rememberUpdatedState(onTextChanged)
    Box(modifier = Modifier.fillMaxSize()) {
        Column {
            CustomInput(state = editableUserInputState)
            LazyColumn {
                items(result) {
                    Text(text = it)
                }
            }

        }

    }
    //状态中的text值变化时调用回调函数
    LaunchedEffect(key1 = editableUserInputState) {
        snapshotFlow { editableUserInputState.text }.collect {
            currentOnDestinationChanged(it)
        }
    }

}

@Composable
fun CustomInput(state: EditableUserInputState = rememberEditableUserInputState(initialText = "")) {
    BasicTextField(
        value = state.text,
        onValueChange = { state.text = it },
        decorationBox = { innerTextField ->
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.padding(vertical = 2.dp)
            ) {

                Box(
                    modifier = Modifier
                        .padding(horizontal = 10.dp)
                        .weight(1f),
                    contentAlignment = Alignment.CenterStart
                ) {
                    if (state.text.isEmpty()) {
                        Text(text = "输入点东西吧", style = TextStyle(color = Color(0, 0, 0, 128)))
                    }
                    innerTextField()
                }
//                if (text.isNotEmpty()) {
//                    IconButton(onClick = { onValueChanged("") }) {
//                        Icon(
//                            imageVector = Icons.Filled.Close, contentDescription = "删除"
//                        )
//                    }
//                }

            }
        },
        modifier = Modifier
            .padding(horizontal = 10.dp)
            .background(Color.White, CircleShape)
            .height(30.dp)
            .fillMaxWidth(),
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
        ),
        keyboardActions = KeyboardActions(onDone = {
            Log.e("tag", "onDone")
        })
    )
}

//创建状态容器
class EditableUserInputState(initialText: String) {

    //以便 Compose 跟踪值的更改,并在发生更改时重组
    var text by mutableStateOf(initialText)


    companion object {
        val Saver: Saver<EditableUserInputState, *> =
            listSaver(save = { listOf(it.text) }, restore = {
                EditableUserInputState(initialText = it[0])
            })
    }
}

//保存状态
@Composable
fun rememberEditableUserInputState(initialText: String): EditableUserInputState =
    rememberSaveable(initialText, saver = EditableUserInputState.Saver) {
        EditableUserInputState(initialText)
    }

snapshotFlow:将 Compose 的 State 转换为 Flow

LauchedEffect在状态发生变化时第一时间收到通知,如果通过改变观察参数key来通知状态的变化,这回中断当前执行中的任务。snapshotFlow可以解决这一问题,它可以将状态转化成一个CoroutineFlow。snapshotFlow{...}内部对State访问的同时,通过“快照”系统订阅起变化,每当State发生变化时,flow就会发送新数据,而且只有在collect之后,block才开始执行。也就是说当一个LaunchedEffect中依赖的State会频繁变换时,不应该使用State的值作为key,而应该将State本身作为key,然后在LaunchedEffect内容使用snapshotFlow依赖状态,使用State作为key是为了当State对象本身变化时重启副作用。

@Composable
fun SnapshotFlowSample() {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) {
        items(1000) { index ->
            Text(text = "Item is $index")
        }
    }

    LaunchedEffect(key1 = listState) {
        //将state转化成flow
        snapshotFlow {
            listState.firstVisibleItemIndex
        }
            .filter { it > 20 }
            .distinctUntilChanged()
            .collect {
                Log.e("tag", "firstVisibleIndex = $it")
            }
    }
}

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

推荐阅读更多精彩内容