#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)