Get data from room db using async & await

Viewed 4671

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.

3 Answers

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. :)

As pointed in the comments: a non suspending function can never return asynchronously. Use next approach to get Profile from DB:

suspend fun getUserFromDB(): Profile {
    val profileDao = AppDatabase.getDatabase(context).getProfileDao()
    return profileDao.getUserProfile().await()
}

Use CoroutineScope with Dispatchers.Main context to launch a coroutine, get Profile and update UI:

CoroutineScope(Dispatchers.Main).launch {
    val profile = getUserFromDB()
    updateUi(profile)
}

Or more concise, without using getUserFromDB() function:

CoroutineScope(Dispatchers.Main).launch {
    val profile = AppDatabase.getDatabase(context).getProfileDao().getUserProfile().await()
    updateUi(profile)
}

You can just return a Profile data type from your query by doing this in your DAO:

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

Don't worry about making the call on the Main thread since Room handles the query on a background thread.

Now depending on your arquitecture you can start a new coroutine with launch to call getUserProfile but since you said you're doing a simple example you can just call getUserProfile like this:

From an Activity:

fun myMethod() = lifecycleScope.launch {
    val profileDao = AppDatabase.getDatabase(this@MyActivity).getProfileDao()
    val profile = profileDao.getUserProfile()
    //Do something with profile here
}

From a Fragment:

fun myMethod() = viewLifecycleOwner.lifecycleScope.launch {
    val profileDao = AppDatabase.getDatabase(this@MyFragment.context).getProfileDao()
    val profile = profileDao.getUserProfile()
    //Do something with profile here
}

From a ViewModel:

fun myMethod() = viewModelScope.launch {
    val profileDao = AppDatabase.getDatabase(getApplication<Application>().applicationContext).getProfileDao()
    val profile = profileDao.getUserProfile()
    //Do something with profile here
}
Related