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
相关产品推荐
相关产品推荐

