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

使用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
    }
}
问题原因分析
  1. ViewModel实例不一致:主页代码中同时使用了homeViewModel和传入的viewModel,若二者是不同实例,会导致多个ViewModel同时持有数据库连接,引发资源冲突。
  2. Flow订阅与线程冲突:lastNextMatchesUiState是通过stateIn创建的共享StateFlow,持续订阅数据库;而getFullTeam()、getCalendar()方法返回的Room原始Flow,在IO线程执行时可能被共享StateFlow的订阅阻塞,导致数据无法正常读取。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 08:57:40