Search code examples
androidkotlinandroid-roomkotlin-coroutinesdeferred

Get data from room db using async & await


I am trying to get data from Room Database using async & await inside Coroutine Scope but getting problem while returning value.

Here is my code:

fun getUserFromDB():Profile {
    val profileDao = AppDatabase.getDatabase(context).getProfileDao()
    CoroutineScope(Dispatchers.IO).launch {
        return profileDao.getUserProfile().await()
    }
}

Dao:

@Query("SELECT * FROM PROFILE LIMIT 1")
suspend fun getUserProfile():Deferred<Profile>

Here I want to return userProfile from the method but I cannot do that inside scope and it will be null if I return from outside of Coroutine scope.

Note: I am not following MVVM pattern here but doing a simple example.


Solution

  • There are a couple things wrong here but I'll first address the main issue.

    fun getUserFromDB() {
        val profileDao = AppDatabase.getDatabase(context).getProfileDao()
        CoroutineScope(Dispatchers.IO).launch {
            val userProfile = profileDao.getUserProfile().await()
        }
        return userProfile
    }
    

    In getUserFromDB, the launch executes asynchronously from getUserFromDB and will not complete before you return.

    To guarantee completion you have two options (if you want to use coroutines).

    1. Mark getUserFromDB as a suspend function. (Highly recommended)
    suspend fun getUserFromDB() {
        val profileDao = AppDatabase.getDatabase(context).getProfileDao()
        val userProfile = withContext(Dispatchers.IO) {
            profileDao.getUserProfile().await()
        }
        return userProfile
    }
    
    1. Use runBlocking.
    fun getUserFromDB() {
        val profileDao = AppDatabase.getDatabase(context).getProfileDao()
        val userProfile = runBlocking(Dispatchers.IO) {
            profileDao.getUserProfile().await()
        }
        return userProfile
    }
    

    Now the main issue is addressed. There are a couple things here that break conventions and guidelines.

    1. getUserProfile should not return Deferred<Profile> (especially if it's already suspending), as this is an unsafe primitive to be passing around, it should just return Profile. This way you don't introduce unwanted concurrency by accident and you don't have to write await().
    @Query("SELECT * FROM PROFILE LIMIT 1")
    suspend fun getUserProfile(): Profile
    
    fun getUserFromDB() {
        val profileDao = AppDatabase.getDatabase(context).getProfileDao()
        CoroutineScope(Dispatchers.IO).launch {
            val userProfile = profileDao.getUserProfile() /* .await() */
        }
        return userProfile
    }
    
    1. When using coroutines with Room, it executes blocking calls in a dedicated thread pool, so you do not have to use one (Dispatchers.IO), it is safe to call in the main thread/dispatcher. More info in this answer Why does the querySkuDetails need to run in IO context? .
    fun getUserFromDB() {
        val profileDao = AppDatabase.getDatabase(context).getProfileDao()
        CoroutineScope(/* Dispatchers.IO */).launch {
            val userProfile = profileDao.getUserProfile().await()
        }
        return userProfile
    }
    
    1. Last and the least, if you create a CoroutineScope without cancelling it later, you should just use GlobalScope to reduce the number of allocations. People say "Avoid using GlobalScope.", which is true but you should equally avoid creating CoroutineScopes without cancelling them (I'd argue it's even worse).
    fun getUserFromDB() {
        val profileDao = AppDatabase.getDatabase(context).getProfileDao()
        GlobalScope.launch(Dispatchers.IO) {
            val userProfile = profileDao.getUserProfile().await()
        }
        return userProfile
    }
    

    TL;DR

    I've structured my answer so that you can apply each change independently when you are ready. If you just want the sum of all the changes then here it is.

    @Query("SELECT * FROM PROFILE LIMIT 1")
    suspend fun getUserProfile(): Profile
    
    suspend fun getUserFromDB(): Profile {
        val profileDao = AppDatabase.getDatabase(context).getProfileDao()
        val userProfile = profileDao.getUserProfile()
        return userProfile
    }
    

    Happy coding. :)