前言
以前,我们使用Activity的时候,如果需要发起网络请求数据,那么直接使用lifecycleScope.launch开启一个协程请求即可,如果页面退出,也会随之取消这个请求。那么在compose中,我们发起一个网络请求?
那么引出我们这篇文章的主题:副作用。(副作用简单来理解就是,一个函数除了计算返回值之外,做的所有"额外的事"。(如网络请求 )
直接举例子:比如我现在要请求网络数据。
kt
@Composable
fun ExploreScreen(viewModel: ExploreViewModel = hiltViewModel()){
val articleList by viewModel.articleList.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.getHomeBanner()//请求Banner数据
viewModel.getHomeInfoList(20)//请求首页数据
}
Box{
LazyColumn {
articleList?.size?.let {
items(it){index->
Text("${articleList?.datas?.get(index)?.title}")
Spacer(modifier = Modifier.height(20.dp))
}
}
}
}
}
kt
@HiltViewModel
class ExploreViewModel @Inject constructor(private val repository: HomeRepository) :
BaseViewModel() {
private val homeRepository by lazy { HomeRepository() }
private val _articleList = MutableStateFlow<ArticleList?>(null)
val articleList = _articleList.asStateFlow()
/**
* 首页列表
* @param page 页码
*/
fun getHomeInfoList(page: Int) {
viewModelScope.launch {
val response = safeApiCall(errorBlock = { code, errorMsg ->
}) {
homeRepository.getHomeInfoList(page)
}
response?.let {
_articleList.value = it
Log.d("", "getHomeInfoList: $it")
}
}
}
/**
* 请求
*/
fun getHomeBanner(){
viewModelScope.launch {
val response = safeApiCall(errorBlock = { code, errorMsg ->
}) {
homeRepository.getHomeBanner()
}
response?.let {
Log.d("", "getHomeBanner: $it")
}
}
}
}
这个LaunchedEffect究竟是什么?没错,这就是在compose中发起请求的一个常规操作,和原生android区分开来。下面我们介绍一下。
一、LaunchedEffect
kt
LaunchedEffect(Unit) {
viewModel.getHomeBanner()//请求Banner数据
viewModel.getHomeInfoList(20)//请求首页数据
}
LaunchedEffect,他是启动了一个协程,然后将协程的作用域绑定到当前组合(Composition)的生命周期 。【Composition是Compose根据你的Composable代码生成的在内存中的UI实例树。 】当 LaunchedEffect
进入组合时,它会启动一个协程来执行其代码块。当 LaunchedEffect
离开组合(即所在的 Composable 函数从 UI 树上被移除)时,它会自动取消该协程,从而避免了资源泄漏。【所以他可以实现lifecycleScope.launch的效果】
Unit是什么? LaunchedEffect
接受一个或多个 key
参数,这是理解其行为的关键。
-
当
key
发生变化时 :LaunchedEffect
会取消当前正在运行的协程,然后重新启动一个新的协程来执行代码块。 -
当
key
没有变化时 :即使其所在的 Composable 函数经历了重组(Recomposition),LaunchedEffect
内的代码块也不会重新执行。
这个例子,key 是 Unit,一个永远不会变的常量,意味着只会执行一次,无论 HomeCompose
因为任何原因(例如父组件状态变化)经历了多少次重组(Recomposition) ,由于 key
(Unit
) 始终未变,LaunchedEffect
内的代码都不会再执行。
当用户导航离开,HomeCompose
从 UI 树中移除(组合被销毁)时,LaunchedEffect
会自动取消其内部的协程。如果此时网络请求还没完成,这些请求会被安全地取消(前提是 viewModelScope
正确使用了协程的取消机制),避免了潜在的内存泄漏。
总结来说:
-
它解决了你代码中 "每次重组都会发起网络请求" 的严重问题。
-
它将网络请求的生命周期与 UI 的生命周期绑定在了一起,实现了安全、高效的一次性初始化操作。
但如果因为外部配置变更(如屏幕旋转)而完全销毁并重建时。 由于这是一个全新的组合 ,之前的 LaunchedEffect(Unit)
已经随着旧组合的销毁而被取消了。在新的组合中,LaunchedEffect(Unit)
是第一次进入组合,因此它的代码块会再次执行,导致网络请求被重复发起。
这个时候我们就需要借助viewmodel来解决了,增加一个标志位
kt
// HomeViewModel.kt
class HomeViewModel : ViewModel() {
// 使用一个标志位来确保初始化只做一次
private var hasInitialized = false
// 一个可以被UI调用的方法,用于初始化或刷新
fun initializeDataIfNeeded() {
if (!hasInitialized) {
hasInitialized = true
loadData()
}
}
fun refreshData() {
// 刷新数据,不考虑标志位
loadData()
}
private fun loadData() {
viewModelScope.launch {
// 并发请求 Banner 和列表数据
// 并将结果合并到 uiState 中
coroutineScope {
launch { getHomeBanner() }
launch { getHomeInfoList(20) }
}
}
}
private suspend fun getHomeBanner() { ... }
private suspend fun getHomeInfoList(count: Int) { ... }
}
在compose中使用
kotlin
// HomeCompose.kt
@Composable
fun HomeCompose(viewModel: HomeViewModel = hiltViewModel()) {
// 关键点:使用 LaunchedEffect(Unit) 来确保只调用一次初始化方法。
// 即使组合因配置变更重建,因为ViewModel是同一个,hasInitialized依然是true,
// 所以 initializeDataIfNeeded() 方法内部不会再次执行网络请求。
LaunchedEffect(Unit) {
viewModel.initializeDataIfNeeded()
}
Button(onClick = { viewModel.refreshData() }) {
Text("手动刷新")
}
}
但有些情况是 LaunchedEffect
解决不了的,比如注册和取消注册监听器,比如我想退出组合的时候调用一个取消注册监听器,就不行了,当组合退出时LaunchedEffect
只会取消协程,这个时候,就需要引入另外一个副作用,DisposableEffect。
二、DisposableEffect
用于需要清理的副作用,与 LaunchedEffect
类似,但允许你提供一个 onDispose
块来定义清理逻辑。
kt
@Composable
fun LocationTracker() {
val context = LocalContext.current
val locationManager = remember { context.getSystemService(LOCATION_SERVICE) as LocationManager }
DisposableEffect(Unit) {
// 副作用:注册监听器
val listener = LocationListener { ... }
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0L, 0f, listener)
// 清理逻辑:在组合退出时,反注册监听器
onDispose {
locationManager.removeUpdates(listener)
}
}
}
scss
DisposableEffect(Unit) {
// 这里也可以不需要额外的初始化
// 清理逻辑:在组合退出时,通知MapView进行生命周期清理
onDispose {
// mapView.onPause()
// mapView.onDestroy()
}
}
三、SideEffect
它的关键特征是:在每次成功的重组(Recomposition)之后执行。
因为 Compose 的重组是可能失败的或跳过的。Compose 可能会多次尝试执行一个可组合函数,但只有最终成功应用于 UI 的那次重组才需要将状态发布到外部世界。SideEffect
保证了你的副作用代码只会在确定性的、最终生效的状态变化后运行。
kt
@Composable
fun UserProfileScreen(userId: String?) {
val analytics: AnalyticsService = remember { AnalyticsService.getInstance() }
SideEffect {
analytics.setUserProperty("user_id", userId)
}
}