通过`Dispatcher s.IO`启动协程时出现NetworkOnMainThreadException异常

w8f9ii69  于 2022-10-09  发布在  Android
关注(0)|答案(3)|浏览(166)

我正在尝试使用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用例:

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的主用户界面线程)。

**问题:**为什么会抛出上述异常,如何纠正此代码?

y0u0uwnf

y0u0uwnf1#

将函数标记为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))
    }
}

但一个更好的解决方案是使用Coroutine适配器进行改装。

svmlkihl

svmlkihl2#

问题是,您并没有在任何地方创建协程。为此,您可以使用更高阶的函数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())
            })
        }
    }

你也可以有很多方法来适应这一点。注此函数永远不会抛出异常。我认为这是有意的,但如果您希望向上传播它,以便能够将其 Package 在try-catch块中,则可以使用continuation.resumeWithException(...)

由于此函数返回一个实际的协例程,因此您的withContext(Dispatchers.IO)应该可以正常工作。希望这能帮上忙!

yhqotfr8

yhqotfr83#

您不应该使用GlobalScope

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

使用Launch()从ViewModel启动协程,并将所有repo函数设置为suspend,并使用withContext(Dispatchers.IO)更改repo函数中的上下文

相关问题