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)
    }
}
相关推荐
未来之窗软件服务11 小时前
服务器运维(十五)自建WEB服务C#PHP——东方仙盟炼气期
android·服务器运维·东方仙盟·东方仙盟sdk·自建web服务
Zender Han16 小时前
Flutter 新版 Google Sign-In 插件完整解析(含示例讲解)
android·flutter·ios·web
来来走走19 小时前
Android开发(Kotlin) LiveData的基本了解
android·开发语言·kotlin
。puppy20 小时前
MySQL 远程登录实验:通过 IP 地址跨机器连接实战指南
android·adb
dongdeaiziji21 小时前
深入理解 Kotlin 中的构造方法
android·kotlin
风起云涌~21 小时前
【Android】浅谈Navigation
android
游戏开发爱好者821 小时前
iOS 商店上架全流程解析 从工程准备到审核通过的系统化实践指南
android·macos·ios·小程序·uni-app·cocoa·iphone
QuantumLeap丶1 天前
《Flutter全栈开发实战指南:从零到高级》- 18 -自定义绘制与画布
android·flutter·ios
.豆鲨包1 天前
【Android】 View事件分发机制源码分析
android·java
花落归零1 天前
Android 小组件AppWidgetProvider的使用
android