将Flow的.first()改为.collect{}后界面无数据,求排查方案
我用Flow和Room实现了一个ViewModel,最初按照官方Codelabs用.first()关联UiState和Room,但插入数据时界面无法自动刷新。换成.collect{}实现持续更新后,界面完全不显示数据,附上代码求排查方案。
ViewModel代码
data class UiState( val searchResult: List<Airport> = listOf(), val favorites: List<Favorite> = listOf(), val selectedAirport: Airport? = null, val flightsForSelectedAirport: List<Airport> = listOf() ) class FlightsScreenViewModel( private val flightRepository: FlightsRepository, private val userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { var uiState by mutableStateOf(UiState()) private set var searchText by mutableStateOf("") private set init { viewModelScope.launch { // searchText = userPreferencesRepository.searchText.first() userPreferencesRepository.searchText.collect{ searchText = it } updateSearchResults() } } private suspend fun updateSearchResults() { // val searchResult = if (searchText != "") // flightRepository.getAirportsByIatOrName(searchText).filterNotNull().first() // else // emptyList() // // val favorites = flightRepository.getFavorites().filterNotNull().first() // // uiState = uiState.copy( // searchResult = searchResult, // favorites = favorites // ) if (searchText != "") { flightRepository.getAirportsByIatOrName(searchText).filterNotNull().collect{ uiState = uiState.copy(searchResult = it) } } else { uiState = uiState.copy(favorites = emptyList()) } flightRepository.getFavorites().filterNotNull().collect{ uiState = uiState.copy(favorites = it) } } fun updateSearchText(searchText: String) { this.searchText = searchText viewModelScope.launch { userPreferencesRepository.saveSearchTextPreference(searchText) updateSearchResults() } } fun selectAirport(airport: Airport?) { viewModelScope.launch { // val flightsForSelectedAirport = if (airport == null) { // emptyList() // } else { // flightRepository.getAllDifferentAirports(airport.id).first() // } // // uiState = uiState.copy( // selectedAirport = airport, // flightsForSelectedAirport = flightsForSelectedAirport // ) if (airport != null) { flightRepository.getAllDifferentAirports(airport.id).collect{ uiState = uiState.copy(flightsForSelectedAirport = it) } } else { uiState = uiState.copy(flightsForSelectedAirport = emptyList()) } uiState = uiState.copy(selectedAirport = airport) } } fun insertFavorite(depart: String, arrive: String) { if (!uiState.favorites.checkIfFavoriteExists(depart, arrive)) { val favorite = Favorite(departureCode = depart, destinationCode = arrive) viewModelScope.launch { flightRepository.insertFavorite(favorite) } } } companion object { val factory : ViewModelProvider.Factory = viewModelFactory { initializer { FlightsScreenViewModel( FlightSearchApplication().container.flightRepository, FlightSearchApplication().container.userPreferencesRepository ) } } } } fun List<Favorite>.checkIfFavoriteExists(depart: String, arrive: String): Boolean{ for (favorite in this){ if (favorite.departureCode == depart && favorite.destinationCode == arrive) return true } return false }
UI展示代码
LazyColumn( modifier = Modifier.padding(8.dp) ) { items(uiState.searchResult) { airport -> AirportDetail(airport, onAirportSelected) } } uiState.selectedAirport?.let { FlightsForAirport( airport = uiState.selectedAirport, arrivals = uiState.flightsForSelectedAirport, favorites = uiState.favorites, onFavoriteSelected = onFavoriteSelected ) }
排查方案
1. 协程阻塞问题
.collect{}是挂起函数,会一直阻塞当前协程直到Flow结束。比如updateSearchResults中,当searchText != ""时,collect会卡住协程,导致后续的flightRepository.getFavorites().collect永远不会执行,同时协程被占用,其他更新逻辑也无法触发。
修复方式:给每个collect单独启动子协程,避免阻塞:
private suspend fun updateSearchResults() { viewModelScope.launch { if (searchText != "") { flightRepository.getAirportsByIatOrName(searchText).filterNotNull().collect{ uiState = uiState.copy(searchResult = it) } } else { uiState = uiState.copy(searchResult = emptyList()) } } viewModelScope.launch { flightRepository.getFavorites().filterNotNull().collect{ uiState = uiState.copy(favorites = it) } } }
2. searchText更新时的Flow订阅问题
当前init中,userPreferencesRepository.searchText.collect会一直阻塞协程,导致updateSearchResults()只执行一次。而且每次调用updateSearchText时,updateSearchResults会重新订阅getAirportsByIatOrName的Flow,之前的订阅没有取消,会造成重复订阅和内存泄漏。
修复方式:使用switchMap处理searchText变化后的Flow切换,同时用协程生命周期管理订阅:
init { viewModelScope.launch { userPreferencesRepository.searchText.collect { newSearchText -> searchText = newSearchText updateSearchResults() } } } private fun updateSearchResults() { viewModelScope.launch { flow { emit(searchText) } .switchMap { text -> if (text.isNotEmpty()) { flightRepository.getAirportsByIatOrName(text).filterNotNull() } else { flowOf(emptyList()) } } .collect { result -> uiState = uiState.copy(searchResult = result) } } }
3. selectAirport中的协程阻塞问题
selectAirport中,collect会阻塞协程,导致uiState = uiState.copy(selectedAirport = airport)永远不会执行,界面无法感知到选中的机场。
修复方式:先更新选中状态,再给collect单独启动子协程:
fun selectAirport(airport: Airport?) { uiState = uiState.copy(selectedAirport = airport) viewModelScope.launch { if (airport != null) { flightRepository.getAllDifferentAirports(airport.id).collect{ uiState = uiState.copy(flightsForSelectedAirport = it) } } else { uiState = uiState.copy(flightsForSelectedAirport = emptyList()) } } }
4. 检查Room返回的Flow有效性
确认Repository中的getAirportsByIatOrName、getFavorites、getAllDifferentAirports是否返回Room的查询Flow(即使用@Query注解且返回Flow<List<T>>)。如果是普通Flow,不会自动监听数据库变化,也不会触发collect回调。
5. 确认UiState更新线程
确保collect中的uiState更新在主线程执行,viewModelScope默认使用主线程调度,若Repository的Flow切换了线程,需用flowOn(Dispatchers.IO)指定后台线程,确保最终在主线程更新状态。
内容的提问来源于stack exchange,提问作者NullPointerException

