Search code examples
kotlinandroid-roomkotlin-coroutineskotlin-stateflow

Is StateFlow usable with Room? If yes, how?


I'm currently looking for ways to implement sorting on Room.

Previously, I was using Flow with "getAll" that contains a MutableList which becomes a LiveData on the ViewModel. Which works perfectly, then I began working on sorting. After researching about "observables", I got an idea that maybe I could use something like StateFlow so that I just need to emit a new value to the "all" List when getting all and when sorting. Also, after reading what other things StateFlow can do, I thought it would be a nice thing to use. But after all the necessary changes I think, it doesn't seem to work.

These are currently my relevant code:

Repo

val allItems: Flow<List<Model>> = flow {
    while(true) {
        val allItems = dao.getAllSub()
        emit(allItems) 
    }
}

//I don't really use this yet
suspend fun sort(field: String, isAsc: Boolean?): List<Model>{
    return dao.sortList(field, isAsc)
}

ViewModel

private val _getAllSub = MutableStateFlow(listOf<Model>())
val getAllSub: StateFlow<List<Model>> = _getAllSub

init {
    viewModelScope.launch {
        repo.allItems
            .catch { exception -> exception.localizedMessage?.let { Log.e("TAG", it) } }
            .collect { model ->
                _getAllSub.value = model
            }
    }
}

On my very simple integration test, I'm getting a List is empty error.

This is my test:

//created a single entity instance
viewModel.insert(model)

val items = viewModel.getAllSub.value
assertThat(items.first()).isEqualTo(model)

Now I'm wondering, did I do something wrong? Did I miss something? Or is my idea even correct or feasible? If someone has a different idea or example of how to properly implement sorting, please let me know. I don't really care about sticking with StateFlow if there is a better way out there.


Solution

  • @bylazy’s answer shows how to simply return Flow from your DAO. Your current implementation spams the database nonstop (not ok since it monopolizes the disk access) and also blocks a thread nonstop (not ok because it uses a dispatcher thread from the common thread pool constantly, which will be a problem if you have multiple of these going at once).

    The second problem could be improved by tacking on .cancellable().flowOn(Dispatchers.IO) but even then it’s not a good solution to be constantly reading the same thing from the database for no good reason. If you directly return a Flow from your DAO, it only requeries the database when the database is changed, so it is passive and usually in a suspended state.

    Your way of turning a Flow into a StateFlow is really convoluted. Just use stateIn on it. This is equivalent to all of your ViewModel code above:

    val getAllSub: StateFlow<List<Model>> = 
        repo.allItems
            .catch { exception -> exception.localizedMessage?.let { Log.e("TAG", it) } }
            .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())