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

Kotlin Jetpack StateFlow提交API后UI不更新问题求解

问题根源与解决方案

你的核心问题有两个:

  1. 提交数据到API成功后,ViewModel没有重新拉取最新的服务器数据,导致StateFlow的状态未更新,UI收不到刷新通知
  2. Composable中维护了本地状态(isPunchIn、isPunchOut等),和ViewModel的业务状态脱节,即使ViewModel数据更新,本地状态也不会同步

以下是具体修复步骤:

1. 完善ViewModel:提交成功后刷新数据

首先要在checkIn/checkOut方法中,API请求成功后调用getHome()重新获取最新数据,同时规范StateFlow的暴露方式(只读对外):

@HiltViewModel
class AttendanceViewModel@Inject constructor(
    private val attendanceHomeUseCase: AttendanceHomeUseCase,
    private val attendanceCheckInUseCase: AttendanceCheckInUseCase, // 假设你有这个UseCase
    private val attendanceCheckOutUseCase: AttendanceCheckOutUseCase, // 假设你有这个UseCase
    private val sharedPreferences: SharedPreferences,
) : ViewModel() {

    // 规范state为只读StateFlow
    private val _state = MutableStateFlow(HomeState())
    val state: StateFlow<HomeState> = _state

    // 添加checkIn/checkOut的状态管理
    private val _stateCheck = MutableStateFlow(CheckInState())
    val stateCheck: StateFlow<CheckInState> = _stateCheck

    private val _stateCheckOut = MutableStateFlow(CheckOutState())
    val stateCheckOut: StateFlow<CheckOutState> = _stateCheckOut

    init {
        getHome()
    }
    
    private fun getHome() {
        attendanceHomeUseCase(sharedUserId().toInt()).onEach { result ->
            when (result) {
                is Resource.Success -> {
                    _state.value = HomeState(home = result.data)
                    Log.d("shared+Tag", result.data!!.toString())
                }
                is Resource.Loading -> {
                    _state.value = HomeState(isLoading = true)
                }
                is Resource.Error -> {
                    _state.value = HomeState(error = result.message ?: "Unexpected error!")
                }
            }
        }.launchIn(viewModelScope)
    }

    fun checkIn(request: CheckInRequest) {
        viewModelScope.launch {
            attendanceCheckInUseCase(request).onEach { result ->
                when (result) {
                    is Resource.Loading -> _stateCheck.value = CheckInState(isLoading = true)
                    is Resource.Success -> {
                        _stateCheck.value = CheckInState(isSuccess = true)
                        // 关键:提交成功后重新拉取最新数据
                        getHome()
                    }
                    is Resource.Error -> _stateCheck.value = CheckInState(error = result.message ?: "Check in failed")
                }
            }.collect()
        }
    }

    fun checkOut(request: CheckOutRequest) {
        viewModelScope.launch {
            attendanceCheckOutUseCase(request).onEach { result ->
                when (result) {
                    is Resource.Loading -> _stateCheckOut.value = CheckOutState(isLoading = true)
                    is Resource.Success -> {
                        _stateCheckOut.value = CheckOutState(isSuccess = true)
                        // 关键:提交成功后重新拉取最新数据
                        getHome()
                    }
                    is Resource.Error -> _stateCheckOut.value = CheckOutState(error = result.message ?: "Check out failed")
                }
            }.collect()
        }
    }

    // 保留其他原有方法...
}

2. 添加CheckIn/CheckOut状态数据类

data class CheckInState(
    val isLoading: Boolean = false,
    val isSuccess: Boolean = false,
    val error: String = ""
)

data class CheckOutState(
    val isLoading: Boolean = false,
    val isSuccess: Boolean = false,
    val error: String = ""
)

3. 重构Composable:移除本地状态,从ViewModel派生

把Composable中维护的本地状态全部替换为从ViewModel的state中派生,确保UI完全响应ViewModel的状态变化:

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
fun PunchItem(
    viewModel: AttendanceViewModel,
    context: Context,
    rosterId: String,
    navController: NavController
) {
    // 使用collectAsStateWithLifecycle更符合生命周期规则
    val state by viewModel.state.collectAsStateWithLifecycle()
    val checkInState by viewModel.stateCheck.collectAsStateWithLifecycle()
    val checkOutState by viewModel.stateCheckOut.collectAsStateWithLifecycle()

    val currentDateTime = LocalDateTime.now()
    val formatter = DateTimeFormatter.ofPattern("d'${getDayOfMonthSuffix(currentDateTime.dayOfMonth)}' MMM")
    val formattedDate = currentDateTime.format(formatter)

    val currentDate = LocalDate.now()
    val formatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd")
    val formattedDate1 = currentDate.format(formatter1)

    // 从ViewModel的state中派生UI状态,不再维护本地状态
    val roster = state.home?.employee_attendance_roster?.first()
    val isPunchIn = roster?.check_in_time != null
    val isCheckOutDone = roster?.check_out_time?.takeIf { it != "00:00:00" } != null
    val isWorkingHrs = roster?.working?.trim()?.takeIf { it !in listOf("00:00:00", "00:00") } != null

    Box(
        modifier = Modifier
            .padding(start = 16.dp, end = 16.dp, top = 16.dp)
            .fillMaxWidth()
            .clip(RoundedCornerShape(2.dp))
            .background(Color(0xFFFFF7F7))
    ) {
        AnimatedVisibility(visible = checkInState.isLoading || checkOutState.isLoading, enter = fadeIn(), exit = fadeOut()) {
            CircularProgressIndicator(modifier = Modifier.align(Alignment.TopStart).padding(8.dp).size(12.dp), color = RedBColor, strokeWidth = 2.dp)
        }
        Row(
            modifier = Modifier.padding(top = 16.dp, bottom = 16.dp).fillMaxWidth(1f).align(Alignment.CenterStart),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(
                text = formattedDate,
                fontSize = 16.sp,
                color = BackColor,
                fontWeight = FontWeight.SemiBold,
                modifier = Modifier.padding(16.dp)
            )

            roster?.check_in_time?.let {
                Log.d("ROSTER_TAG", "inTime: $it")
                PunchIn(text = it)
            }

            Spacer(modifier = Modifier.width(16.dp))

            roster?.check_out_time?.takeIf { it != "00:00:00" }?.let {
                Log.d("ROSTER_TAG", "outTime: $it")
                PunchOut(text = it)
            }

            Spacer(modifier = Modifier.width(16.dp))

            roster?.working?.trim()?.takeIf { it !in listOf("00:00:00", "00:00") }?.let {
                WorkingHours(text = it)
            }

            Spacer(modifier = Modifier.width(16.dp))

        }

        AnimatedVisibility(
            visible = !isWorkingHrs,
            modifier = Modifier.align(Alignment.CenterEnd),
        ) {
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier
                    .clickable {
                        if (!isPunchIn) {
                            val punchInTime = getPunchInTime()
                            getLocation(context) {
                                viewModel.checkIn(
                                    CheckInRequest(
                                        viewModel.sharedUserId(),
                                        rosterId,
                                        it.third,
                                        it.second,
                                        it.first,
                                        "",
                                        "",
                                        "",
                                        "",
                                        "",
                                        "",
                                        context.packageName,
                                    )
                                )
                            }
                            navController.navigate(
                                if (viewModel.sharedRoleId() == "2") {
                                    Screen.AttendanceScreen.route
                                } else {
                                    Screen.DashboardScreen.route
                                }
                            )
                        } else {
                            val punchOutTime = getPunchOutTime()
                            getLocation(context) {
                                viewModel.checkOut(
                                    CheckOutRequest(
                                        viewModel.sharedUserId(),
                                        rosterId,
                                        it.third,
                                        it.second,
                                        it.first,
                                        "",
                                        "",
                                        "",
                                        "",
                                        "",
                                        "",
                                        context.packageName,
                                    )
                                )
                            }
                            navController.navigate(
                                if (viewModel.sharedRoleId() == "2") {
                                    Screen.AttendanceScreen.route
                                } else {
                                    Screen.DashboardScreen.route
                                }
                            )
                        }
                    }
            ) {
                Text(
                    text = if (!isPunchIn) "Punch In" else "Punch Out",
                    fontSize = 12.sp,
                    color = RedColor,
                    fontWeight = FontWeight.Normal,
                    modifier = Modifier
                        .padding(top = 16.dp, end = 4.dp, bottom = 16.dp, start = 16.dp)

                )
                Icon(
                    imageVector = Icons.Outlined.ArrowForward,
                    contentDescription = "",
                    tint = RedColor,
                    modifier = Modifier
                        .padding(top = 16.dp, end = 16.dp, bottom = 16.dp, start = 0.dp)
                        .size(16.dp)
                )
            }
        }
    }
}

核心修复点说明

  • 提交后刷新数据:API请求成功后调用getHome(),确保ViewModel获取最新的服务器数据并更新StateFlow,触发UI刷新
  • 状态统一管理:所有业务状态都放在ViewModel中,Composable只做状态派生和UI渲染,避免本地状态与ViewModel数据不一致
  • 生命周期安全:使用collectAsStateWithLifecycle替代collectAsState(),在Composable进入后台时自动停止收集Flow,避免不必要的资源消耗和内存泄漏

内容的提问来源于stack exchange,提问作者250 yazan aesmael

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 12:27:01