关于副作用的解释,只发现了掘金上的一篇文章写的挺好的,这里就不照搬了。所以本篇只是简单引用一部分。本篇主要示例源自官方文档
用一句话概括副作用:一个函数的执行过程中,除了返回函数值之外,对调用方还会带来其他附加影响,例如修改全局变量或修改参数等。
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
)
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")
}
}
}
}
这个函数主要是使值变化的话,如果值相同的话不会导致组合重组
具体参考以下链接:
Compose中的附带效应(四)rememberUpdatedState
Jetpack Compose中的附带效应简介及使用
LaunchedEffect 和 rememberUpdatedState
有时候需要在组合函数变化或者退出时候做些处理,可以使用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()
}
}
}
如需与非 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是将非 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
。使用此函数可确保仅当计算中使用的状态之一发生变化时才会进行计算。
这个函数和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 */
}
}
将 Compose 的 State 转换为 Flow(不懂Flow,仅作为记录)。
使用 snapshotFlow
将 State
对象转换为冷 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 中有一些效应(如 LaunchedEffect
、produceState
或 DisposableEffect
)会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新的效应。
这些 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
示例。但在这样做之前,请审慎考虑,并确保您确实需要这么做。
Compose 中的附带效应
https://developer.android.google.cn/jetpack/compose/side-effects
Jetpack Compose Side Effect:如何处理副作用
https://juejin.cn/post/6930785944580653070