Compose - 附带效应 EffectPI

文章探讨了Compose中组合函数的概念,强调了纯函数的重要性,以及如何通过EffectAPI如LaunchedEffect、DisposableEffect和SideEffect来管理副作用。这些API用于处理生命周期、避免状态泄漏,并确保在重组时正确执行副作用。同时,提到了rememberCoroutineScope和衍生状态函数如derivedStateOf和snapshotFlow在管理UI状态中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概念

Compose中的附带效应指组合函数作用域内引起外部的状态变化,组合函数是用来声明界面的,不相关的操作都是副作用(Side Effect)。
组合函数的特点重组可能舍弃(中途打断代码再重新执行)、并行执行(调用和被调用的可组合项在不同线程)、可能执行非常频繁(耗时操作卡顿)。
组合需要处理的副作用

执行时机要明确:例如挂载时、重组时。

执行次数要可控:是否应该随着重组反复执行。

不会造成泄漏:移除时释放资源。

1.1 纯函数

函数与外界交换数据只能通过形参和返回值进行,不会对外界环境造成影响。此外,纯函数是幂等的,唯一输入(参数)决定唯一输出(返回值),不会因为运行次数的增加导致返回值的不同。这对于声明式UI框架至关重要,因为它们都是通过函数的反复执行来渲染UI的,函数执行的时机和次数都不可控,但是函数的执行结果必须可控,因此,我们要求这些函数组件必须用纯函数实现。

1.2 副作用

函数内部与外界进行了交互,产生了其它结果(如修改外部变量)。虽然我们不希望函数执行中出现副作用,但现实情况是有一些逻辑只能作为副作用来处理。例如一些IO操作、计时、日志埋点等,这些都是会对外界或收到外界影响的逻辑,不能无限制的反复执行,所以需要能够合理地处理一些副作用。

  • 副作用的执行时机是明确的,例如在Recomposition时。
  • 副作用的执行次数是可控的,不应该随着函数反复执行。
  • 副作用不会造成泄露,例如对于注册要提供适当的时机进行反注册

1.3 组合函数的生命周期

由于没有暴露对应的生命周期回调方法,可以在组合项中使用不同的附带效应来完成相似的作用。

onActiveEnter:首次挂载到组件树上显示。可以使用 LaunchedEffect 挂载时就执行。
onCommit

Composition:重组刷新UI(执行0/N次)。

可以使用 SideEffect 每次重组时都执行。
onDisposeLeave:从组件树上移除不再显示。可以使用 DisposableEffect 移除时执行回调。

二、Effect API

效应是一种可组合函数,该函数不会发出界面,并且在组合完成后不会产生附带效应。自适应界面本质上是异步的,副作用往往也都是耗时操作(动画也是),因此引入协程而非回调来解决问题。

  • 重启效应:那些带有 key 的效应, key是个变长参数,能传入多个 key 控制 block 的重新执行(之前block中的代码未执行完会先取消协程然后再次执行),key 不变时发生重组不会重新执行 block (由于常量不变可以使用Unit、true当作key,这样就遵循了调用点的生命周期)。
LaunchedEffectkey不变的话不受重组影响,key变化先取消协程再重新执行,组合函数被移除时自动取消协程。作用域中提供了协程环境。
SideEffect无kay所以每次重组都执行。用来将状态共享给非Compose管理的对象(如remember记住的值不会因为重组改变)。
DisposableEffectkey不变的话不受重组影响,key变化先取消协程再重新执行,组合函数被移除时自动取消协程。取消协程前都会回调重写的 onDispose() 释放资源。多用于监听 Activity 生命周期。
rememberCoroutineScope

提供协程作用域对象,remrember返回的该对象不受重组影响,开启的子协程也不会受重组影响,组合函数被移除时自动取消协程。解决了像 onClick 这种不是组合函数(无重组作用域)的地方无法调用上面三个API开启协程的情况,可在不同子元素中启动协程来统一管理多个子协程,常用于动画。

状态rememberUpdatedState

让不受重组影响的附带效应如 LaunchedEffect 在作用域中捕获的外部状态是最新的值。由于 LaunchedEffect 不受重组影响,执行 delay 的三秒期间捕获的外部参数状态发生多次变化,delay结束后老的参数已经不存在了无法获取,新的参数也无法得到获取。使用 rememberUpdatedState 包裹一下该状态,LaunchedEffect 就能在 delay 三秒后读取到该状态的最新值。

produceState提供协程作用域来生产状态,如将Flow、LiveData、RxJava转为状态。基于现有API创建自己的效应,移除时会自动取消协程。
derivedStateOf

合并多个状态减少重组。一个状态基于另一个或多个状态得出,即对条件状态经过计算后得出结果状态。对条件状态进行过滤,避免每次条件状态更新都要连带自己重组。通常使用remember的key可以实现,有些情况的状态无法用作key,例如元素改变了而List没变。

snapshotFlow状态转为Flow,状态值变化就发送到Flow,前后两次状态值相同不发送。

2.1 LaunchedEffect()

@Composable

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

内部使用 remember() 来包裹 block,如果 key 不发生变化传入的 block 是不会重新被执行,因此不受重组影响,也因此 block 内部从开始执行到执行到读取的状态的代码,这期间如果状态有变化,依然还是最初那个值,虽然可以通过 key 变化重新执行 block 来获取最新状态,但是内部其它的代码也重新执行了(使用 rememberUpdatedState() 解决)。

@Composable
fun Show() {
    val viewModel = viewModel<MainViewModel>()
    val state = remember { mutableStateOf(false) }
    LaunchedEffect(state) {
        viewModel.loadData()
    }
    val data = viewModel.dataState
}
//传入Unit在外部用if判断状态来控制 LaunchedEffect 挂载和移除
//就不会用状态当作key每次改变都执行block,未执行完还能取消
@Composable
fun Show() {
    val state = remember { mutableStateOf(false) }
    if (state) {
        LaunchedEffect(Unit) {
            viewModel.loadData()
        }
    }
}

 

@Composable
fun Demo() {
    var count by remember { mutableStateOf(0) }
    Column {
        Button(onClick = { count ++ }) { Text(text = "当前值:$count") }
        DelayText("$count")
    }
}

@Composable
fun DelayText(str: String) {
    var text by remember { mutableStateOf("") }
    LaunchedEffect(key1 = Unit) {
        delay(3000L)
        text = str
    }
    Text(text = "延迟后的值:$text")
}

2.2 DisposableEffect()

@Composable
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
)

effect(即函数作用域)中必须调用 onDespose() 函数。

inline fun onDispose( crossinline onDisposeEffect: () -> Unit  ): DisposableEffectResult
DisposableEffect(isIntercept) {
    if (isIntercept){
        backDispatcher.addCallback(backCallback)
    }
    onDispose {    //作用域中必须调用onDespose()
        backCallback.remove()
    }
}
@Composable
fun Demo() {
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
        //创建观察者
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_CREATE -> {}
                Lifecycle.Event.ON_START -> {}
                Lifecycle.Event.ON_RESUME -> {}
                Lifecycle.Event.ON_PAUSE -> {}
                Lifecycle.Event.ON_STOP -> {}
                Lifecycle.Event.ON_DESTROY -> {}
                else -> {}
            }
        }
        //把观察者加到生命周期里
        lifecycleOwner.lifecycle.addObserver(observer)
        //可组合项被移除时,移除观察者
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

2.3 SideEffect()

@Composable
fun SideEffect(
    effect: () -> Unit
)

相当于简化版的DisposableEffect(无key控制,无onDespose回调)。

//返回值非Unit类型的组合,函数名称采用常规的小写开头
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {...}    //不会随重组改变
    SideEffect {
        //将重组后的user新值传递给非Compose管理的对象analytics
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

2.4 rememberCoroutineScope()

@Composable
inline fun rememberCoroutineScope(
    crossinline getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext }
): CoroutineScope
@Composable
fun MyComposable() {
    val scope = rememberCoroutineScope()
    var str by remember { mutableStateOf("默认文字") }
    Button(onClick = {
        scope.launch {
            str = "更改后文字"
        }
    }) {
        Text("点击更改")
    }
    Text(str)
}

2.5 rememberUpdatedState()

@Composable

fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

//创建和更新状态集于同一个函数中完成。

//延迟3s后执行传入的onTimeOut,等待的这3s期间更改了传参onTimeout引发重组
//使用 rememberUpdatedState() 就可以让不受重组影响的LaunchedEffect拿到更新后的onTimeout
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
    val currentOnTimeout by rememberUpdatedState(onTimeout)
    LaunchedEffect(Unit) {
        delay(3000)
        currentOnTimeout()
    }
}
//按钮在倒计时5秒内点击会改变最终显示的值,但不会打断倒计时
@Composable
fun One() {
    var numState by remember { mutableStateOf(0) }
    Column {
        Button(onClick = { numState += 1 }) {
            Text(text = "点击改变值$numState")  //按钮文字即时显示是否变化(重组)
        }
        Two(num = numState)
    }
}
@Composable
fun Two(num: Int) {
    val numState by rememberUpdatedState(newValue = num)
    LaunchedEffect(Unit) {    //传入Unit当key不会随重组而重新执行
        repeat(5){
            delay(1000)
            Log.e("----------------","倒计时:${ 5 - it }")    //倒数54321
        }
        Log.e("----------------","最终值:$numState")    //按钮累计点击次数
    }
}
//监听返回键,点击按钮会改变打印内容(重组)
//但LaunchedEffect不会重新执行却跟随改变了打印内容
@Composable
fun One(backDispatcher: OnBackPressedDispatcher) {
    val printA: () -> Unit = { Log.e("---------------", "print A") }
    val printB: () -> Unit = { Log.e("---------------", "print B") }
    var printState by remember { mutableStateOf(printA) }
    Button(onClick = { printState = if (printState == printA) printB else printA }) {
        Text(text = "点击改变打印内容")
    }
    Two(backDispatcher = backDispatcher, block = printState)
}
@Composable
fun Two(backDispatcher: OnBackPressedDispatcher, block: () -> Unit) {
    val blockState by rememberUpdatedState(block)
    val backCallback = remember {   //重写返回键的监听回调
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                blockState()
            }
        }
    }
    LaunchedEffect(Unit) {  //传入Unit当key不会随重组而重新执行
        backDispatcher.addCallback(backCallback)    //设置回调
    }
}
//Activity中调用并传入
//One(onBackPressedDispatcher)

2.6 produceState()

@Composable
fun <T> produceState(
    initialValue: T,
    key1: Any?,
    key2: Any?,
    producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> 
@Composable
fun demo(   //返回值非Unit的组合,函数名采用常规小写开头
    url: String,
    repository: ImageRepository
) = produceState(   //返回值类型是 State<ImageResult<ImageBitmap>>
    initialValue = ImageResult.Loading as ImageResult<ImageBitmap>,
    key1 = url, //key值变化都会重新执行block
    key2 = repository
) {
    val image = repository.loadImage(url)    //调用挂起函数
    value = if (image == null) ImageResult.Error else ImageResult.Success(image)
}
sealed interface ImageResult<T> {
    object Loading : ImageResult<ImageBitmap>
    object Error : ImageResult<ImageBitmap>
    data class Success(val imageBitmap: ImageBitmap) : ImageResult<ImageBitmap>
}
class ImageRepository {
    suspend fun loadImage(url: String): ImageBitmap? = withContext(Dispatchers.IO) { null }
}

2.7 derivedStateOf()

fun <T> derivedStateOf(
    calculation: () -> T,
): State<T>
//每次重组遍历titles看是否包含关键字的标题,非常耗性能
//只在每次更新titles的时候去遍历过滤出包含关键字的标题
@Composable
fun Demo(
    keywords:List<String> = listOf("关键字1", "关键字2", "关键字3")
) {
    val titles = remember { mutableStateListOf<String>() }
    val result = remember(keywords) {
        derivedStateOf {
            titles.filter { keywords.contains(it) }
        }
    }
    LazyColumn(modifier = Modifier.fillMaxWidth()) {
        items(result) { ... }   //包含关键字的标题的列表
        items(titles) { ... }   //全部标题的列表
    }
}
@Composable
fun Demo() {
    val list = remember { mutableStateListOf<String>() }
    val showText by remember { derivedStateOf{ list.size.toString() } }
}

2.8 snapshotFlow()

fun <T> snapshotFlow(
    block: () -> T
): Flow<T>
val listState = rememberLazyListState()
LazyColumn(state = listState) {
    items(100) { Text(text = "Item $it") }
}
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .filter { it > 20 }    //从索引20开始收集
        .distinctUntilChanged()
        .collect {...}
}
<think>好的,我现在要帮用户解决如何使用docker-compose -f指令启动指定的docker-compose-energy.yml文件中的服务。首先,我需要回忆一下docker-compose的基本命令结构。 用户提到了使用-f选项,所以根据Docker Compose的文档,-f是用来指定自定义的compose文件的。默认情况下,docker-compose会找当前目录下的docker-compose.yml文件,但如果用户有自己的文件,比如这里的docker-compose-energy.yml,就需要用-f来指定。 接下来,我需要确认正确的命令格式。基本结构应该是docker-compose -f [文件名] up,然后可能需要加上其他选项,比如-d在后台运行。所以基础命令可能是docker-compose -f docker-compose-energy.yml up -d。这样就能启动服务并在后台运行。 然后,我需要考虑用户可能的后续需求,比如如何停止服务。这时候应该用docker-compose -f 文件名 down。还有查看日志的命令,比如docker-compose -f 文件名 logs -f。这些都是常见的使用场景,应该包含在回答里,方便用户后续操作。 另外,用户可能对docker-compose.yml文件的结构不太熟悉,或者需要检查配置是否正确。可能需要提醒他们在启动前验证文件格式是否正确,可以使用docker-compose config命令。不过用户的问题主要是启动,所以这部分可以作为补充信息。 还要注意,如果用户的环境中有多个compose文件,可能需要合并或者其他操作,但根据问题描述,用户只需要启动一个特定的文件,所以暂时不需要涉及多文件的情况。 最后,确保回答的结构清晰,先给出基础启动命令,然后列出常用操作,这样用户能一步步跟着操作。同时引用相关文档部分,比如引用中的docker-compose up -d和docker-compose down,来增强可信度。</think>根据Docker Compose的使用规范,启动指定YAML文件的服务需使用`-f`参数指定配置文件路径,具体操作步骤如下: **基础启动命令** ```bash docker-compose -f docker-compose-energy.yml up -d ``` **参数说明** - `-f`:指定自定义的Compose文件路径 - `up`:创建并启动服务容器 - `-d`:以守护进程模式(后台运行) **常用扩展操作** 1. **停止并删除服务** ```bash docker-compose -f docker-compose-energy.yml down ``` 该命令会停止容器并移除网络/数据卷(除非配置了持久化卷)[^2] 2. **查看实时日志** ```bash docker-compose -f docker-compose-energy.yml logs -f ``` 3. **重建镜像并启动** ```bash docker-compose -f docker-compose-energy.yml up -d --build ``` 当修改了Dockerfile或依赖文件后需要重建镜像时使用[^3] 4. **仅验证配置文件** ```bash docker-compose -f docker-compose-energy.yml config ``` 用于检查YAML文件语法正确性,验证通过无输出[^2] **注意事项** 1. 文件路径支持相对路径和绝对路径,例如: ```bash docker-compose -f ./config/docker-compose-energy.yml up -d ``` 2. 如果文件中包含`build`指令,会优先使用本地构建的镜像而非仓库镜像[^3] 3. 端口映射冲突时会出现错误提示,需检查`ports`配置项是否与其他服务冲突[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值