附带效应
学习内容
- 如何从 Compose 代码观察数据流以更新界面
- 附带效应 API,如
LaunchedEffect
、rememberUpdatedState
、DisposableEffect
、produceState
和derivedStateOf
- 如何使用
rememberCoroutineScope
API 在可组合项中创建协程并调用挂起函数
因为官网所给的案例中,需要申请谷歌地图密钥,会有一些麻烦。而且还需要了解项目中的组件代码,增加了学习成本
所以该部分使用一些 小demo 进行学习
在了解 compose 的高级状态和附带效应之前,我们先来了解Compose 中可组合项的生命周期
生命周期
当 Jetpack Compose 首次运行可组合项时,在初始组合期间,它将跟踪调用的可组合项。然后,当应用的状态发生变化时,Jetpack Compose 会安排重组。重组是指 Jetpack Compose 重新执行可能因状态更改而更改的可组合项,然后更新组合以反映所有更改
组合只能通过初始组合生成且只能通过重组进行更新。重组是修改组合的唯一方式
可组合项的生命周期通过以下事件定义:进入组合,执行 0 次或多次重组,然后退出组合
重组通常由对 State<T>
对象的更改触发。Compose 会跟踪这些操作,并运行组合中读取该特定 State<T>
的所有可组合项以及这些操作调用的无法跳过的所有可组合项
如果某一可组合项多次被调用,在组合中将放置多个实例。每次调用在组合中都有自己的生命周期
@Composable
fun MyComposable() {
Column {
Text("Hello")
Text("World")
}
}
图中某一元素具有不同颜色,则表明它是一个独立实例
组合中可组合项的剖析
组合中可组合项的实例由其调用点进行标识。Compose 编译器将每个调用点都视为不同的调用点。从多个调用站点调用可组合项会在组合中创建多个可组合项实例
调用点是调用可组合项的源代码位置
在重组期间,可组合项调用的可组合项与上个组合期间调用的可组合项不同,Compose 将确定调用或未调用的可组合项,对于在两次组合中均调用的可组合项,如果其输入未更改,Compose 将避免重组这些可组合项
@Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput()
}
@Composable
fun LoginInput() { /* ... */ }
在上面的代码段中,LoginScreen
将有条件地调用 LoginError
可组合项,并始终调用 LoginInput
可组合项。每个调用都有唯一的调用点和源位置,编译器将使用它们对调用进行唯一识别
图中颜色相同,表示尚未重组
LoginInput
两次调用中,LoginInput
实例仍将在不同重组中保留下来。因为LoginInput
不包含任何在重组过程中更改过的参数,因此 Compose 将跳过对LoginInput
的调用
额外信息帮助重组
多次调用同一可组合项也会多次将其添加到组合中。如果从同一个调用点多次调用某个可组合项,Compose 就无法唯一标识对该可组合项的每次调用,因此除了调用点之外,还会使用执行顺序来区分实例
执行顺序来区分实例,在某些情况下会导致发生意外行为
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
// 遍历 movie 数据创建对应的 MovieOverview控件
MovieOverview(movie)
}
}
}
在上面的示例中,Compose 除了使用调用点之外,还使用执行顺序来区分组合中的实例。如果列表底部新增了一个 movie
,Compose 可以重复使用组合中既有的实例,因为这些实例在列表中的位置没有发生变化,因此这些实例的 movie
输入是相同的
但是,如果因在列表顶部或中间新增内容,移除项目或对项目进行重新排序而导致 movies
列表发生改变,将导致输入参数在列表中的位置已更改的所有 MovieOverview
调用发生重组
MovieOverview
中不同的颜色表示该可组合项已重组向列表顶部插入一条数据,导致列表中元素顺序发生变化。也导致了对应
movie
元素的MovieOverview
执行顺序变化,Compose 根据执行顺序和前一次组合相比较导致所有都需要重组
很显然,除了新插入的MovieOverview
,其余的MovieOverview
都应该是不需要重组的。所以我们需要通过key
标识实例
通过调用带有一个或多个传入值的键可组合项来封装代码块,这些值将被组合以用于在组合中标识该实例。key
的值不必是全局唯一的,只需要在调用点处调用可组合项的作用域内确保其唯一性即可
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
//id 值在 movie 中是唯一的
key(movie.id) {
MovieOverview(movie)
}
}
}
}
使用上述代码后,即使列表中的元素发生变化,Compose 也能识别 MovieOverview
的各个调用,还可以重复使用这些调用
向列表中添加新元素后,由于
MovieOverview
可组合项具有唯一键,因此 Compose 会识别未更改的MovieOverview
实例,并且可重复使用它们一些可组合项提供对
key
可组合项的内置支持。例如,LazyColumn
接受在items
DSL 中指定自定义key
@Composable fun MoviesScreen(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
稳定类型
如果可组合项中所有输入都处于稳定状态且没有变化时,可以跳过重组
稳定类型必须符合以下协定:
- 对于相同的两个实例,其
equals
的结果将始终相同 - 如果类型的某个公共属性发生变化,组合将收到通知
- 所有公共属性类型也都是稳定
满足上述条件 Compose 编译器也会将其视为稳定的类型
可以使用
@Stable
注解来显式标记为稳定的类型
稳定不可变类型
- 所有基元值类型:
Boolean
、Int
、Long
、Float
、Char
等 - 字符串
- 所有函数类型 (lambda)
上述类型都遵循稳定协定,因为它们是不可变的。由于不可变类型绝不会发生变化,它们就永远不必通知组合更改方面的信息
稳定可变类型
Compose 的 MutableState
类型是一种稳定但可变的类型。如果 MutableState
中保留了值,状态对象整体会被视为稳定对象,因为 State
的 .value
属性如有任何更改,Compose 就会收到通知
当作为参数传递到可组合项的所有类型都很稳定时,系统会根据可组合项在界面树中的位置来比较参数值,以确保相等性。如果所有值自上次调用后未发生变化,则会跳过重组
比较时使用
equals
方法
@Stable
Compose 仅在可以证明稳定的情况下才会认为类型是稳定的。例如,接口通常被视为不稳定类型、具有可变公共属性的类型(实现可能不可变)的类型也被视为不稳定类型
如果 Compose 无法推断类型是否稳定,但您想强制 Compose 将其视为稳定类型,请使用 @Stable
注解对其进行标记
// 将类型标记为稳定类型,有利于智能重组
@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: Throwable?
val hasError: Boolean
get() = exception != null
}
在上面的代码段中,由于
UiState
是接口,因此 Compose 通常认为此类型不稳定。通过添加@Stable
注解,您可以告知 Compose 此类型稳定,让 Compose 优先选择智能重组
附带效应介绍
函数层面的附带效应
纯函数:函数与外界交换数据只能通过函数参数和函数返回值进行,纯函数不会对外界环境产生任何变化
fun add(a: Int, b: Int): Int {
return a + b
}
附带效应:指一个操作、函数或表达式在其内部与外界进行了互动,产生运算以外的其他结果,则该操作、函数或表达式具有副作用
其中最典型的情况是修改了外部环境的变量值
val a = 0
fun add(b: Int): Int {
a += b
return a
}
Compose 中的附带效应
可组合项中不应该有附带效应,但实际情况中有一部分逻辑需要通过附带效应实现,例如:网络图片加载、日志记录、显示另一个控件等,当然这些操作有几个前提:
- 执行时机是明确的,比如:在重组是触发
- 执行次数是可控的,不应该随函数重复执行
- 不会造成泄露,比如:资源文件的释放
切勿过于依赖于执行可组合函数所产生的附带效应,因为可能会跳过函数的重组
若非必要,在需要执行附带效应时,应通过
onClick
等回调的方式触发
按我的理解是可组合项中可以存在附带效应,但不合理的使用可组合项中的附带效应会导致 Compose 重组时出现不可预测的结果,所以需要通过Compose 提供的Effect API触发附带效应,API中带有对于可组合项生命周期的管理
LaunchedEffect
如需从可组合项内安全调用挂起函数,请使用 LaunchedEffect
可组合项
当 LaunchedEffect
进入组合时,它会启动一个协程,并将代码块作为参数传递。如果 LaunchedEffect
退出组合,协程将取消。如果使用不同的Key
重组 LaunchedEffect
,系统将取消现有协程,并在新的协程中启动新的挂起函数
*Effect*是一种可组合函数,该函数不会发出界面,会在组合完成后产生附带效应
在
Scaffold
中显示Snackbar
是通过SnackbarHostState.showSnackbar
函数完成的,该函数为挂起函数
@Composable
fun LaunchedEffectExample(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
var showSnack by remember {
mutableStateOf(false)
}
if (showSnack) {
LaunchedEffect(key1 = Unit) {
scaffoldState.snackbarHostState.showSnackbar(
message = "我是一个Snack",
actionLabel = "哦"
)
}
}
Scaffold(modifier = modifier, scaffoldState = scaffoldState) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
Button(onClick = { showSnack = !showSnack }) {
Text("LaunchedEffect")
}
}
}
}
在上面的代码中,如果showSnack
为true
,则会触发协程,如果为false
,则将取消协程
由于
LaunchedEffect
调用点在 if 语句中,因此当该语句为 false 时,如果之前LaunchedEffect
包含在组合中,则会被移除,因此,协程将被取消所以此处点击多次按钮不会弹出多个
SnackBar
rememberCoroutineScope
由于 LaunchedEffect
是可组合函数,因此只能在其他可组合函数中使用
因为存在作用域限制,所以为了在可组合项外启动协程,以便协程在退出组合后自动取消,请使用 rememberCoroutineScope
此外,rememberCoroutineScope
还可以作用于需要手动控制一个或多个协程的生命周期的场合,例如在用户事件发生时取消动画
rememberCoroutineScope
是一个可组合函数,会返回一个 CoroutineScope
,该 CoroutineScope 绑定到调用它的组合点。调用点退出组合后,作用域将取消
例如,在回调函数中启动协程,调用suspend
函数
@Composable
fun RememberCoroutineScopeExample(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
val scope = rememberCoroutineScope()
Scaffold(modifier = modifier, scaffoldState = scaffoldState) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
Button(onClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(
message = "我是一个Snack",
actionLabel = "我知道了"
)
}
}) {
Text("RememberCoroutine")
}
}
}
}
Button
参数onClick: () -> Unit
为一个普通函数,并不是可组合函数。通过rememberCoroutineScope
我们可以在可组合项外启动协程,该协程作用域与RememberCoroutineScopeExample
生命周期绑定所以此处点击多次按钮会弹出多个
SnackBar
rememberUpdatedState
当其中一个键参数发生变化时,LaunchedEffect
会重启。不过,在某些情况下,可能希望跟踪某个值,该值发生变化,但并不希望 LaunchedEffect
重启。为此,需要使用 rememberUpdatedState
来创建对可跟踪和更新值的引用
例如,通过rememberUpdatedState
在不重启的情况下,更换超时事件处理函数
@Composable
fun RememberUpdatedStateExample(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
var showMethod by remember { mutableStateOf(0) }
val color = { select: Boolean ->
if (select) Color.Magenta else Color.Gray
}
val texts = listOf("哦", "我知道了")
val timeoutList = List<suspend () -> Unit>(texts.size) {
return@List {
scaffoldState.snackbarHostState.showSnackbar(
message = "你超时了~( ̄▽ ̄)~*",
actionLabel = texts[it]
)
}
}
Scaffold(modifier = modifier, scaffoldState = scaffoldState) { innerPadding ->
Column(
...
) {
repeat(texts.size) {
MaterialTheme(colors = MaterialTheme.colors.copy(primary = color(showMethod == it))) {
Button(
onClick = { showMethod = it },
modifier = Modifier.width(120.dp)
) {
Text(texts[it])
}
}
}
LoadingScreen(timeoutList[showMethod])
}
}
}
@Composable
fun LoadingScreen(onTimeout: suspend () -> Unit, n: Int = 10) {
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
var i = 0
while (i < n) {
delay(1000)
Log.e("LoadingScreen", "delay ${i++}")
}
currentOnTimeout()
}
//假设界面一直在加载
}
假设我们为界面加载时间过长设置了处理事件,根据状态不同处理方式也不同,例如:横竖屏
通过常量(如
Unit
或true
)将作为Key
参数可以创建与调用点的生命周期相匹配的LaunchedEffect
,但是官网并不推荐这种写法
为了确保 onTimeout
lambda 始终包含重组 LoadingScreen
时使用的最新值,onTimeout
需使用 rememberUpdatedState
函数封装。LaunchedEffect
中应使用代码中返回的 State
、即currentOnTimeout
@Composable fun <T> rememberUpdatedState(newValue: T): State<T> = remember { mutableStateOf(newValue) }.apply { value = newValue }
其实
rememberUpdatedState
就是将创建State<T>
和更新值在一个函数中完成了而已
DisposableEffect
DisposableEffect
会在Key
发生变化或可组合项退出组合后执行的附带效应 ( 一般用于清理工作 ) 。如果 DisposableEffect
的Key
发生变化,可组合项需要处理(执行清理操作)其当前效应,并通过再次调用效应进行重置
例如,我们通过Switch
控制 Back 事件是否被拦截,并使用DisposableEffect
添加或删除回调函数
@Composable
fun DisposableEffectSample(modifier: Modifier = Modifier, backDispatcher: OnBackPressedDispatcher) {
// 控制是否拦截 back 事件
var isIntercept by remember {
mutableStateOf(false)
}
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Switch(checked = isIntercept, onCheckedChange = { isIntercept = !isIntercept })
Text(text = "是否拦截 back 事件")
}
//false,BackPressHandler 会从组合中移除
if (isIntercept) {
BackPressHandler(backDispatcher){
Log.e("DisposableEffectSample", "OnBackPressed done..." )
}
}
}
@Composable
fun BackPressHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit = {}) {
val currentOnBack by rememberUpdatedState(onBack)
val backCallback = remember {
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
currentOnBack()
}
}
}
DisposableEffect(backDispatcher) {
backDispatcher.addCallback(backCallback)
Log.e("BackPressHandler", "DisposableEffect invoke" )
//Key值变化或所在可组合项从组合树中移除时调用
onDispose {
Log.e("BackPressHandler", "DisposableEffect onDispose invoke" )
backCallback.remove()
}
}
}
在上面的代码中,根据isIntercept
状态控制BackPressHandler
是否添加到组合中,DisposableEffect
会在可组合项退出组合后执行,将 callback 移除避免内存泄漏
OnBackPressedDispatcher
可以注册回调函数,并在back事件触发时调用回调
DisposableEffect
必须在其代码块中添加onDispose
子句作为最终语句。否则,IDE 将显示构建时错误
SideEffect
如需与非 Compose 管理的对象共享 Compose 状态,请使用 SideEffect
可组合项,因为每次成功重组时都会调用该可组合项
例如,分析库可能允许通过将自定义元数据(在此示例中为“用户属性”)附加到所有后续分析事件,来细分用户群体。如果要将当前用户的用户类型传递给分析库,使用 SideEffect
更新其值
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* 阿巴阿巴 */
}
// 每一次重组成功后,都会将当前用户的用户类型传递给分析库
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
官方推荐含返回值类型的可组合项应采用常规 Kotlin 函数命名方式命名,以小写字母开头
produceState
produceState
会启动一个协程,该协程将作用域与可组合项生命周期绑定,并可以在协程内给返回的 State
进行赋值。使用此协程将非 Compose 状态转换为 Compose 状态,例如将外部订阅驱动的状态(如 Flow、LiveData 或 RxJava)引入组合
例如,网络加载图片根据加载情况转换为状态,显示对应控件
class ImageRepository private constructor(_context: Context) {
val context: Context = _context
companion object {
@SuppressLint("StaticFieldLeak")
private var sInstance: ImageRepository? = null
fun getInstance(context: Context): ImageRepository {
if (sInstance == null) {
synchronized(this) {
if (sInstance == null) {
sInstance = ImageRepository(context)
}
}
}
return sInstance!!
}
}
private val imageLoader by lazy {
ImageLoader(context = context)
}
suspend fun loadNetworkImage(url: String): ImageBitmap {
val request = ImageRequest.Builder(context)
.data(url)
.build()
return (imageLoader.execute(request).drawable as BitmapDrawable).bitmap.asImageBitmap()
.apply {
//增加延迟,方便看出状态转换效果
delay(3_000)
}
}
}
sealed class Result<T>() {
object Loading : Result<ImageBitmap>()
object Error : Result<ImageBitmap>()
data class Success(val image: ImageBitmap) : Result<ImageBitmap>()
}
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<ImageBitmap>> {
Log.e("ProduceStateExample", "loadNetworkImage: invoke" )
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, url, imageRepository) {
// value = Result.Loading
val image = imageRepository.loadNetworkImage(url)
//value 为 MutableState 中的属性
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
@Composable
fun ProduceStateExample() {
val urlList = listOf(
"https://upload.jianshu.io/users/upload_avatars/22683414/11b218ff-7c9a-43dd-a84a-b3b41de13318.jpg",
"https://upload-images.jianshu.io/upload_images/22683414-b9b5573a2593782d.jpg",
"https://upload-images.jianshu.io/upload_images/22683414-085dd9419c890908.jpg"
)
val imageRepository = ImageRepository.getInstance(LocalContext.current)
// 通过控制索引值的变化,从集合中取出相应的网络图片路径
var index by remember { mutableStateOf(0) }
// 加载网络图片,返回一个 State,当 State 为不同的值时,显示不同的可组合项
val result by loadNetworkImage(urlList[index], imageRepository)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when (result) {
is Result.Success -> {
Image(
bitmap = (result as Result.Success).image,
contentDescription = "image load success"
)
}
is Result.Error -> {
Image(
imageVector = Icons.Rounded.Warning,
contentDescription = "image load error",
modifier = Modifier.size(200.dp, 200.dp)
)
}
else -> {
CircularProgressIndicator() // 进度条
}
}
Button(
onClick = {
index %= urlList.size
if (++index == urlList.size) index = 0
}
) {
Text(text = "选择第 $index 张图片")
}
}
}
在上面的代码中,produceState
启动一个协程,根据initialValue
先创建一个State
,然后协程中根据loadNetworkImage
结果再为State
赋新值
produceState
会进入组合时启动,在其退出组合时取消。同时只要Key
发生变化就会进行重组,但initialValue
只会在第一次的时候赋值,后续重组不会重新赋值如需移除对该数据源的订阅,在
produceState
中使用awaitDispose
函数
图片加载框架使用
coil
,如需使用请添加依赖和网络请求权限// build.gradle dependencies { implementation 'io.coil-kt:coil-compose:1.4.0' ... }
derivedStateOf
如果某个状态是从其他状态对象计算或派生得出的,请使用 derivedStateOf
。使用此函数可确保仅当计算中使用的状态之一发生变化时才会进行计算
以下示例展示了基本的“待办事项”列表,其中具有用户定义的高优先级关键字的任务将首先显示:
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
val todoTasks = remember { mutableStateListOf<String>() }
//highPriorityTasks 只会在 todoTasks(计算源) 或 highPriorityKeywords(Key)
//发生变化时重组,其他情况下不会发生重组
val highPriorityTasks by remember(highPriorityKeywords) {
derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
}
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(highPriorityTasks) { /* ... */ }
items(todoTasks) { /* ... */ }
}
//剩余的UI部分可以让用户添加列表元素
}
}
在以上代码中,derivedStateOf
保证每当 todoTasks
发生变化时,系统都会执行 highPriorityTasks
计算,并相应地更新界面。如果 highPriorityKeywords
发生变化,系统将执行 remember
代码块,并且会创建新的派生状态对象并记住该对象,以代替旧的对象
此外,更新
derivedStateOf
生成的状态不会导致可组合项在声明它的位置(TodoList
可组合项)重组,Compose 仅会对返回状态为已读的可组合项(在本例中,指LazyColumn
中的可组合项)进行重组
snapshotFlow
使用 snapshotFlow
将 State<T>
对象转换为冷 Flow。snapshotFlow
会在收集到块时运行该块,并发出从块中读取的 State 对象的结果。当在 snapshotFlow
块中读取的 State
对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值
下列示例,系统在用户滚动经过要分析的列表项目:
@Composable
fun TodoList(){
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.filter { it > 5 }
.collect {
MyAnalyticsService.sendScrolledItemEvent($it)
}
}
}
在上方代码中,listState.firstVisibleItemIndex
被转换为一个 Flow,通过 Flow 运算符只分析索引大于 5 的列表项
总结
Effect | 启动协程 | Key | 作用 |
---|---|---|---|
LaunchedEffect |
启动 |
Key 变化时重组 |
加入组合时会启动一个协程,退出会销毁 |
DisposableEffect |
不启动 |
Key 变化时重组 |
在可组合项退出组合后执行 |
SideEffect |
不启动 | 没有Key 值 |
重组时会执行该效应 |
rememberCoroutineScope
可以在可组合项外启动协程,以便协程在退出组合后自动取消
如果
Effect
分别对应了可组合项的进入、离开、重组的生命周期状态
高级状态 | 启动协程 | 作用 |
---|---|---|
rememberUpdatedState |
不启动 | 在不改变Key 值情况下,更新Effect 中的值 |
produceState |
启动 | 将协程作用域与可组合项生命周期绑定,并可以在协程内给返回的 State 进行赋值 |
derivedStateOf |
不启动 | 从其他状态对象计算或派生得出新的状态 |
snapshotFlow |
不启动 | 将 State<T> 对象转换为冷 Flow |
重启效应
Compose 中有一些效应(如 LaunchedEffect
、produceState
或 DisposableEffect
)会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新的效应
API典型形式为:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
如果用于重启效应的参数不是适当的参数,可能会出现问题:
- 如果重启效应次数不够,可能会导致应用出现错误
- 如果重启效应次数过多,效率可能不高
一般来说,效应代码块中使用的可变和不可变变量应作为参数添加到效应可组合项中。除此之外,您还可以添加更多参数,以便强制重启效应
如果更改变量不应导致效应重启,则应将该变量封装在
rememberUpdatedState
中如果由于变量封装在一个不含键的 remember 中使之没有发生变化,则无需将变量作为键传递给效应
应将效应中使用的变量添加为效应可组合项的参数,或使用
rememberUpdatedState
使用常量作为Key
可以使用 true
、Unit
等常量作为效应键,使其遵循调用点的生命周期。它实际上具有有效的用例,如上面所示的 LaunchedEffect
示例。但在这样做之前,请审慎考虑,并确保您确实需要这么做
Jeptpack Compose 官网教程学习笔记到此差不多结束了,测试和无障碍部分暂时省略