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)
    }
}
相关推荐
XeonYu3 小时前
Kotlin 协程之 Flow 操作符大全
kotlin·flow·flow 操作符
BD_Marathon16 小时前
【MySQL】函数
android·数据库·mysql
西西学代码17 小时前
安卓开发---耳机的按键设置的UI实例
android·ui
maki07721 小时前
虚幻版Pico大空间VR入门教程 05 —— 原点坐标和项目优化技巧整理
android·游戏引擎·vr·虚幻·pico·htc vive·大空间
千里马学框架1 天前
音频焦点学习之AudioFocusRequest.Builder类剖析
android·面试·智能手机·车载系统·音视频·安卓framework开发·audio
fundroid1 天前
掌握 Compose 性能优化三步法
android·android jetpack
TeleostNaCl1 天前
如何在 IDEA 中使用 Proguard 自动混淆 Gradle 编译的Java 项目
android·java·经验分享·kotlin·gradle·intellij-idea
旷野说1 天前
Android Studio Narwhal 3 特性
android·ide·android studio
maki0771 天前
VR大空间资料 01 —— 常用VR框架对比
android·ue5·游戏引擎·vr·虚幻·pico