OkHttp (extend)
OkHttp — це бібліотека та водночас HTTP‑клієнт із відкритим кодом для Java та Kotlin, розроблений компанією Square, яка також створила Retrofit.
OkHttp надає простий та легкий у використанні API для виконання HTTP‑запитів, включаючи підтримку протоколів HTTP/1.1 і HTTP/2. Бібліотека підтримує всі стандартні методи HTTP, може обробляти кілька одночасних запитів та пропонує розширені можливості: кешування запитів/відповідей, об'єднання підключень у пул (connection pooling), аутентифікацію тощо.
Зміст:
- Переваги OkHttp
- Основні класи та методи
- Простий GET‑запит (синхронний/асинхронний)
- Серіалізація/десеріалізація
- Простий POST‑запит
- Особливості роботи з HTTPS
- Аутентифікація на сервері
- Використання разом із ViewModel
Переваги OkHttp
OkHttp — це бібліотека нижчого рівня порівняно з Retrofit. Це означає, що HTTP‑запити, автоматизовані у Retrofit за допомогою анотацій, доведеться писати вручну. Проте саме в цьому і полягає головна перевага бібліотеки: вона надає ширший функціонал та налаштування з'єднань, що може підвищити продуктивність та зменшити використання пам'яті. До речі, Retrofit "під капотом" використовує OkHttp.
Бібліотека створена як легка та ефективна, з акцентом на зниження затримок і підвищення працездатності. Це досягається завдяки застосуванню різних методів оптимізації, таких як повторне використання з'єднань, стиснення та конвеєризація.
Переваги OkHttp:
- Гнучкість: Бібліотека надає більше контролю над процесом мережевої взаємодії завдяки додатковим функціям, наприклад, кастомній обробці запитів і відповідей.
- Легкість: OkHttp — компактніша бібліотека порівняно з Retrofit, що дозволяє мінімізувати обсяг використовуваної додатком пам'яті.
- Кешування: Бібліотека має вбудовану підтримку HTTP‑кешування, що може підвищити продуктивність і знизити навантаження на мережу.
- Аутентифікація: OkHttp надає гнучкий і розширюваний API для аутентифікації, що спрощує реалізацію різних її моделей.
- Перехоплювачі (Interceptors): Це механізм, який дозволяє легко налаштовувати запити та відповіді, а також є чудовим вибором для додатків, що потребують розширеної обробки запитів.
- WebSockets: OkHttp забезпечує вбудовану підтримку WebSockets, що дозволяє легко реалізувати комунікацію з сервером у режимі реального часу.
Основні класи та методи
1) Налаштування клієнта та запиту
Клас OkHttpClient — це клієнт для HTTP‑викликів, який використовується для відправки запитів і читання відповідей.
- OkHttpClient.Builder — клас, що надає методи для налаштування клієнта (кеш, аутентифікація, перехоплювачі, тайм‑аути тощо). Завершується налаштування викликом методу
build(), який повертає екземпляр класу OkHttpClient.
Рекомендація: створюйте один екземпляр OkHttpClient і використовуйте його повторно для всіх HTTP‑викликів. Це дозволяє зменшити затримки та економити пам'ять завдяки повторному використанню пулів з'єднань і потоків. Створення клієнта для кожного запиту призводить до марнування ресурсів.
Клас Request представляє HTTP‑запит.
- Request.Builder дозволяє встановити параметри запиту, такі як
urlта заголовки.
Робота із заголовками:
header(name, value)— встановлює одне значення для заголовка, видаляючи всі існуючі.addHeader(name, value)— додає нове значення заголовка, не видаляючи існуючі.
Для читання заголовків із відповіді:
header(name)— повертає останнє значення заголовка абоnull, якщо значення відсутнє.headers(name)— повертає всі значення заголовка у вигляді списку.
Для встановлення цільового URL використовується метод url(). Завершення налаштування запиту виконується викликом методу build().
2) Відправка запиту
- Метод newCall класу OkHttpClient готує запит до виконання. Приймає об'єкт Request і повертає об'єкт Call.
Клас Call представляє запит, готовий до виконання. Він не може бути виконаний двічі та підтримує скасування.
Методи для виконання запиту:
execute()— синхронний виклик, блокує потік до отримання відповіді або виникнення помилки.enqueue()— асинхронний виклик, виконує запит у майбутньому, повертає responseCallback із відповіддю або винятком у разі помилки.
3) Читання відповіді
Клас Response представляє HTTP‑відповідь. Тіло відповіді є одноразовим і має бути закрите після використання. Усі інші властивості незмінні.
Перед обробкою тіла необхідно перевірити успішність запиту:
- Метод isSuccessful() перевіряє код статусу HTTP‑відповіді. Повертає
true, якщо код у діапазоні 200-300, інакше —false.
Для отримання тіла відповіді використовується метод body(), який повертає об'єкт ResponseBody.
Робота з ResponseBody
ResponseBody — це одноразовий потік даних від сервера до клієнта. Підтримує передавання великих відповідей, наприклад, потокового відео.
Методи для роботи з тілом відповіді:
bytes(),string()— читають текст відповіді в пам'ять і повертають у вигляді масиву байтів або строки. Підходять для малих відповідей.source,byteStream,charStream— використовуються для потокового читання:source— повертає BufferedSource для роботи з потоком байтів.byteStream— повертає InputStream.charStream— повертає Reader для роботи з потоком символів.
Якщо використати body() без вказаних методів, буде повернуто сам об'єкт ResponseBody, з яким нічого не можна зробити.
Простий GET-запит (синхронний/асинхронний)
Перед використанням бібліотеки потрібно додати відповідну залежність у ini:
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. Коли відповідь доступна для читання, виконується зворотний виклик (callback). Цей виклик відбудеться після того, як будуть готові заголовки відповіді. Читання тіла відповіді все ще може блокувати потік. OkHttp наразі не пропонує асинхронних API для отримання тіла відповіді по частинах.
Callback має два абстрактні методи:
onResponseВикликається, коли HTTP-відповідь була успішно отримана від віддаленого сервера.onFailureВикликається, коли запит не може бути виконаний через проблеми з підключенням, тайм-аут або його скасування. Оскільки в мережі може статися збої під час з'єднання з сервером, можливий випадок, коли віддалений сервер встигає прийняти запит до збою.
Серіалізація/десеріалізація
У цьому пункті коротко розглянуто серіалізацію та десеріалізацію об'єктів (їх перетворення в певну послідовність байтів, яку можна передати по мережі, і навпаки).
Для того, щоб перетворити об'єкт у рядок JSON або навпаки, можна скористатися бібліотеками Gson і/або Moshi.
Коротко, якщо вам потрібна проста у використанні бібліотека та широкий набір функцій, то вибирайте Gson. Якщо потрібна продуктивність та ефективне використання пам'яті, то найкращим вибором буде Moshi.
Розглянемо приклад серіалізації за допомогою Moshi (Java):
Moshi moshi = new Moshi.Builder().build();
// Створення адаптера
JsonAdapter<SomeClass> jsonAdapterRequest =
moshi.adapter(SomeClass.class);
// Серіалізація, SomeClassInstance - екземпляр класу SomeClass
String jsonRequest = jsonAdapterRequest.toJson(SomeClassInstance);
Те ж саме в Kotlin:
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory()).build()
// Створення адаптера
val jsonAdapterRequest = moshi.adapter(SomeClass::class.java)
// Серіалізація, SomeClassInstance - екземпляр класу SomeClass
val jsonRequest = jsonAdapterRequest.toJson(SomeClassInstance)
Для серіалізації необхідно створити об'єкт Moshi, адаптер і передати йому тип серіалізованого об'єкта. У цьому випадку це тип Class.
Якщо потрібно серіалізувати складніший об'єкт, наприклад, колекцію, тип можна передати двома способами.
- За допомогою методу
Types.newParameterizedType(), який створює новий параметризований тип.
JsonAdapter<List<SomeClass>> jsonAdapterRequest = moshi.adapter(
Types.newParameterizedType(List.class, SomeClass.class)
);
- За допомогою класу
TypeTokenбібліотеки Gson. Клас використовується для передачі інформації про типи під час виконання програми. Конструктор класу повертає представлений клас із заданого типу.
JsonAdapter<List<SomeClass>> jsonAdapterRequest =
moshi.adapter(new TypeToken<List<SomeClass>>(){}.getType());
Різниця між способами полягає в тому, що TypeToken є більш типобезпечним (typesafe), а Types.newParameterizedType більш ефективним.
Десеріалізація здійснюється аналогічним чином.
Java:
JsonAdapter<SomeClass> jsonAdapterResponse =
moshi.adapter(SomeClass.class);
// Десеріалізація
String jsonResponse = jsonAdapterResponse.fromJson(receivedData);
Kotlin:
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, такі як створення власної специфікації підключення, закріплення сертифіката та налаштування довірених сертифікатів.
Аутентифікація на сервері
Аутентифікацію на сервері можна реалізувати двома способами:
- Вручну додати заголовок аутентифікації. Корисно, коли потрібна аутентифікація тільки для одного запиту. Для того, щоб додавати заголовок до всіх запитів клієнта, можна створити перехоплювач. Цей спосіб корисний, якщо у вас статичний ключ API або токен, який потрібно надсилати з кожним запитом.
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()
- Використовувати інтерфейс Authenticator — корисно, коли необхідно динамічно аутентифікуватися або потрібна додаткова настройка процесу аутентифікації.
Інтерфейс дозволяє виконати або попередню аутентифікацію перед підключенням до сервера, або реактивну аутентифікацію після отримання відповіді від веб-сервера або проксі-сервера.
Розглянемо приклад реактивної аутентифікації. У такому випадку, якщо код стану відповіді рівний 401 (Unauthorized), OkHttp надсилає повторний запит, що містить заголовок "Authorization".
При цьому важливо зробити перевірку, чи була в початковому запиті спроба аутентифікації. Якщо так, то, ймовірно, подальші спроби будуть марними, і аутентифікатор має відмовитися від них.
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
MainViewModel.java
import android.util.Log;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
public class MainViewModel extends ViewModel {
private static final String TAG = "MainViewModel";
private static final String BASE_URL = "https://your-api-url.com"; // Замість цього вставте ваш URL
private static final okhttp3.MediaType JSON = okhttp3.MediaType.parse("application/json; charset=utf-8");
private final MutableLiveData<String> _response = new MutableLiveData<>();
public LiveData<String> response = _response;
private final OkHttpClient client = new OkHttpClient();
public void getResponseFromServer() {
String jsonRequest = "your request body"; // Тіло вашого запиту
RequestBody body = RequestBody.create(jsonRequest, JSON);
Request request = new Request.Builder().url(BASE_URL).post(body).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d(TAG, "Ошибка подключения: " + e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
throw new IOException("Запрос к серверу не был успешен: " +
response.code() + " " + response.message());
}
_response.postValue(response.body().string());
}
});
}
}
SomeApiService.java
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
public class SomeApiService {
private static final String BASE_URL = "https://your-api-url.com"; // Замість цього вставте ваш URL
private static final okhttp3.MediaType JSON = okhttp3.MediaType.parse("application/json; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public interface RequestCallback {
void onSuccess(String response);
void onFailure(String error);
}
public void makeRequest(final RequestCallback callback) {
String jsonRequest = "your request body"; // Тіло вашого запиту
RequestBody body = RequestBody.create(jsonRequest, JSON);
Request request = new Request.Builder().url(BASE_URL).post(body).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
callback.onFailure(e.toString());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
callback.onFailure("Запрос к серверу не был успешен: " +
response.code() + " " + response.message());
} else {
callback.onSuccess(response.body().string());
}
}
});
}
}
SomeApi.java
public class SomeApi {
public static final SomeApiService someService = new SomeApiService();
}
MainViewModel.java (Оновлений для використання SomeApi)
import android.util.Log;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class MainViewModel extends ViewModel {
private static final String TAG = "MainViewModel";
private final MutableLiveData<String> _response = new MutableLiveData<>();
public LiveData<String> response = _response;
public void getResponseFromApi() {
SomeApi.someService.makeRequest(new SomeApiService.RequestCallback() {
@Override
public void onSuccess(String response) {
_response.postValue(response);
}
@Override
public void onFailure(String error) {
Log.d(TAG, "Ошибка подключения: " + error);
}
});
}
}
Висновок
OkHttp - гнучка бібліотека, що виступає в ролі HTTP-клієнта.
На відміну від Retrofit налаштовувати клієнт, писати запити й обробляти відповіді необхідно вручну. Це одночасно і перевага, і недолік OkHttp. Недолік полягає в необхідності писати багато шаблонного коду. Перевага - можливість кастомізувати з'єднання.
Через слабку кастомізованість у деяких випадках Retrofit може не підійти, і без OkHttp не обійтися. Також завдяки кастомізації OkHttp можна підвищити продуктивність і зменшити використання пам'яті.
Приклад okHttp з MVVM
Цей приклад демонструє, як використовувати MVVM, OkHttp, Moshi, ViewBinding і Android Data Binding для отримання, додавання та видалення постів з JSONPlaceholder.
dependencies {
// AndroidX (ViewBinding, DataBinding, RecyclerView)
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.databinding:viewbinding:7.4.2'
// Moshi (для парсингу JSON)
implementation 'com.squareup.moshi:moshi:1.13.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
// OkHttp (для HTTP запитів)
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
// ViewModel & LiveData
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
// AndroidX Lifecycle (для використання ViewModel)
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
}
android {
...
viewBinding {
enabled = true
}
dataBinding {
enabled = true
}
}
1. Модель даних (Post.java)
import com.squareup.moshi.Json;
public class Post {
@Json(name = "id")
private int id;
@Json(name = "title")
private String title;
@Json(name = "body")
private String body;
// Геттери та сеттери
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
2. ViewModel (PostViewModel.java)
import android.util.Log;
import androidx.databinding.ObservableArrayList;
import androidx.lifecycle.ViewModel;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import java.io.IOException;
import java.util.List;
public class PostViewModel extends ViewModel {
private static final String TAG = "PostViewModel";
private static final String BASE_URL = "https://jsonplaceholder.typicode.com/posts";
private static final String ADD_URL = "https://jsonplaceholder.typicode.com/posts";
private final ObservableArrayList<Post> posts = new ObservableArrayList<>();
private final OkHttpClient client = new OkHttpClient();
private final Moshi moshi = new Moshi.Builder().build();
private final Types type = Types.newParameterizedType(List.class, Post.class);
// Метод для завантаження постів
public ObservableArrayList<Post> getPosts() {
loadPosts();
return posts;
}
private void loadPosts() {
Request request = new Request.Builder().url(BASE_URL).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "Error fetching posts: ", e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
Log.e(TAG, "Request failed with code: " + response.code());
return;
}
String jsonResponse = response.body().string();
List<Post> postList = moshi.adapter(type).fromJson(jsonResponse);
if (postList != null) {
posts.clear();
posts.addAll(postList); // Оновлюємо ObservableArrayList
}
}
});
}
// Метод для додавання нового поста
public void addPost(Post newPost) {
Request request = new Request.Builder()
.url(ADD_URL)
.post(okhttp3.RequestBody.create(
moshi.adapter(Post.class).toJson(newPost).getBytes()))
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "Error adding post: ", e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
Log.e(TAG, "Request failed with code: " + response.code());
return;
}
// Додано новий пост
posts.add(newPost);
}
});
}
// Метод для видалення поста
public void deletePost(Post post) {
Request request = new Request.Builder()
.url(BASE_URL + "/" + post.getId())
.delete()
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "Error deleting post: ", e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
Log.e(TAG, "Request failed with code: " + response.code());
return;
}
// Видалено пост
posts.remove(post);
}
});
}
}
3. Activity (MainActivity.java)
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.mvvmexample.databinding.ActivityMainBinding;
import androidx.databinding.DataBindingUtil;
public class MainActivity extends AppCompatActivity {
private PostViewModel postViewModel;
private PostAdapter postAdapter;
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
postViewModel = new ViewModelProvider(this).get(PostViewModel.class);
postAdapter = new PostAdapter(postViewModel);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerView.setAdapter(postAdapter);
// Спостерігаємо за ObservableArrayList
postViewModel.getPosts().addOnListChangedCallback(new ObservableArrayList.OnListChangedCallback<ObservableArrayList<Post>>() {
@Override
public void onChanged(ObservableArrayList<Post> sender) {
postAdapter.notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(ObservableArrayList<Post> sender, int positionStart, int itemCount) {
postAdapter.notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeInserted(ObservableArrayList<Post> sender, int positionStart, int itemCount) {
postAdapter.notifyItemRangeInserted(positionStart, itemCount);
}
@Override
public void onItemRangeMoved(ObservableArrayList<Post> sender, int fromPosition, int toPosition, int itemCount) {
}
@Override
public void onItemRangeRemoved(ObservableArrayList<Post> sender, int positionStart, int itemCount) {
postAdapter.notifyItemRangeRemoved(positionStart, itemCount);
}
});
// Завантажуємо пости при запуску
postViewModel.getPosts();
binding.addPostButton.setOnClickListener(v -> {
Post newPost = new Post();
newPost.setTitle("New Post");
newPost.setBody("This is a new post");
postViewModel.addPost(newPost);
});
}
}
4. Адаптер для RecyclerView (PostAdapter.java)
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.example.mvvmexample.R;
import com.example.mvvmexample.databinding.ItemPostBinding;
public class PostAdapter extends RecyclerView.Adapter<PostAdapter.PostViewHolder> {
private final PostViewModel postViewModel;
public PostAdapter(PostViewModel postViewModel) {
this.postViewModel = postViewModel;
}
@Override
public PostViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ItemPostBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(parent.getContext()),
R.layout.item_post, parent, false);
return new PostViewHolder(binding);
}
@Override
public void onBindViewHolder(PostViewHolder holder, int position) {
Post post = postViewModel.getPosts().get(position);
holder.bind(post);
}
@Override
public int getItemCount() {
return postViewModel.getPosts().size();
}
public static class PostViewHolder extends RecyclerView.ViewHolder {
private final ItemPostBinding binding;
public PostViewHolder(ItemPostBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(Post post) {
binding.setPost(post);
binding.setViewModel(postViewModel);
binding.executePendingBindings();
}
}
}
5. Layout для Activity (activity_main.xml)
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/addPostButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add Post" />
</LinearLayout>
</layout>
6. Layout для елемента списку (item_post.xml)
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{post.title}" />
<TextView
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{post.body}" />
<Button
android:id="@+id/deleteButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete"
android:onClick="@{() -> viewModel.deletePost(post)}" />
</LinearLayout>
</layout>
Цей код включає:
- Заміна на
ObservableArrayListдля підтримки спостереження за змінами. - Використання Moshi для парсингу JSON.
- Використання ViewBinding для доступу до елементів інтерфейсу.
- Додавання нових постів через OkHttp.
- Видалення постів через OkHttp.
- Використання Android Data Binding для прив'язки даних.
- Зміст:
- Переваги OkHttp
- Основні класи та методи
- Простий GET-запит (синхронний/асинхронний)
- Синхронний запит (Java):
- Синхронний запит (Kotlin):
- Асинхронний запит (Java):
- Асинхронний запит (Kotlin):
- Callback має два абстрактні методи:
- Серіалізація/десеріалізація
- Простий POST-запит
- Особливості роботи з HTTPS
- Аутентифікація на сервері
- Приклад з ViewModel
- Висновок
- Приклад okHttp з MVVM
- 1. Модель даних (Post.java)
- 2. ViewModel (PostViewModel.java)
- 3. Activity (MainActivity.java)
- 4. Адаптер для RecyclerView (PostAdapter.java)
- 5. Layout для Activity (activity_main.xml)
- 6. Layout для елемента списку (item_post.xml)