# Design Patterns Common design patterns for building robust Flutter applications. ## Command Pattern Encapsulates actions as objects with state management and Result handling. ### Purpose - Encapsulate complex logic - Expose running and error states - Prevent multiple simultaneous executions - Type-safe error handling with Result type ### Implementation See `command.dart` asset for template implementation. Adapt imports and naming to the target project before copying it into app code. **Usage:** ```dart class MyViewModel extends ChangeNotifier { // Command without arguments Command0 get loadData => _loadDataCommand; late final _loadDataCommand = Command0(_loadData); // Command with one argument Command1 get saveItem => _saveCommand; late final _saveCommand = Command1(_saveItem); Future> _loadData() async { // Logic here return Result.ok(null); } Future> _saveItem(String item) async { // Logic here return Result.ok(null); } } ``` **In View:** ```dart ListenableBuilder( listenable: viewModel.loadData, builder: (context, child) { if (viewModel.loadData.running) { return CircularProgressIndicator(); } if (viewModel.loadData.error) { return Text('Error: ${viewModel.loadData.result?.asError.error}'); } return ElevatedButton( onPressed: () => viewModel.loadData.execute(), child: Text('Load Data'), ); }, ); ``` ### Benefits - Prevents double-taps - Exposes loading, error, success states - Type-safe Result handling - Encapsulates complex logic ## Result Type Type-safe error handling without exceptions. ### Purpose - Represent success or failure states - Type-safe error handling - Avoid try-catch blocks - Clear error propagation ### Implementation See `result.dart` asset for template implementation. Use it only when the app does not already have an equivalent typed error/result abstraction. **Usage:** ```dart Future> fetchUser(String id) async { try { final response = await http.get('/users/$id'); final user = User.fromJson(response.body); return Result.ok(user); } catch (e) { return Result.error(Exception('Failed to fetch user: $e')); } } // Consume result final result = await fetchUser('123'); switch (result) { case Ok(): print('User: ${result.value.name}'); case Error(): print('Error: ${result.error}'); } ``` ### Benefits - Explicit error handling - Type-safe - No uncaught exceptions - Clear state representation ## Repository Pattern Abstraction over data sources providing single source of truth. ### Purpose - Hide data source complexity - Provide single source of truth - Enable caching and offline support - Facilitate testing ### Implementation ```dart class TodoRepository { final ApiService _apiService; final DatabaseService _databaseService; final BehaviorSubject> _todos = BehaviorSubject.seeded([]); Stream> get todos => _todos.stream; TodoRepository(this._apiService, this._databaseService) { _loadTodos(); } Future _loadTodos() async { try { final todos = await _databaseService.getTodos(); _todos.add(todos); _refreshFromServer(); } catch (e) { // Handle error } } Future _refreshFromServer() async { try { final todos = await _apiService.fetchTodos(); await _databaseService.saveTodos(todos); _todos.add(todos); } catch (e) { // Keep cached data on network error } } Future> addTodo(Todo todo) async { try { final savedTodo = await _apiService.createTodo(todo); await _databaseService.saveTodo(savedTodo); final updated = [..._todos.value, savedTodo]; _todos.add(updated); return Result.ok(savedTodo); } catch (e) { return Result.error(e is Exception ? e : Exception(e.toString())); } } } ``` ### Benefits - Unified data access interface - Caching layer - Error handling centralized - Easy to mock for testing ## Optimistic UI Update UI immediately, then sync with server. ### Purpose - Improve perceived performance - Better user experience - Handle network delays gracefully ### Implementation ```dart class TodoViewModel extends ChangeNotifier { final TodoRepository _repository; List _todos = []; TodoViewModel(this._repository); List get todos => _todos; Command0 get addTodo => _addTodoCommand; late final _addTodoCommand = Command0(_executeAddTodo); Future> _executeAddTodo() async { // Optimistic update final tempTodo = Todo( id: DateTime.now().millisecondsSinceEpoch.toString(), title: 'New Todo', isComplete: false, ); _todos = [..._todos, tempTodo]; notifyListeners(); // Actual API call final result = await _repository.addTodo(tempTodo); if (result case Error()) { // Rollback on error _todos = _todos.where((t) => t.id != tempTodo.id).toList(); notifyListeners(); return result; } // Update with server response final serverTodo = result.asOk.value; _todos = _todos.map((t) => t.id == tempTodo.id ? serverTodo : t).toList(); notifyListeners(); return Result.ok(serverTodo); } } ``` ### Benefits - Instant UI response - Better perceived performance - Graceful error handling with rollback ## Offline-First Cache data locally, sync when online. ### Purpose - Support offline usage - Reduce network calls - Improve performance - Handle poor connectivity ### Key Concepts - Local cache as primary data source - Background sync when online - Optimistic updates with conflict resolution - Network status monitoring ### Implementation Pattern ```dart class TodoRepository { final ApiService _api; final LocalStorage _local; Stream> get todos { // Stream from local cache (always available) return _local.todosStream; } Future sync() async { if (!await _hasConnection()) return; final remote = await _api.getTodos(); // Conflict resolution: remote wins await _local.saveTodos(remote); } Future> addTodo(Todo todo) async { // Save locally first (offline support) await _local.saveTodo(todo); // Sync when online if (await _hasConnection()) { final result = await _api.createTodo(todo); switch (result) { case Ok(): await _local.saveTodo(result.value); return result; case Error(): return result; } } return Result.ok(todo); } } ``` ## Dependency Injection Provide dependencies externally rather than creating internally. ### Purpose - Testability (inject mocks) - Loose coupling - Flexibility (swap implementations) - Clear dependencies ### Implementation Patterns **Constructor Injection:** ```dart class TodoViewModel extends ChangeNotifier { final TodoRepository _repository; TodoViewModel(this._repository); } ``` **Provider Setup:** ```dart void main() { final apiService = ApiService(); final databaseService = DatabaseService(); final repository = TodoRepository(apiService, databaseService); final viewModel = TodoViewModel(repository); runApp( MultiProvider( providers: [ Provider.value(value: repository), ChangeNotifierProvider.value(value: viewModel), ], child: MyApp(), ), ); } ``` **Usage:** ```dart class TodoScreen extends StatelessWidget { @override Widget build(BuildContext context) { final viewModel = context.watch(); // Use viewModel } } ``` ### Benefits - Easy testing with mocks - Clear dependency graph - Flexible implementation swapping - Follows inversion of control principle ## Key-Value Storage Simple data persistence for app settings and small data. ### Purpose - Store app settings - Persist user preferences - Cache small data ### Implementation (SharedPreferences) ```dart class SettingsRepository { final SharedPreferences _prefs; SettingsRepository(this._prefs); static const _themeKey = 'theme_mode'; static const _languageKey = 'language'; ThemeMode get themeMode { final value = _prefs.getString(_themeKey); return value == 'dark' ? ThemeMode.dark : ThemeMode.light; } Future setThemeMode(ThemeMode mode) async { await _prefs.setString(_themeKey, mode == ThemeMode.dark ? 'dark' : 'light'); } String get language => _prefs.getString(_languageKey) ?? 'en'; Future setLanguage(String lang) async { await _prefs.setString(_languageKey, lang); } } ``` ## SQL Database Structured data storage for complex data models. ### Purpose - Store complex relational data - Query and filter efficiently - Transaction support - Offline-first data layer ### Implementation (sqflite) ```dart class TodoDatabase { static const _tableName = 'todos'; static const _id = 'id'; static const _title = 'title'; static const _isComplete = 'is_complete'; final Database _database; TodoDatabase(this._database); Future init() async { await _database.execute(''' CREATE TABLE $_tableName ( $_id TEXT PRIMARY KEY, $_title TEXT NOT NULL, $_isComplete INTEGER NOT NULL ) '''); } Future> getTodos() async { final results = await _database.query(_tableName); return results.map((map) => Todo.fromJson(map)).toList(); } Future saveTodo(Todo todo) async { await _database.insert( _tableName, todo.toJson(), conflictAlgorithm: ConflictAlgorithm.replace, ); } Future deleteTodo(String id) async { await _database.delete( _tableName, where: '$_id = ?', whereArgs: [id], ); } } ``` ## When to Use Each Pattern | Pattern | Use When | |---------|----------| | Command | Encapsulating user actions, need loading/error states | | Result | Type-safe error handling, avoiding exceptions | | Repository | Abstracting data sources, need caching | | Optimistic UI | Instant UI response, network latency issues | | Offline-First | Poor connectivity, need offline support | | Dependency Injection | Testing, loose coupling | | Key-Value Storage | App settings, small data, preferences | | SQL Database | Complex data, queries, relationships |