Що ви опануєте
Ви навчитеся розділяти UI, стан екрана й джерело даних; пояснювати ViewModel, coroutine та repository; обробляти Loading, Content і Error. Приклад — завантаження навчального переліку з HTTPS API. Рішення має враховувати відсутність мережі, повторення й скасування без блокування головного потоку.
Відповідальності
UI відображає стан і передає події. ViewModel зберігає UI-related state у визначеному scope та координує сценарії. Repository надає контракт отримання даних і приховує конкретне джерело. DTO, data transfer object, представляє формат відповіді API; предметна модель може відрізнятися. Розділення допомагає перевірити логіку без реального network call.
ViewModel не повинен тримати Activity або довгоживуче посилання на UI, оскільки це може подовжити життя об’єктів. Зміна конфігурації не повинна автоматично створювати зайвий запит, якщо стан уже завантажений. Для process death потрібне відновлення збережених параметрів і даних, а не припущення безсмертності ViewModel.
Модель стану
sealed interface TopicsState {
data object Loading : TopicsState
data class Content(val titles: List<String>) : TopicsState
data class Error(val message: String) : TopicsState
}
Sealed interface обмежує визначений набір реалізацій і допомагає compiler перевірити when. Data object є singleton станом без додаткових полів. Content і Error містять потрібні дані. Порожній успішний список відрізняється від технічної помилки; UI повинен показувати ці результати за різними правилами.
Coroutine та suspend
Coroutine — організоване обчислення, яке може призупинятися. suspend дозволяє функції брати участь у такій процедурі; це не гарантує виконання поза main. viewModelScope керує coroutine життям ViewModel. withContext(Dispatchers.IO) переносить блокувальне I/O до відповідного dispatcher, але саме блокувальне API все ще потребує timeouts і визначеної реакції на cancellation.
interface TopicsRepository {
suspend fun loadTitles(): List<String>
}
class TopicsViewModel(private val repository: TopicsRepository) : ViewModel() {
var state by mutableStateOf<TopicsState>(TopicsState.Loading)
private set
fun refresh() {
viewModelScope.launch {
state = TopicsState.Loading
try {
state = TopicsState.Content(repository.loadTitles())
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: Exception) {
state = TopicsState.Error("Не вдалося завантажити дані")
}
}
}
}
Фрагмент потребує lifecycle ViewModel/viewModelScope, Compose state delegates та kotlinx.coroutines imports. UI читає observable state. CancellationException передається далі, щоб скасування не перетворилося на звичайний Error. Production refresh потребує також політики одночасних запитів: скасування попереднього або заборони повторного запуску. Інакше повільна стара відповідь може замінити новішу.
HTTPS, permissions та відповіді
Для мережі потрібна декларація INTERNET у manifest. Це normal permission, який не потребує runtime діалогу. Відповідь HTTP має status code; лише погоджені успішні статуси дозволяють обробляти body як потрібні дані. Потрібно перевіряти timeouts, формат і обов’язкові поля. TLS перевірку не вимикають для обходу помилок.
Дані API є зовнішнім входом. Null або відсутнє поле мають предметну реакцію. Технічний stack trace не слід показувати як повідомлення користувачу; для налагодження залишають потрібний безпечний журнал. Не записуйте токени чи персональні відповіді в Logcat. API ключі, вбудовані в клієнт, не є захищеним server secret.
Flow і життєвий цикл
Flow передає послідовність значень асинхронно. StateFlow утримує поточний стан і повідомляє про зміни. Для Compose на Android collectAsStateWithLifecycle виконує lifecycle-aware збір. Це підходить для repository з локальним джерелом та оновленнями. MutableState у наведеному короткому прикладі є простішим observable holder для одного сценарію.
Не створюйте два незалежні джерела для одного екранного стану без правила синхронізації. Визначте, чи UI показує кешовані дані під час Loading. Offline-first організація часто використовує локальне сховище як джерело читання й sync як окремий процес. Вона потребує правил конфліктів, актуальності й повторення, а не тільки прапорця «немає інтернету».
Помилки й повторення
Повтор має відповідати операції. Читання зазвичай можна повторити за тим самим контрактом, а створення запису може потребувати idempotency key. Не додавайте нескінченний retry без затримки. Для Error дайте зрозумілу причину на потрібному рівні та дію повтору. Після скасування не показуйте стару відповідь для вже іншого об’єкта.
Типові помилки та практика
Помилки: network на main, repository викликається в composable тілі, catch поглинає cancellation, автоматичний повтор створює дубль й ViewModel зберігає Activity. Перевіряйте логіку fake repository: успіх, порожньо, помилка, затримка й скасування. Реальний API перевіряє окремо транспортний контракт.
Практичний аналіз
Опишіть refresh із двома натисканнями, де перша відповідь повільніша. Який результат має залишитися? Назвіть політику скасування. Для offline режиму визначте, звідки UI читає дані й як показує їхню актуальність. Рішення має містити перевірку кожного стану.
Повний цикл отримання даних
Repository приховує спосіб отримання даних і повертає предметний результат. ViewModel координує операцію та публікує стан для UI. Стан Loading, Success або Error допомагає зробити всі гілки явними. Success з порожнім списком може бути допустимим результатом і потребувати власного повідомлення, а не автоматичного переходу в Error.
Після натискання повторного завантаження визначте, що відбувається з попереднім запитом: його скасовують, ігнорують повтор або дозволяють паралельність з контролем актуальності. Якщо старий запит завершується після нового, він може перезаписати свіжий стан. Для відповідної політики використовують Job, ідентифікатор запиту або оператори потоку; рішення залежить від сценарію.
HTTP-успіх описує транспортний результат. Тіло відповіді ще потребує перевірки структури й допустимості полів. Код 200 з відсутнім title не задовольняє контракт нашого екрану. Таймаут, відсутність мережі та некоректний JSON повинні завершувати операцію визначеним станом без нескінченного індикатора. Детальну технічну інформацію зберігають у діагностиці, а користувач отримує зрозумілу дію.
CancellationException слід передавати далі, щоб зберегти механізм скасування корутин. Перехоплення всіх Exception із перетворенням на Error може помилково показати відмову при звичайному завершенні екрана. Для даних з обмеженим доступом також визначте політику збереження та очищення кешу. У власному сценарії намалюйте шлях запит → перевірка → стан і покажіть, де обробляється кожна відмова.
Схема процесу
UI надсилає refresh.
Самоперевірка
Як обґрунтувати відповідь своїми словами?
Підсумок
Архітектура Android визначає джерело стану, контракт даних і lifecycle операцій. Suspend не замінює керування блокувальним I/O. Timeouts, cancellation та політика повторення потрібні для передбачуваної роботи з API.