Mobile Application Architecture
An application assembled in one ViewController with 2000 lines. Network calls, business logic, UI updates — all in one place. Adding a new feature without regression is hard, writing tests is impossible. This isn't "bad code" — it's the absence of architecture. And it happens more often than expected, even in production applications with millions of users.
Architectural patterns in mobile solve one task: separate UI from logic so that each part is testable and replaceable.
MVVM: the basic pattern
Model-View-ViewModel is the standard for iOS (SwiftUI + Combine/async, UIKit + Combine) and Android (Jetpack ViewModel + StateFlow + Compose). ViewModel contains UI state and business logic. View only displays state and passes user intent to ViewModel. Model is data and its source.
Key rule: ViewModel doesn't know about UIKit or Android View classes. No UIKit imports, no Context dependencies (except Application context via Hilt). This guarantees testability: ViewModel is tested as pure Kotlin/Swift code without Android Instrumented Test.
MVVM covers 70% of needs. The remaining 30% is where strict feature isolation, team scaling, and complex state management flow are needed.
Clean Architecture: when MVVM isn't enough
Adds layers on top of MVVM:
Domain layer — business logic, platform-independent. UseCase (or Interactor) contains one business rule: GetUserOrdersUseCase, PlaceOrderUseCase. Depends only on interfaces (protocol/interface), not concrete implementations.
Data layer — repository implementations. OrderRepositoryImpl implements OrderRepository from domain. Knows about Retrofit, Room, UserDefaults. ViewModel doesn't know where data comes from — network or cache.
Presentation layer — ViewModel + View. Knows about Domain, doesn't know about Data.
Dependency rule: dependencies point only inward. Domain depends on nothing. Data and Presentation depend on Domain.
Presentation → Domain ← Data
This allows substituting implementation: a test uses an in-memory repository instead of a network one, the interface stays the same.
Practical caveat: Clean Architecture adds files and layers. For a small application, this is overhead. Justified from ~15 features and with 3+ developers.
BLoC for Flutter: predictable state flow
BLoC (Business Logic Component) is the standard pattern in Flutter community. The flutter_bloc library implements it via two types: Bloc (Event → State) and Cubit (State without Events, only methods).
Bloc handles Event and emits new State through on<EventType> handlers. State is immutable — a new object for each change. BlocBuilder redraws only the part of the tree where state changed.
// 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's advantage is testability. blocTest from bloc_test package lets you verify: given this Event, with this initial State, BLoC should emit this State. No UI, no Flutter framework mocks.
VIPER: for large iOS projects
VIPER (View, Interactor, Presenter, Entity, Router) — the most strict separation of concerns for iOS. Each component has a protocol and concrete implementation.
- View — only UI, delegates everything to Presenter
- Interactor — business logic, network and data work
- Presenter — mediator between View and Interactor, formats data for View
- Entity — data models (clean structures)
- Router — navigation between modules
Each module (screen or feature) is a separate VIPER module. This eliminates coupling between features and lets large teams work in parallel without conflicts.
Cost: lots of files, lots of protocols. Template code is generated via Sourcery or custom Xcode templates. VIPER is justified for applications with 10+ developers and 50+ screens.
TCA (The Composable Architecture)
TCA from Point-Free — a more modern VIPER alternative for iOS/macOS. Core concepts: State (immutable feature state), Action (all possible events), Reducer (State + Action → new State + Effect), Store (holds State, handles Actions).
Scope lets you build large features from small ones composably: parent Reducer delegates part of State to child. Each feature is tested in isolation via TestStore with precise control over Effects.
TCA has a steep learning curve, but gives predictability hard to achieve otherwise: every state change is an explicit Action with specific source.
Pattern Selection: practical table
| Pattern | Platform | Team | When to choose |
|---|---|---|---|
| MVVM | iOS, Android, Flutter | 1–5 | Starting standard, MVP, small projects |
| MVVM + Clean | iOS, Android | 3–10 | Medium projects, testability is critical |
| BLoC | Flutter | 2–8 | Flutter with predictable state management |
| VIPER | iOS | 5–20 | Large iOS projects, modular architecture |
| TCA | iOS/macOS | 3–15 | Strict testability, Swift Concurrency |
There's no universal answer. Architecture is chosen based on team size, testability requirements, and application support horizon.
What happens without architecture
Typical scenario after 18 months without architecture: 40% of development time goes to debugging regressions. A new developer spends a week understanding the code before making the first PR. Tests aren't written "because it's hard to mock." Adding a feature requires understanding half the codebase.
Choosing architecture at the start is an investment that pays back in 3–6 months.







