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

Jetpack Compose获取数据后UI状态无法正常更新问题排查

问题:Jetpack Compose UI始终停留在加载状态,无法切换到成功/错误状态

我正在开发一款Jetpack Compose应用,通过BooksViewModel从服务器获取书籍数据,UI需要根据Loading、Success或Error状态更新。但目前UI仅显示加载状态,无法切换至Success或Error状态,即使关闭网络也不显示错误信息或Snackbar。

BookScreen 可组合函数

@Composable
internal fun BookScreen(
    onBackPressed: () -> Unit,
    viewModel: BooksViewModel = koinViewModel()
) {

    val uiState by viewModel.booksState.collectAsStateWithLifecycle()
    val snackBarHostState = remember { SnackbarHostState() }

    BackHandler(onBack = onBackPressed)

    Scaffold(
        snackbarHost = { SnackbarHost(snackBarHostState) }
    ) { contentPadding ->
        Log.e("BookScreen", "BookScreen: $uiState")
        when (uiState) {
            is BooksUiState.Error -> {
                LaunchedEffect(key1 = uiState) {
                    Log.e("BookScreen", "BookScreen: Error")
                    snackBarHostState.showSnackbar("Unknown Error")
                }
            }

            is BooksUiState.Loading -> {
                Log.e("BookScreen", "BookScreen: Loading")
                LoadingOverlay(isLoading = (uiState as BooksUiState.Loading).isLoading) {}
            }

            is BooksUiState.Success -> {
                Log.e("BookScreen", "BookScreen: Success")
                BookContent(
                    books = (uiState as BooksUiState.Success).books,
                    contentPadding = contentPadding
                )
            }
        }
    }
}

@Composable
private fun BookContent(
    books: List<BookApiModel>,
    contentPadding: PaddingValues,
) {
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .padding(horizontal = 16.dp, vertical = 16.dp)
            .consumeWindowInsets(contentPadding)
            .imePadding(),
        contentPadding = contentPadding
    ) {
        items(items = books, key = { it.id }) { book ->
            Text(text = book.title, color = Color.White)
        }
    }
}

BooksViewModel 代码

class BooksViewModel(private val getBooksUseCase: GetBooksUseCase) : ViewModel() {

    val booksState: StateFlow<BooksUiState> = getBooksFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = BooksUiState.Loading(true)
        )

    private fun getBooksFlow(): Flow<BooksUiState> = flow {
        val result = getBooksUseCase()
        result.onSuccess { books ->
            Log.e("BooksViewModel", "getBooksFlow: $books")
            emit(BooksUiState.Success(books = books))
        }.onFailure { error ->
            Log.e("BooksViewModel", "getBooksFlow: $error")
            emit(BooksUiState.Error(error = error))
        }
    }.catch { error ->
        Log.e("BooksViewModel", "getBooksFlow: $error")
        // Handle any unexpected exception during the flow execution
        emit(BooksUiState.Error(error = error))
    }.onCompletion {
        Log.e("BooksViewModel", "getBooksFlow: onCompletion")
        // Ensure loading is set to false, even if cancelled or an error happens.
        emit(BooksUiState.Loading(isLoading = false))
    }
}

已确认信息

  • API运行正常
  • Success和Error状态已被ViewModel发送
  • 关闭网络时,ViewModel能捕获错误但UI无响应

更新的日志信息

API成功时的日志

2025-03-12 13:30:49.882 14615-14615 BookScreen              com.vivek.bookapp                  E  BookScreen: Loading(isLoading=true)
2025-03-12 13:30:49.882 14615-14615 BookScreen              com.vivek.bookapp                  E  BookScreen: Loading
2025-03-12 13:30:54.081 14615-14615 BooksViewModel          com.vivek.bookapp                  E  getBooksFlow: [BookApiModel(id=100, title=Code Complete: A Practical Handbook of Software Construction, price=2954, currencyCode=EUR, author=Mike Riley), BookApiModel(id=200, title=The Pragmatic Programmer, price=3488, currencyCode=EUR, author=Andrew Hunt and Dave Thomas), BookApiModel(id=300, title=iOS Forensic Analysis, price=4604, currencyCode=EUR, author=Sean Morrisey), BookApiModel(id=400, title=Ghost in the Wires: My Adventures as the World's Most Wanted Hacker, price=1493, currencyCode=EUR, author=Kevin Mitnick), BookApiModel(id=500, title=Handling Unexpected Errors, price=1399, currencyCode=GBP, author=Charles R. Ash), BookApiModel(id=600, title=Android Application Development For Dummies, price=1979, currencyCode=USD, author=Donn Felker)]
2025-03-12 13:30:54.082 14615-14615 BooksViewModel          com.vivek.bookapp                  E  getBooksFlow: onCompletion
2025-03-12 13:30:54.088 14615-14615 BookScreen              com.vivek.bookapp                  E  BookScreen: Loading(isLoading=false)
2025-03-12 13:30:54.088 14615-14615 BookScreen              com.vivek.bookapp                  E  BookScreen: Loading

API失败时的日志

2025-03-12 13:39:04.844 15004-15004 BookScreen              com.vivek.bookapp                  E  BookScreen: Loading(isLoading=true)
2025-03-12 13:39:04.844 15004-15004 BookScreen              com.vivek.bookapp                  E  BookScreen: Loading
2025-03-12 13:39:04.938 15004-15004 BooksViewModel          com.vivek.bookapp                  E  getBooksFlow: java.net.UnknownHostException: Unable to resolve host "tpbookserver.herokuapp.com": No address associated with hostname
2025-03-12 13:39:04.939 15004-15004 BooksViewModel          com.vivek.bookapp                  E  getBooksFlow: onCompletion
2025-03-12 13:39:04.952 15004-15004 BookScreen              com.vivek.bookapp                  E  BookScreen: Loading(isLoading=false)
2025-03-12 13:39:04.952 15004-15004 BookScreen              com.vivek.bookapp                  E  BookScreen: Loading

问题原因分析

从日志可以明确看到核心问题:onCompletion操作符会在Flow结束前最后执行一次emit,直接覆盖了之前发送的Success/Error状态。

不管getBooksFlow中先发送了Success还是Error,onCompletion里的emit(BooksUiState.Loading(isLoading = false))都会在最后执行,导致UI最终收到的状态是Loading(false),所以when分支始终进入Loading分支,无法显示成功内容或错误提示。

修复方案

方案1:移除onCompletion中的emit(推荐)

初始状态已经设置了Loading(true),在获取数据完成后(成功/失败),Success/Error状态本身就代表加载完成,不需要额外发送Loading(false)。直接删除onCompletion块即可:

private fun getBooksFlow(): Flow<BooksUiState> = flow {
    val result = getBooksUseCase()
    result.onSuccess { books ->
        Log.e("BooksViewModel", "getBooksFlow: $books")
        emit(BooksUiState.Success(books = books))
    }.onFailure { error ->
        Log.e("BooksViewModel", "getBooksFlow: $error")
        emit(BooksUiState.Error(error = error))
    }
}.catch { error ->
    Log.e("BooksViewModel", "getBooksFlow: $error")
    emit(BooksUiState.Error(error = error))
}
// 移除onCompletion块

方案2:重新设计UI状态模型(更健壮)

如果需要明确控制加载状态,可以将loading作为状态的一个属性,而不是单独的状态类。比如调整密封类:

sealed class BooksUiState {
    data class Loaded(val books: List<BookApiModel>) : BooksUiState()
    data class Error(val error: Throwable) : BooksUiState()
    object Loading : BooksUiState()
}

然后在ViewModel中调整逻辑:

private fun getBooksFlow(): Flow<BooksUiState> = flow {
    emit(BooksUiState.Loading)
    val result = getBooksUseCase()
    result.onSuccess { books ->
        emit(BooksUiState.Loaded(books = books))
    }.onFailure { error ->
        emit(BooksUiState.Error(error = error))
    }
}.catch { error ->
    emit(BooksUiState.Error(error = error))
}

这样UI可以根据状态类型直接判断:显示加载遮罩、书籍列表或错误提示。


内容的提问来源于stack exchange,提问作者Compose Learner

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 22:48:10