Lesson 30 min

Design Patterns in Dart

00:00 / 00:00

Why Design Patterns Matter in Dart

Design patterns are reusable solutions to recurring software design problems. In Dart and Flutter, specific patterns appear constantly: Singleton for services, Repository for data access, Observer for state changes, Builder for complex widget/object construction.

Learning patterns is not about memorizing solutions — it is about recognizing problem shapes and knowing which solution fits.

Singleton Pattern

dart
// Thread-safe Singleton (Dart's single-isolate makes this simpler) class AuthService { // Private constructor AuthService._(); // Single instance static final AuthService instance = AuthService._(); String? _token; Future<bool> login(String email, String password) async { final response = await apiClient.post('/login', {'email': email, 'password': password}); _token = response.data['token']; return _token != null; } bool get isAuthenticated => _token != null; } // Usage: final auth = AuthService.instance; await auth.login('user@example.com', 'password123');

Repository Pattern

dart
// Abstract contract abstract interface class UserRepository { Future<User?> getById(String id); Future<List<User>> getAll(); Future<void> create(User user); Future<void> update(User user); Future<void> delete(String id); } // Concrete implementation class RemoteUserRepository implements UserRepository { final ApiClient _api; RemoteUserRepository(this._api); Future<User?> getById(String id) async { final response = await _api.get('/users/$id'); return User.fromJson(response.data); } Future<List<User>> getAll() async { final response = await _api.get('/users'); return (response.data as List).map((e) => User.fromJson(e)).toList(); } // ... other methods } // Mock for testing class MockUserRepository implements UserRepository { final _store = <String, User>{}; Future<User?> getById(String id) async => _store[id]; Future<List<User>> getAll() async => _store.values.toList(); Future<void> create(User user) async => _store[user.id] = user; Future<void> update(User user) async => _store[user.id] = user; Future<void> delete(String id) async => _store.remove(id); }

Observer Pattern (via Streams)

dart
// Dart Streams ARE the Observer pattern class EventBus { EventBus._(); static final EventBus instance = EventBus._(); final _controller = StreamController<AppEvent>.broadcast(); Stream<AppEvent> get stream => _controller.stream; void emit(AppEvent event) => _controller.add(event); void dispose() => _controller.close(); } // Emit events anywhere EventBus.instance.emit(UserLoggedInEvent(userId: '123')); // Listen anywhere EventBus.instance.stream .whereType<UserLoggedInEvent>() .listen((event) => print('User logged in: ${event.userId}'));

Builder Pattern

dart
class QueryBuilder { String _table = ''; final _conditions = <String>[]; int? _limit; String? _orderBy; QueryBuilder from(String table) { _table = table; return this; // Return this for chaining } QueryBuilder where(String condition) { _conditions.add(condition); return this; } QueryBuilder limit(int count) { _limit = count; return this; } QueryBuilder orderBy(String field) { _orderBy = field; return this; } String build() { var query = 'SELECT * FROM $_table'; if (_conditions.isNotEmpty) { query += ' WHERE ${_conditions.join(' AND ')}'; } if (_orderBy != null) query += ' ORDER BY $_orderBy'; if (_limit != null) query += ' LIMIT $_limit'; return query; } } // Fluent API usage: final query = QueryBuilder() .from('users') .where('is_active = true') .where('age > 18') .orderBy('created_at DESC') .limit(10) .build();

Summary

Patterns are a vocabulary for design conversations. The Repository pattern decouples your business logic from data sources. Singleton manages shared service instances. Observer (via Streams) enables loose coupling between components. Builder makes complex object construction readable. Master these four and you'll handle 80% of real-world Dart/Flutter architecture needs.

WhatsApp