Android Jetpack Compose基础之副作用Effect
什么是副作用Side Effect
官方解释:发生在可组合函数作用域之外的应用状态的变化叫做副作用
由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无副作用的。
Composable在执行过程中,凡事会影响外界的操作都属于副作用,比很多一次性的事件如Toast、保存文件、获取远程或本地数据。因重组会造成Composabl频繁执行,显然这些事件不应该反复被执行,所以我们需要使用Compose提供的副作用API;
它作用是让副作用API只发生在Composable生命周期的特定阶段,从而实现行为可预期。【实现监控Compsable的生命周期变化】
DisposableEffect
源码
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
interface DisposableEffectResult {
fun dispose()
}
private val InternalDisposableEffectScope = DisposableEffectScope()
private class DisposableEffectImpl(
private val effect: DisposableEffectScope.() -> DisposableEffectResult
) : RememberObserver {
private var onDispose: DisposableEffectResult? = null
//Called when this object is successfully remembered by a composition.
//This method is called on the composition's apply thread.
override fun onRemembered() {
onDispose = InternalDisposableEffectScope.effect()
}
//Called when this object is forgotten by a composition.
//This method is called on the composition's apply thread.
override fun onForgotten() {
onDispose?.dispose()
onDispose = null
}
//Called when this object is returned by the callback to remember
//but is not successfully remembered by a composition.
override fun onAbandoned() {
// Nothing to do as [onRemembered] was not called.
}
}
/**
* Receiver scope for [DisposableEffect] that offers the [onDispose] clause that should be
* the last statement in any call to [DisposableEffect].
*/
class DisposableEffectScope {
/**
* Provide [onDisposeEffect] to the [DisposableEffect] to run when it leaves the composition
* or its key changes.
*/
//【方法3】:这个方法必须实现啦啦啦啦
inline fun onDispose(
crossinline onDisposeEffect: () -> Unit
): DisposableEffectResult = object : DisposableEffectResult {
override fun dispose() {
onDisposeEffect()
}
}
}
作用
他可以感知Composable生命周期中的加入组合和退出组合的变化,加入组合它会启动一个协程,并将代码块作为参数传递,当键发生变化或退出组合协程将自动取消并回调OnDispose(标注方法3),从而实现注销回调,避免泄漏。
示例
/**
* DisposableEffect 对于需要在键发生变化或可组合项退出组合后进行清理的附带效应
*/
@Composable
fun onObserverLifecycleScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, onStop: () -> Unit
) {
//为了确保 onStart lambda 始终包含重组 onObserverLifecycleScreen 时使用的最新值
val currentOnStart by rememberUpdatedState(newValue = onStart)
//可监听组合项退出组合时的回调,及控件销毁使的挂起销毁的回调操作
val currentOnStop by rememberUpdatedState(newValue = onStop)
DisposableEffect(key1 = lifecycleOwner) {
val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
Log.i("SideEffect","onObserverLifecycleScreen DisposableEffect addObserver $observer")
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
Log.i("SideEffect","onObserverLifecycleScreen DisposableEffect onDispose")
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
SideEffect
源码
@Composable
@NonRestartableComposable
@ExplicitGroupsComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(
effect: () -> Unit
) {
/* A Compose internal function. DO NOT call directly.
Record a function to call when changes to the corresponding tree are applied to the applier.
This is used to implement SideEffect.
*/
currentComposer.recordSideEffect(effect)
}
作用
如需与非 Compose 管理的对象共享 Compose 状态,请使用 SideEffect 可组合项。
1、Composable重组不一定会成功结束,有的重组可能会中途失败,SideEffec只会在每次【成功重组】时才会执行,如果重组失败则不会被触发。
2、因会重组会频繁发生,所以它不能用来处理耗时或者异步的副作用逻辑
示例
官方提供的示例代码,分析用户行为数据
@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
FirebaseAnalytics()
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
LaunchedEffect
源码
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
private val scope = CoroutineScope(parentCoroutineContext)
private var job: Job? = null
override fun onRemembered() {
// This should never happen but is left here for safety
job?.cancel("Old job was still running!")
job = scope.launch(block = task)
}
override fun onForgotten() {
job?.cancel(LeftCompositionCancellationException())
job = null
}
override fun onAbandoned() {
job?.cancel(LeftCompositionCancellationException())
job = null
}
}
作用
1、支持在副作用内执行异步任务,block作用域为CoroutineScope(注意:副作用通常时在主线程中执行的,如果需要在副作用中执行耗时任务时,优先选择LaunchedEffect处理副作用)
2、从LaunchedEffectImpl方法中可以看到,当Composable进入组合时会启动协程执行block中的内容,当Composable被移除组合时,协程会被自动取消
3、当被观察的key发生变化时,当前协程会自动结束,同时开启新协程,见onRemembered方法
示例
官方示例
@Composable
fun MyScreen(
state: UiState<List<Movie>>,
snackbarHostState: SnackbarHostState
) {
// If the UI state contains an error, show snackbar
if (state.hasError) {
// `LaunchedEffect` will cancel and re-launch if
// `scaffoldState.snackbarHostState` changes
LaunchedEffect(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.
snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
}
}
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { contentPadding ->
// ...
}
}
当state hasError为 true时,显示一个SnackBar(属于挂起函数),当snackbarHostState变化时,将启动一个新协程,SnackBar重新显示一次内容,当state hasError 为false时,副作用会被移除组合,协程会被取消
rememberCoroutineScope
由于LaunchedEffect是composable函数,它只能在其他composable函数中被调用。因此想从非composable函数中创建coroutine时需要另寻他法。谷歌提供了rememberCoroutineScope用于在非composable函数中创建coroutine
源码
@Composable
inline fun rememberCoroutineScope(
crossinline getContext: @DisallowComposableCalls () -> CoroutineContext =
{ EmptyCoroutineContext }
): CoroutineScope {
val composer = currentComposer
val wrapper = remember {
CompositionScopedCoroutineScopeCanceller(
createCompositionCoroutineScope(getContext(), composer)
)
}
return wrapper.coroutineScope
}
@PublishedApi
internal class CompositionScopedCoroutineScopeCanceller(
val coroutineScope: CoroutineScope
) : RememberObserver {
override fun onRemembered() {
// Nothing to do
}
override fun onForgotten() {
coroutineScope.cancel(LeftCompositionCancellationException())
}
override fun onAbandoned() {
coroutineScope.cancel(LeftCompositionCancellationException())
}
}
@PublishedApi
@OptIn(InternalComposeApi::class)
internal fun createCompositionCoroutineScope(
coroutineContext: CoroutineContext,
composer: Composer
) = if (coroutineContext[Job] != null) {
CoroutineScope(
Job().apply {
completeExceptionally(
IllegalArgumentException(
"CoroutineContext supplied to " +
"rememberCoroutineScope may not include a parent job"
)
)
}
)
} else {
val applyContext = composer.applyCoroutineContext
CoroutineScope(applyContext + Job(applyContext[Job]) + coroutineContext)
}
作用特点
1、rememberCoroutineScope可以返回一个coroutineScope,便于开发者手动控制该coroutine的生命周期,例如有用户点击事件时启动该coroutine。
2、rememberCoroutineScope返回的coroutineScope会和其调用点的生命周期保持一致,当调用点所在的Composition退出时,该coroutineScope会被取消。
@Composable
fun onRememberCoroutineScope() {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
flow {
delay(5000)
emit("rememberCoroutineScope")
}
.flowOn(Dispatchers.IO)
.collect {
Logger.i("onRememberCoroutineScope scope launch $it")
}
}
}) {
Text(text = "rememberCoroutineScope")
}
}
Button的OnClick方法Compsable,如果需要在点击事件里面启动协程,则需要使用rememberCoroutineScope
rememberUpdatedState
LaunchedEffect当key发生变化时就会取消当前协程并开启新的协程,但有时候我们不希望当前协程被中断,只要能够实现实时获取最新状态时,可以使用rememberUpdatedState
###源码
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
remember确保实例可以跨越重组,副作用里访问的是mutableState中最新的值
示例
@Composable
fun OnRememberUpdatedSate(lastClickColor: String = "UNKNOW") {
val lastClickColorUpdate by rememberUpdatedState(newValue = lastClickColor)
LaunchedEffect(key1 = Unit) {
delay(5000)
Logger.i("OnRememberUpdatedSate lastClickColorUpdate= $lastClickColorUpdate")
}
}
Logger可以总会打印出最新的值
snapshotFlow
通过rememberUpdatedState我们可以实现获取最新状态,但是状态发生改变时,LaunchedEffect无法第一时间收到通知,如果通过key的变化来通知状态变化,则会中断当前的协程,成本过高。
我们可以通过snapshotFlow实现将 Compose 的 State 转换为 Flow
源码
fun <T> snapshotFlow(
block: () -> T
): Flow<T> = flow {
// Objects read the last time block was run
val readSet = MutableScatterSet<Any>()
val readObserver: (Any) -> Unit = {
if (it is StateObjectImpl) {
it.recordReadIn(ReaderKind.SnapshotFlow)
}
readSet.add(it)
}
// This channel may not block or lose data on a trySend call.
val appliedChanges = Channel<Set<Any>>(Channel.UNLIMITED)
// Register the apply observer before running for the first time
// so that we don't miss updates.
val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
val maybeObserved = changed.any {
it !is StateObjectImpl || it.isReadIn(ReaderKind.SnapshotFlow)
}
if (maybeObserved) {
appliedChanges.trySend(changed)
}
}
try {
var lastValue = Snapshot.takeSnapshot(readObserver).run {
try {
enter(block)
} finally {
dispose()
}
}
emit(lastValue)
while (true) {
var found = false
var changedObjects = appliedChanges.receive()
// Poll for any other changes before running block to minimize the number of
// additional times it runs for the same data
while (true) {
// Assumption: readSet will typically be smaller than changed set
found = found || readSet.intersects(changedObjects)
changedObjects = appliedChanges.tryReceive().getOrNull() ?: break
}
if (found) {
readSet.clear()
val newValue = Snapshot.takeSnapshot(readObserver).run {
try {
enter(block)
} finally {
dispose()
}
}
if (newValue != lastValue) {
lastValue = newValue
emit(newValue)
}
}
}
} finally {
unregisterApplyObserver.dispose()
}
}
内部在对State访问的同时,通过快照系统订阅变化,当State发生变化时,flow就会发送新数据,如果没有变化则不发送,
###示例
官方示例:系统在用户滚动经过要分析的列表的首个项目时记录下来的
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
注意:如果LaunchedEffect中的State会频繁发生变化时,不应该使用State的值作为key(实现当State本身发生改变时重启副作用),而应该将State本身最为key,然后在内部启动snapshotFlow依赖状态。
创建状态的副作用API
produceState
SideState通常用来将Compose的State暴露给外部使用,而produceState则是将一个外部数据源(如LiveData、rxjava或普通数据)转换成State
源码
@Composable
fun <T> produceState(
initialValue: T,
producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
private class ProduceStateScopeImpl<T>(
state: MutableState<T>,
override val coroutineContext: CoroutineContext
) : ProduceStateScope<T>, MutableState<T> by state {
override suspend fun awaitDispose(onDispose: () -> Unit): Nothing {
try {
suspendCancellableCoroutine<Nothing> { }
} finally {
onDispose()
}
}
}
produceState中的协程任务会随着LaunchedEffect的onDispose被自动停止,LaunchedEffect内部的awaitDispose方法中可以处理不基于协程的逻辑,比如注册一个回调来实现释放资源。
示例
官方示例:使用 produceState 从网络加载图像。loadNetworkImage 可组合函数会返回可以在其他可组合项中使用的 State。
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {
// Creates a State<T> 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 inputs.
return produceState<Result<Image>>(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
可以实现将一个或者多个State转换成另一个State,derivedStateOf 中的block可以依赖其它State创建并返回一个新的State,当block中的state发生改变时,也会同时更新,并实现相关的组合进行重组
注意当一个计算结果依赖较多的State时,使用derivedStateOf有助于减少重组次数,提高性能,否则可以使用remeber(key1,key2)实现来提高性能
副作用参数使用优化
当副作用中的block存在可变化的值,但是没有指定为key,有可能会出现没有及时响应变化而出现bug,
应该遵循如下原则:当一个状态的变化需要造成副作用终止时,才将其添加为观察的key,否则应该使用rememberUpdatedState包装后,在副作用中使用,以避免中断执行中的副作用。
示例:详见DisposableEffect中的示例代码,通过rememberUpdatedState实现currentOnStart和currentOnStop只要保证在回调时它们是最新的值,而不是终止副作用
/**
* DisposableEffect 对于需要在键发生变化或可组合项退出组合后进行清理的附带效应
*/
@Composable
fun onObserverLifecycleScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, onStop: () -> Unit
) {
//为了确保 onStart lambda 始终包含重组 onObserverLifecycleScreen 时使用的最新值
val currentOnStart by rememberUpdatedState(newValue = onStart)
//可监听组合项退出组合时的回调,及控件销毁使的挂起销毁的回调操作
val currentOnStop by rememberUpdatedState(newValue = onStop)
DisposableEffect(key1 = lifecycleOwner) {
val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
Log.i("SideEffect","onObserverLifecycleScreen DisposableEffect addObserver $observer")
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
Log.i("SideEffect","onObserverLifecycleScreen DisposableEffect onDispose")
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
额外介绍详见官方文档:https://developer.android.google.cn/jetpack/compose/side-effects?hl=zh-cn#derivedstateof
——note end———枯燥