Архітектура мобільних додатків
Додаток зібраний в одному ViewController на 2000 рядків. Сітьові виклики, бізнес-логіка, оновлення UI — всё в одному місці. Додати нову фішку без регресії складно, написати тест неможливо. Це не «поганий код» — це відсутність архітектури. І це трапляється частіше, ніж можна чекати, навіть у production-додатках з мільйонами користувачів.
Архітектурні паттерни в мобільній розробці вирішують одне завдання: відокремити UI від логіки так, щоб кожна частина була тестованою та заміною.
MVVM: базовий паттерн
Model-View-ViewModel — стандарт для iOS (SwiftUI + Combine/async, UIKit + Combine) та Android (Jetpack ViewModel + StateFlow + Compose). ViewModel містить стан UI та бізнес-логіку. View тільки відображає стан та передає намір користувача в ViewModel. Model — дані та їхнє джерело.
Ключове правило: ViewModel не знає про UIKit або Android View-класи. Немає імпортів UIKit, немає Context-залежностей (крім Application context через Hilt). Це гарантує тестованість: ViewModel тестується як чистий Kotlin/Swift-код без Android Instrumented Test.
MVVM закриває 70% потреб. Решта 30% — де потрібна сувора ізоляція фіч, масштабування команди, складний flow управління станом.
Clean Architecture: коли MVVM недостатньо
Додає шари поверх MVVM:
Domain-шар — бізнес-логіка, незалежна від платформи. UseCase (або Interactor) містить одне бізнес-правило: GetUserOrdersUseCase, PlaceOrderUseCase. Залежить тільки від інтерфейсів (protocol/interface), не від конкретних реалізацій.
Data-шар — реалізація репозиторіїв. OrderRepositoryImpl реалізує OrderRepository з domain. Знає про Retrofit, Room, UserDefaults. ViewModel не знає, звідки дані — з мережі або кешу.
Presentation-шар — ViewModel + View. Знає про Domain, не знає про Data.
Правило залежностей: залежності спрямовані тільки всередину. Domain не залежить ні від чого. Data та Presentation залежать від Domain.
Presentation → Domain ← Data
Це дає можливість підміняти реалізацію: тест використовує in-memory репозиторій замість сітьового, інтерфейс залишається тим же.
Практична застереження: Clean Architecture додає файли та шари. Для невеликого додатку це overhead. Обґрунтовано від ~15 фіч та при команді 3+ розробників.
BLoC для Flutter: передбачуваний потік станів
BLoC (Business Logic Component) — стандартний паттерн у Flutter-спільноті. Бібліотека flutter_bloc реалізує його через два типи: Bloc (Event → State) та Cubit (State без Events, тільки методи).
Bloc обробляє Event та емітує новий State через on<EventType> хендлери. Стан іммутабельний — новий об'єкт на кожне змінення. BlocBuilder перересовує тільки ту частину дерева, де змінився state.
// Event
abstract class CartEvent {}
class AddItemToCart extends CartEvent {
final String productId;
AddItemToCart(this.productId);
}
// State
abstract class CartState {}
class CartLoaded extends CartState {
final List<CartItem> items;
CartLoaded(this.items);
}
// Bloc
class CartBloc extends Bloc<CartEvent, CartState> {
CartBloc(this._cartRepository) : super(CartLoaded([])) {
on<AddItemToCart>(_onAddItem);
}
Future<void> _onAddItem(AddItemToCart event, Emitter<CartState> emit) async {
final current = state as CartLoaded;
final updated = await _cartRepository.addItem(event.productId);
emit(CartLoaded(updated));
}
}
Перевага BLoC — тестованість. blocTest з пакета bloc_test дозволяє перевірити: при такому-то Event, з таким-то початковим State, BLoC повинен емітувати такий-то State. Без UI, без моків для Flutter-фреймворку.
VIPER: для великих iOS-проектів
VIPER (View, Interactor, Presenter, Entity, Router) — найстрогіше розділення обов'язків для iOS. Кожен компонент має протокол та конкретну реалізацію.
- View — тільки UI, делегує всё Presenter
- Interactor — бізнес-логіка, робота з мережею та даними
- Presenter — посередник між View та Interactor, форматує дані для View
- Entity — моделі даних (чисті структури)
- Router — навігація між модулями
Кожен модуль (екран або фіча) — окремий VIPER-модуль. Це виключає coupling між фічами і дозволяє великим командам працювати паралельно без конфліктів.
Ціна: багато файлів, багато протоколів. Шаблонний код генерується через Sourcery або кастомні Xcode-шаблони. VIPER обґрунтований для додатків з 10+ розробниками та 50+ екранами.
TCA (The Composable Architecture)
TCA від Point-Free — більш сучасна альтернатива VIPER для iOS/macOS. Основні концепції: State (іммутабельний стан фіч), Action (усі можливі події), Reducer (State + Action → новий State + Effect), Store (тримає State, обробляє Actions).
Scope дозволяє composable будувати великі фіч з малих: батьківський Reducer делегує частину State дочірньому. Кожна фіча тестується ізольовано через TestStore з точним контролем над Effects.
TCA має крутий крок навчання, але дає передбачуваність, яку складно отримати іншим способом: кожне змінення стану — явний Action з конкретним джерелом.
Вибір паттерну: практична таблиця
| Паттерн | Платформа | Команда | Коли вибирати |
|---|---|---|---|
| MVVM | iOS, Android, Flutter | 1–5 | Стартовий стандарт, MVP, невеликі проекти |
| MVVM + Clean | iOS, Android | 3–10 | Середні проекти, тестованість критична |
| BLoC | Flutter | 2–8 | Flutter з передбачуваним state management |
| VIPER | iOS | 5–20 | Великі iOS-проекти, модульна архітектура |
| TCA | iOS/macOS | 3–15 | Сувора тестованість, Swift Concurrency |
Універсальної відповіді немає. Архітектуру вибирають під розмір команди, вимоги до тестованості та горизонт підтримки додатку.
Що відбувається без архітектури
Типовий сценарій через 18 місяців без архітектури: 40% часу розробки йде на дебаг регресій. Новий розробник розбирається в коді тиждень перед тим, як зробити перший PR. Тести не пишуться, «тому що складно мокувати». Додання нової фіч потребує розуміння половини кодової бази.
Вибір архітектури на старті — це інвестиція з повертаємістю через 3–6 місяців.







