Jetpack Compose LazyColumn异步获取Firebase计数初始为null问题
Jetpack Compose LazyColumn异步计数加载异常修复
问题现象
使用Jetpack Compose开发Android应用时,LazyColumn每行需要展示对应条目的分析项数量,现有实现存在异常:应用启动时条目计数显示为null,只有点击行跳转至其他页面再返回后,才会显示正确数值。
原有问题代码
ViewModel计数函数
fun countOfAnalyze(currentPairId: String) = CoroutineScope(Dispatchers.IO).launch { val newRef = database.collection("Pairs") .document(currentUserE) .collection("Pair") .document(currentPairId) .collection("Analysis") val documents= newRef.get().await() fun counter() : Int{ var counter=0 for (document in documents) { counter++ } return counter } val count= async { counter() } pairCountMap[currentPairId] = count.await() }
LazyColumn行项Composable
@Composable fun CurrencyRow(currency: PairListModel, navController: NavController, viewModel: ProfileViewModel) { viewModel.countOfAnalyze(currency.id!!) // ------> HERE CALL TO COUNTER FUNCTİON val currencyName= if (currency.currency!= null) { currency.currency!!.uppercase() } else { "" } Row( modifier = Modifier .fillMaxWidth() .border(BorderStroke(2.dp, MaterialTheme.colors.onBackground), RoundedCornerShape(2.dp)) .clickable { val encodedUrl = URLEncoder .encode(currency.currency, StandardCharsets.UTF_8.toString()) .uppercase() navController.navigate("pair_detail_screen/${currency.id}/${encodedUrl}") } ) { Text( text = currencyName , fontSize = 24.sp, fontWeight = ExtraBold, modifier = Modifier .padding(16.dp) .weight(3f) ) Column(modifier = Modifier.weight(2f)) { Text( text = "Analizlerim : ${viewModel.pairCountMap[currency.id]}",// HERE SHOW // THE NUMBERS OF ANALYSIS fontSize = 12.sp, modifier = Modifier.padding(8.dp) ) Text( text = "Başarı Oranı : "/*TODO*/, fontSize = 12.sp, modifier = Modifier.padding(8.dp) ) } } Spacer(modifier = Modifier.padding(4.dp)) }
问题根因
- 存储计数的
pairCountMap是普通Map,不属于Compose可观察状态类型,协程执行完更新Map值时,Compose无法感知数据变化,不会触发界面重组,因此首次加载时一直显示初始的null;只有跳转页面返回触发整页重组时,才会读取到Map里已经写入的计数值,显示正确数字。 - 计数函数直接在Composable函数体中调用,Compose每次重组(首次加载、动画触发、其他状态变化)都会重新执行函数体,导致重复启动协程发起冗余请求。
- 每次调用计数函数都手动新建
CoroutineScope,没有和ViewModel生命周期绑定,页面销毁时协程不会自动取消,存在内存泄漏风险。 - 计数逻辑冗余:Firebase返回的
QuerySnapshot本身提供了size()方法直接获取文档总数,不需要手动遍历循环计数,额外嵌套的async操作也没有实际作用。 - 没有做请求缓存,同一个条目的计数会被反复请求,浪费流量和性能。
正确实现方案
第一步:改造ViewModel层
替换可观察状态容器,使用ViewModel自带的协程作用域,增加缓存判断,简化计数逻辑:
import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class ProfileViewModel : ViewModel() { // 替换为可观察的状态Map,数据变化时自动触发Compose重组 val pairCountMap = mutableStateMapOf<String, Int>() fun countOfAnalyze(currentPairId: String) { // 已加载过计数的条目直接返回,避免重复请求 if (pairCountMap.containsKey(currentPairId)) return // 使用viewModelScope,自动跟随ViewModel生命周期取消协程,避免内存泄漏 viewModelScope.launch(Dispatchers.IO) { runCatching { database.collection("Pairs") .document(currentUserE) .collection("Pair") .document(currentPairId) .collection("Analysis") .get() .await() }.onSuccess { snapshot -> // 直接调用snapshot.size()获取文档总数,无需手动遍历 pairCountMap[currentPairId] = snapshot.size() }.onFailure { // 请求异常时设置默认值0,避免显示null pairCountMap[currentPairId] = 0 } } } }
第二步:改造Composable层调用逻辑
将数据请求移到LaunchedEffect副作用作用域中,仅当条目ID变化时触发一次请求,避免重组重复调用,同时对空值做兜底处理:
@Composable fun CurrencyRow(currency: PairListModel, navController: NavController, viewModel: ProfileViewModel) { // LaunchedEffect绑定currency.id作为key,仅当id变化时启动协程执行请求 LaunchedEffect(currency.id) { currency.id?.let { viewModel.countOfAnalyze(it) } } val currencyName = currency.currency?.uppercase() ?: "" // 计数未加载完成时默认显示0,避免出现null val analyzeCount = viewModel.pairCountMap[currency.id] ?: 0 Row( modifier = Modifier .fillMaxWidth() .border(BorderStroke(2.dp, MaterialTheme.colors.onBackground), RoundedCornerShape(2.dp)) .clickable { val encodedUrl = URLEncoder .encode(currency.currency, StandardCharsets.UTF_8.toString()) .uppercase() navController.navigate("pair_detail_screen/${currency.id}/${encodedUrl}") } ) { Text( text = currencyName , fontSize = 24.sp, fontWeight = ExtraBold, modifier = Modifier .padding(16.dp) .weight(3f) ) Column(modifier = Modifier.weight(2f)) { Text( text = "Analizlerim : $analyzeCount", fontSize = 12.sp, modifier = Modifier.padding(8.dp) ) Text( text = "Başarı Oranı : "/*TODO*/, fontSize = 12.sp, modifier = Modifier.padding(8.dp) ) } } Spacer(modifier = Modifier.padding(4.dp)) }
可选优化
- 如果列表数据量较大,可以在获取LazyColumn条目列表时批量预加载所有条目的计数,减少每行单独发起请求的性能开销。
- 可以给Firestore集合添加快照监听,当分析项增删时自动更新计数,无需页面返回刷新即可拿到最新值。
内容的提问来源于stack exchange,提问作者Sevban Bayır
相关产品推荐
相关产品推荐

