Jeptpack Compose 官网教程学习笔记(八)附带效应

附带效应

学习内容

  • 如何从 Compose 代码观察数据流以更新界面
  • 附带效应 API,如 LaunchedEffectrememberUpdatedStateDisposableEffectproduceStatederivedStateOf
  • 如何使用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 注解来显式标记为稳定的类型

稳定不可变类型
  • 所有基元值类型:BooleanIntLongFloatChar
  • 字符串
  • 所有函数类型 (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")
            }
        }
    }
}

在上面的代码中,如果showSnacktrue,则会触发协程,如果为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()
    }
    //假设界面一直在加载
}

假设我们为界面加载时间过长设置了处理事件,根据状态不同处理方式也不同,例如:横竖屏

通过常量(如 Unittrue)将作为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发生变化或可组合项退出组合后执行的附带效应 ( 一般用于清理工作 ) 。如果 DisposableEffectKey发生变化,可组合项需要处理(执行清理操作)其当前效应,并通过再次调用效应进行重置

例如,我们通过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

使用 snapshotFlowState<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 中有一些效应(如 LaunchedEffectproduceStateDisposableEffect)会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新的效应

API典型形式为:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

如果用于重启效应的参数不是适当的参数,可能会出现问题:

  • 如果重启效应次数不够,可能会导致应用出现错误
  • 如果重启效应次数过多,效率可能不高

一般来说,效应代码块中使用的可变和不可变变量应作为参数添加到效应可组合项中。除此之外,您还可以添加更多参数,以便强制重启效应

  • 如果更改变量不应导致效应重启,则应将该变量封装在 rememberUpdatedState

  • 如果由于变量封装在一个不含键的 remember 中使之没有发生变化,则无需将变量作为键传递给效应

应将效应中使用的变量添加为效应可组合项的参数,或使用 rememberUpdatedState

使用常量作为Key

可以使用 trueUnit 等常量作为效应键,使其遵循调用点的生命周期。它实际上具有有效的用例,如上面所示的 LaunchedEffect 示例。但在这样做之前,请审慎考虑,并确保您确实需要这么做

Jeptpack Compose 官网教程学习笔记到此差不多结束了,测试和无障碍部分暂时省略

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容