Why Architecture Matters in Mobile Applications
When building mobile applications with Flutter, it is tempting to couple widget trees directly with HTTP calls or database queries. For simple prototypes, this works. However, as applications scaleβlike in TB Care Mobile (healthcare patient monitoring) or Semara Lombok AI (in-room hospitality tablets)βtight coupling leads to untestable code, fragile state, and difficult refactoring.
Clean Architecture solves this by separating software into concentric layers with a strict dependency rule: inner layers know nothing about outer layers.
The Three Core Layers
In our Flutter projects, we organize the codebase into three main layers:
lib/
βββ core/ # Shared utilities, failure models, network clients
βββ features/
β βββ feature_name/
β βββ data/ # Models, Data Sources (Remote/Local), Repositories Implementation
β βββ domain/ # Entities, Use Cases, Repository Contracts
β βββ presentation/ # Blocs / Notifiers, Pages, Widgets
1. The Domain Layer (Independent of Flutter)
The domain layer contains the pure business rules of your application. It contains Entities and Use Cases, completely free from any Flutter framework dependencies or external packages:
// domain/entities/patient.dart
class Patient {
final String id;
final String fullName;
final DateTime nextMedicationSchedule;
final bool isAdherent;
const Patient({
required this.id,
required this.fullName,
required this.nextMedicationSchedule,
required this.isAdherent,
});
}
// domain/repositories/patient_repository.dart (Contract)
abstract class PatientRepository {
Future<Either<Failure, Patient>> getPatientDetails(String patientId);
Future<Either<Failure, void>> logMedicationIntake(String patientId, DateTime time);
}
2. The Data Layer (Connecting to External APIs)
The data layer implements the contracts defined by the domain layer. It handles network communication via Dio or Http, local SQLite/Hive caching, and model serialization:
// data/models/patient_model.dart
class PatientModel extends Patient {
const PatientModel({
required super.id,
required super.fullName,
required super.nextMedicationSchedule,
required super.isAdherent,
});
factory PatientModel.fromJson(Map<String, dynamic> json) {
return PatientModel(
id: json['id'] as String,
fullName: json['full_name'] as String,
nextMedicationSchedule: DateTime.parse(json['next_schedule'] as String),
isAdherent: json['is_adherent'] as bool,
);
}
}
3. The Presentation Layer (UI & State)
The presentation layer interacts exclusively with Use Cases through reactive state holders (such as BLoC, Riverpod, or StateNotifiers). The UI merely observes states and dispatches user events.
Offline-First Resiliency
For field applications like SIM UKK Mobile or healthcare utilities, network connections are often unstable. By employing the Repository pattern, we seamlessly switch between local SQLite/Isar caches and remote REST APIs:
- Always return cached local data instantly for zero-latency screen renders.
- Trigger a background synchronization request to the REST API.
- Emit updated data to the UI once synchronization succeeds.
Practical Lessons from GDG on Campus Mentorship
As a Mobile Dev Mentor at GDG on Campus Telkom University, the most common mistake I see students make is over-complicating small apps with too much boilerplate. Clean Architecture is not about writing dozens of files for a static counter; it is about establishing clear boundaries where business logic can be tested independently of UI changes.
Summary
Adopting Clean Architecture in Flutter provides:
- Testability: Business logic can be unit-tested without mocking Flutter widgets.
- Maintainability: Swapping Firebase for a self-hosted PostgreSQL REST API requires changes only in the data source layer.
- Team Scalability: Multiple developers can work concurrently across domain, data, and UI layers without merge conflicts.