Нативна розробка Android-застосунку на Kotlin
Клієнт приходить з готовим дизайном, іноді з прототипом на Figma, та питанням: «Чому не можна просто взяти React Native?» Відповідь залежить від того, що саме потрібно застосунку. Якщо це робота з Bluetooth LE, складна навігація по стеку екранів, фонова геолокація або запис екрана — нативний Android на Kotlin избавляє від цілого класу проблем, які в кросс-платформі вирішуються через костилі та нативні модулі, тобто фактично тим же кодом, тільки приховано за абстракцією.
Kotlin — основна мова Android-розробки з 2019 року. Google переписує власні бібліотеки з Java на Kotlin, Jetpack Compose існує тільки на Kotlin, та нові API типу kotlinx.coroutines або Flow просто не мають повноцінних аналогів для Java-стека. Вибір Kotlin — це не переважання, а дотримання екосистеми.
З чого насправді складається сучасний Android-проект
Архітектура типового комерційного застосунку на 2024 рік виглядає так: Clean Architecture з розбивкою на шари data / domain / presentation, MVVM як паттерн presentation-шару, Hilt для dependency injection. Навігація — через Navigation Component з NavGraph або більш гнучкий Decompose для складних вложених стеків.
UI будується на Jetpack Compose. Це не «новомодна технологія» — це уже стандарт. Compose убирає розрив між станом та UI: нема notifyDataSetChanged(), нема ViewHolder бойлерплейту, нема синхронізації між XML та кодом. @Composable-функція просто описує, як виглядає UI при даному стані, та Compose сам пересчитує те, що змінилось, через smart recomposition.
Для управління станом використовуємо StateFlow + ViewModel. Для складних UI зі shared state між кількома екранами — MVI-паттерн з єдиним UiState та UiEffect. Бізнес-логіка живе в UseCase-класах, які не знають про Android-специфіку та легко покриваються unit-тестами без Robolectric.
Мережевий шар: Retrofit 2 + OkHttp з ланцюгом інтерсепторів для авторизації, логування та retry-логіки. Серіалізація — kotlinx.serialization або Gson/Moshi по переважанням команди. Локальне зберігання — Room з TypeConverters для кастомних типів та @Transaction для атомарних операцій.
Фонові задачі — WorkManager для відкладених та періодичних операцій, coroutines з правильними 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 dead lifecycle говорять саме про це.
Багатопоточність на головному потоці. Доступ до бази даних Room на main thread у debug-сборці видає IllegalStateException — це добре, одразу видно. Але декодування Bitmap 4K-фото в onBindViewHolder при використанні старого RecyclerView-підходу не видає виключень, просто дропає кадри. Coil та Glide вирішують це через coroutines та worker threads, але тільки якщо правильно настройкований ImageLoader.
Неправильні scope у Hilt-компонентів. @Singleton репозиторій з @ActivityScoped залежністю всередину — та Hilt чесно падає з [Dagger/MissingBinding] на сборці. Не в рантайме — на сборці. Це добре, але розібратися в довгому stack trace Dagger-кодогенерації вміє не кожен.
Як будується робота
Починаємо з технічного аудиту вимог: список екранів, інтеграції (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 через JUnit5 + MockK, UI-тесты через Espresso або Compose Testing API. Для складних flow — інтеграційні тесты з in-memory Room database.
Перед публікацією — обфускація через R8, перевірка android:exported для всіх компонентів (вимога Google Play з API 31), тестування на кількох пристроях різних виробників через Firebase Test Lab.
Орієнтири по строкам
| Тип проекту | Оцінка |
|---|---|
| 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 потрібно розуміти до початку розробки. Тому перший крок — детальне ТЗ, а не оцінка «на глаз».







