Search code examples
androidmvvmandroid-roomkotlin-coroutines

How should I use room database among with coroutines, and flow?


I'm trying to learn how I should re-write my room database (dao, repository, viewmodel) so it can be more efficient. I haven't using any of these, and I have a hard time with trying to find resources to base on, because many of them either don't use repository (so I've started to think that I have implemented it unnecesseairly) or they are using hilt, and I'm kinda overwhelmed by new stuff to learn.

How I should implement repository, and viewmodel to have flow and coroutines inside?


Solution

  • Well, your question is very generic, but I will do my best to answer, I suppose you have at least a basic knowledge of coroutines, flows, and Hilt, if you don't, no problem, at least try to learn something new, I tried to make it simple as much as possible.

    Scenario:

    Suppose there is a simple application that displays books info to the user, the user can add any book to the favorite, delete them from favorite, and have a screen to display the favorite books.

    We have a simple entity class called Book:

    @Entity
    data class Book(
       @PrimaryKey
       val ispn: String
       val title: String,
       val description: String,
       val pages: Int
    )
    

    Now, let's create a DAO interface with Flow and suspend functions:

    @Dao
    interface FavoriteBooksDao {
        @Query("SELECT * FROM book")
        fun selectAll(): Flow<List<Book>> // No need to add suspend keyword, because it returns Flow, flows already uses coroutines.
    
        @Insert
        suspend fun insert(book: Book) // Simply, add suspend keyword to the function, to make it work with coroutines.
    
        @Delete
        suspend fun delete(book: Book) // Simply, add suspend keyword to the function, to make it work with coroutines.
    }
    

    Explanation:

    We have 3 functions: selectAll(): To retrieve the list of favorite books. insert(): To insert a new book. delete(): To delete a book.

    To make inserting and deleting work with coroutines, add the suspend keyword for both functions. For the selectAll() function, it returns a flow, you can think of it as a replacement for LiveData, this allows us to observe the changes on the book table when a new book is inserted or deleted, selectAll() will emit a new list after the insertion/deletion, allowing you to update your UI. Note that selectAll() is not a suspend function, because it returns a flow, flows already use coroutines, so we don't need suspend keyword.

    Now let's create the repository:

    class FavoriteBooksRepository @Inject constructor(
        private val dao: FavoriteBooksDao
    ) {
    
        fun getAll() = dao.selectAll() //Non-suspending function
    
        suspend fun add(book: Book) = dao.insert(book) //suspending function
    
    
        suspend fun remove(book: Book) = dao.delete(book) //suspending function
    
    }
    

    Explanation:

    Now, you need the DAO instance in your repository, inject it using Hilt.

    You have 3 functions in the repository which will call the DAO functions, you will have add() and remove() as suspend functions as you declare insert() and delete() as suspending functions in your DAO, and getAll() is not suspending as selectAll() in your DAO because it returns a flow as said earlier.

    Finally, let's implement the ViewModel:

    @HiltViewModel
    class FavoriteBooksViewModel @Inject constructor(
        private val repository: FavoriteBooksRepository
    ): ViewModel() {
    
        // This is a mutable state flow that will be used internally in the viewmodel, empty list is given as initial value.
        private val _favoriteBooks = MutableStateFlow(emptyList<Book>())
    
        //Immutable state flow that you expose to your UI
        val favoriteBooks = _favoriteBooks.asStateFlow()
    
         init {
              getFavoriteBooks()
         }
    
    
        /**
         * This function is used to get all the books from the database, and update the value of favoriteBooks.
         * 1. viewModelScope.launch is used to launch a coroutine within the viewModel lifecycle.
         * 2. repository.getAll() is used to get all the books from the database.
         * 3. flowOn(Dispatchers.IO) is used to change the dispatcher of the flow to IO, which is optimal for IO operations, and does not block the main thread.
         * 4. collect is a suspending function used to collect the flow of books list, and assign the value to favoriteBooks.
         * 5. each time the flow emits a new value, the collect function will be called with the list of books.
         */
        fun getFavoriteBooks() {
            viewModelScope.launch { //this: CoroutineScope
                repository.getAll().flowOn(Dispatchers.IO).collect { books: List<Book> ->
                    _favoriteBooks.update { books }
                }
            }
        }
    
        /**
         * This function is used to add a book to the database.
         * 1. viewModelScope.launch is used to launch a coroutine within the viewModel lifecycle.
         * 2. Dispatchers.IO is used to change the dispatcher of the coroutine to IO, which is optimal for IO operations, and does not block the main thread.
         * 3. repository.add(book) is used to add the book to the database.
         */
        fun addBook(book: Book) {
            viewModelScope.launch(Dispatchers.IO) { //this: CoroutineScope
                repository.add(book)
            }
        }
    
        /**
         * This function is used to remove a book from the database.
         * 1. viewModelScope.launch is used to launch a coroutine within the viewModel lifecycle.
         * 2. Dispatchers.IO is used to change the dispatcher of the coroutine to IO, which is optimal for IO operations, and does not block the main thread.
         * 3. repository.remove(book) is used to remove the book from the database.
         */
        fun removeBook(book: Book) {
            viewModelScope.launch(Dispatchers.IO) { //this: CoroutineScope
                repository.remove(book)
            }
        }
    }
    

    Explanation:

    Now, you need the Repository instance in your viewModel, inject it using Hilt.

    I have added documentation that describes all the work and functions separately to make it clear as much as possible, also notice that the functions are not suspending, because we are launching a viewModelScope coroutine, that will be enough, no need to mark these functions as suspending.

    That's how you integrate coroutines and flows with your application in your Room database, repository, and viewModels. You can do more advanced operations on flows and coroutines to make your application more robust and efficient, as you learn more. You can add more operations and codes depending on your application requirements, and I tried to represent it in the simplest format.

    Finally, thank you for your time reading this, hope this helps you.