What is separation of concerns ?
Answer
Overview
Separation of Concerns (SoC) is a software design principle that states: each part of your code should have one clear responsibility and should not overlap with other parts.
Think of a restaurant: the chef cooks, the waiter serves, the cashier handles payments. They are separate — each person knows their job.
Why It Matters
| Without SoC | With SoC |
|---|---|
| UI code mixed with API calls | UI only handles display |
| Business logic inside widgets | Logic lives in separate classes |
| Hard to test individual pieces | Each layer is independently testable |
| Hard to maintain or change | Easy to swap out one layer |
The Layers in Flutter
text┌────────────────────────────────────┐ │ View / UI Layer │ ← Widgets, Screens ├────────────────────────────────────┤ │ Presentation / State Layer │ ← ViewModel, BLoC, Controller ├────────────────────────────────────┤ │ Business Logic Layer │ ← Use Cases, Services ├────────────────────────────────────┤ │ Data / Repository Layer │ ← Repository, API calls ├────────────────────────────────────┤ │ Model Layer │ ← Data models, entities └────────────────────────────────────┘
❌ Bad Example — No Separation of Concerns
dart// Everything crammed into the widget — bad practice! class UserScreen extends StatefulWidget { _UserScreenState createState() => _UserScreenState(); } class _UserScreenState extends State<UserScreen> { String? userName; void initState() { super.initState(); // ❌ API call directly inside widget http.get(Uri.parse('https://api.example.com/user/1')).then((response) { final data = jsonDecode(response.body); setState(() => userName = data['name']); // ❌ parsing inside widget }); } Widget build(BuildContext context) { return Text(userName ?? 'Loading...'); } }
✅ Good Example — With Separation of Concerns
1. Model Layer
dartclass UserModel { final int id; final String name; final String email; UserModel({required this.id, required this.name, required this.email}); factory UserModel.fromJson(Map<String, dynamic> json) { return UserModel( id: json['id'], name: json['name'], email: json['email'], ); } }
2. Repository Layer (Data)
dartclass UserRepository { final Dio _dio; UserRepository(this._dio); Future<UserModel> getUser(int id) async { final response = await _dio.get('/users/$id'); return UserModel.fromJson(response.data); } }
3. Business Logic / Use Case
dartclass GetUserUseCase { final UserRepository _repository; GetUserUseCase(this._repository); Future<UserModel> execute(int userId) { if (userId <= 0) throw Exception('Invalid user ID'); return _repository.getUser(userId); } }
4. State / ViewModel Layer
dartclass UserViewModel extends ChangeNotifier { final GetUserUseCase _useCase; UserModel? user; bool isLoading = false; String? error; UserViewModel(this._useCase); Future<void> loadUser(int id) async { isLoading = true; notifyListeners(); try { user = await _useCase.execute(id); } catch (e) { error = e.toString(); } finally { isLoading = false; notifyListeners(); } } }
5. View Layer
dartclass UserScreen extends StatelessWidget { Widget build(BuildContext context) { final vm = context.watch<UserViewModel>(); if (vm.isLoading) return CircularProgressIndicator(); if (vm.error != null) return Text('Error: ${vm.error}'); return Text('Hello, ${vm.user?.name}'); // ✅ View only renders — no logic here } }
Benefits of Separation of Concerns
- Testability — Each layer can be unit tested in isolation
- Maintainability — Change one layer without touching others
- Reusability — Repository and use cases can be reused across screens
- Readability — Each file has a single, clear purpose
- Team scalability — Different devs can work on different layers
SoC in Flutter Architecture Patterns
| Pattern | How SoC is Applied |
|---|---|
| MVC | Model, View, Controller separated |
| MVP | Model, View (interface), Presenter separated |
| MVVM | Model, ViewModel, View separated |
| BLoC | Events → BLoC → States → UI |
| Clean Architecture | Domain, Data, Presentation layers |
Key Rule: If a widget is doing more than rendering UI, you have a separation of concerns violation. Move logic to a ViewModel, Controller, or Repository.