Исключение NetworkOnMainThreadException при запуске сопрограмм через `Dispatchers.IO`

#android #kotlin #kotlin-coroutines #networkonmainthread

#Android #kotlin #kotlin-сопрограммы #networkonmainthread

Вопрос:

Я пытаюсь выполнить базовый сетевой вызов, используя шаблон View / ViewModel / UseCase / Repository. Основной асинхронный вызов выполняется через сопрограммы, которые оба запускаются с помощью Dispatchers.IO .

Для начала, вот соответствующий код:

ViewModel:

 class ContactHistoryViewModel @Inject constructor(private val useCase: GetContactHistory) : BaseViewModel() {
    // ...
    fun getContactHistory(userId: Long, contactId: Long) {
        useCase(GetContactHistory.Params(userId, contactId)) { it.either(::onFailure, ::onSuccess) }
    }
}
  

GetContactHistory UseCase:

 class GetContactHistory @Inject constructor(private val repository: ContactRepository) : UseCase<ContactHistory, GetContactHistory.Params>() {

    override suspend fun run(params: Params) = repository.getContactHistory(params.userId, params.contactId)
    data class Params(val userId: Long, val contactId: Long)
}
  

Базовый класс UseCase, используемый выше:

 abstract class UseCase<out Type, in Params> where Type : Any {

    abstract suspend fun run(params: Params): Either<Failure, Type>

    operator fun invoke(params: Params, onResult: (Either<Failure, Type>) -> Unit = {}) {
        val job = GlobalScope.async(Dispatchers.IO) { run(params) }
        GlobalScope.launch(Dispatchers.IO) { onResult(job.await()) }
    }
}
  

Наконец, репозиторий:

 class ContactDataRepository(...) : SyncableDataRepository<ContactDetailDomainModel>(cloudStore.get(), localStore),
        ContactRepository {

    override fun getContactHistory(userId: Long, contactId: Long): Either<Failure, ContactHistory> {
        return request(cloudStore.get().getContactHistory(userId, contactId), {it}, ContactHistory(null, null))
    }

    /**
     * Executes the request.
     * @param call the API call to execute.
     * @param transform a function to transform the response.
     * @param default the value returned by default.
     */
    private fun <T, R> request(call: Call<T>, transform: (T) -> R, default: T): Either<Failure, R> {
        return try {
            val response = call.execute()
            when (response.isSuccessful) {
                true -> Either.Right(transform((response.body() ?: default)))
                false -> Either.Left(Failure.GenericFailure())
            }
        } catch (exception: Throwable) {
            Either.Left(Failure.GenericFailure())
        }
    }
}
  

Краткие сведения:
Размещение точки останова отладки в этом catch{} блоке в репозитории (видно непосредственно выше) показывает, что android.os.NetworkOnMainThreadException создается. Это странно, учитывая, что обе сопрограммы запускаются с контекстом Dispatchers.IO , а не Dispatchers.Main (основной поток пользовательского интерфейса Android).

Вопрос: Почему генерируется вышеупомянутое исключение и как можно исправить этот код?

Комментарии:

1. Вам нужно будет предоставить трассировку стека, чтобы даже начать отвечать на этот вопрос, поскольку мы не можем сделать вывод из предоставленного кода, что на самом деле вызывает функцию и вызывает исключение.

Ответ №1:

Пометка функции suspend не делает ее приостановленной, вы должны убедиться, что работа действительно выполняется в фоновом потоке.

У вас есть это

 override suspend fun run(params: Params) = repository.getContactHistory(params.userId, params.contactId)
  

который вызывает это

 override fun getContactHistory(userId: Long, contactId: Long): Either<Failure, ContactHistory> {
    return request(cloudStore.get().getContactHistory(userId, contactId), {it}, ContactHistory(null, null))
}
  

Все они синхронны, ваш suspend модификатор здесь ничего не делает.

Быстрым решением было бы изменить ваш репозиторий следующим образом

 override suspend fun getContactHistory(userId: Long, contactId: Long): Either<Failure, ContactHistory> {
return withContext(Dispatchers.IO) {
    request(cloudStore.get().getContactHistory(userId, contactId), {it}, ContactHistory(null, null))
    }
}
  

Но гораздо лучшим решением было бы использовать адаптер сопрограммы для модернизации.

Комментарии:

1. Даже если функция приостановки никогда не приостанавливается, она вызывается в CoroutineScope.async контексте, поэтому она должна выполняться в диспетчере ввода-вывода независимо.

2. Пометка приостановки функции делает ее приостановленной. Проблема здесь в том, что он был приостановлен в Main.

Ответ №2:

Проблема в том, что вы нигде не создаете сопрограмму. Для этого вы можете использовать функцию более высокого порядка suspendCoroutine . Простой пример был бы таким:

 private suspend fun <T, R> request(call: Call<T>, transform: (T) -> R, default: T): Either<Failure, R> {
        return suspendCoroutine { continuation ->
            continuation.resume(try {
                val response = call.execute()
                when (response.isSuccessful) {
                    true -> Either.Right(transform((response.body() ?: default)))
                    false -> Either.Left(Failure.GenericFailure())
                }
            } catch (exception: Throwable) {
                Either.Left(Failure.GenericFailure())
            })
        }
    }
  

Есть много способов, которыми вы тоже можете воспользоваться. Обратите внимание, что эта функция никогда не выдаст исключение. Я думаю, что это предназначено, но если вы хотите распространить его вверх, чтобы иметь возможность обернуть его, например, в try-catch блок, вы могли бы использовать continuation.resumeWithException(...) .

Поскольку эта функция возвращает фактическую сопрограмму, ваша withContext(Dispatchers.IO) должна работать так, как задумано. Надеюсь, это поможет!

Ответ №3:

Вы не должны использовать GlobalScope

https://elizarov.medium.com/the-reason-to-avoid-globalscope-835337445abc

Запустите сопрограмму из ViewModel с помощью launch() и выполните все функции репо suspend , а также измените контекст в функции репо с помощью withContext(Dispatchers.IO)