Flutter Architecture Patterns
•7 min read
This guide covers different architectural approaches and best practices for organizing Flutter applications.
1. Clean Architecture
1.1. Domain Layer
// Domain/entities/user.dart class User { final String id; final String name; final String email; User({ required this.id, required this.name, required this.email, }); } // Domain/repositories/user_repository.dart abstract class UserRepository { Future<User> getUser(String id); Future<List<User>> getUsers(); Future<void> saveUser(User user); } // Domain/usecases/get_user.dart class GetUser { final UserRepository repository; GetUser(this.repository); Future<User> call(String id) async { return await repository.getUser(id); } }
1.2. Data Layer
// Data/repositories/user_repository_impl.dart class UserRepositoryImpl implements UserRepository { final UserDataSource dataSource; UserRepositoryImpl(this.dataSource); @override Future<User> getUser(String id) async { return await dataSource.getUser(id); } @override Future<List<User>> getUsers() async { return await dataSource.getUsers(); } @override Future<void> saveUser(User user) async { await dataSource.saveUser(user); } } // Data/datasources/user_data_source.dart abstract class UserDataSource { Future<User> getUser(String id); Future<List<User>> getUsers(); Future<void> saveUser(User user); }
2. MVVM Pattern
2.1. ViewModel
class UserViewModel extends ChangeNotifier { final UserRepository repository; User? _user; bool _isLoading = false; User? get user => _user; bool get isLoading => _isLoading; UserViewModel(this.repository); Future<void> loadUser(String id) async { _isLoading = true; notifyListeners(); try { _user = await repository.getUser(id); } catch (e) { // Handle error } finally { _isLoading = false; notifyListeners(); } } }
2.2. View
class UserView extends StatelessWidget { final String userId; const UserView({Key? key, required this.userId}) : super(key: key); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => UserViewModel(context.read<UserRepository>()), child: Consumer<UserViewModel>( builder: (context, viewModel, child) { if (viewModel.isLoading) { return const CircularProgressIndicator(); } final user = viewModel.user; if (user == null) { return const Text('User not found'); } return Column( children: [ Text('Name: ${user.name}'), Text('Email: ${user.email}'), ], ); }, ), ); } }
3. BLoC Pattern
3.1. BLoC Implementation
// Events abstract class UserEvent {} class LoadUser extends UserEvent { final String id; LoadUser(this.id); } // States 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 repository; UserBloc(this.repository) : super(UserInitial()) { on<LoadUser>(_onLoadUser); } Future<void> _onLoadUser(LoadUser event, Emitter<UserState> emit) async { emit(UserLoading()); try { final user = await repository.getUser(event.id); emit(UserLoaded(user)); } catch (e) { emit(UserError(e.toString())); } } }
3.2. BLoC Usage
class UserScreen extends StatelessWidget { final String userId; const UserScreen({Key? key, required this.userId}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => UserBloc(context.read<UserRepository>()) ..add(LoadUser(userId)), child: BlocBuilder<UserBloc, UserState>( builder: (context, state) { if (state is UserLoading) { return const CircularProgressIndicator(); } if (state is UserError) { return Text('Error: ${state.message}'); } if (state is UserLoaded) { final user = state.user; return Column( children: [ Text('Name: ${user.name}'), Text('Email: ${user.email}'), ], ); } return const SizedBox(); }, ), ); } }
4. Repository Pattern
4.1. Repository Implementation
class UserRepositoryImpl implements UserRepository { final UserLocalDataSource localDataSource; final UserRemoteDataSource remoteDataSource; final NetworkInfo networkInfo; UserRepositoryImpl({ required this.localDataSource, required this.remoteDataSource, required this.networkInfo, }); @override Future<User> getUser(String id) async { if (await networkInfo.isConnected) { try { final remoteUser = await remoteDataSource.getUser(id); await localDataSource.cacheUser(remoteUser); return remoteUser; } catch (e) { return await localDataSource.getUser(id); } } else { return await localDataSource.getUser(id); } } }
4.2. Data Sources
abstract class UserLocalDataSource { Future<User> getUser(String id); Future<void> cacheUser(User user); } abstract class UserRemoteDataSource { Future<User> getUser(String id); } class NetworkInfo { Future<bool> get isConnected async { // Implementation return true; } }
5. Dependency Injection
5.1. Service Locator
class ServiceLocator { static final ServiceLocator _instance = ServiceLocator._internal(); factory ServiceLocator() => _instance; ServiceLocator._internal(); final Map<Type, Object> _services = {}; void register<T>(T service) { _services[T] = service; } T get<T>() { if (!_services.containsKey(T)) { throw Exception('Service $T not found'); } return _services[T] as T; } }
5.2. Usage
void setupDependencies() { final locator = ServiceLocator(); // Register services locator.register<UserRepository>(UserRepositoryImpl( localDataSource: UserLocalDataSourceImpl(), remoteDataSource: UserRemoteDataSourceImpl(), networkInfo: NetworkInfo(), )); // Register BLoCs locator.register<UserBloc>(UserBloc(locator.get<UserRepository>())); }
6. Best Practices
-
Clean Architecture
- Separate concerns
- Define clear boundaries
- Follow dependency rule
-
State Management
- Choose appropriate pattern
- Keep state minimal
- Handle errors properly
-
Dependency Injection
- Use service locator
- Register dependencies
- Follow SOLID principles
-
Testing
- Write unit tests
- Mock dependencies
- Test edge cases
-
Code Organization
- Follow consistent structure
- Use meaningful names
- Document complex logic
Conclusion
Remember these key points:
- Choose appropriate architecture
- Follow clean code principles
- Implement proper state management
- Use dependency injection
- Write testable code
By following these practices, you can:
- Build maintainable apps
- Scale your codebase
- Improve testability
- Enhance collaboration
Keep improving your Flutter architecture!