Виммельбух, 1, перетяжка
Виммельбух, 1, перетяжка
Виммельбух, 1, перетяжка

Основы OkHttp в Android-разработке

Как применять OkHttp — библиотеку и, по совместительству, HTTP-клиент с открытым исходным кодом для Java и Kotlin.

4К открытий6К показов

OkHttp — библиотека и по совместительству HTTP-клиент с открытым исходным кодом для Java и Kotlin, разработанная Square, которая также создала Retrofit.

OkHttp предоставляет простой, легкий в использовании API для выполнения HTTP-запросов, включая поддержку протоколов HTTP/1.1 и HTTP/2. Библиотека поддерживает все стандартные методы HTTP и может легко обрабатывать несколько одновременных запросов, а также предоставляет расширенные возможности: кэширование запросов/ответов, объединение подключений в пул (connection pooling), аутентификация и др.

О том, почему иногда стоит использовать OkHttp, а не вездесущий Retrofit, можно посмотреть в видео от Android Broadcast. Краткое пояснение дано в следующем пункте статьи.

В статье подробно рассмотрены основные объекты и методы библиотеки и представлены основы работы с ней в Android-разработке.

Содержание:

  • Преимущества OkHttp
  • Основные классы и методы
  • Простой GET-запрос (синхронный/асинхронный)
  • Сериализация/десериализация
  • Простой POST-запрос
  • Особенности работы с HTTPS
  • Аутентификация на сервере
  • Использование вместе с ViewModel

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

OkHttp — это библиотека более низкого уровня, чем Retrofit. Это означает, что HTTP-запросы, автоматизированные в Retrofit с помощью аннотаций, придётся писать вручную. Однако в этом и главный плюс библиотеки: она предоставляет более обширный функционал и настройки соединения, что может повысить производительность и сократить использование памяти. К слову, Retrofit под капотом использует OkHttp.

Библиотека разработана как легкая и эффективная, с акцентом на снижение задержек и повышение работоспособности. Это достигается за счет применения различных методов оптимизации, таких как повторное использование соединений, сжатие и конвейеризация.

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

  • Гибкость: Библиотека предоставляет больше контроля над процессом сетевого взаимодействием за счёт дополнительных функций, например, пользовательской обработки запросов и ответов.
  • Лёгкость: OkHttp — более компактная библиотека, чем Retrofit, что позволяет минимизировать размер используемой приложением памяти.
  • Кэширование: Библиотека имеет встроенную поддержку HTTP-кэширования, что может повысить производительность и снизить нагрузку на сеть.
  • Аутентификация: OkHttp предоставляет гибкий и расширяемый API аутентификации, что упрощает реализацию различных её моделей.
  • Перехватчики (Interceptors): Это механизм, позволяющий легко настраивать запросы и ответы, а также хороший выбор для приложений, требующих расширенной обработки запросов.
  • WebSockets: OkHttp обеспечивает встроенную поддержку WebSockets, что позволяет легко реализовать коммуникацию с сервером в режиме реального времени.

Основные объекты и методы

Настройка клиента и запроса

Класс OkHttpClient — клиент для HTTP-вызовов, который можно использовать для отправки запросов и чтения ответов.

OkHttpClient.Builder — класс предоставляющий методы для настройки клиента, например кэш, аутентификация, перехватчики, тайм-ауты и др. По завершению настройки используется метод build(), который возвращает экземпляр класса OkHttpClient.

OkHttp работает лучше при создании одного экземпляра OkHttpClient и повторном его использовании для всех HTTP-вызовов. Так происходит потому, что каждый клиент содержит свой собственный пул соединений и пул потоков. Повторное использование соединений и потоков уменьшает задержку и экономит память. И наоборот, создание клиента для каждого запроса приводит к трате ресурсов на незадействованные пулы.

Класс Request представляет собой HTTP-запрос. Request.Builder позволяет установить параметры запроса, например url и заголовки.

В целом, HTTP-заголовки представляют собой что-то похожее на Map: каждое поле имеет одно значение или не имеет его вовсе. Однако некоторые заголовки могут иметь несколько значений. В связи с этим для добавления заголовка к запросу применяются два метода:

  • header(name, value) — устанавливает только одно значение заголовка name. При этом все существующие значения заголовка будут удалены, и после этого будет установлено новое значение.
  • addHeader(name, value) — добавляет заголовок без удаления уже имеющихся значений.

При чтении заголовка из ответа используйте header(name), чтобы вернуть последнее вхождение заголовка (зачастую это единственное вхождение). Если значение отсутствует, header(name) вернет null. Чтобы прочитать все значения заголовка в виде списка, используйте headers(name).

Для установки целевого URL-адреса запроса используется метод url(). По завершению настройки запроса используется метод build(), который возвращает объект Request.

Отправка запроса

newCall — метод класса OkHttpClient, который подготавливает запрос к выполнению в будущем. Принимает объект Request и возвращает объект Call.

Класс Call (вызов) — это запрос, который был подготовлен к выполнению. Вызов может быть отменен. Поскольку экземпляр класса представляет одну пару запрос/ответ, он не может быть выполнен дважды. Для выполнения запроса существуют два метода:

  • execute() — при синхронном вызове. Метод незамедлительно выполняет запрос и блокирует поток до тех пор, пока ответ не будет доступен для обработки или пока не возникнет ошибка.
  • enqueue() — при асинхронном вызове. Метод назначает запрос на выполнение в определенный момент в будущем. Диспетчер определяет, когда будет выполнен запрос: обычно сразу же, если в данный момент не выполняются несколько других запросов. Позже клиент получает объект responseCallback либо с HTTP-ответом, либо с исключением в случае возникновения ошибки.

Чтение ответа

Класс Response представляет HTTP-ответ. Тело ответа — свойство экземпляра класса, которое может быть использовано только один раз и затем закрыто. Все остальные свойства неизменяемы.

Прежде чем как-либо использовать тело ответа, необходимо проверить, был ли запрос к серверу успешен. Для этого существует метод isSuccessful() вышеупомянутого класса. Метод проверяет код состояния (status code) HTTP-ответа и возвращает значение true, если код находится в диапазоне 200-300. Если код находится за пределами этого диапазона, он возвращает значение false, указывающее, что запрос не был успешным.

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

Для получения тела ответа используется метод body() класса Response, который возвращает экземпляр класса ResponseBody.

ResponseBody — одноразовый поток от сервера к клиенту, содержащий тело ответа в виде необработанных байтов. Каждое тело ответа поддерживается активным подключением к веб-серверу.

Класс ResponseBody поддерживает потоковую передачу очень больших ответов. Например, его можно использовать для чтения ответа, размер которого превышает всю память, выделенную текущему процессу. Можно даже передавать в потоковом режиме ответ, объем которого превышает общий объем памяти на текущем устройстве, что является обычным требованием для стриминговых видео-приложений.

Класс не загружает весь ответ в память, поэтому тело ответа может быть считано только один раз. Для этого существует несколько методов:

  • bytes() и string() — считывают весь текст ответа в память, а затем возвращают его в виде массива байтов или строки соответственно. Методы следует использовать только для небольших ответов. При считывании больших ответов будет вызвана ошибка OutOfMemoryError.
  • source, byteStream, charStream — предназначены для потокового чтения ответа. Метод source возвращает объект BufferedSource, позволяющий читать тело ответа в виде потока байтов. byteStream работает аналогично, но возвращает объект InputStream. charStream — возвращает объект Reader, который позволяет читать тело ответа в виде потока символов.

Если использовать body() без упомянутых методов, то будет получен сам объект ResponseBody, с которым ничего особо не поделаешь.

Простой GET-запрос (синхронный/асинхронный)

Перед использованием библиотеки нужно добавить соответствующую зависимость в Gradle:

			implementation 'com.squareup.okhttp3:okhttp:4.10.0'
		

Номер последней версии можно посмотреть на Maven Central.

Синхронный запрос (Java):

			OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("Запрос к серверу не был успешен: " +
                response.code() + " " + response.message());
    }
    // пример получения конкретного заголовка ответа
    System.out.println("Server: " + response.header("Server"));
    // вывод тела ответа
    System.out.println(response.body().string());
} catch (IOException e) {
    System.out.println("Ошибка подключения: " + e);
}
		

Синхронный запрос (Kotlin):

			val client = OkHttpClient()

val request = Request.Builder()
    .url("https://publicobject.com/helloworld.txt")
    .build()

try {
    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) {
            throw IOException("Запрос к серверу не был успешен:" +
                    " ${response.code} ${response.message}")
        }
        // пример получения конкретного заголовка ответа
        println("Server: ${response.header("Server")}")
        // вывод тела ответа
        println(response.body!!.string())
    }
} catch (e: IOException) {
    println("Ошибка подключения: $e");
}
		

В то время как в Java используются методы объектов, в Kotlin иногда используются их свойства. Например, свойство body объекта Response.

Каждое тело ответа поддерживается ограниченным ресурсом. Поэтому после использования оно должно быть закрыто. Закрытие ресурса освобождает все системные средства, которые были выделены ресурсу, и делает его доступным для сбора мусора (garbage collection). Если не закрыть тело ответа, произойдет утечка ресурсов, что в конечном итоге может привести к замедлению или крашу приложения.

Для закрытия ресурса можно использовать метод close(), но предпочтительнее использовать блок try-with-resources (Java) и метод use (Kotlin). Обе конструкции выполняют блок кода относительно заданного ресурса, а затем корректно закрывают его, независимо от того, вызвано исключение или нет.

Асинхронный запрос (Java):

			OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        e.printStackTrace();
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        try (ResponseBody responseBody = response.body()) {
            if (!response.isSuccessful()) {
                throw new IOException("Запрос к серверу не был успешен: " +
                        response.code() + " " + response.message());
            }

            // пример получения всех заголовков ответа
            Headers responseHeaders = response.headers();
            for (int i = 0, size = responseHeaders.size(); i < size; i++) {
                // вывод заголовков
                System.out.println(responseHeaders.name(i) + ": "
                        + responseHeaders.value(i));
            }
            // вывод тела ответа
            System.out.println(responseBody.string());
        }
    }
});
		

Асинхронный запрос (Kotlin):

			val client = OkHttpClient()

val request = Request.Builder()
    .url("http://publicobject.com/helloworld.txt")
    .build()

client.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        e.printStackTrace()
    }

    override fun onResponse(call: Call, response: Response) {
        response.use {
            if (!response.isSuccessful) {
                throw IOException("Запрос к серверу не был успешен:" +
                        " ${response.code} ${response.message}")
            }
            // пример получения всех заголовков ответа
            for ((name, value) in response.headers) {
                println("$name: $value")
            }
            // вывод тела ответа
            println(response.body!!.string())
        }
    }
})
		

Асинхронный запрос выполняется в потоке Worker. Когда ответ доступен для чтения выполняется обратный вызов (сallback). Этот вызов выполнится после того, как будут готовы заголовки ответа. Чтение тела ответа все еще может блокировать поток. OkHttp в настоящее время не предлагает асинхронных API для получения тела ответа по частям.

Callback имеет два абстрактных метода:

  • onResponse — вызывается, когда HTTP-ответ был успешно получен от удаленного сервера.
  • onFailure — вызывается, когда запрос не может быть выполнен из-за проблем с подключением, тайм-аута или при его отмене. Поскольку в сети может произойти сбой во время соединения с сервером, возможен случай, когда удаленный сервер успевает принять запрос до сбоя.

Сериализация/десериализация

В данном пункте кратко рассмотрена сериализация и десериализация объектов (их преобразование в определённую последовательность байтов, которую можно передать по сети, и наоборот).

Для того, чтобы преобразовать объект в строку JSON или наоборот можно воспользоваться библиотеками Gson и/или Moshi.

Вкратце, если вам нужна проста использования и широкий набор функций, то выбираете Gson. Если нужна производительность и эффективное использование памяти, то лучшим выбором будет Moshi.

Рассмотрим пример сериализации с помощью Moshi (Java).

			// Создание объекта Moshi
Moshi moshi = new Moshi.Builder().build();
// Создание адаптера
JsonAdapter<SomeClass> jsonAdapterRequest =
        moshi.adapter(SomeClass.class);
// Сериализация, SomeClassInstance - экземляр класса SomeClass
String jsonRequest = jsonAdapterRequest.toJson(SomeClassInstance);
		

То же самое в Kotlin:

			// Создание объекта Moshi,
// для Kotlin необходимо добавлять KotlinJsonAdapterFactory
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory()).build()
// Создание адаптера
val jsonAdapterRequest = moshi.adapter(SomeClass::class.java)
// Сериализация, SomeClassInstance - экземляр класса SomeClass
val jsonRequest = jsonAdapterRequest.toJson(SomeClassInstance)
		

Для сериализации необходимо создать объект Moshi, адаптер и передать ему тип сериализуемого объекта. В данном случае это тип Class.

Если требуется сериализовать более сложный объект, например коллекцию, то тип можно передать двумя способами.

1) С помощью метода Types.newParameterizedType(), который создает новый параметризованный тип.

			// Java
JsonAdapter<List<SomeClass>> jsonAdapterRequest = moshi.adapter(
        Types.newParameterizedType(List.class, SomeClass.class)
);
		
			// Kotlin
val jsonAdapterRequest = moshi.adapter<List<SomeClass>>(
    Types.newParameterizedType(List::class.java, SomeClass::class.java)
		

2) С помощью класса TypeToken библиотеки Gson. Класс используется для передачи информации о типах во время выполнения программы. Конструктор класса возвращает представленный класс из заданного типа.

			// Java
JsonAdapter<List<SomeClass>> jsonAdapterRequest =
        moshi.adapter(new TypeToken<List<SomeClass>>(){}.getType());
		
			// Kotlin
val jsonAdapterRequest = moshi.adapter<List<SomeClass>>(
    object : TypeToken<List<SomeClass?>?>(){}.type
)
		

Разница способов состоит в том, что TypeToken более типобезопасен (typesafe), а Types.newParameterizedType более эффективен.

Десериализация осуществляется аналогичным образом.

			// Java

// Создание адаптера для десериализации
// Используется тот же объект Moshi, что и при сериализации
JsonAdapter<SomeClass> jsonAdapterResponse = 
        moshi.adapter(SomeClass.class);
// Десериализация
String jsonResponse = jsonAdapterResponse.fromJson(receivedData);
		
			// Kotlin

// Создание адаптера для десериализации
// Используется тот же объект Moshi, что и при сериализации
val jsonAdapterResponse = moshi.adapter(SomeClass::class.java)
// Десериализация
val jsonResponse = jsonAdapterResponse.fromJson(receivedData)
		

При сериализации/десериализации Moshi может вызывать разного рода исключения, к примеру если десериалируемая строка не является строкой JSON или если строка не соответствует объекту, в который её пытаются преобразовать.

Если серверная и клиентская часть настроены правильно, то такого не должно происходить. Но всё же рекомендуется оборачивать операции Moshi в блок try-catch.

Простой POST-запрос

Чтобы сделать POST-запрос, используется метод post() класса Request.Builder. Метод принимает RequestBody, который он добавляет к запросу.

POST-запрос в Java:

			MediaType JSON = MediaType.get("application/json; charset=utf-8");
String jsonRequest = "Some request";

OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(jsonRequest, JSON);
Request.Builder requestBuilder = new Request.Builder().url(serverUrl).post(body);
Request request = requestBuilder.build();

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("Запрос к серверу не был успешен: " +
                response.code() + " " + response.message());
    }
    System.out.println(response.body().string());
} catch (IOException e) {
    System.out.println("Ошибка подключения: " + e);
}
		

POST-запрос в Kotlin:

			val jsonRequest = "some request"
val JSON = "application/json; charset=utf-8".toMediaType()

val client = OkHttpClient()
val body: RequestBody = jsonRequest.toRequestBody(JSON)
val request = Request.Builder().url(serverUrl).post(body).build()

try {
    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) {
            throw IOException("Запрос к серверу не был успешен:" +
                    " ${response.code} ${response.message}")
        }
        println(response.body!!.string())
    }
} catch (e: IOException) {
    println("Ошибка подключения: $e")
}
		

Объект MediaType необходим для описания типа содержимого тела запроса или ответа. Обычно он используется для установки заголовка “Content-Type” в HTTP-запросе.

Чтобы получить объект MediaType можно использовать один из статических методов одноименного класса:

  • MediaType.parse(String) — создает новый экземпляр MediaType с указанным типом содержимого и кодировкой. Функция возвращает медиатип для строки, или null, если строка не является правильно сформированным медиатипом.
  • MediaType.get(String) — работает аналогично MediaType.parse, но если строка сформирована неправильно, то вызывает исключение IllegalArgumentException.

В Kotlin используется метод toMediaType() объекта String. Метод является аналогом MediaType.get(String).

RequestBody — класс, представляющий собой тело запроса. Экземпляр класса создаётся с помощью метода create.

RequestBody.create(MediaType, String) создает тело запроса с указанным содержимым и его типом. Метод имеет несколько реализаций. Содержимое можно передать в виде массива байтов, файла, строки или объекта okio.ByteString. Тип содержимого всегда указывается с помощью объекта MediaType. Этот объект также устанавливает заголовку “Content-type” соответствующее значение, поэтому вручную устанавливать этот заголовок не нужно.

Аналогом RequestBody.create(MediaType, String) в Kotlin является метод toRequestBody(MediaType?) объекта String.

Особенности работы с HTTPS

OkHttp пытается балансировать между двумя задачами:

  • Подключение к максимально возможному количеству хостов. Сюда входят как современные хосты, на которых используются последние версии boringssl, так и немного устаревшие хосты, на которых используются старые версии OpenSSL.
  • Безопасность соединения. Сюда входит проверка удаленного веб-сервера с помощью сертификатов и конфиденциальность данных, передаваемых с помощью надежных шифров.

При согласовании соединения с HTTPS-сервером OkHttp должен знать, какие предлагать версии TLS и наборы шифров. Для клиента, который хочет максимизировать возможность соединения с различными серверами, это будут устаревшие версии TLS и слабые по конструкции наборы шифров. Для клиента, который хочет максимизировать безопасность, это будут только последняя версия TLS и самые сильные наборы шифров.

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

  • RESTRICTED_TLS – безопасная конфигурация, предназначенная для удовлетворения более строгих требований по соответствию.
  • MODERN_TLS – безопасная конфигурация, позволяющая подключаться к современным HTTPS-серверам.
  • COMPATIBLE_TLS – безопасная конфигурация, которая подключается к безопасным, но менее современным серверам HTTPS.
  • CLEARTEXT – небезопасная конфигурация, которая используется для URL-адресов http://.

По умолчанию OkHttp будет пытаться установить соединение MODERN_TLS. Если соединение MODERN_TLS не удастся, okhttp3 переключится на другой тип соединения. Точный механизм отката зависит от конкретной реализации okhttp3 и конфигурации, установленной разработчиками.

Настроить конфигурацию можно следующим образом:

			OkHttpClient client = new OkHttpClient.Builder()
    .connectionSpecs(Arrays.asList(
        ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
    .build()
		

В официальной документации можно найти дополнительные способы работы с HTTPS, такие как создание собственной спецификации подключения, закрепление сертификата и настройка доверенных сертификатов.

Аутентификация на сервере

Аутентификацию на сервере можно реализовать двумя способами.

1) Вручную добавить заголовок аутентификации. Полезно в случае, если нужна аутентификация только для одного запроса. Для того, чтобы добавлять заголовок ко всем запросам клиента, можно создать перехватчик. Способ полезен, если у вас статический ключ API или токен, который нужно отправлять с каждым запросом.

			// Kotlin

val client = OkHttpClient().newBuilder().addInterceptor { chain ->
        val originalRequest = chain.request()

        val builder = originalRequest.newBuilder()
               .header("Authorization", Credentials.basic("username", "password"))
        val newRequest = builder.build()
        chain.proceed(newRequest)
    }.build()
		

2) Использовать интерфейс Authenticator — полезно, когда необходимо динамически аутентифицироваться или нужна дополнительная настройка процесса аутентификации.

Интерфейс позволяет выполнить либо предварительную аутентификацию перед подключением к серверу, либо реактивную аутентификацию после получения ответа от веб-сервера или прокси-сервера.

Рассмотрим пример реактивной аутентификации. В таком случае, если код состояния ответа равен 401 (Unauthorized), OkHttp посылает повторный запрос, включающий заголовок “Authorization”.

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

			// Java

OkHttpClient.Builder client = new OkHttpClient.Builder();
client.authenticator((route, response) -> {
    if (response.request().header("Authorization") != null) {
        return null; // Остановить попытки аутентификации,
        // т.к. у нас уже не получилось это сделать
    }
    String credential = Credentials.basic("name", "password");
    return response.request().newBuilder()
            .header("Authorization", credential).build();
});
		

Здесь метод authenticator с помощью лямбда-функции устанавливает экземпляр интерфейса Authenticator, который предоставляет механизм для проверки ответа от сервера и возвращает запрос, включающий в себя учетные данные клиента. Метод Credentials.basic используется для кодирования имени пользователя и пароля при базовой аутентификации.

Использование вместе с ViewModel

Простой асинхронный запрос в ViewModel можно сделать следующим образом.

			// MainViewModel.kt

private val _response = MutableLiveData<String>()
val response: LiveData<String> = _response

private val client = OkHttpClient()

fun getResponseFromServer() {

    val jsonRequest = "your request body"

    val body = jsonRequest.toRequestBody(JSON)
    val request = Request.Builder().url(BASE_URL).post(body).build()

    client.newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            Log.d(TAG, "Ошибка подключения: $e")
        }

        override fun onResponse(call: Call, response: Response) {
            response.use {
                if (!response.isSuccessful) {
                    throw Exception("Запрос к серверу не был успешен:" +
                            " ${response.code} ${response.message}")
                }
                _response.postValue(response.body!!.string())
            }
        }
    })
}
		

Метод postValue передаёт задачу по установке значения главному потоку. Если попытаться присвоить значение напрямую, то будет вызвано исключение java.lang.IllegalStateException: Cannot invoke setValue on a background thread.

Необходимо делать именно асинхронный запрос, чтобы не блокировать поток интерфейса и чтобы приложение оставалось отзывчивым. Либо можно самостоятельно настроить синхронный вызов в другом потоке.

Начиная с SDK 10 при попытке синхронного вызова в главном потоке будет вызвано исключение android.os.NetworkOnMainThreadException.

Чтобы сделать запрос в отдельном api-файле и передать ответ переменной из ViewModel можно воспользоваться механизмом callback.

			// SomeApiService.kt

interface RequestCallback {
    fun onSuccess(response: String)
    fun onFailure(error: String)
}

class SomeApiService () {

    private val client = OkHttpClient()

    fun makeRequest(callback: RequestCallback) {

        val jsonRequest = "your request body"

        val body = jsonRequest.toRequestBody(JSON)
        val request = Request.Builder().url(BASE_URL).post(body).build()

        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.d(TAG, "Ошибка подключения: $e")
                callback.onFailure(e.toString());
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    if (!response.isSuccessful) {
                        throw Exception("Запрос к серверу не был успешен:" +
                                " ${response.code} ${response.message}")
                    }
                    callback.onSuccess(response.body!!.string())
                }
            }
        })
    }
}

object SomeApi {
    val someService : SomeApiService by lazy {
        SomeApiService()
    }
}
		
			// MainViewModel.kt

private val _response = MutableLiveData<String>()
val response: LiveData<String> = _response

private val client = OkHttpClient()

fun getResponseFromApi() {
    SomeApi.someService.makeRequest(object : RequestCallback {
        override fun onSuccess(response: String) {
            _response.postValue(response)
        }

        override fun onFailure(error: String) {
            Log.d(TAG, "Ошибка подключения: $error")
        }
    })
}
		

В файле SomeApiService.kt находится интерфейс RequestCallback, класс SomeApiService с методом makeRequest, который делает запрос к серверу, и объект SomeApi, через который будет осуществляться доступ к экземпляру класса.

В MainViewModel.kt функция getResponseFromApi реализует интерфейс RequestCallback и передает его в качестве параметра методу makeRequest().

Представленный код можно обернуть во viewModelScope.launch {…}, чтобы запрос был отменён при очистке (разрушении) MainViewModel.

Заключение

OkHttp — гибкая библиотека, выступающая в роли HTTP-клиента.

В отличии Retrofit настраивать клиент, писать запросы и обрабатывать ответы необходимо вручную. Это одновременно и преимущество и недостаток OkHttp. Недостаток заключается в необходимости писать много шаблонного кода. Преимущество — возможность кастомизировать соединение.

Из-за слабой кастомизируемости в некоторых случаях Retrofit может не подойти, и без OkHttp не обойтись. Также благодаря кастомизации OkHttp можно повысить производительность и уменьшить использование памяти.

Полезные ресурсы: Подробнее про Authenticator; Различные примеры использования библиотеки; Перехватчики (Interceptors).

Следите за новыми постами
Следите за новыми постами по любимым темам
4К открытий6К показов