返回

Compose LazyColumn高效加载Single数据:State与Flow方案

Android

从Single<List>填充LazyColumn的方案

在Jetpack Compose开发中,经常需要将异步数据源中的数据加载到界面中,特别是像LazyColumn这样的列表组件。 本文将探讨如何有效地利用Single<List<Type>>返回的数据来填充LazyColumn。 这通常涉及到数据加载、状态管理和Compose的生命周期等多个方面。 我们会针对这个问题提供可行的解决方案,以及相应的代码示例和解释。

问题分析

在给定的代码片段中,FeatureConfigViewModel 使用 Single<List<Feature>> 从存储库获取数据。 数据处理逻辑被封装在 loadFeatures 函数中,并通过订阅更新 featureConfigs 变量。 然而,界面中LazyColumn并没有直接绑定到ViewModel中更新后的状态,所以会出现featureConfigs = listOf<Feature>()后列表无法显示内容的问题。 这表明需要引入一些机制来将数据绑定到Compose的渲染逻辑。

核心问题是 Compose UI 是通过状态驱动的。 我们需要一种方法让 LazyColumnfeatureConfigs 改变时更新自己。 简单来说,featureConfigs 更新发生在ViewModel内部,而LazyColumn是UI层的代码,两个没有直接的“绑定关系”; 所以尽管ViewModel 的 featureConfigs 更新了,UI层无法自动知晓。

解决方案 1:使用State

一种常用的解决方案是使用 Compose 的 State API 来观察ViewModel中的数据变化。 需要将 featureConfigs 从简单的 List 转换成 State 对象,并使用 mutableStateOf 或者 derivedStateOf 将其转化为 Compose 可以识别并监听的状态。 ViewModel 可以更新状态值,同时界面层就可以观察到,并进行更新。

实现步骤:

  1. 在 ViewModel 中,将 featureConfigs 的类型从 List<Feature> 修改为 MutableState<List<Feature>> 。 默认初始化空列表 mutableStateOf(emptyList())
  2. 当接收到来自Single的数据时,更新 mutableStateOf的值,而不是直接赋值给变量。
  3. 在 Compose 中使用 configs = viewModel.featureConfigs.value 从ViewModel获取数据。 由于此时featureConfigs是一个状态对象, Compose 将自动重新渲染 LazyColumn ,此时显示的数据也正是 mutableStateOf 内的list。

代码示例:

ViewModel class:

import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import io.reactivex.Single
import javax.inject.Inject
// 其他导入省略
class FeatureConfigViewModel @Inject constructor(
private val featuresRepository: FeaturesRepository,
private val localFeatureConfigOverrider: LocalFeatureConfigOverrider): ViewModel() {

var featureConfigs = mutableStateOf(emptyList<Feature>())


//init 和其他 loadFeatures 函数的代码保持不变

private fun loadFeatures(featuresSingle: Single<List<Feature>>) {
    val d = featuresSingle.subscribe({ featureList ->
        val features = featureList.toMutableList().sortedBy { it.key }
        featureConfigs.value = features // 这里用.value 来更新mutableStateOf状态对象内的list值。
    }) { throwable ->
        Logger.e(throwable)
    }
}

Compose代码:

val configs = viewModel.featureConfigs.value

LazyColumn(modifier = Modifier
            .weight(weight = 1f, fill = false)
            .fillMaxSize()) {
        items(items = configs) {
            LazyColumnDescription(
                name = it.key,
                description = it.description,
            )
        }
    }

解决方案 2: 使用 Flow

另一个常见的解决方案是使用 Kotlin Flow 来表示异步数据流,并借助 Jetpack Compose 的 State Flow Collector将数据绑定到界面层。 Flow 可以很自然的将异步的数据转变成数据流。 ViewModel可以暴露一个Flow, Compose通过collect来观察并更新 UI 。

实现步骤:

  1. 在 ViewModel 中,创建一个 StateFlow<List<Feature>> 对象,作为数据源。 并默认初始化空列表 MutableStateFlow(emptyList())
  2. 将数据获取到的 Single 转为 Flow ,可以使用 rxjava3.kotlin.flowable()扩展函数 。 使用 collect() 操作符对数据进行处理,在flow的内部处理List<Feature>,并调用 stateFlowemit 来发布。
  3. Compose中使用collectAsState(),它可以把Flow转换成State 对象,之后和上个方法一样,通过 viewModel.featureConfigs.value 来读取最新的 List<Feature> 值。

代码示例:

ViewModel class:

import androidx.compose.runtime.State
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import androidx.lifecycle.viewModelScope
import io.reactivex.Single
import io.reactivex.rxkotlin.flowable
// 其他导入省略

class FeatureConfigViewModel @Inject constructor(
    private val featuresRepository: FeaturesRepository,
    private val localFeatureConfigOverrider: LocalFeatureConfigOverrider
) : ViewModel() {


   private val _featureConfigs = MutableStateFlow<List<Feature>>(emptyList())
    val featureConfigs: StateFlow<List<Feature>> get() = _featureConfigs

    //init 和其他 loadFeatures 函数的代码保持不变
     private fun loadFeatures(featuresSingle: Single<List<Feature>>) {
         viewModelScope.launch(Dispatchers.IO) {
             featuresSingle.flowable()
                 .collect { featureList ->
                     val features = featureList.toMutableList().sortedBy { it.key }
                      _featureConfigs.emit(features) // 改变 flow 中维护的状态
                    }
         }
    }

}

Compose代码:

import androidx.compose.runtime.collectAsState
val configs by viewModel.featureConfigs.collectAsState()


 LazyColumn(modifier = Modifier
            .weight(weight = 1f, fill = false)
            .fillMaxSize()) {
        items(items = configs) {
            LazyColumnDescription(
                name = it.key,
                description = it.description,
            )
        }
    }

额外建议

在实际项目中,需要注意线程问题。 Single 订阅通常在后台线程执行, 而UI更新需要在主线程上进行。 可以使用 observeOn(AndroidSchedulers.mainThread()) 或者利用 Coroutines 和 viewModelScope 来确保UI更新的线程正确。 另外,记得处理 Single 的错误情况,比如用 onErrorReturnItem() 返回一个默认的空列表。 为了提高代码的可读性,考虑将数据加载过程封装成单独的方法或者使用Kotlin扩展函数。

这两个方法都可以有效地解决将Single<List<Type>>的结果加载到LazyColumn的问题,它们都是现代Compose 开发的最佳实践。

请注意,代码示例假设使用了相关的依赖库,比如 androidx.compose.runtimeandroidx.lifecycle. 为了保证最佳开发效率,应始终保持库版本的更新。