You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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读取的是新实例中未更新的初始空列表。

具体修复步骤

  1. 为ViewModel添加@HiltViewModel注解
    你的ViewModel使用Hilt注入,但缺少@HiltViewModel注解,导致Hilt无法正确管理ViewModel的生命周期和实例唯一性。修改后代码:

    @HiltViewModel
    class DashboardViewModel @Inject constructor(
        private val savedStateHandle: SavedStateHandle,
        private val localDatabaseUseCase: LocalDatabaseUseCase,
        @ApplicationContext context: Context,
    ) : ViewModel() {
        // 原有代码不变
    }
    
  2. 修改UI层ViewModel获取方式
    当前通过参数传递ViewModel容易导致实例不一致,应使用Hilt的hiltViewModel()函数获取与宿主绑定的实例,保证共享同一个ViewModel:

    @Composable
    fun ListProductsScreen(
        navController: NavController,
        context: Context,
    ) {
        val viewModel: DashboardViewModel = hiltViewModel()
        val productList by viewModel.productList.collectAsState()
        // 原有UI代码不变
    }
    
  3. 验证导航后的StateFlow收集
    修复实例一致性后,StateFlow的最新值会自动被UI层收集,因为StateFlow是热流且会保留最新状态,只要UI处于活跃状态就能获取到更新后的数据。

额外验证点

  • 确保Application类添加了@HiltAndroidApp注解,Hilt基础配置正确。
  • 如果需要跨多个导航页面共享ViewModel,可使用导航图作用域的ViewModel(通过hiltViewModel(navGraphViewModels = ...)获取),当前场景下与Activity绑定的ViewModel已满足需求。

内容的提问来源于stack exchange,提问作者Manuel Lucas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 14:44:49