Получение данных из базы данных комнаты с помощью async и ожидания

Я пытаюсь получить данные из базы данных номеров, используя async & await внутри Coroutine Scope, но при возврате значения возникают проблемы.

Вот мой код:

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

Дао:

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

Здесь я хочу вернуть userProfile из метода, но я не могу сделать это внутри области видимости, и он будет нулевым, если я вернусь извне Coroutine scope.

Примечание. Я здесь не следую шаблону MVVM, а делаю простой пример.


person Android Dev    schedule 24.06.2020    source источник
comment
Вы просто не можете, функция без приостановки никогда не может возвращаться асинхронно, вам может потребоваться приостановить функцию, иначе используйте runBlocking для блокирующего возврата результата.   -  person Animesh Sahu    schedule 24.06.2020
comment
@AnimeshSahu Можете уточнить?   -  person Android Dev    schedule 25.06.2020
comment
Ответы ниже уже объяснили это :)   -  person Animesh Sahu    schedule 25.06.2020


Ответы (3)


Здесь есть несколько ошибок, но сначала я остановлюсь на основной проблеме.

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

В getUserFromDB launch выполняется асинхронно с getUserFromDB и не завершится до того, как вы вернетесь.

Чтобы гарантировать завершение, у вас есть два варианта (если вы хотите использовать сопрограммы).

  1. Отметьте getUserFromDB как функцию приостановки. (Настоятельно рекомендуется)
suspend fun getUserFromDB() {
    val profileDao = AppDatabase.getDatabase(context).getProfileDao()
    val userProfile = withContext(Dispatchers.IO) {
        profileDao.getUserProfile().await()
    }
    return userProfile
}
  1. Используйте runBlocking.
fun getUserFromDB() {
    val profileDao = AppDatabase.getDatabase(context).getProfileDao()
    val userProfile = runBlocking(Dispatchers.IO) {
        profileDao.getUserProfile().await()
    }
    return userProfile
}

Теперь решен главный вопрос. Здесь есть пара вещей, которые нарушают условности и правила.

  1. getUserProfile не должен возвращать Deferred<Profile> (особенно если он уже приостанавливается), поскольку это небезопасный примитив для передачи, он должен просто возвращать Profile. Таким образом, вы случайно не введете нежелательный параллелизм и вам не придется писать 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. При использовании сопрограмм с Room он выполняет блокирующие вызовы в выделенном пуле потоков, поэтому вам не нужно его использовать (Dispatchers.IO), безопасно вызывать в основном потоке / диспетчере. Дополнительная информация в этом ответе Почему querySkuDetails нужно запускать в контексте ввода-вывода?.
fun getUserFromDB() {
    val profileDao = AppDatabase.getDatabase(context).getProfileDao()
    CoroutineScope(/* Dispatchers.IO */).launch {
        val userProfile = profileDao.getUserProfile().await()
    }
    return userProfile
}
  1. И наконец, если вы создадите CoroutineScope, не отменяя его позже, вы должны просто использовать GlobalScope, чтобы уменьшить количество выделений. Люди говорят Избегайте использования GlobalScope., Что верно, но вы также должны избегать создания CoroutineScope, не отменяя их (я бы сказал, что это еще хуже).
fun getUserFromDB() {
    val profileDao = AppDatabase.getDatabase(context).getProfileDao()
    GlobalScope.launch(Dispatchers.IO) {
        val userProfile = profileDao.getUserProfile().await()
    }
    return userProfile
}

TL;DR

Я структурировал свой ответ так, чтобы вы могли применять каждое изменение независимо, когда будете готовы. Если вам просто нужна сумма всех изменений, то вот она.

@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
}

Удачного кодирования. :)

person Dominic Fischer    schedule 24.06.2020
comment
Как мне отменить Coroutine Scope, когда работа будет выполнена? - person Android Dev; 25.06.2020
comment
На CoroutineScope есть .cancel() метод. - person Dominic Fischer; 25.06.2020

Как указано в комментариях: функция без приостановки никогда не может возвращаться асинхронно. Используйте следующий подход, чтобы получить Profile из БД:

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

Используйте CoroutineScope с Dispatchers.Main контекстом для запуска сопрограммы, получения Profile и обновления пользовательского интерфейса:

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

Или более кратко, без использования функции getUserFromDB():

CoroutineScope(Dispatchers.Main).launch {
    val profile = AppDatabase.getDatabase(context).getProfileDao().getUserProfile().await()
    updateUi(profile)
}
person Sergey    schedule 24.06.2020
comment
await будет ждать завершения задачи? После выполнения задачи возвращает результат. Это правильно? - person Android Dev; 25.06.2020
comment
Разве первый подход, о котором вы упомянули, не будет работать в основном потоке или Select Query будет обрабатывать его в фоновом режиме? - person Android Dev; 25.06.2020
comment
сопрограмма будет работать в основном потоке, getUserFromDB() - это функция приостановки, она приостановит сопрограмму, не блокируя основной поток. updateUi (profile) будет выполняться в основном потоке. - person Sergey; 25.06.2020
comment
@Sergey Каким должен быть тип возвращаемого значения getUserProfile () при использовании await? - person Sumit Shukla; 07.07.2020
comment
@SumitShukla, должно быть Deferred<Profile>. - person Sergey; 07.07.2020

Вы можете просто вернуть тип данных Profile из своего запроса, выполнив это в своем DAO:

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

Не беспокойтесь о вызове в основном потоке, поскольку Room обрабатывает запрос в фоновом потоке.

Теперь, в зависимости от вашей архитектуры, вы можете запустить новую сопрограмму с launch для вызова getUserProfile, но, поскольку вы сказали, что делаете простой пример, вы можете просто вызвать getUserProfile следующим образом:

Из мероприятия:

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

Из фрагмента:

fun myMethod() = viewLifecycleOwner.lifecycleScope.launch {
    val profileDao = AppDatabase.getDatabase([email protected]).getProfileDao()
    val profile = profileDao.getUserProfile()
    //Do something with profile here
}

Из ViewModel:

fun myMethod() = viewModelScope.launch {
    val profileDao = AppDatabase.getDatabase(getApplication<Application>().applicationContext).getProfileDao()
    val profile = profileDao.getUserProfile()
    //Do something with profile here
}
person Glenn Sandoval    schedule 24.06.2020
comment
Разве переменная профиля не будет иметь значение NULL, поскольку запрос будет выполняться в фоновом потоке. - person Sumit Shukla; 07.07.2020
comment
@SumitShukla getUserProfile() - это приостановка вызова, и он будет возобновлен, как только Room ответит. - person Glenn Sandoval; 07.07.2020
comment
@SumitShukla зачем? или как? - person Glenn Sandoval; 07.07.2020