Answer
Overview
Flutter architecture guidelines provide best practices for organizing code, managing state, and building scalable, maintainable apps. These guidelines follow principles from Clean Architecture, SOLID, and Flutter-specific patterns.
1. Separation of Concerns
Divide code into distinct layers: Presentation, Domain, and Data.
Layers
textPresentation (UI) ↓ Domain (Business Logic) ↓ Data (API, Database)
Presentation Layer:
- Widgets, pages, screens
- BLoC, ViewModel, or state management
- User interactions
Domain Layer:
- Entities (business objects)
- Use cases (business logic)
- Repository interfaces
Data Layer:
- API clients, database access
- Data models (JSON serialization)
- Repository implementations
Example
dart// Domain Layer class User { final String id; final String name; User({required this.id, required this.name}); } abstract class UserRepository { Future<User> getUser(String id); } // Data Layer class UserRepositoryImpl implements UserRepository { final ApiClient apiClient; UserRepositoryImpl(this.apiClient); Future<User> getUser(String id) async { final json = await apiClient.fetchUser(id); return User.fromJson(json); } } // Presentation Layer class UserBloc extends Bloc<UserEvent, UserState> { final UserRepository userRepository; UserBloc(this.userRepository) : super(UserInitial()) { on<FetchUser>(_onFetchUser); } Future<void> _onFetchUser(FetchUser event, Emitter<UserState> emit) async { emit(UserLoading()); try { final user = await userRepository.getUser(event.id); emit(UserLoaded(user)); } catch (e) { emit(UserError(e.toString())); } } }
2. Dependency Inversion (SOLID)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
dart// ✅ Good: Depends on abstraction (interface) class UserBloc { final UserRepository userRepository; // Interface, not implementation UserBloc(this.userRepository); } // ❌ Bad: Depends on concrete implementation class UserBloc { final ApiClient apiClient; // Direct dependency on API client }
Benefit: Easy to swap implementations (e.g., mock for testing).
dart// Test with mock repository final mockRepo = MockUserRepository(); final bloc = UserBloc(mockRepo); // Production with real repository final realRepo = UserRepositoryImpl(ApiClient()); final bloc = UserBloc(realRepo);
3. Single Responsibility (SOLID)
Each class should have one reason to change.
dart// ❌ Bad: Does too much class UserService { Future<User> fetchUser(String id) async { /* API call */ } void saveToDatabase(User user) { /* Database */ } void logEvent(String event) { /* Analytics */ } } // ✅ Good: Separate concerns class UserRepository { Future<User> fetchUser(String id) async { /* API call */ } } class UserDatabase { void saveUser(User user) { /* Database */ } } class AnalyticsService { void logEvent(String event) { /* Analytics */ } }
4. Immutability
Use immutable objects to prevent bugs and simplify state management.
dart// ✅ Immutable entity class User { final String id; final String name; const User({required this.id, required this.name}); User copyWith({String? id, String? name}) { return User( id: id ?? this.id, name: name ?? this.name, ); } } // Usage final user = User(id: '1', name: 'Alice'); final updatedUser = user.copyWith(name: 'Bob'); // New instance
5. Repository Pattern
Use repositories to abstract data sources (API, database, cache).
dartabstract class ProductRepository { Future<List<Product>> getProducts(); Future<Product> getProductById(String id); } class ProductRepositoryImpl implements ProductRepository { final ApiClient apiClient; final Database database; ProductRepositoryImpl(this.apiClient, this.database); Future<List<Product>> getProducts() async { // Try cache first final cachedProducts = await database.getProducts(); if (cachedProducts.isNotEmpty) return cachedProducts; // Fetch from API final products = await apiClient.fetchProducts(); await database.saveProducts(products); return products; } Future<Product> getProductById(String id) async { return apiClient.fetchProductById(id); } }
6. Use Cases (Domain Logic)
Encapsulate business logic in use cases (also called interactors).
dartclass LoginUseCase { final AuthRepository authRepository; LoginUseCase(this.authRepository); Future<User> call(String email, String password) async { // Business logic if (!email.contains('@')) { throw Exception('Invalid email'); } if (password.length < 6) { throw Exception('Password too short'); } return authRepository.login(email, password); } } // Usage in BLoC class AuthBloc extends Bloc<AuthEvent, AuthState> { final LoginUseCase loginUseCase; AuthBloc(this.loginUseCase) : super(AuthInitial()) { on<LoginRequested>((event, emit) async { emit(AuthLoading()); try { final user = await loginUseCase(event.email, event.password); emit(AuthSuccess(user)); } catch (e) { emit(AuthError(e.toString())); } }); } }
7. Dependency Injection
Use Dependency Injection for loose coupling and testability.
dart// ✅ Good: Constructor injection class UserBloc { final UserRepository userRepository; UserBloc(this.userRepository); // Injected } // Setup with get_it or injectable final getIt = GetIt.instance; getIt.registerLazySingleton<UserRepository>( () => UserRepositoryImpl(ApiClient()), ); getIt.registerFactory<UserBloc>( () => UserBloc(getIt<UserRepository>()), ); // Usage final userBloc = getIt<UserBloc>();
8. State Management
Choose state management based on app complexity.
| App Size | Recommendation |
|---|---|
| Small | Provider, Riverpod, setState |
| Medium | BLoC, Riverpod |
| Large | BLoC + Clean Architecture |
Example (BLoC):
dart// Event abstract class UserEvent {} class FetchUser extends UserEvent { final String id; FetchUser(this.id); } // State abstract class UserState {} class UserInitial extends UserState {} class UserLoading extends UserState {} class UserLoaded extends UserState { final User user; UserLoaded(this.user); } class UserError extends UserState { final String message; UserError(this.message); } // BLoC class UserBloc extends Bloc<UserEvent, UserState> { final UserRepository userRepository; UserBloc(this.userRepository) : super(UserInitial()) { on<FetchUser>(_onFetchUser); } Future<void> _onFetchUser(FetchUser event, Emitter<UserState> emit) async { emit(UserLoading()); try { final user = await userRepository.getUser(event.id); emit(UserLoaded(user)); } catch (e) { emit(UserError(e.toString())); } } }
9. Error Handling
Handle errors at every layer.
dart// Data Layer class UserRepositoryImpl implements UserRepository { Future<User> getUser(String id) async { try { final response = await apiClient.get('/users/$id'); return User.fromJson(response.data); } catch (e) { throw NetworkException('Failed to fetch user: $e'); } } } // Domain Layer (Use Case) class FetchUserUseCase { final UserRepository userRepository; FetchUserUseCase(this.userRepository); Future<Result<User>> call(String id) async { try { final user = await userRepository.getUser(id); return Result.success(user); } catch (e) { return Result.failure(e.toString()); } } } // Presentation Layer (BLoC) on<FetchUser>((event, emit) async { emit(UserLoading()); final result = await fetchUserUseCase(event.id); result.fold( (user) => emit(UserLoaded(user)), (error) => emit(UserError(error)), ); });
10. Testing
Write tests for each layer.
Unit Tests (Domain Layer):
darttest('LoginUseCase validates email', () async { final useCase = LoginUseCase(mockAuthRepository); expect( () => useCase('invalid-email', 'password'), throwsException, ); });
Widget Tests (Presentation Layer):
darttestWidgets('Shows loading indicator', (tester) async { await tester.pumpWidget(MyApp()); expect(find.byType(CircularProgressIndicator), findsOneWidget); });
Integration Tests:
darttestWidgets('Complete login flow', (tester) async { await tester.pumpWidget(MyApp()); await tester.enterText(find.byKey(Key('email')), 'test@example.com'); await tester.enterText(find.byKey(Key('password')), 'password'); await tester.tap(find.text('Login')); await tester.pumpAndSettle(); expect(find.text('Welcome'), findsOneWidget); });
11. Folder Structure
Feature-first structure (recommended for large apps):
textlib/ features/ authentication/ data/ models/ repositories/ datasources/ domain/ entities/ repositories/ usecases/ presentation/ pages/ widgets/ bloc/ home/ data/ domain/ presentation/ core/ utils/ constants/ widgets/ main.dart
12. Code Quality
Linting:
yaml# analysis_options.yaml include: package:flutter_lints/flutter.yaml linter: rules: - prefer_const_constructors - avoid_print - use_key_in_widget_constructors
Run:
bashflutter analyze # Check for errors flutter format lib/ # Auto-format code
13. Performance
dart// ✅ Use const constructors const Text('Hello'); // ✅ Avoid rebuilding entire tree class MyWidget extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ const Header(), // Never rebuilds DynamicContent(), // Only this rebuilds ], ); } } // ✅ Use ListView.builder for long lists ListView.builder( itemCount: 1000, itemBuilder: (context, index) => ListTile(...), )
14. Security
dart// ✅ Use secure storage for tokens final storage = FlutterSecureStorage(); await storage.write(key: 'auth_token', value: token); // ✅ Use HTTPS only final response = await http.get(Uri.parse('https://api.example.com')); // ❌ Never hardcode API keys // const apiKey = 'sk-1234567890'; ❌
Summary Checklist
- Separate code into Presentation, Domain, Data layers
- Use dependency injection (get_it, injectable)
- Implement repository pattern
- Use immutable entities (const, copyWith)
- Write unit, widget, and integration tests
- Follow SOLID principles
- Use BLoC or Riverpod for state management
- Handle errors at every layer
- Use const constructors for performance
- Follow Flutter linting rules
Learn more: