Avatar ·

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

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

Почему возникает ошибка и как её исправить

Ошибка "Inappropriate blocking method call" возникает, потому что метод OkHttpClient.newCall().execute() является блокирующим — он останавливает текущий поток до получения ответа. В корутинах это нарушает принцип неблокирующего выполнения: поток должен быть свободен для других корутин, а не ждать I/O.

Есть два правильных подхода:

1. Использование suspendCancellableCoroutine (рекомендуется)

Этот подход превращает колбэк-стиль OkHttp в корутину:

import kotlinx.coroutines.*
import okhttp3.*
import java.io.IOException

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

    val call = client.newCall(request)
    
    // Обработка отмены корутины
    continuation.invokeOnCancellation {
        call.cancel()
    }

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

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

Как это работает:

  • suspendCancellableCoroutine приостанавливает корутину и предоставляет объект Continuation.
  • Внутри мы вызываем enqueue() — асинхронный метод OkHttp, который выполняется в фоновом потоке.
  • Когда ответ получен, мы вызываем continuation.resume(), чтобы возобновить корутину.
  • При отмене корутины вызовется invokeOnCancellation, который отменит HTTP-запрос.
  • Важно проверять continuation.isActive перед resume, чтобы избежать повторного возобновления.

2. Использование корутинового Dispatchers.IO

Если вы уверены, что блокирующий вызов короткий, можно обернуть его в withContext(Dispatchers.IO):

suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    val client = OkHttpClient()
    val request = Request.Builder()
        .url("https://api.example.com/data")
        .build()
    val response = client.newCall(request).execute()
    response.body?.string() ?: ""
}

Этот способ проще, но менее эффективен, так как занимает поток из пула IO. Он подходит для простых случаев, но не рекомендуется для высоконагруженных приложений.

Дополнительно: работа с Moshi

После получения JSON строки вы можете распарсить её с помощью Moshi:

data class ApiResponse(val data: String)

suspend fun fetchAndParse(): ApiResponse {
    val json = fetchData() // из примера выше
    val moshi = Moshi.Builder().build()
    val adapter = moshi.adapter(ApiResponse::class.java)
    return adapter.fromJson(json) ?: throw RuntimeException("Failed to parse")
}

Лучшая практика: используйте первый подход с suspendCancellableCoroutine — он полностью корутино-совместим, поддерживает отмену и не блокирует потоки. В продакшене также стоит настроить OkHttpClient с таймаутами и пулом соединений.

<<>>

Альтернатива: использование библиотеки Ktor Client

Если вы не привязаны к OkHttp, рассмотрите Ktor Client — он изначально спроектирован для корутин и не требует обёрток:

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*

suspend fun fetchData(): String {
    val client = HttpClient()
    val response: HttpResponse = client.get("https://api.example.com/data")
    return response.bodyAsText()
}

Преимущества:

  • Нет блокирующих вызовов — всё асинхронно на уровне корутин.
  • Легко настраивается (сериализация, таймауты, перехватчики).
  • Интеграция с Moshi через плагин ktor-client-moshi.

Недостаток: нужно подключать новую библиотеку и менять код. Если проект уже использует OkHttp, лучше оставить первый подход.

Также можно использовать OkHttp с enqueue() и колбэками, но это менее удобно в корутинах — приходится вручную управлять Continuation.

Выбирайте подход под свои задачи: для нового проекта — Ktor, для существующего с OkHttp — suspendCancellableCoroutine.