使用State Flow时Room数据库致Jetpack Compose应用冻结
问题描述
我的应用基于Jetpack Compose与Kotlin框架,主界面位于Nav Host内,包含三个元素:两个可点击卡片(点击后通过导航路由跳转到新Compose页面,页面加载时通过ViewModel获取数据),以及一个名为LastMatches的卡片(需通过ViewModel读取Room数据库数据)。
移除LastMatches卡片时应用运行正常,但在主页使用val matches by homeViewModel.lastNextMatchesUiState.collectAsState()获取该卡片数据后,整个UI都无法读取数据库,包括跳转到其他页面也存在同样问题。
相关代码片段
主页代码
fun HomepageScreen( viewModel: DataViewModel, onRosterButtonClicked:(Int) -> Unit, onCalendarButtonClicked:(Int) -> Unit, onMatchClicked:(Int) -> Unit, ) { val matches by homeViewModel.lastNextMatchesUiState.collectAsState() Column { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { HomePageCard( modifier = Modifier, icon = Icons.Filled.CalendarMonth, desc = "Calendar", onClicked = onCalendarButtonClicked ) Spacer( modifier = Modifier .weight(1f) ) HomePageCard( modifier = Modifier, icon = Icons.Filled.PeopleAlt, desc = "Team", onClicked = onRosterButtonClicked ) } LastMatches( viewModel = viewModel, onCardClick = onMatchClicked, modifier = Modifier .fillMaxWidth() .padding(16.dp), ) } }
TeamScreen代码
@Composable fun TeamScreen( modifier: Modifier =Modifier, contentPadding: PaddingValues = PaddingValues(8.dp), onCardClick:(Int) -> Unit, viewModel: DataViewModel ) { viewModel.getFullTeam() when(val state = viewModel.teamUiState){ is TeamUiState.Success-> { val players = state.playerList.collectAsState(initial = emptyList()) LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = contentPadding ) { itemsIndexed(items=players.value) { _, player -> TeamListItem( player = player, onCardClick = onCardClick, viewModel = viewModel ) } } } is TeamUiState.Loading->{ LoadingScreen() } is TeamUiState.Error->{ErrorScreen()} } }
CalendarScreen代码
fun CalendarScreen( modifier: Modifier =Modifier, contentPadding: PaddingValues = PaddingValues(8.dp), onCardClick:(Int) -> Unit, viewModel: DataViewModel ) { viewModel.getCalendar() when(val state = viewModel.calendarUiState){ is CalendarUiState.Success -> { val matches = state.matches.collectAsState(initial = emptyList()) LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = contentPadding){ itemsIndexed(matches.value){_, match-> CalendarListItem( match=match, onCardClick = onCardClick, dataUiModel = viewModel ) } } } is CalendarUiState.Loading->{ // LoadingScreen() } is CalendarUiState.Error->{ErrorScreen()} } }
ViewModel与状态类代码
sealed interface DataUiState { data class Success(val playerList: List<dbPlayer>) : DataUiState object Error : DataUiState object Loading : DataUiState } sealed interface TeamUiState{ data class Success (val playerList: Flow<List<dbPlayer>>): TeamUiState object Error: TeamUiState object Loading: TeamUiState } sealed interface PlayerUiState{ data class Success (val player: Flow<List<dbPlayer>>): PlayerUiState object Error: PlayerUiState object Loading: PlayerUiState } sealed interface CalendarUiState{ data class Success (val matches: Flow<List<MatchCalendarEntry>>): CalendarUiState object Error: CalendarUiState object Loading: CalendarUiState } data class LastNextMatchesUiState(val lastNextMatches:List<MatchPastNext> = listOf()) class DataViewModel(private val clappsicsDAO: ClappsicsDAO): ViewModel() { companion object { private const val TIMEOUT_MILLIS = 5_000L val factory : ViewModelProvider.Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as ClappsicsApplication) DataViewModel(application.clappsicDatabase.clappsicsDAO()) } } } var teamUiState:TeamUiState by mutableStateOf(TeamUiState.Loading) private set var playerUiState:PlayerUiState by mutableStateOf(PlayerUiState.Loading) private set var calendarUiState:CalendarUiState by mutableStateOf(CalendarUiState.Loading) private set fun getFullTeam(){ viewModelScope.launch{ teamUiState = TeamUiState.Loading teamUiState = try{ TeamUiState.Success( clappsicsDAO.getPlayers() ) }catch(e: Error){ TeamUiState.Error } } } fun getPlayer(playerID:Int){ viewModelScope.launch(Dispatchers.IO){ playerUiState = PlayerUiState.Loading playerUiState = try{ PlayerUiState.Success( clappsicsDAO.getPlayer(playerID) ) }catch(e: Error){ PlayerUiState.Error } } } var playerId : Int = 1 fun setPlayer(id : Int){ playerId = id } fun getCalendar(){ viewModelScope.launch(Dispatchers.IO){ calendarUiState = CalendarUiState.Loading calendarUiState = try{ CalendarUiState.Success( clappsicsDAO.getMatches() ) }catch(e: Error){ CalendarUiState.Error } } } val lastNextMatchesUiState: StateFlow<LastNextMatchesUiState> = clappsicsDAO.getLastNexMatch().map{LastNextMatchesUiState(it)} .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), initialValue = LastNextMatchesUiState() ) var match_id : Int = 0 fun setMatchId( id : Int){ match_id = id } }
问题原因分析
- ViewModel实例不一致:主页代码中同时使用了
homeViewModel和传入的viewModel,若二者是不同实例,会导致多个ViewModel同时持有数据库连接,引发资源冲突。 - Flow订阅与线程冲突:
lastNextMatchesUiState是通过stateIn创建的共享StateFlow,持续订阅数据库;而getFullTeam()、getCalendar()方法返回的Room原始Flow,在IO线程执行时可能被共享StateFlow的订阅阻塞,导致数据无法正常读取。 - UI状态管理混乱:用
mutableStateOf包裹Flow的方式,会在页面重组时重复调用取数方法,生成多个Flow订阅,进一步加剧数据库访问的冲突问题。
解决方案
1. 统一ViewModel实例使用
确保主页中仅使用传入的同一个ViewModel实例,避免多实例竞争数据库资源:
fun HomepageScreen( viewModel: DataViewModel, onRosterButtonClicked:(Int) -> Unit, onCalendarButtonClicked:(Int) -> Unit, onMatchClicked:(Int) -> Unit, ) { // 替换为传入的viewModel,保持实例统一 val matches by viewModel.lastNextMatchesUiState.collectAsState() // 其余代码不变 }
2. 统一用StateFlow管理所有UI状态
放弃mutableStateOf包裹Flow的方式,将所有UI状态改为StateFlow,统一调度和共享:
class DataViewModel(private val clappsicsDAO: ClappsicsDAO): ViewModel() { companion object { private const val TIMEOUT_MILLIS = 5_000L // factory代码不变 } // 重构teamUiState为StateFlow val teamUiState: StateFlow<TeamUiState> = flow { emit(TeamUiState.Loading) try { emit(TeamUiState.Success(clappsicsDAO.getPlayers())) } catch (e: Exception) { emit(TeamUiState.Error) } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), initialValue = TeamUiState.Loading ) // 重构calendarUiState为StateFlow val calendarUiState: StateFlow<CalendarUiState> = flow { emit(CalendarUiState.Loading) try { emit(CalendarUiState.Success(clappsicsDAO.getMatches())) } catch (e: Exception) { emit(CalendarUiState.Error) } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), initialValue = CalendarUiState.Loading ) // lastNextMatchesUiState代码不变 // 移除原getFullTeam()、getCalendar()方法,StateFlow会自动触发数据加载 }
3. 页面中正确收集StateFlow
在子页面中直接收集ViewModel的StateFlow,避免重复调用取数方法:
@Composable fun TeamScreen( modifier: Modifier =Modifier, contentPadding: PaddingValues = PaddingValues(8.dp), onCardClick:(Int) -> Unit, viewModel: DataViewModel ) { val teamState by viewModel.teamUiState.collectAsState() when(teamState){ is TeamUiState.Success-> { val players = (teamState as TeamUiState.Success).playerList.collectAsState(initial = emptyList()) LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = contentPadding ) { itemsIndexed(items=players.value) { _, player -> TeamListItem( player = player, onCardClick = onCardClick, viewModel = viewModel ) } } } is TeamUiState.Loading-> LoadingScreen() is TeamUiState.Error-> ErrorScreen() } }
4. 移除不必要的线程指定
Room DAO返回的Flow默认在IO线程执行,无需在ViewModel的方法中手动指定Dispatchers.IO,避免线程调度冲突。
内容的提问来源于stack exchange,提问作者Catosh
相关产品推荐
相关产品推荐

