Мобильная разработка
Android Architecture Components
Google I/O 2017: архитектура по умолчанию
До 2017 года Android-разработчики были в ситуации iOS до SwiftUI: фреймворк не диктовал архитектуру. MVP, MVVM, MVI, Clean Architecture - каждая команда изобретала своё. Google в мае 2017 выпустил Android Architecture Components как часть Architecture Guidance. Диего Торрес Романо и команда Android изучили тысячи приложений в Play Store и выявили повторяющиеся паттерны. Вместо нового API - стандартизация существующих практик. ViewModel пережил поворот экрана, Room заменил SQLiteOpenHelper, LiveData принесла lifecycle-awareness. В 2021 году Jetpack Compose переосмыслил весь UI-слой, но Architecture Components остались фундаментом.
Пользователь заполняет форму из 15 полей. Поворачивает телефон. Форма пуста. Всё с начала. До 2017 года это была нормой для Android-разработки. Данные умирали при повороте экрана - Activity пересоздавалась, и всё что было в полях, в переменных, в результатах сетевых запросов - исчезало. Миллионы строк boilerplate для onSaveInstanceState, сотни тысяч приложений с одним и тем же костылем. Google признал это публично на I/O 2017 и выпустил ViewModel. Два года спустя - Room и WorkManager. Architecture Components изменили не то как выглядит Android-код. Они изменили то, как он думает о данных.
- Gmail для Android: ViewModel хранит черновик письма при повороте экрана и при смене вкладок
- Google Photos: WorkManager для фоновой загрузки фото - гарантирует загрузку даже после перезагрузки телефона
- WhatsApp: Room как локальная база сообщений - 100M+ сообщений хранятся без единой строки сырого SQL
- Uber: WorkManager для отправки GPS-трека водителя при нестабильном соединении - eventually consistent delivery
ViewModel и StateFlow: данные, которые переживают поворот экрана
2017 год, Google I/O. Разработчики Android решают одну и ту же задачу миллионы раз: пользователь поворачивает телефон - Activity пересоздаётся, сетевой запрос улетает заново, форма очищается. Хранить данные в `onSaveInstanceState`? Bundle принимает только примитивы. Сериализовывать в JSON вручную? Каждый делал свой костыль. Google посмотрел на кодовые базы тысяч приложений и выпустил Architecture Components - не новый API, а формализацию того, что все делали неправильно.
ViewModel живёт дольше, чем Activity. Это не магия - это отдельный scoped lifecycle, привязанный к ViewModelStore, который Activity не уничтожает при смене конфигурации. При повороте экрана система вызывает `onDestroy()` на старой Activity и создаёт новую - но ViewModelStore переходит неизменным. ViewModel получает вызов `onCleared()` только когда пользователь окончательно покидает экран.
Данные внутри ViewModel нужно как-то передавать во View. LiveData - первоначальное решение Google: observable-обёртка, lifecycle-aware. Подписка автоматически снимается при `onStop()` и восстанавливается при `onStart()`. Но в 2021 году команда Kotlin выпустила StateFlow в стабильную версию - и с тех пор Google рекомендует StateFlow для новых проектов. LiveData - для legacy.
`viewModelScope` - это `CoroutineScope`, привязанный к lifecycle ViewModel. При `onCleared()` все корутины автоматически отменяются. Никаких утечек, никаких "job already completed" в логах. `repeatOnLifecycle(STARTED)` на стороне View гарантирует, что подписка на StateFlow не активна когда экран в фоне - это экономит батарею.
Паттерн `private _state: MutableStateFlow` + `public state: StateFlow` - не условность. View не должна иметь возможность напрямую вызвать `_state.value = ...`. Это то же разделение, что в Redux: Actions - единственный путь изменения состояния, прямая мутация запрещена. Без этого однонаправленный поток превращается в спагетти.
Почему ViewModel не уничтожается при повороте экрана?
Room: SQLite без boilerplate и без SQL-инъекций
До Room Android-разработчики писали `SQLiteOpenHelper` - 200 строк кода для одной таблицы. Cursor, ContentValues, rawQuery со строками без проверки типов. SQL-инъекция при неосторожной конкатенации. Миграции - это отдельный ручной SQL в `onUpgrade`. Google посмотрел на то, как Hibernate и JPA упростили Java backend, и сделал Room: annotation processor, генерирующий весь boilerplate во время компиляции.
Архитектура Room состоит из трёх слоёв. Entity - data class с `@Entity`, маппится на таблицу. DAO (Data Access Object) - интерфейс с `@Query`, `@Insert`, `@Update`, `@Delete`. Database - абстрактный класс с `@Database`, синглтон. Annotation processor на этапе `./gradlew build` генерирует реализацию DAO: валидирует SQL-запросы статически, проверяет типы, добавляет prepared statements.
Миграции в Room - явный SQL с версией. При несовпадении версии без зарегистрированной миграции Room бросает исключение (или падает с `IllegalStateException` в debug). Это лучше, чем молчаливая потеря данных. `exportSchema = true` сохраняет JSON-снимок схемы в git - история схемы читаема как changelog.
Room и ORM - это не одно и то же. Hibernate - полноценный ORM с lazy loading, entity relationships, session management, N+1 проблемой. Room - более легковесный маппинг без lazy loading. `@Relation` в Room - всегда eager. Это осознанный выбор: мобильные базы данных малы, а предсказуемость важнее гибкости.
Что происходит если в Room DAO написать SQL с синтаксической ошибкой?
WorkManager: фоновые задачи, которые гарантированно выполнятся
Задача: отправить аналитику после закрытия приложения. Загрузить фото пользователя на сервер когда появится Wi-Fi. Обновить кэш каждые 6 часов. Android до 2018 года: AsyncTask, IntentService, JobScheduler, Firebase JobDispatcher, GCMNetworkManager - каждый API работает по-разному на разных версиях Android и разных вендорах. Samsung убивает фоновые процессы агрессивнее, чем чистый AOSP. Xiaomi требует отдельных разрешений для автозапуска. WorkManager стал единым API поверх всех платформенных механизмов.
WorkManager гарантирует выполнение задачи даже если приложение закрыто или устройство перезагрузилось - при условии что задача соответствует ограничениям (Constraints). Внутри WorkManager выбирает механизм сам: JobScheduler на Android 6+, AlarmManager + BroadcastReceiver на более старых. Задачи персистируются в Room (именно Room - Architecture Components используют друг друга) и восстанавливаются после перезагрузки.
Periodic work - для задач по расписанию. Минимальный интервал 15 минут - ограничение Android Doze Mode. Система может отложить выполнение, но в конечном итоге задача выполнится. Это не cron с точностью до секунды - это "eventually consistent" планировщик, оптимизированный под батарею.
WorkManager - это не замена Kotlin Coroutines или RxJava. Coroutines - для асинхронного кода внутри живого приложения. WorkManager - для задач, которые должны выполниться независимо от жизненного цикла приложения. `CoroutineWorker` объединяет оба: WorkManager управляет гарантией выполнения, Coroutines - асинхронностью внутри Worker.
WorkManager - замена Retrofit или OkHttp для сетевых запросов
WorkManager - планировщик с гарантией выполнения. Retrofit делает запрос прямо сейчас. WorkManager планирует задачу на будущее с условиями и retry-логикой. Внутри Worker можно использовать Retrofit
Смешение слоёв приводит к неправильной архитектуре: сетевой код в Worker-е оправдан только если нужна гарантия "доставки" при закрытом приложении. Для обычных запросов в живом приложении - viewModelScope.launch + Retrofit
Приложение загружает фото пользователя на сервер. Пользователь закрывает приложение в середине загрузки. Что произойдёт с задачей в WorkManager?
Ключевые идеи
- **ViewModel** переживает поворот экрана через ViewModelStore - данные не теряются при configuration change
- **StateFlow vs LiveData**: StateFlow - современный стандарт для новых проектов, LiveData - legacy. Паттерн `_private MutableStateFlow` + `public StateFlow` обеспечивает однонаправленный поток
- **Room** компилирует SQL во время сборки - ошибки в запросах не доходят до пользователей, `Flow<>` в DAO даёт автообновление при изменении данных
- **WorkManager** - для задач с гарантией выполнения вне lifecycle приложения: загрузка, синхронизация, аналитика. Внутри использует Room для персистентности очереди
- **Связь компонентов**: WorkManager использует Room, ViewModel использует StateFlow из Room DAO - Architecture Components проектировались как единая экосистема
Связанные темы
Architecture Components - фундамент Android-разработки, на котором строятся более сложные паттерны:
- State Management: однонаправленный поток данных — ViewModel + StateFlow реализует те же принципы однонаправленного потока что и Redux/Flux
- MVI и MVVM архитектуры на Android — MVI и MVVM строятся поверх ViewModel, Room и WorkManager как строительных блоков
- Kotlin Coroutines и Flow — viewModelScope и CoroutineWorker - точка интеграции Architecture Components с Coroutines
Вопросы для размышления
- ViewModel переживает поворот экрана, но не переживает process death (когда система убивает процесс из-за нехватки памяти). Для этого есть SavedStateHandle - автоматическая сериализация через Bundle. Где граница между тем что стоит хранить в ViewModel и тем что нужно персистировать в Room или SavedStateHandle?
- WorkManager гарантирует eventual execution, но не точное время. Минимальный интервал периодической задачи - 15 минут. Android Doze Mode может откладывать задачи часами. Как это ограничение меняет архитектуру мобильных приложений по сравнению с серверными cron-задачами?
- Room компилирует SQL во время сборки - это повышает надёжность, но снижает гибкость: нельзя строить динамические запросы через annotation processor. Для сложной фильтрации нужен rawQuery или QueryBuilder API. Где граница между статической безопасностью и необходимой гибкостью?
Связанные уроки
- mob-05 — StateFlow и MVVM-паттерн - фундамент для понимания ViewModel
- mob-07 — Kotlin Coroutines - стандартный async-механизм внутри ViewModel
- mob-11 — MVI/MVVM на Android строятся поверх Architecture Components
- db-03-acid — Room enforcement ACID-транзакций - те же гарантии, что в PostgreSQL
- prog-13-patterns — Observer pattern - основа LiveData и StateFlow подписок
- comp-01-intro