Compose 中的附帶效應(yīng)

附帶效應(yīng)是指發(fā)生在可組合函數(shù)作用域之外的應(yīng)用狀態(tài)的變化。由于可組合項(xiàng)的生命周期和屬性(例如不可預(yù)測(cè)的重組、以不同順序執(zhí)行可組合項(xiàng)的重組或可以舍棄的重組),可組合項(xiàng)在理想情況下應(yīng)該是無(wú)附帶效應(yīng)的。凡是回影響外界的操作都屬于副作用,比如彈toast,保存本地文件,訪問(wèn)遠(yuǎn)程或本地?cái)?shù)據(jù)等。在Compose中常用的以及介紹的一共有8種,本篇將詳細(xì)介紹其使用。

LaunchedEffect:在某個(gè)可組合項(xiàng)的作用域內(nèi)運(yùn)行掛起函數(shù)

常用的構(gòu)造函數(shù)如下

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

用于處理異步任務(wù),在Composable進(jìn)入組件樹時(shí)或者key發(fā)生變化時(shí)啟動(dòng)協(xié)程執(zhí)行block中的內(nèi)容,可以在其中啟動(dòng)子協(xié)程或者調(diào)用掛起函數(shù)。如果key發(fā)生變化,當(dāng)前協(xié)程會(huì)自動(dòng)結(jié)束并開啟新的協(xié)程,Composable進(jìn)入onDispose(離開組件樹)時(shí),協(xié)程會(huì)自動(dòng)取消。使用時(shí)的demo如下

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

    //這種寫法第一次也會(huì)彈出,每次更新state值時(shí)(改變key值)
    //都會(huì)重新調(diào)用LaunchedEffect中的block函數(shù)
    /*   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:獲取組合感知作用域,以便在可組合項(xiàng)外啟動(dòng)協(xié)程

在非Composable環(huán)境中使用協(xié)程,比如在Button的onClick中使用協(xié)程顯示SnackBar,并希望在onDispose(離開組件樹)時(shí)自動(dòng)取消,此時(shí)可以使用rememberCoroutineScope。它可以在當(dāng)前Composable進(jìn)入onDispose時(shí)自動(dòng)取消,簡(jiǎn)單的示例如下

@Composable
fun RememberCoroutineScopeSample() {
    val scaffoldState = rememberScaffoldState()
    //創(chuàng)建協(xié)程作用域,在多個(gè)地方使用(floatingActionButton,IconButton中的onClick)
    val scope = rememberCoroutineScope()

    Scaffold(
        scaffoldState = scaffoldState,
        //標(biāo)題欄區(qū)域
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = "腳手架示例"
                    )
                },
                navigationIcon = {
                    IconButton(onClick = {
                        scope.launch {
                            scaffoldState.drawerState.open()
                        }
                    }) {
                        Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
                    }
                })
        },
        //屏幕內(nèi)容區(qū)域
        content = {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Text(text = "屏幕內(nèi)容區(qū)域")
            }
        },

        //左側(cè)抽屜
        drawerContent = {

            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Text(text = "抽屜中的內(nèi)容")
            }
        },

        //懸浮按鈕
        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:在效應(yīng)中引用某個(gè)值,該效應(yīng)在值改變時(shí)不應(yīng)重啟

如果key值有更新,那么LaunchedEffect在重組時(shí)就會(huì)被重新啟動(dòng),但是有時(shí)候需要在LaunchedEffect中使用最新的參數(shù)值,但是又不想重新啟動(dòng)LaunchedEffect,此時(shí)就需要使用rememberUpdateState。它的作用就是給某個(gè)參數(shù)創(chuàng)建一個(gè)引用,并保證其值被使用時(shí)是最新值,而且參數(shù)改變時(shí)不重啟effect。比如對(duì)生命周期的監(jiān)聽

@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 ->
            //回調(diào)onStart或者onStop
            when (event) {
                Lifecycle.Event.ON_START -> {
                    currentOnStart()
                }
                Lifecycle.Event.ON_STOP -> {
                    currentOnStop()
                }
                else -> {}
            }


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

在上面的代碼中,當(dāng)LifecycleOwner變化時(shí),需要終止對(duì)當(dāng)前LifecycleOwner的監(jiān)聽,并重新注冊(cè)O(shè)bserver,因此必須將其添加為觀察參數(shù)。而currentOnStart和currentOnStop只要保證在回調(diào)它們的時(shí)候可以獲取最新值即可,所以應(yīng)該通過(guò)rememberUpdateState包裝后在副作用中使用,不應(yīng)該因?yàn)樗麄兊淖儎?dòng)終止副作用。

DisposableEffect:需要清理的效應(yīng)

它可以感知Composable的onActive(進(jìn)入組件樹)和onDispose(離開組件樹),允許通過(guò)副作用完成一些預(yù)處理和收尾處理,比如注冊(cè)監(jiān)聽和注銷系統(tǒng)返回鍵。

@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時(shí)執(zhí)行一次
  • 如果key為其他變量,則block在onActive以及參數(shù)變化時(shí)的onUpdate中執(zhí)行,比如這里的backDispatcher變化時(shí),block會(huì)再次被執(zhí)行,也就是注冊(cè)新的backCallback。當(dāng)有新的副作用到來(lái)時(shí),前一次的副作用會(huì)執(zhí)行onDispose,此外當(dāng)Composable進(jìn)入onDispose時(shí)也會(huì)執(zhí)行。

SideEffect:將 Compose 狀態(tài)發(fā)布為非 Compose 代碼

SideEffect是簡(jiǎn)化版的DisposableEffect,sideEffect并未接收任何key,所以每次重組都會(huì)執(zhí)行其block。因而它不能用來(lái)處理耗時(shí)或者異步的副作用邏輯。示例如下

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

produceState:將非 Compose 狀態(tài)轉(zhuǎn)換為 Compose 狀態(tài)

它可以將非Compose(如Flow、LiveData或RxJava)狀態(tài)轉(zhuǎn)化為Compose狀態(tài),它接收一個(gè)λ表達(dá)式作為函數(shù)體,能將這些入?yún)⒔?jīng)過(guò)一系列操作后生成一個(gè)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:將一個(gè)或多個(gè)狀態(tài)對(duì)象轉(zhuǎn)換為其他狀態(tài)

derivedStateOf用來(lái)將一個(gè)或多個(gè)State轉(zhuǎn)換成另一個(gè)state。derivedStateOf{...}的block中可以依賴其他State創(chuàng)建并返回一個(gè)DerivedState,當(dāng)block中依賴的State發(fā)生變化時(shí),會(huì)更新此DerivedState,依賴此DerivedState的所有Composable會(huì)因其變化而重組。比如實(shí)現(xiàn)本地?cái)?shù)據(jù)檢索,在輸入框中的內(nèi)容發(fā)生變化時(shí),列表數(shù)據(jù)會(huì)自動(dòng)刷新。

//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
    }
}

//頁(yè)面邏輯
@Composable
fun DerivedStateOfDemo() {
    val vm: MainVM = viewModel()
    val result by remember {
        derivedStateOf {
            vm.list.filter { it.contains(vm.keyword.value, false) }
        }
    }
    //調(diào)用方處理監(jiān)聽邏輯
    val onTextChanged: (String) -> Unit = { vm.onValueChanged(it) }
    //創(chuàng)建狀態(tài)容器
    val editableUserInputState = rememberEditableUserInputState(initialText = "")
    //保證重組時(shí)使用最新的onTextChanged方法
    val currentOnDestinationChanged by rememberUpdatedState(onTextChanged)
    Box(modifier = Modifier.fillMaxSize()) {
        Column {
            CustomInput(state = editableUserInputState)
            LazyColumn {
                items(result) {
                    Text(text = it)
                }
            }

        }

    }
    //狀態(tài)中的text值變化時(shí)調(diào)用回調(diào)函數(shù)
    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 = "輸入點(diǎn)東西吧", 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")
        })
    )
}

//創(chuàng)建狀態(tài)容器
class EditableUserInputState(initialText: String) {

    //以便 Compose 跟蹤值的更改,并在發(fā)生更改時(shí)重組
    var text by mutableStateOf(initialText)


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

//保存狀態(tài)
@Composable
fun rememberEditableUserInputState(initialText: String): EditableUserInputState =
    rememberSaveable(initialText, saver = EditableUserInputState.Saver) {
        EditableUserInputState(initialText)
    }

snapshotFlow:將 Compose 的 State 轉(zhuǎn)換為 Flow

LauchedEffect在狀態(tài)發(fā)生變化時(shí)第一時(shí)間收到通知,如果通過(guò)改變觀察參數(shù)key來(lái)通知狀態(tài)的變化,這回中斷當(dāng)前執(zhí)行中的任務(wù)。snapshotFlow可以解決這一問(wèn)題,它可以將狀態(tài)轉(zhuǎn)化成一個(gè)CoroutineFlow。snapshotFlow{...}內(nèi)部對(duì)State訪問(wèn)的同時(shí),通過(guò)“快照”系統(tǒng)訂閱起變化,每當(dāng)State發(fā)生變化時(shí),flow就會(huì)發(fā)送新數(shù)據(jù),而且只有在collect之后,block才開始執(zhí)行。也就是說(shuō)當(dāng)一個(gè)LaunchedEffect中依賴的State會(huì)頻繁變換時(shí),不應(yīng)該使用State的值作為key,而應(yīng)該將State本身作為key,然后在LaunchedEffect內(nèi)容使用snapshotFlow依賴狀態(tài),使用State作為key是為了當(dāng)State對(duì)象本身變化時(shí)重啟副作用。

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容