Search code examples
kotlinandroid-roomkotlin-flow

Room insert not trigger the Flow


in viewModel, I subscribe to a flow that calls the database (by Room). But when I insert data, the flow does not return updated data.

View model function that subscribes:

fun initData(tournamentId: Long) {
    viewModelScope.launch {
        tournamentRepository.getStream(tournamentId).map { it?.toModel() }
            .collect { tournament ->
                if (tournament != null) {
                    participantRepository.getAllByTournament(tournamentId)
                        .map { it.map { it.playerId } }.collect { playersId ->
                            playerRepository.getAllByIds(playersId)
                                .map { it.map { it.toModel() } }
                                .collect { players ->
                                    println(players)
                                    _uiState.value = _uiState.value.copy(
                                        tournament = _uiState.value.tournament.copy(
                                            id = tournament.id,
                                            type = tournament.type,
                                            name = tournament.name,
                                            players = players
                                        )
                                    )
                                }
                        }
                }
            }
    }
}

View model function that inserts:

fun addUser() {
    viewModelScope.launch {
        val idPlayer: Long =
            playerRepository.insert(
                PlayerEntity(
                    name = "Player${_uiState.value.tournament.players.size + 1}",
                    guest = true
                )
            )
        participantRepository.insert(
            ParticipantEntity(
                tournamentId = _uiState.value.tournament.id,
                playerId = idPlayer
            )
        )
    }
}

participant repository:

class OfflinePlayerRepository(private val playerDao: PlayerDao):PlayerRepository {
override fun getAllStream(): Flow<List<PlayerEntity>> = playerDao.getAllItems()
override fun getStream(id: Long): Flow<PlayerEntity?> = playerDao.getItem(id)
override suspend fun insert(player: PlayerEntity):Long = playerDao.insert(player)
override suspend fun insertAll(players: List<PlayerEntity>): List<Long> = playerDao.insertAll(players)
override suspend fun delete(player: PlayerEntity) = playerDao.delete(player)
override suspend fun update(player: PlayerEntity) = playerDao.update(player)
override fun getAllByIds(ids: List<Long>): Flow<List<PlayerEntity>> = playerDao.getAllByIds(ids)
override suspend fun deleteById(id: Long) = playerDao.deleteById(id)
}

participant Dao:

@Dao
interface PlayerDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(player: PlayerEntity):Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAll(players: List<PlayerEntity>):List<Long>
@Update
suspend fun update(player: PlayerEntity)
@Delete
suspend fun delete(player: PlayerEntity)
@Query("SELECT * from players WHERE id = :id")
fun getItem(id: Long): Flow<PlayerEntity>
@Query("SELECT * from players ORDER BY name ASC")
fun getAllItems(): Flow<List<PlayerEntity>>
@Query("SELECT * from players where id in(:ids) ORDER BY name ASC")
fun getAllByIds(ids:List<Long>): Flow<List<PlayerEntity>>
@Query("DELETE FROM players WHERE id=:id")
suspend fun deleteById(id: Long)
}

What should I change so that this flow returns data when I add a player?


Solution

  • Flows from Room are infinite. They monitor the database for changes, so they never reach a last item and finish being collected.

    Your problem is your use of nested collect calls on infinite flows. When you call collect on an infinite flow, it never returns because it's always waiting for another emission. So when you collect inside a collect, it the outer lambda will never get past the first item.

    First, you're unnecessarily upcasting this to a nullable type, which is making your downstream code have to do null checks, so in your repo change

    override fun getStream(id: Long): Flow<PlayerEntity?> = playerDao.getItem(id)
    

    to

    override fun getStream(id: Long): Flow<PlayerEntity> = playerDao.getItem(id)
    

    To avoid these map and nested collect calls, you can use flatMapLatest and combine. flatMapLatest is using nested collect calls under the hood, but it automatically cancels them when new source flow values are emitted. In your ViewModel:

    fun initData(tournamentId: Long) {
        val tournaments = tournamentRepository.getStream(tournamentId)
        val playerModels = participantRepository.getAllByTournament(tournamentId)
            .map { participants -> participants.map { it.playerId } } 
            .flatMapLatest { playerIdsList -> playerRepository.getAllByIds(playerIdsList) }
            .map { playersList -> playersList.map { it.toModel } }
    
        tournaments.combine(playerModels) { tourney, playerModels ->
            Log.i("MyViewModel", playerModels)
            val newTournament = _uiState.value.tournament.copy(
                    id = tourney.id,
                    type = tourney.type,
                    name = tourney.name,
                    players = playerModels
                )
            _uiState.value = _uiState.value.copy(tournament = newTournament)
            Unit // not sure if this line is necessary
        }.launchIn(viewModelScope)
    }
    

    By the way, I think this would all be considerably simpler if you used a single database so you could use SQL JOIN operations (which are easy to do in Room, see here) to get your flow of tournaments with players directly from your DAO.