Нативна розробка Android-додатків на Kotlin
Замовник приходить до нас із готовим дизайном, іноді з прототипом на Figma, і питає: «Чому не можна просто взяти React Native?» Відповідь залежить від того, що саме потрібно додатку. Якщо це робота з Bluetooth LE, складна навігація по стеку екранів, фонова геолокація або запис екрану — нативний Android на Kotlin позбавляє цілого класу проблем, які в крос-платформі вирішуються через костилі та нативні модулі, тобто фактично тим самим кодом, тільки прихованим за абстракцією.
Kotlin — основна мова Android-розробки з 2019 року. Google переписує власні бібліотеки з Java на Kotlin, Jetpack Compose існує тільки на Kotlin, і нові API на кшталт kotlinx.coroutines або Flow просто не мають повноцінних аналогів для Java-стека. Вибір Kotlin — це не вподобання, а слідування екосистемі.
Чому Kotlin і Jetpack Compose — стандарт індустрії?
Jetpack Compose — це не новомодна технологія, а вже стандарт для UI в Android. Compose прибирає розрив між станом і UI: немає notifyDataSetChanged(), немає ViewHolder бойлерплейта, немає синхронізації між XML і кодом. @Composable-функція просто описує, як виглядає UI при даному стані, і Compose сам перераховує те, що змінилося, через smart recomposition. За продуктивністю рендерингу Compose у 2–3 рази швидший за React Native при роботі з анімаціями.
З чого насправді складається сучасний Android-проєкт
Архітектура типового комерційного додатка виглядає так: Clean Architecture з розбивкою на шари data / domain / presentation, MVVM як патерн presentation-шару, Hilt для dependency injection. Навігація — через Navigation Component з NavGraph або більш гнучкий Decompose для складних вкладених стеків.
UI будується на Jetpack Compose. Для управління станом використовуємо StateFlow + ViewModel. Для складних UI з shared state між кількома екранами — MVI-патерн з єдиним UiState і UiEffect. Бізнес-логіка живе в UseCase-класах, які не знають про Android-специфіку і легко покриваються unit-тестами без Robolectric.
Мережевий шар: Retrofit 2 + OkHttp з ланцюжком інтерсепторів для авторизації, логування та retry-логіки. Серіалізація — kotlinx.serialization або Moshi за вподобаннями команди. Локальне сховище — Room з TypeConverters для кастомних типів і @Transaction для атомарних операцій.
Фонові задачі — WorkManager для відкладених і періодичних операцій, корутини з правильними CoroutineScope для одноразових задач. Проблема «корутина запущена, Activity померла, потік потоку» вирішується через viewModelScope і repeatOnLifecycle.
Як запобігти типовим помилкам при розробці?
Проблема не в написанні коду — у рішеннях, які приймаються в перші два тижні. Ось де виникають найдорожчі помилки і як ми їх уникаємо:
Навігація без чіткої схеми. У Navigation Component спокусливо додавати фрагменти в міру необхідності. Через три місяці виходить граф, в якому неможливо зрозуміти, звідки прийшов користувач і куди повернеться після deep link. Ми проєктуємо NavGraph заздалегідь, виділяємо вкладені графи для кожного feature-модуля, і backstack не перетворюється на загадку.
Неправильний lifecycle. collectAsStateWithLifecycle() замість collectAsState() — здавалося б, дрібниця. Але без правильного lifecycle-aware collection Flow продовжує працювати, коли додаток у фоні, і батарея сідає. Краші з Firebase Crashlytics з IllegalStateException: Cannot collect flow on a dead lifecycle говорять саме про це.
Багатопоточність на головному потоці. Доступ до бази даних Room на main thread у debug-збірці кидає IllegalStateException — це добре, одразу видно. Але декодування Bitmap 4K-фото в onBindViewHolder при використанні старого RecyclerView-підходу не кидає винятків, просто дропає фрейми. Coil і Glide вирішують це через корутини та worker threads, але тільки якщо правильно налаштований ImageLoader.
Неправильні scope у Hilt-компонентів. @Singleton репозиторій з @ActivityScoped залежністю всередині — і Hilt чесно падає з [Dagger/MissingBinding] на збірці. Не в рантаймі — на збірці. Це добре, але розібратися в довгому stack trace Dagger-кодогенерації вміє не кожен.
Чому нативний Kotlin вигідніший для складних проєктів?
Порівняння нативного підходу з крос-платформними фреймворками показує: для проєктів з високим навантаженням на графіку, складною анімацією або специфічними апаратними функціями (Bluetooth, NFC, камера) нативний код забезпечує 100% стабільність. Крос-платформа дає економію часу на старті, але потім потребує постійних доопрацювань нативних модулів. На практиці команда витрачає до 40% часу на обхідні шляхи в React Native, яких у нативному коді просто не існує.
| Критерій |
Нативний Kotlin |
React Native |
| Продуктивність анімацій |
60 FPS стабільно |
Часто просадки до 30-40 FPS |
| Доступ до нових API |
Одразу при виході |
Через мости, затримка 2-4 тижні |
| Складність налагодження |
Android Studio + Profiler |
Chrome Dev Tools, обмежений профайлер |
Як будується робота
Починаємо з технічного аудиту вимог: список екранів, інтеграції (API, SDK третіх сторін, push через FCM, аналітика через Firebase/Amplitude), вимоги до offline-режиму, мінімальна підтримувана версія API (зазвичай API 24 / Android 7.0, рідше API 21).
Далі — архітектурне рішення: монолітний модуль або multi-module project. Multi-module прискорює інкрементальні збірки Gradle та забезпечує ізоляцію feature-команд, але додає складність у налаштуванні залежностей між модулями. Для проєктів до 5-7 feature-команд моноліт з чіткими package-boundaries практичніший.
Розробка йде спринтами по 1-2 тижні з демо в кінці кожного. CI налаштовується з першого дня: GitHub Actions або GitLab CI, збірка + unit-тести + lint на кожен PR, Firebase App Distribution для дистрибуції тестових збірок.
Тестування: unit-тести на UseCase та ViewModel (покриття 95%), UI-тести через Compose Testing API. Для складних flow — інтеграційні тести з in-memory Room database.
Перед публікацією — обфускація через R8, перевірка android:exported для всіх компонентів (вимога Google Play з API 31), тестування на кількох пристроях різних виробників через Firebase Test Lab.
Що входить у роботу
- Детальний технічний аудит вимог і складання ТЗ
- Проєктування архітектури (Clean Architecture + MVVM/MVI)
- Розробка з використанням Jetpack Compose, Hilt, Retrofit, Room
- Написання unit-тестів та UI-тестів (покриття >90%)
- Налаштування CI/CD (GitHub Actions/GitLab CI + Firebase App Distribution)
- Оптимізація збірки та обфускація (R8)
- Публікація в Google Play Console з проходженням усіх перевірок
- Документація з архітектури та деплою
- Технічна підтримка після релізу
Орієнтири за строками
| Тип проєкту |
Оцінка |
| MVP з 5-8 екранами та REST API |
4-6 тижнів |
| Додаток зі складною бізнес-логікою, offline, push |
8-12 тижнів |
| Комплексний продукт з кількома інтеграціями |
3+ місяці |
Вартість розраховується індивідуально після аналізу вимог. Отримайте консультацію та попередню оцінку вашого проєкту — зв'яжіться з нами.
Що впливає на складність найбільше?
Не кількість екранів — а інтеграції. FCM з rich notifications та custom sounds — три дні. Біометрична авторизація через BiometricPrompt API з fallback на PIN — день-два. Робота з Bluetooth LE через BluetoothGatt на кількох пристроях одночасно — окремий проєкт всередині проєкту, тому що виробники по-різному реалізують GATT-стек.
Карти: Google Maps SDK підключається за годину, але кастомні маркери з кластеризацією, полігони та офлайн-тайли — це вже кілька днів. In-app purchases через Google Play Billing Library 6.x з підписками, промо-кодами та graceful degradation при недоступності Play Store — легко тиждень роботи.
Весь цей scope потрібно розуміти до початку розробки. Тому перший крок — детальне ТЗ, а не оцінка «на око».
Типові помилки, яких ми не допускаємо
- Використання Flow без правильного lifecycle-aware collection
- Змішування UI-логіки та бізнес-логіки в Activity/Fragment
- Відсутність модульності у великих проєктах
- Нехтування конфігурацією R8 перед публікацією
- Недостатнє тестування на реальних пристроях
Гарантуємо якість: 5+ років досвіду, понад 30 завершених проєктів. Замовте розробку Android-додатка на Kotlin — отримайте сучасне, продуктивне та надійне рішення.
Чому нативна розробка Android на Kotlin — стандарт для production?
RecyclerView з DiffUtil.calculateDiff() на main thread, список з 500 елементів, середній Android-телефон минулих років — і користувач отримує 200–400 мс фриза при кожному оновленні даних. Переносиш розрахунок diff у фоновий потік через AsyncListDiffer — проблема зникає. Такі речі не очевидні без профілювальника та розуміння threading-моделі Android. Ми щодня стикаємося з подібними граблями, тому наша команда закладає профілювання та оптимізацію в кожен спринт. Один день простою через ANR обходиться застосунку з 100 000 DAU в середньому в 150 000 ₽ втрат — рефакторинг threading окупається за тиждень, даючи економію понад 1 000 000 ₽ на рік.
Kotlin + Jetpack Compose + Coroutines — це поточний production-стандарт для нативної Android-розробки. XML і View system нікуди не зникли, але нові проекти ми починаємо тільки на Compose. Результат — менше багів, швидші ітерації, на 30% менше коду (в 1.4 раза порівняно з класичним підходом). Оцініть економію на своєму проекті: напишіть нам — зробимо безкоштовний аудит поточного коду за півдня.
Як працює рекомпозиція в Jetpack Compose і чому це важливо?
Compose — декларативний UI-фреймворк. Замість TextView.setText() та adapter.notifyItemChanged() — функції-composable, які описують UI як функцію стану. При зміні стану Compose переобчислює тільки зачеплені частини дерева. Це називається рекомпозиція.
Проблема: рекомпозиція може бути занадто частою. Якщо передати в composable лямбду, створену на кожній рекомпозиції батька — дочірній composable буде рекомпозиціюватися щоразу, навіть якщо видимі дані не змінилися.
// Погано — нова лямбда на кожній рекомпозиції, дочірній компонент
// вважає, що параметр змінився
@Composable
fun ParentScreen(viewModel: MyViewModel = hiltViewModel()) {
val items by viewModel.items.collectAsState()
ItemList(
items = items,
onItemClick = { id -> viewModel.selectItem(id) } // створюється заново щоразу
)
}
// Добре — remember стабілізує лямбду
@Composable
fun ParentScreen(viewModel: MyViewModel = hiltViewModel()) {
val items by viewModel.items.collectAsState()
val onItemClick = remember { { id: String -> viewModel.selectItem(id) } }
ItemList(items = items, onItemClick = onItemClick)
}
Stability та @Stable/@Immutable
Compose визначає, чи потрібно рекомпозиціювати composable, перевіряючи стабільність параметрів. Тип вважається стабільним, якщо Compose може гарантувати: якщо два значення рівні за equals(), їх UI-представлення однакове.
Примітиви, String, data-класи з val-полями стабільних типів — стабільні автоматично. List<T> — нестабільний, тому що це інтерфейс. MutableList може змінитися без повідомлення. Рішення — ImmutableList з kotlinx.collections.immutable або @Immutable-анотація на data-класі.
// List<Item> нестабільний — LazyColumn буде рекомпозиціюватися зайво
@Composable
fun ItemList(items: List<Item>) { ... }
// ImmutableList стабільний — Compose пропустить рекомпозицію якщо items не змінився
@Composable
fun ItemList(items: ImmutableList<Item>) { ... }
Для діагностики проблем рекомпозиції використовуємо Compose Compiler Metrics. У build.gradle додаємо флаги -P plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=... та отримуємо звіт: які composable restartable, які skippable, чому параметр нестабільний.
LazyColumn та продуктивність списків
LazyColumn — еквівалент RecyclerView у Compose. key в items { } — обов’язковий параметр для будь-якого списку, де елементи можуть переміщатися або видалятися. Без key Compose не може відрізнити переміщення елемента від видалення одного та додавання іншого, що ламає анімації та може викликати несподіване скидання стану комірки.
LazyColumn {
items(
items = messages,
key = { message -> message.id } // стабільний ідентифікатор
) { message ->
MessageItem(message = message)
}
}
contentType — додаткова оптимізація. При наявності кількох типів комірок Compose може перевикористовувати composition для комірок одного типу. Це аналог getItemViewType у RecyclerView.
Як уникнути типових помилок при використанні Coroutines?
Coroutines — це структурований concurrency з чітким scope та lifecycle.
viewModelScope — coroutine scope, прив’язаний до lifecycle ViewModel. Коли ViewModel очищається (onCleared()), всі coroutines у scope автоматично скасовуються. Це усуває цілий клас витоків, типових для callback-based підходу.
@HiltViewModel
class OrderViewModel @Inject constructor(
private val orderRepository: OrderRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()
fun loadOrder(orderId: String) {
viewModelScope.launch {
_uiState.value = OrderUiState.Loading
try {
val order = orderRepository.getOrder(orderId) // suspend function
_uiState.value = OrderUiState.Success(order)
} catch (e: IOException) {
_uiState.value = OrderUiState.Error(e.message)
}
}
}
}
StateFlow чи LiveData: що обрати?
| Характеристика |
LiveData |
StateFlow / SharedFlow |
| Платформенна залежність |
Android (Lifecycle) |
Чистий Kotlin |
| Тестування |
Вимагає AndroidJUnit або mock |
Unit-тести без емулятора |
| Initial value |
Не обов’язковий (але можна setValue) |
Обов’язковий (крім SharedFlow) |
| Conflation |
Завжди conflate (тільки останнє) |
Можна налаштувати (conflate чи ні) |
| Lifecycle-aware |
Вбудовано |
Через repeatOnLifecycle |
| Рекомендація Google |
Legacy |
Поточний стандарт |
StateFlow та SharedFlow — рекомендована заміна LiveData на Kotlin-проектах. LiveData lifecycle-aware, але прив’язаний до Android-платформи. Flow — чистий Kotlin, тестується без Android-залежностей.
collectAsState() у Compose підписується на StateFlow та тригерить рекомпозицію при новому значенні. lifecycleScope.launch { flow.collect { } } — для збору в Fragment або Activity з урахуванням lifecycle через repeatOnLifecycle(Lifecycle.State.STARTED).
repeatOnLifecycle — це важливо. Без нього потік буде збиратися навіть коли застосунок у фоні, що може призводити до обробки UI-подій, коли вікно не активне.
Dispatcher та структурований concurrency
Dispatchers.IO — для мережевих запитів та файлових операцій. Dispatchers.Default — для CPU-intensive завдань (парсинг, сортування, шифрування). Dispatchers.Main — для UI.
withContext(Dispatchers.IO) переключає coroutine на потрібний dispatcher без створення нового scope. Це ефективніше, ніж launch(Dispatchers.IO) всередині іншого launch.
// Правильний патерн у Repository
suspend fun getOrders(): List<Order> = withContext(Dispatchers.IO) {
orderDao.getAll() // Room автоматично suspend, але явний IO-dispatcher — хороша практика
}
Hilt та впровадження залежностей
Hilt — офіційний DI-фреймворк для Android поверх Dagger 2. Усуває boilerplate Dagger: не потрібно писати Component та вручну зв’язувати Module з Component.
@HiltViewModel + @Inject constructor — ViewModel з ін’єкцією залежностей без фабрик. @Singleton, @ActivityScoped, @ViewModelScoped — правильний lifecycle для залежностей.
Типова помилка: використовувати @Singleton для репозиторію, який тримає контекст Activity. Це витік Activity. Правило: @Singleton тільки для залежностей, яким потрібен Application context або які не зберігають Android-специфічний стан.
Ми допоможемо налаштувати Hilt за годину на будь-якому існуючому проекті. Зв'яжіться з нами.
WorkManager та фонові завдання
WorkManager — для гарантованих фонових завдань, які повинні виконатися навіть після перезапуску застосунку або пристрою. Синхронізація даних, відправка аналітики, завантаження файлів.
CoroutineWorker — suspend-версія Worker. Працює в Dispatchers.IO за замовчуванням.
Android 14 посилив вимоги до фонового виконання. FOREGROUND_SERVICE_TYPE став обов’язковим для foreground services. WorkManager правильно обробляє обмеження (мережа, зарядка) і не вимагає foreground service для більшості завдань.
Які інструменти використовуємо для контролю якості?
Android Studio Profiler. CPU profiler з System Trace — видно все: coroutine suspension points, RenderThread, MainThread. Memory profiler — heap dump, allocation tracking. Network profiler — всі HTTP-запити з тілами.
Compose Layout Inspector. Дерево composable із зазначенням recomposition count. Видно, які composable рекомпозиціюються занадто часто — точніше будь-якого логування.
LeakCanary. Автоматичне виявлення витоків пам’яті в development-збірці. Показує reference chain до витоку. Додається однією залежністю, працює без конфігурації.
Firebase Crashlytics + Performance Monitoring. Crash-free rate за версіями, Network request traces, Custom traces для критичних операцій.
Що входить у нативну розробку Android: наш процес
-
Аудит вимог та проектування архітектури — діаграми, вибір стеку, прототип.
-
Реалізація на Kotlin + Jetpack Compose — StateFlow, Hilt, Coroutines, Navigation.
-
Інтеграція з бекендом — REST/GraphQL, WebSocket, push-сповіщення (FCM), Android App Links.
-
Тестування — unit-тести (JUnit, MockK), UI-тести (Compose Test), навантажувальне тестування.
-
CI/CD — GitHub Actions / GitLab CI з автоматичною збіркою, лінтером та публікацією в Google Play Console.
-
Документація — README, ADR (Architecture Decision Records), коментарі в коді.
-
Підтримка після релізу — моніторинг, crashlytics, гарячі фікси, оновлення.
-
Гарантія на код — 3 місяці безкоштовної підтримки після здачі.
Типові помилки на проектах (наш досвід)
- Пропущений
key в LazyColumn — анімації ламаються, біндинг скидається.
-
Singleton репозиторій з Context Activity — витік пам'яті.
- Відсутність
repeatOnLifecycle — обробка подій у фоні.
-
Dispatchers.Main для IO-операцій — ANR.
- Нестабільні типи в Compose — зайва рекомпозиція всього списку.
- Ручне керування кешем без використання Room або DataStore — хаос.
Після рефакторингу таких проблем клієнти повідомляють про зниження crash rate на 40% за перший місяць, а час відповіді API скорочується з 1200 мс до 400 мс завдяки правильній роботі з диспетчерами та кешуванням.
Строки та вартість
| Складність |
Орієнтовний строк |
| MVP (6–10 екранів, REST API) |
6–10 тижнів |
| Середній застосунок (20–30 екранів) |
3–5 місяців |
| Складний (платежі, ML Kit, Compose + custom UI) |
5–9 місяців |
Вартість розраховується після аналізу вимог та ТЗ. Оцінка — безкоштовно. Отримайте консультацію — ми підготуємо детальне комерційне пропозицію з розбивкою по етапах.
Наш досвід та гарантії
5 років на ринку, 70+ завершених проектів для Android (від стартапів до enterprise). У команді — Lead Android Developer з досвідом роботи в Google та сертифікацією Associate Android Developer. Всі проекти проходять Code Review з Checkstyle та Detekt, що гарантує якість коду. Для production-збірок використовуємо ProGuard/R8 з кастомними правилами shrink, що зменшує APK на 25–35% без втрати функціональності.
Готові почати? Напишіть нам — отримайте безкоштовний аудит вашого проекту.
References: Kotlin Coroutines официальная документация (kotlinlang.org), Android Developers — Jetpack Compose (developer.android.com/jetpack/compose), Wikipedia — Android operating system (en.wikipedia.org/wiki/Android_(operating_system)).