Compose的附加效应(十四)

文章目录

    • 一、前言
    • 二、LaunchedEffect
    • 三、rememberCoroutineScope
    • 四、rememberUpdatedState
    • 五、DisposableEffect
    • 六、SideEffect
    • 七、produceState
    • 八、derivedStateOf
    • 九、snapshotFlow
    • 十、重启效应
    • 十一、使用常量作为键
    • 十二、参考链接

一、前言

​ 关于副作用的解释,只发现了掘金上的一篇文章写的挺好的,这里就不照搬了。所以本篇只是简单引用一部分。本篇主要示例源自官方文档

​ 用一句话概括副作用:一个函数的执行过程中,除了返回函数值之外,对调用方还会带来其他附加影响,例如修改全局变量或修改参数等。

二、LaunchedEffect

LaunchedEffect意思为在某个可组合项的作用域内运行挂起函数。官方示例无法直接运行,修改后如下(这里使用mutableStateOf而不用其它的兼容库):


class ComposeEffectActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mviViewModel = MviViewModel()
        setContent {
            MyScreen(mviViewModel)
        }
    }

    @Composable
    fun MyScreen(
        state: MviViewModel,
        scaffoldState: ScaffoldState = rememberScaffoldState()
    ) {
        val uiState by remember {
            state.uiState
        }
        // If the UI state contains an error, show snackbar
        if (uiState.hasError) {

            // `LaunchedEffect` will cancel and re-launch if
            // `scaffoldState.snackbarHostState` changes
            LaunchedEffect(scaffoldState.snackbarHostState) {
                // Show snackbar using a coroutine, when the coroutine is cancelled the
                // snackbar will automatically dismiss. This coroutine will cancel whenever
                // `state.hasError` is false, and only start when `state.hasError` is true
                // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
                scaffoldState.snackbarHostState.showSnackbar(
                    message = "Error message",
                    actionLabel = "Retry message"
                )
            }
        }

        Scaffold(scaffoldState = scaffoldState) {
            /* ... */
            Button(onClick = {
                state.onHashErrorChange(true)
            }){
                Text("点我")
            }
        }
    }
}


class MviViewModel{
    val uiState = mutableStateOf(UiState())
    fun onHashErrorChange(hasError: Boolean) {
        uiState.value = uiState.value.copy(hasError = hasError)
        Log.e("YM","--->${uiState}")
    }
}

data class UiState(
    val hasError: Boolean = false
)

三、rememberCoroutineScope

LaunchedEffect是一个可组合函数,所以只能在可组合函数内使用。假如我们需要在可组合函数外启动协程,在可组合函数声明周期结束时候取消,可以使用rememberCoroutineScope。另外rememberCoroutineScope还可以控制多个协程。在这里可以使用一个以下示例,当点击Button时, 显示一个Snackbar

@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(scaffoldState = scaffoldState) {
        Column {
            /* ... */
            Button(
                onClick = {
                    // Create a new coroutine in the event handler
                    // to show a snackbar
                    scope.launch {
                        scaffoldState.snackbarHostState
                            .showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

四、rememberUpdatedState

这个函数主要是使值变化的话,如果值相同的话不会导致组合重组
具体参考以下链接:
Compose中的附带效应(四)rememberUpdatedState
Jetpack Compose中的附带效应简介及使用
LaunchedEffect 和 rememberUpdatedState

五、DisposableEffect

有时候需要在组合函数变化或者退出时候做些处理,可以使用DisposableEffect

例如,需要注册 OnBackPressedCallback 才能监听在 OnBackPressedDispatcher 上按下的返回按钮。 如需在 Compose 中监听这些事件,请根据需要使用 DisposableEffect 注册和取消注册其它回调。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mviViewModel = MviViewModel()
        setContent {
            BackHandler(onBackPressedDispatcher){//该代码是在Activity中使用
                Log.e("YM","LandingScreen事件已过")
            }
        }

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

    // Safely update the current `onBack` lambda when a new one is provided
    val currentOnBack by rememberUpdatedState(onBack)

    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
        // Always intercept back events. See the SideEffect for
        // a more complete version
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                currentOnBack()
            }
        }
    }

    // If `backDispatcher` changes, dispose and reset the effect
    DisposableEffect(backDispatcher) {
        // Add callback to the backDispatcher
        backDispatcher.addCallback(backCallback)

        // When the effect leaves the Composition, remove the callback
        onDispose {//组合被移除时会调用这个函数
            backCallback.remove()
        }
    }
}

六、SideEffect

如需与非 Compose 管理的对象共享 Compose 状态,请使用 SideEffect 可组合项,因为每次成功重组时都会调用该可组合项(我这个也没发现有什么用处)。

这个代码是根据文档进行记录的

@Composable
fun BackHandler(
    backDispatcher: OnBackPressedDispatcher,
    enabled: Boolean = true, // Whether back events should be intercepted or not
    onBack: () -> Unit
) {
    /* ... */
    val backCallback = remember { /* ... */ }

    // On every successful composition, update the callback with the `enabled` value
    // to tell `backCallback` whether back events should be intercepted or not
    SideEffect {
        backCallback.isEnabled = enabled
    }

    /* Rest of the code */
}

七、produceState

produceState是将非 Compose 状态转换为 Compose 状态。

这个比较有用,比如有些地方需要一些state值来对UI进行重组,但是这些状态的来源却并没有生产状态,可以使用这个进行转换

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // Creates a State with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new keys.
    return produceState(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

八、derivedStateOf

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

如果某个状态是从其他状态对象计算或派生得出的,请使用 derivedStateOf。使用此函数可确保仅当计算中使用的状态之一发生变化时才会进行计算。
这个函数和remember的区别是,假如触发了多个一样的值,remember依然会导致UI进行重组,但是derivedStateOf只会在数据不同的时候才会导致UI进行重组
这个函数主要可以理解为将多个值合并成State。不管这个值是否是State。另外当其返回的值或者所引用的State发生改变时都会引发重组,如下

@Composable
fun TodoList(
    highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
) {
    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or
    // highPriorityKeywords change, not on every recomposition
    val highPriorityTasks by remember(todoTasks, highPriorityKeywords) {
        derivedStateOf {
            todoTasks.filter { it.containsWord(highPriorityKeywords) }
        }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

九、snapshotFlow

将 Compose 的 State 转换为 Flow(不懂Flow,仅作为记录)。

使用 snapshotFlowState 对象转换为冷 Flow。snapshotFlow 会在收集到块时运行该块,并发出从块中读取的 State 对象的结果。当在 snapshotFlow 块中读取的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值(此行为类似于 Flow.distinctUntilChanged 的行为)。

下列示例显示了一项附带效应,是系统在用户滚动经过要分析的列表的首个项目时记录下来的:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

十、重启效应

本段摘自官网(不太理解这段话)

Compose 中有一些效应(如 LaunchedEffectproduceStateDisposableEffect)会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新的效应。

这些 API 的典型形式是:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

由于此行为的细微差别,如果用于重启效应的参数不是适当的参数,可能会出现问题:

  • 如果重启效应次数不够,可能会导致应用出现错误。
  • 如果重启效应次数过多,效率可能不高。

一般来说,效应代码块中使用的可变和不可变变量应作为参数添加到效应可组合项中。除此之外,您还可以添加更多参数,以便强制重启效应。如果更改变量不应导致效应重启,则应将该变量封装在 rememberUpdatedState 中。如果由于变量封装在一个不含键的 remember 中使之没有发生变化,则无需将变量作为键传递给效应。

在上面显示的 DisposableEffect 代码中,效应将其块中使用的 backDispatcher 作为参数,因为对它们的任何更改都会导致效应重启。

@Composable
fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) {
    /* ... */
    val backCallback = remember { /* ... */ }

    DisposableEffect(backDispatcher) {
        backDispatcher.addCallback(backCallback)
        onDispose {
            backCallback.remove()
        }
    }
}

无需使用 backCallback 作为 DisposableEffect 键,因为它的值在组合中绝不会发生变化;它封装在不含键的 remember 中。如果未将 backDispatcher 作为参数传递,并且该代码发生变化,那么 BackHandler 将重组,但 DisposableEffect 不会进行处理和重启。这将导致问题,因为此后会使用错误的 backDispatcher

十一、使用常量作为键

您可以使用 true 等常量作为效应键,使其遵循调用点的生命周期。它实际上具有有效的用例,如上面所示的 LaunchedEffect 示例。但在这样做之前,请审慎考虑,并确保您确实需要这么做。

十二、参考链接

  1. Compose 中的附带效应

    https://developer.android.google.cn/jetpack/compose/side-effects

  2. Jetpack Compose Side Effect:如何处理副作用

    https://juejin.cn/post/6930785944580653070

你可能感兴趣的:(JetPack,android)