Administrator
Administrator
Published on 2024-11-29 / 51 Visits
3
0

副作用(Effect APIs)

文档链接

1.LaunchedEffect

Composable组件初始化时执行一次。在可组合项内运行挂起函数,如果依赖的Key发生变化(如果前一次协程正在工作将会提前结束)重新运行挂起函数。传入1~N个KEY

var a by remember { mutableStateOf(0) }
LaunchedEffect(a) {
    println("a changed $a")
}
Button(onClick = {
    a++
}) {
    Text("增加")
}

2.produceState:将非Compose状态转换为Compose状态

在协程中将任意值从非Compose状态转为Compose状态

可以传入key值(支持1~3个或vararg动态参数),在key变化状态时重新启动producer。也可以不传,在compose中只运行一次。

fun <T> produceState(
    initialValue: T,
    producer: suspend ProduceStateScope<T>.() -> Unit
): State<T>

fun <T> produceState(
    initialValue: T,
    key1: Any?,
    producer: suspend ProduceStateScope<T>.() -> Unit
): State<T>

下方代码中,ApiClient是retrofit HTTP库,执行网络请求。

    val warehouseResponse : State<List<GetAllWarehouseResponseItem>?> = produceState<List<GetAllWarehouseResponseItem>?>(null) {
        val result = ApiClient.apiService.getAllWarehouse()
        value = result.data //result.data的类型是List<GetAllWarehouseResponseItem>?
    }
    Column {
        warehouseResponse.value?.forEach { item ->
            Text(item.name)
        }
    }

在输入内容时,重新发送网络请求。

    var s by remember { mutableStateOf("") }
    val warehouseResponse : State<List<GetAllWarehouseResponseItem>?> = produceState<List<GetAllWarehouseResponseItem>?>(
      null,
      s /* 变化时重新运行lambda */
    ) {
        val result = ApiClient.apiService.getAllWarehouse(s)
        value = result.data //result.data的类型是List<GetAllWarehouseResponseItem>?
    }
    Column {
        warehouseResponse.value?.forEach { item ->
            Text(item.name)
        }
        TextField(value = s, placeholder = {
            Text("请输入搜索内容")
        }, onValueChange = { s = it })
    }

3.derivedStateOf

根据一个或多个状态派生出新的只读状态。当状态与页面的更新频率不需要一致时,使用这个API

官方文档的示例:

@Composable
// When the messages parameter changes, the MessageList
// composable recomposes. derivedStateOf does not
// affect this recomposition.
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

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

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

listState.firstVisibleItemIndex本就是可以让UI重组的状态,但如果不用derivedStateOf,则每容器的滚动条每滚动一点都会触发一次AnimatedVisibility元素的重组,性能不佳

官方文档的错误示例:

// DO NOT USE. Incorrect usage of derivedStateOf.
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }

val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!!
val fullNameCorrect = "$firstName $lastName" // This is correct

4.snapshotFlow

块内的state变化时会收集变化的新值,如果与上一次发出的值不相等,则会发出新的值,相同的值不会触发收集(collect)

个人理解:监听复杂的状态(如List中的某个成员的某个state)变化,而执行一个事件

以下代码实现的效果是需要移除元素时,(假设items每个元素对应一个组件)只需将对应索引的visible.targetState设为false,就会在动画结束后移除(而不是立即 移除,这样就没有动画效果,很突兀)

val turquoiseColors =
    listOf(
        Color(0xff07688C),
        Color(0xff1986AF),
        Color(0xff50B6CD),
        Color(0xffBCF8FF),
        Color(0xff8AEAE9),
        Color(0xff46CECA)
    )
     class ColoredItem(val visible: MutableTransitionState<Boolean>, val itemId: Int) {
        val color: Color
            get() = turquoiseColors.let { it[itemId % it.size] }
    }
    val items: MutableList<ColoredItem> = remember { mutableStateListOf() }
    Column {
        LaunchedEffect(Unit) {
            snapshotFlow {
                //任意一个成员的visible改变且对应的元素动画已经结束了就会
                items.firstOrNull { it.visible.isIdle && !it.visible.targetState }
            }
                .collect {
                    if (it != null) {
                        // 存在不可见且动画结束的元素,可以在此执行删除操作。不关注抛出来的it,而是使用items.removeAll+items.filter去移除已经消失且动画结束的所有元素,因为可能在短时间内被移除了多个
                    }
                }
        }
}
snapshotFlow {}
  .onStart {
    //事件:开始收集
  }
  .onCompletion {
    //事件:停止收集
  }
  .collect {
    
  }

5.SideEffect

在可组合函数发生重组后(完成后)执行一次。官方文档说:

使用 SideEffect 可确保在每次成功重组后执行该效果。另一方面,在保证成功重新组合之前执行效果是不正确的,直接在可组合项中编写效果就是这种情况。

重点是在重组后,并且只执行一次。下面代码的println("外侧代码")可能会在重组时意外的运行多次,它不可控、不可靠。(虽然我在运行下面这段代码时是符合我的预期的......)

GPT4的回答:

SiteEffect 用来处理这样的副作用。你可以将它用于在 Compose 中执行一次性的操作,比如进行数据请求、更新全局状态、执行动画、日志记录、订阅事件等。它的行为和 LaunchedEffect 类似,但是它有自己的特性和适用场景。

我还不知道它具体有什么用,可能需要写到具体业务逻辑才会明白......

    var s by remember { mutableStateOf(0) }
    SideEffect {
        println("siteEffect $s")
    }
    println("outside $s")
    Column {
        Button(onClick = {
            s++
        }) {
            Text("plus")
        }
        Text("s: $s")
    }

点击多次后的日志:(好像没啥意外的....)

2024-11-30 19:27:15.086 15327-15327 System.out              com.dmjyb.cangku                     I  outside 0
2024-11-30 19:27:15.106 15327-15327 System.out              com.dmjyb.cangku                     I  siteEffect 0
2024-11-30 19:27:18.622 15327-15327 System.out              com.dmjyb.cangku                     I  outside 1
2024-11-30 19:27:18.623 15327-15327 System.out              com.dmjyb.cangku                     I  siteEffect 1
2024-11-30 19:27:20.240 15327-15327 System.out              com.dmjyb.cangku                     I  outside 2
2024-11-30 19:27:20.241 15327-15327 System.out              com.dmjyb.cangku                     I  siteEffect 2
2024-11-30 19:27:21.557 15327-15327 System.out              com.dmjyb.cangku                     I  outside 3
2024-11-30 19:27:21.559 15327-15327 System.out              com.dmjyb.cangku                     I  siteEffect 3
2024-11-30 19:27:23.256 15327-15327 System.out              com.dmjyb.cangku                     I  outside 4
2024-11-30 19:27:23.257 15327-15327 System.out              com.dmjyb.cangku                     I  siteEffect 4

6.rememberUpdatedState

适用于在保证可组合函数内的LaunchedEffect(或其它DisposableEffect等)访问状态变量的参数是最新的值

也就是通过参数传递过来的状态,保证其在Effect APIs中访问得到的是最新的值,而不是旧的值。

下面的代码在ChildScreen中如果不用rememberUpdatedState 那么在点击Increment counter和赋值新的lambda按纽后,访问的依然是旧的值和旧的函数引用

@Composable
fun ParentScreen() {
    var counter by remember { mutableStateOf(0) }

    var onTimeout by remember { mutableStateOf({
        println("Parent Timeout triggered with counter: $counter")
    }) }
    println("parent recomposition")
    Column {
        Button(onClick = { counter++ }) {
            Text("Increment counter")
        }
        Text("parent val: $counter")
        Button(onClick = {
            onTimeout = {
                println("new lambda")
            }
        }) {
            Text("赋值新的lambda")
        }
        ChildScreen(counter, onTimeout = onTimeout)
    }
}

@Composable
fun ChildScreen(i:Int, onTimeout: () -> Unit) {
    val currentOnTimeout by rememberUpdatedState(onTimeout)
    val currentInt by rememberUpdatedState(i)
    LaunchedEffect(true) {
        while (true) {
            delay(2000)
            currentInt
            currentOnTimeout()
        }
    }
}

7.DisposableEffect

对于需要在键发生变化或可组合项退出组合后进行清理的附带效应

    var a by remember { mutableStateOf(0) }
    DisposableEffect(a) {
        println("DisposableEffect")
        onDispose {
            println("onDispose")
        }
    }
    println("recomposition")
    Column {
        Button(onClick = {
            a++
        }) {
            Text("增长")
        }
        Text("a: $a")
    }

以上代码执行日志:

recomposition
DisposableEffect
a++
recomposition
onDispose
DisposableEffect
a++
recomposition
onDispose
DisposableEffect

第一次进入组件时,会执行DisposableEffect但不会执行onDispose;每次a变化时,都会重新启动DisposableEffect{},但是会先执行onDispose 用于清理上一次初始化/监听的一些资源;在组件退出时会执行onDispose

也可以不监听key值,只用于初始化和退出时销毁

    DisposableEffect(Unit) {
        println("启动的时候执行")
        onDispose {
            println("退出的时候销毁启动时打开的资源")
        }
    }

8.rememberCoroutineScope

在composable中启动协程,去处理动画、延时、网络请求等耗时的工作

    val scope = rememberCoroutineScope()
    val a = remember { Animatable(0f) }
    Button(onClick = {
        scope.launch {
            delay(2000)
            a.animateTo(1f)
        }
    }) {
        Text("Button")
    }


Comment