Avatar ·

Как исправить ошибку "inappropriate blocking method call" в Kotlin Coroutines с OkHttp?

Привет, коллеги! Я пишу приложение на Kotlin с использованием корутин для асинхронной работы. Для HTTP-запросов использую OkHttp, а для парсинга JSON — Moshi. Всё работает, но я постоянно вижу в IntelliJ IDEA предупреждение "Inappropriate blocking method call" на вызове `execute()` у OkHttp. Вот мой код: ```kotlin suspend fun fetchData(): String { val client = OkHttpClient() val request = Request.Builder() .url("https://api.example.com/data") .build() // Здесь IDE подсвечивает execute() как блокирующий вызов val response = client.newCall(request).execute() return response.body?.string() ?: "" } ``` Я понимаю, что `execute()` — это блокирующий метод, который не стоит вызывать внутри корутины. Но как тогда правильно сделать запрос? Я читал про `enqueue()`, но не понимаю, как интегрировать его с корутинами. Пробовал использовать `withContext(Dispatchers.IO)`, но предупреждение всё равно остаётся. Может, нужно использовать другую библиотеку? Или есть способ "обмануть" IDE? Подскажите, как правильно организовать HTTP-запросы в корутинах без этого предупреждения и, главное, без блокировки потока. Использую Kotlin 1.9, OkHttp 4.12, Moshi 1.15. Заранее спасибо! <<>>

Правильный способ: использование Dispatchers.IO и suppression аннотации

Проблема в том, что вы вызываете блокирующий метод execute() внутри корутины, которая по умолчанию работает на Dispatchers.Main (или другом не-блокирующем диспатчере). IDE предупреждает, что это может заблокировать поток корутины. Решение — явно переключиться на Dispatchers.IO, предназначенный для блокирующих операций, и подавить предупреждение с помощью аннотации @Suppress("BlockingMethodInNonBlockingContext").

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request

suspend fun fetchData(): String {
    val client = OkHttpClient()
    val request = Request.Builder()
        .url("https://api.example.com/data")
        .build()

    return withContext(Dispatchers.IO) {
        @Suppress("BlockingMethodInNonBlockingContext")
        val response = client.newCall(request).execute()
        response.body?.string() ?: ""
    }
}

Почему это работает?

  • Dispatchers.IO использует пул потоков, оптимизированных для блокирующих операций (например, сетевых запросов или чтения файлов).
  • withContext временно переключает контекст корутины на указанный диспетчер, выполняя блок кода в подходящем потоке.
  • После завершения корутина автоматически возвращается на исходный диспетчер (например, Main для UI).
  • Аннотация @Suppress убирает предупреждение IDE, так как вы осознанно используете блокирующий метод в правильном контексте.

Альтернатива: использование enqueue() с корутинами

Если вы хотите избежать блокировки совсем, можно использовать асинхронный метод enqueue() и преобразовать его в корутину с помощью suspendCancellableCoroutine:

import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

suspend fun fetchDataAsync(): String {
    val client = OkHttpClient()
    val request = Request.Builder()
        .url("https://api.example.com/data")
        .build()

    return suspendCancellableCoroutine { continuation ->
        val call = client.newCall(request)
        continuation.invokeOnCancellation { call.cancel() }

        call.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                continuation.resumeWithException(e)
            }

            override fun onResponse(call: Call, response: Response) {
                continuation.resume(response.body?.string() ?: "")
            }
        })
    }
}

Какой подход выбрать?

  • Первый вариант (withContext + @Suppress) — проще и короче. Подходит для большинства случаев, особенно если вы не делаете тысячи параллельных запросов.
  • Второй вариант (suspendCancellableCoroutine) — чисто асинхронный, без блокировки потоков. Лучше для высоконагруженных систем или когда нужно отменять запросы при отмене корутины.
  • Третий вариант — использовать специализированную библиотеку, например Ktor или Fuel, которые изначально поддерживают корутины.

В вашем случае с OkHttp и Moshi рекомендую первый вариант, так как он минимально меняет код и решает проблему. Главное — не забывайте про Dispatchers.IO, иначе вы рискуете заблокировать главный поток в Android-приложении.

<<>>

Использование Ktor Client как альтернатива OkHttp

Если вы готовы сменить HTTP-клиент, рассмотрите Ktor Client — он изначально спроектирован для Kotlin Coroutines и не имеет проблем с блокирующими вызовами. Вот пример интеграции с Moshi:

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

// Для парсинга JSON используем kotlinx.serialization (аналог Moshi)
@Serializable
data class MyData(val id: Int, val name: String)

suspend fun fetchDataWithKtor(): MyData {
    val client = HttpClient()
    val response: HttpStatement = client.get("https://api.example.com/data")
    val jsonString = response.bodyAsText()
    return Json.decodeFromString(jsonString)
}

Почему это лучше?

  • Нет блокирующих методов — все запросы асинхронны по умолчанию.
  • Не нужно использовать Dispatchers.IO или подавлять предупреждения.
  • Ktor поддерживает плагины для сжатия, логирования, сериализации и т.д.
  • Легко интегрируется с Moshi через дополнительный модуль ktor-client-moshi (если не хотите переходить на kotlinx.serialization).

Недостаток: нужно переписывать весь код HTTP-запросов. Но если вы только начинаете проект или готовы к рефакторингу, это избавит вас от головной боли с блокирующими вызовами навсегда.