Android | Compose 生命周期和附带效应

简介: Android | Compose 生命周期和附带效应

前言


可组合项 应该没有附带效应,但是,如果在对应用状态进行转变时需要使用可组合项。此时你应该使用 Effect API , 以便以可以预测的方式来执行这些附带效应


附带效应是指在可组合函数范围之外发生的应用状态变化,用一句话概况就是:一个函数在执行的过程中,除了返回数值意以外,对调用方还会带来其他附加的影响,例如修改全局变量和参数等。


生命周期


当 Compose 首次运行可组合项的时候,在初始组合期间,他将跟踪为了描述界面而调用的组合项。当应用的状态发生变化时,Compose 会安排重组。重组指的是 Compose 重新执行可能因状态更改而更改的组合项。


组合只能通过初始组合生成且之鞥你通过重组更新。重组是修改组合的唯一方式。


可组合项的生命周期通过以下事件定义:进入组合,执行0次或者多次,最后退出组合


0a2653c851af460fa595bd959398a8f1.png


状态和效应用例


如官方文档所述,可组合项应当没有附带效应,如果需要更改应用状态,则就应该使用 Effect API ,以便以可预测的方式来执行这些附带效应


效应简称 Effect ,如果 API 上有 Effect 关键字的一般就是处理效应的了。Compose 中用的最多的就是 SideEffect 和 DisposableEffect 了。


LaunchedEffect


在某个可组合项的作用域内运行挂起函数。如果需要从组合项中安全带的调用挂起函数,请使用 LaunchedEffect 可组合项。


当 LaunchedEffect 进入组合时,他会启动一个协程,并将代码块作为参数传递。如果 LaunchedEffect 退出组合,协程将会取消。


如果使用不同的键重组 LaunchedEffect ,系统将取消现有的协程,并在新的协程中启动新的挂起函数。


例如在一个顶级的页面中进行网络请求,请求是通过 LaunchedEffect 中创建的协程来完成的,如果发生这个过程中函数重组了,协程也会相应的取消,并重新创建协程在重新执行。下面示例中将请求的结果当做成了键,这样当请求成功后,下次重组的时候也不会重新执行协程。如果重新重新获取数据,只需要修改 value 即可,例如示例中的按钮点击事件。


@Composable
fun HomeDetail() {
    val state = rememberUserState()
    LaunchedEffect(key1 = state) {
      //模拟网络操作  
        delay(3000)
        state.value = MyUserState()
    }
    state.value?.run {
        Log.e("---345--->", this.name);
        Log.e("---345--->", this.toString());
        Scaffold() {
            Column() {
                Spacer(modifier = Modifier.padding(top = 50.dp))
                Button(onClick = {
                    state.value = MyUserState("张三",50)
                }) {
                    Text(text = "按钮")
                }
                Spacer(modifier = Modifier.padding(top = 100.dp))
                Text(text = "姓名 ${this@run.name}")
                Text(text = "姓名 ${this@run.age}")
            }
        }
    }
}
@Composable
fun rememberUserState(myUserState: MutableState<MyUserState?> = mutableStateOf(null)) =
    remember() {
        mutableStateOf(myUserState.value)
    }
}
class MyUserState(
    var name: String = "345",
    var age: Int = 20
)


另外,官方还提供了一种用法:


@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {
    // 如果 UI 状态包含错误,则显示提示栏
    if (state.hasError) {
        // `LaunchedEffect` 将取消并重新启动,如果`scaffoldState.snackbarHostState` 改变
        LaunchedEffect(scaffoldState.snackbarHostState) {
            // 使用协程显示snackbar,当协程被取消时snackbar 会自动关闭。当 `state.hasError` 为 false 时,此协程将取消,并且仅在 `state.hasError` 为 true(由于上述 if-check)或 `scaffoldState.snackbarHostState` 更改时启动
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }
    Scaffold(scaffoldState = scaffoldState) {
        /* ... */
    }
}


在上面代码中,如果状态发生错误,则会触发协程,如果没有错误,将会取消协程。由于 LaunchedEffect 调用点在 if 语句中,隐藏当该语句为 false 时,如果LaunchedEffect 包含在组合中,则会被移除,隐藏协程将会被取消。


rememberCoroutineScope


获取组合感知作用域,以便可以在组合外启动协程


由于 LaunchedEffect 是可组合函数,只能在可组合函数中使用。为了在可组合外启动协程,但是存在于作用域的限制,以便协程在退出组合时自动取消,这种情况就可以使用 rememberCoroutineScope 。此外,如果您需要手动控制一个或者多个协程生命周期,也可以使用它。


rememberCoroutineScope 是一个可组合函数,会返回一个 CoroutineScope ,该协程绑定到调用他的组合点。调用退出组合后,作用域取消。


下面看一下小栗子,可组合函数退出后,内部的协程就会被取消。


var coroutineScope: CoroutineScope? = null
@Composable
fun HomeDetail(
    activity: MainActivity
) {
    val state = rememberUserState()
    coroutineScope = rememberCoroutineScope()
    coroutineScope?.launch {
        while (true) {
            delay(1000)
            Log.e("---345--->", "345");
        }
    }
}


下面是官方的例子


@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
    val scope = rememberCoroutineScope()
    Scaffold(scaffoldState = scaffoldState) {
        Column {
            Button(
                onClick = {
                    scope.launch {
                        scaffoldState.snackbarHostState.showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}


rememberUpdatedState


在效应中引用某个值,该效应在值改变时不重启


当一个键发生变化时,LaunchedEffect 会重启。但是在有些时候你可能希望在改效应中捕获某个值,但是如果这个值发生变化,你并不想效应重启。因此需要使用 rememberUpdatedState 来创建对可捕获和更新的该值的引用。这种方法对于包含长期操作的效应非常有用。


例如,LandingScreen 会在一段时间内消失或者重组,改函数内部的等待三秒钟也不应该重启。这种情况就可以使用 rememberUpdatedState。如下:


@Composable
fun LandingScreen(onTimeout: () -> Unit) {
    // 这将始终引用 LandingScreen 重构的最新 onTimeout 函数
    val currentOnTimeout by rememberUpdatedState(onTimeout)
    // 创建与 LandingScreen 的生命周期相匹配的效果。
    // 如果 LandingScreen 重组,延迟不应再次开始。
    LaunchedEffect(true) {
        delay(3000)
        currentOnTimeout()
    }
    /* Landing screen content */
}
@Composable
fun HomeDetail() {
    LandingScreen {
        Log.e("---345--->", "----");
    }
    //....
}


DisposableEffect


需要清理的效应


对于需要在键发生变化或者可组合项退出的时候进行清理的附带效应,可以使用 DisposableEffect。如果 DisposableEffect 键发生变化,可组合项需要清理当前效应,并通过再次调用进行重置。


@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // 开启事件
    onStop: () -> Unit // 停止时间
) {
    // 当提供了一个新的lambdas时,可以安全地更新当前的lambdas
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)
    // 如果“lifecycleOwner”改变,处置并重置效果
    DisposableEffect(lifecycleOwner) {
        // 创建一个观察者来触发我们所记得的回调来发送事件
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }
        // 将观察者添加到生命周期中
        lifecycleOwner.lifecycle.addObserver(observer)
        // 当组合函数离开时,会执行
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
    /* Home screen content */
}


例如上面,通过 lifecycleOwner ,通过 Lifecycle 事件发送具体的事件。


在 DisposableEffect 里面将 observer 添加到了 lifecycleOwner 中,如果 lifecycleOwner 发生了改变,则系统就会通过 lifecycleOwner 处理并重启效应。


当可组合函数退出的时候就会 removeObserver


注意,在 onDispose 中方式空块并不是最佳的做法,可以想想是否还存在更适合的场景


还有 onDispose 必须作为最终的语句,否则将会报错


SideEffect


将 Compose 状态发布为 非 Compose 代码。


如果需要与非 Compose 管理的对象共享 Compose 状态,请使用 SideEffect 可组合项,因为每次成功重组都会调用该可组合项,


例如:每次重组的时候都设置状态栏


@Composable
private fun SetImmersion() {
    if (isImmersion()) {
        val systemUiController = rememberSystemUiController()
        SideEffect {
            systemUiController.run {
                setSystemBarsColor(color = Color.Transparent, darkIcons = isDark())
                setNavigationBarColor(color = Color.Black)
            }
        }
    }
}


下面是官方栗子:


@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }
    // 在每一次成功的组合中,从当前的用户更新FirebaseAnalytics的userType,确保未来的分析事件附加此元数据
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}


produceState


将非 Compose 状态转为 Compose 状态


produceState 会启动一个协程,该协程将作用域限定为可将值推送到返回的 State 组合,使用此协程就可以将非 Compose 状态转为 Compose 状态,例如将 Flow,LiveData 等引入到组合。


即使 produceState 创建了一个协程,它也可以用于观察非挂起的数据源。如需要移除对该数据源的引用,请直接使用 awaitDispose 函数。


@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {
    // 创建一个以 Result.Loading 作为初始值的 State<T 如果 `url` 或 `imageRepository` 发生变化,正在运行的生产者将取消并使用新的输入重新启动。
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
        // 在协程中,可以进行挂起调用
        val image = imageRepository.load(url)
        // 使用错误或成功结果更新状态。这将触发读取State时的重组
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}


derivedStateOf


如果某个状态是从其他状态对象计算或者派生出来的,请使用 derivedStateOf,使用此函数可以确保当计算中使用的状态之一发生变化时才会进行计算


@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
    val todoTasks = remember { mutableStateListOf<String>() }
    // 只在todoTasks或highPriorityKeywords变化时计算高优先级任务,而不是在每次重组时
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
    }
    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}


在上面代码中,derivedStateOf 保证当 todoTasks 发生变化时,系统都会执行 highPriorityTasks 进行计算,并相应的更新界面。


如果 highPriorityTasks 发生变化,系统将会执行 remember 代码块,并且会创建新的派生状态对象并记住该对象,以代替旧对象。


snapshotFlow


将 Compose 的 State 转为 Flow


val listState = rememberLazyListState()
LazyColumn(state = listState) {
    // ...
}
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}


当 snapshotFlow 块中读取的 State 对象之一发生变化时,如果与之前发出的值不相等,Flow 就会向收集器发出新值。


在上面代码中 listState.firstVisibleItemIndex 被转为一个 Flow,从而可以受益于 Flow 的强大功能。


最后


Compose 提供了一系列的 Effect API 来有效的以可预测的方式执行这些附带效应,在日常开发中我们可以合理的使用 Effect Api 以求最安全的代码。


相关文章
|
5月前
|
存储 缓存 Android开发
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首
这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。
|
5月前
|
存储 数据库 Android开发
安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐
为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤: 1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。 2. **创建UI按钮**:在界面中创建添加和删除按钮。 3. **数据库功能**:使用Room数据库来存储音频文件信息。 4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。 5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。
|
1月前
|
Android开发 开发者 UED
深入理解安卓应用开发中的生命周期管理
本文旨在探讨安卓应用开发中生命周期管理的重要性,以及如何有效利用生命周期解决常见问题。通过分析安卓应用生命周期的不同阶段及其特点,提供实用的代码示例和调试技巧,帮助开发者优化应用性能,提升用户体验。
40 8
|
1月前
|
Java Android开发 UED
深入探索安卓应用开发中的生命周期管理:从创建到销毁的全过程
在安卓应用开发中,理解并妥善管理应用及活动(Activity)的生命周期至关重要。本文将详细解析从应用创建到销毁的整个生命周期过程,以及如何通过高效管理提升应用性能与用户体验。
70 4
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
62 4
|
3月前
|
Android开发 UED 开发者
探索安卓应用的生命周期管理
【8月更文挑战第31天】在开发安卓应用时,理解并妥善处理应用的生命周期是至关重要的。本文将通过浅显易懂的方式,带你了解安卓应用的生命周期,并通过代码示例展示如何在实际应用中进行有效的生命周期管理。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和实用技巧。
|
4月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
5月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
5月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
5月前
|
缓存 Android开发 Kotlin
【安卓app开发】kotlin Jetpack Compose框架 | 先用OKhttp下载远程音频文件再使用ExoPlayer播放
使用 Kotlin 的 Jetpack Compose 开发安卓应用时,可以结合 OkHttp 下载远程音频文件和 ExoPlayer 进行播放。在 `build.gradle` 添加相关依赖后,示例代码展示了如何下载音频并用 ExoPlayer 播放。代码包括添加依赖、下载文件、播放文件及简单的 Compose UI。注意,示例未包含完整错误处理和资源释放,实际应用需补充这些内容。