StateFlow.collectAsState()获取ViewModel非空列表却返回空列表问题排查
StateFlow更新后UI层collectAsState()获取空列表问题
开发应用时,使用StateFlow从远程API获取数据,遇到UI层通过collectAsState()获取数据返回空列表,但ViewModel中该列表实际非空的问题。日志显示ViewModel内的_productList已成功更新为非空列表,但UI层始终拿到初始的空列表。
相关代码
DashboardViewModel.kt
class DashboardViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val localDatabaseUseCase: LocalDatabaseUseCase, @ApplicationContext context: Context, ) : ViewModel() { @Inject lateinit var shiftOpenUseCase: ShiftOpenUseCase //THIS IS THE DATA WHICH EXIST IN VIEWMODEL private val _productList: MutableStateFlow<List<ProductDomain>> = MutableStateFlow(listOf()) val productList: StateFlow<List<ProductDomain>> = _productList.asStateFlow() fun shiftOpen( token: String, navController: NavController, context: Context, terminalCode: String, ) { _showLoading.value = true viewModelScope.launch(Dispatchers.IO) { when (val resp = shiftOpenUseCase.shiftOpen( token = token, time = TimestampDTO.now().printDefault() )) { is WrapperResponse.Success -> { Log.d("es.paytef", "Entra en el shiftOpen WrapperResponse.Success") //Keep into Flow val productDataList = resp.data?.products?.map { MainMapper.productToProductData(it) } val productDomainList = productDataList?.map { MainMapper.productDataToProductDomain(it) } Log.d("es.paytef", "productDomainList in shiftOpen $productDomainList") if(productDomainList?.isNotEmpty() == true){ Log.d("es.paytef", "entra en el if(productDomainList?.isNotEmpty() == true)") _productList.value = productDomainList Log.d("es.paytef", "_productList in shiftOpen after emit: ${_productList.value}") } //Keep list of product replacing the old except the price withContext(Dispatchers.IO) { productDataList?.map { localDatabaseUseCase.insertProduct( it, ) } } //Call manageWorkShift to open a turn getTurnTransactionInfoOrCloseTurn( context = context, request = ApiWorkShiftRequest( closeShift = false, listType = ApiWorkShiftRequest.ListType.TERMINAL, pinpad = terminalCode, printTransactionList = false, showTransactionList = false, returnTransactionList = false, transactionListContentType = ApiWorkShiftRequest.TransactionListContentType.TEXT ) ) //navigate to PriceProduct screen withContext(Dispatchers.Main) { navController.navigate(Screens.ListProducts.route) _showLoading.value = false } } is WrapperResponse.Error -> { Log.d("es.paytef", "Entra en el shiftOpen WrapperResponse.Error") _messageDialog.value = resp.message ?: context.getString(R.string.errorService) _showDialog.value = true _showLoading.value = false } } } } }
ListProductsScreen.kt
@Composable fun ListProductsScreen( navController: NavController, context: Context, viewModel: DashboardViewModel, ) { val productList by viewModel.productList.collectAsState() //Recover empty list CepsaStandaloneAppTheme { if (showLoading) { Box( modifier = Modifier .fillMaxSize() // it will fill parent box .padding(8.dp) ) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } } if (showDialog) { SimpleAlertDialog( context = context, show = true, title = context.getString(R.string.insertOperation), text = messageDialog, onConfirm = viewModel::onDialogConfirm, onDismiss = viewModel::onDialogDismiss, elevation = 10 ) } Scaffold( topBar = { Surface(shadowElevation = 16.dp) { TopAppBar( title = { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Spacer(modifier = Modifier.size((LocalConfiguration.current.screenWidthDp.dp) / 3)) Text(context.getString(R.string.titleListPrice)) } }, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = Color.LightGray, titleContentColor = Color.Black ) ) } }, contentWindowInsets = WindowInsets(16.dp) ) { paddingValues -> Surface(modifier = Modifier.padding(paddingValues)) { Log.d("es.paytef", "productList before if() in UI: $productList") if (productList.isEmpty()) { Column( modifier = Modifier.wrapContentSize(Alignment.Center) ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = context.getString(R.string.emptyProductList), textAlign = TextAlign.Center ) FloatingActionButton( onClick = { Toast.makeText( context, "Cargando productos locales...", Toast.LENGTH_LONG ).show() navController.navigate(Screens.DashboardScreen.route) }, contentColor = Color.White, containerColor = PrimaryColor, shape = RoundedCornerShape(10), modifier = Modifier .fillMaxWidth() .padding(start = 8.dp, bottom = 8.dp, end = 24.dp) .align(Alignment.BottomCenter) ) { Text(text = context.getString(R.string.updatePrice)) } } } } else { Column( modifier = Modifier.wrapContentSize(Alignment.Center) ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { LazyVerticalGrid( state = listState, contentPadding = paddingValues, columns = GridCells.Fixed(2) ) { items(productList.size) { index -> Box(modifier = Modifier) { ProductPriceComponent( item = productList[index], modifier = Modifier .fillMaxSize() .clickable { viewModel.setSelectedProduct(productList[index]) navController.navigate(Screens.DetailProduct.route) } ) } } } FloatingActionButton( onClick = { productList.map { product -> viewModel.insertProduct( product = MainMapper.productDomaintoProductData(product), context = context ) } navController.navigate(Screens.DashboardScreen.route) }, contentColor = Color.White, containerColor = PrimaryColor, shape = RoundedCornerShape(10), modifier = Modifier .fillMaxWidth() .padding(start = 8.dp, bottom = 8.dp, end = 24.dp) .align(Alignment.BottomCenter) ) { Text(text = context.getString(R.string.updatePrice)) } } } } } } } }
Logcat输出
Entra en el shiftOpen WrapperResponse.Success productDomainList in shiftOpen [ProductDomain(productID=asdad12l, time=, name=ad-blue, product_price=null, maxPrice=3.00, minPrice=0.10, type_sell=null, type_product=FUEL_VOL, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null), ProductDomain(productID=asdad12, time=, name=shell-gnl, product_price=null, maxPrice=3.00, minPrice=0.10, type_sell=null, type_product=GAS, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null), ProductDomain(productID=asdad1, time=, name=shell-v-power, product_price=null, maxPrice=3.00, minPrice=0.10, type_sell=null, type_product=FUEL_IMP, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null), ProductDomain(productID=asdad, time=, name=lavado, product_price=null, maxPrice=500, minPrice=0.10, type_sell=null, type_product=LAVADO, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null), ProductDomain(productID=asda, time=, name=tienda, product_price=null, maxPrice=1500.00, minPrice=0.10, type_sell=null, type_product=TIENDA, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null)] entra en el if(productDomainList?.isNotEmpty() == true) _productList in shiftOpen after emit: [ProductDomain(productID=asdad12l, time=, name=ad-blue, product_price=null, maxPrice=3.00, minPrice=0.10, type_sell=null, type_product=FUEL_VOL, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null), ProductDomain(productID=asdad12, time=, name=shell-gnl, product_price=null, maxPrice=3.00, minPrice=0.10, type_sell=null, type_product=GAS, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null), ProductDomain(productID=asdad1, time=, name=shell-v-power, product_price=null, maxPrice=3.00, minPrice=0.10, type_sell=null, type_product=FUEL_IMP, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null), ProductDomain(productID=asdad, time=, name=lavado, product_price=null, maxPrice=500, minPrice=0.10, type_sell=null, type_product=LAVADO, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null), ProductDomain(productID=asda, time=, name=tienda, product_price=null, maxPrice=1500.00, minPrice=0.10, type_sell=null, type_product=TIENDA, imageB64=, taxPercent=21, flagID=id del flag que contiene este producto, accepted=null)] productList before if() in UI: []
问题分析与解决方案
核心原因
UI层拿到的DashboardViewModel实例与更新_productList的实例不是同一个,导致UI读取的是新实例中未更新的初始空列表。
具体修复步骤
为ViewModel添加
@HiltViewModel注解
你的ViewModel使用Hilt注入,但缺少@HiltViewModel注解,导致Hilt无法正确管理ViewModel的生命周期和实例唯一性。修改后代码:@HiltViewModel class DashboardViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val localDatabaseUseCase: LocalDatabaseUseCase, @ApplicationContext context: Context, ) : ViewModel() { // 原有代码不变 }修改UI层ViewModel获取方式
当前通过参数传递ViewModel容易导致实例不一致,应使用Hilt的hiltViewModel()函数获取与宿主绑定的实例,保证共享同一个ViewModel:@Composable fun ListProductsScreen( navController: NavController, context: Context, ) { val viewModel: DashboardViewModel = hiltViewModel() val productList by viewModel.productList.collectAsState() // 原有UI代码不变 }验证导航后的StateFlow收集
修复实例一致性后,StateFlow的最新值会自动被UI层收集,因为StateFlow是热流且会保留最新状态,只要UI处于活跃状态就能获取到更新后的数据。
额外验证点
- 确保Application类添加了
@HiltAndroidApp注解,Hilt基础配置正确。 - 如果需要跨多个导航页面共享ViewModel,可使用导航图作用域的ViewModel(通过
hiltViewModel(navGraphViewModels = ...)获取),当前场景下与Activity绑定的ViewModel已满足需求。
内容的提问来源于stack exchange,提问作者Manuel Lucas
相关产品推荐
相关产品推荐

