Android :Comnpose各种副作用的使用

前言

以前,我们使用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)
    }
}
相关推荐
Zhangzy@1 分钟前
Rust 编译优化选项
android·开发语言·rust
某空m1 小时前
【Android】View滑动的实现
android
芝麻开门-新起点2 小时前
Android 和 iOS 系统版本及开发适配
android·ios·cocoa
iOS阿玮3 小时前
别问了,我自己的产品也卡审了44个小时!
uni-app·app·apple
2501_915918413 小时前
iOS描述文件功能解析
android·macos·ios·小程序·uni-app·cocoa·iphone
用户69371750013843 小时前
一文彻底搞懂 Android 依赖配置:implementation vs testImplementation,再也不混淆!
android
TimeFine5 小时前
Android WebView暗夜模式适配
android
studyForMokey5 小时前
【Android Activity】生命周期深入理解
android·kotlin
浅影歌年5 小时前
Android 嵌入h5顶部状态栏空白
android
来来走走7 小时前
kotlin学习 lambda编程
android·学习·kotlin