Implementing Design Patterns
1. Implementing Repository Pattern
| Aspect | Description | Implementation |
|---|---|---|
| Repository Interface | Defines contract for data access operations | abstract class UserRepository { Future<User> findById(String id); Future<List<User>> findAll(); Future<void> save(User user); } |
| Concrete Implementation | Provides actual implementation using data sources | class UserRepositoryImpl implements UserRepository { final UserDataSource dataSource; UserRepositoryImpl(this.dataSource); @override Future<User> findById(String id) { return dataSource.fetchUser(id); } @override Future<List<User>> findAll() { return dataSource.fetchAllUsers(); } @override Future<void> save(User user) { return dataSource.saveUser(user); } } |
| Data Source | Abstracts data source (local, remote) | abstract class UserDataSource { Future<User> fetchUser(String id); Future<List<User>> fetchAllUsers(); Future<void> saveUser(User user); } |
Example: Using Repository in a Bloc
class UserBloc extends Bloc<UserEvent, UserState> { final UserRepository userRepository; UserBloc(this.userRepository) : super(UserInitial()); @override Stream<UserState> mapEventToState(UserEvent event) async* { if (event is FetchUser) { yield UserLoading(); try { final user = await userRepository.findById(event.userId); yield UserLoaded(user); } catch (e) { yield UserError(e.toString()); } } }}
Note: Repository pattern helps in decoupling the UI layer from data sources, making it easier to switch between local and remote data.
2. Implementing Singleton Pattern
| Aspect | Description | Notes |
|---|---|---|
| Implementation Method | static final Singleton instance = Singleton._internal(); |
Private constructor ensures single instance |
| Constructor | Singleton._internal(); |
Private constructor prevents external instantiation |
| Access Method | Singleton.instance |
Provides global access to the singleton instance |
Example: Singleton Implementation in Flutter
class Singleton {
static final Singleton _instance = Singleton._internal();
factory Singleton() {
return _instance;
}
Singleton._internal();
}
Note: The Singleton pattern in Flutter is useful for managing global state or configuration, but use it sparingly to avoid tight coupling and maintainability issues.
3. Implementing Factory Pattern
| Aspect | Description | Implementation |
|---|---|---|
| Purpose | Creates objects without specifying the exact class to be instantiated | Define a factory method in a base class or interface |
| Types | Simple Factory, Factory Method, Abstract Factory | Simple Factory: Single factory class; Factory Method: Subclass decides the type; Abstract Factory: Creates families of related objects |
| Benefits | Loose coupling, extensibility, centralized object creation logic | Easy to add new types without modifying existing code |
Example: Simple Factory in Flutter
abstract class Animal {
String makeSound();
}
class Dog extends Animal {
@override
String makeSound() => "Woof!";
}
class Cat extends Animal {
@override
String makeSound() => "Meow!";
}
class AnimalFactory {
static Animal createAnimal(String type) {
switch (type) {
case "dog":
return Dog();
case "cat":
return Cat();
default:
throw Exception("Unknown animal type");
}
}
}
// Usage
final dog = AnimalFactory.createAnimal("dog");
print(dog.makeSound()); // Woof!
Note: The Factory Pattern is useful for creating objects with similar interfaces but different implementations, promoting code reuse and flexibility.
4. Using Builder Pattern
| Aspect | Description | Usage |
|---|---|---|
| Builder Pattern | Creates complex objects step-by-step using a builder class | Useful for constructing UI widgets with many optional parameters |
Example: Builder Pattern in Flutter
class MyWidgetBuilder {
String title = '';
int count = 0;
Color color = Colors.blue;
MyWidgetBuilder setTitle(String title) {
this.title = title;
return this;
}
MyWidgetBuilder setCount(int count) {
this.count = count;
return this;
}
MyWidgetBuilder setColor(Color color) {
this.color = color;
return this;
}
Widget build() {
return Container(
child: Text('$title: $count'),
color: color,
);
}
}
// Usage
Widget widget = MyWidgetBuilder()
.setTitle('Hello')
.setCount(42)
.setColor(Colors.red)
.build();
Benefits of Builder Pattern in Flutter
- Improves readability for widgets with many parameters
- Enables fluent API for widget construction
- Reduces boilerplate code in widget creation
Note: The Builder pattern is particularly useful in Flutter for constructing complex UIs with optional parameters, enhancing code maintainability and clarity.
5. Implementing Observer Pattern
| Component | Description | Usage |
|---|---|---|
| ValueNotifier | Simple observable object that notifies listeners when its value changes. | final counter = ValueNotifier<int>(0); |
| ChangeNotifier | More flexible than ValueNotifier, supports multiple listeners and custom logic. | class CounterModel extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } } |
| Provider | Framework-level solution for state management using ChangeNotifier or ValueNotifier. | final counter = Provider.of<CounterModel>(context); |
Example: Using ChangeNotifier
import 'package:flutter/material.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Observer Pattern')),
body: Center(
child: Consumer<CounterModel>(
builder: (context, counter, child) =>
Text('Count: ${counter.count}'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final counter = Provider.of<CounterModel>(context, listen: false);
counter.increment();
},
child: Icon(Icons.add),
),
);
}
}
Example: Using ValueNotifier
import 'package:flutter/material.dart';
class ValueNotifierExample extends StatelessWidget {
final counter = ValueNotifier<int>(0);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ValueNotifier Example')),
body: Center(
child: ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, child) =>
Text('Count: $value'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => counter.value++,
child: Icon(Icons.add),
),
);
}
}
Observer Pattern Implementation Summary
- ValueNotifier: Best for simple, single-value state changes.
- ChangeNotifier: Ideal for more complex state with multiple listeners and custom logic.
- Provider: Provides a scalable way to manage state across the app using ChangeNotifier or ValueNotifier.
6. Implementing Strategy Pattern
| Component | Description | Implementation |
|---|---|---|
| Strategy Interface | Defines the contract for different algorithms | |
| Concrete Strategies | Implement the strategy interface with specific logic | |
| Context | Uses a strategy to perform the operation | |
Example: Using Strategy Pattern in Flutter
class SortingWidget extends StatelessWidget {
final SortContext context = SortContext();
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () {
context.setSortStrategy(BubbleSort());
// Sort data using bubble sort
},
child: Text('Bubble Sort'),
),
ElevatedButton(
onPressed: () {
context.setSortStrategy(QuickSort());
// Sort data using quick sort
},
child: Text('Quick Sort'),
),
],
);
}
}
Note: Strategy pattern allows switching between sorting algorithms at runtime, making the code more flexible and maintainable.
7. Using Dependency Injection
| Pattern | Description | Usage in Flutter |
|---|---|---|
| Provider | State management solution that allows sharing data between widgets | Use ChangeNotifierProvider or Provider to inject dependencies |
| GetIt | Simple service locator for dependency injection | Register services with GetIt.I.registerSingleton<Service>(service) |
| InheritedWidget | Framework-level mechanism to pass data down the widget tree | Extend InheritedWidget to share data across multiple widgets |
Example: Using Provider for DI
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class UserService {
Future<String> fetchUserData() async {
return 'User Data';
}
}
class UserWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => UserService(),
child: Consumer<UserService>(
builder: (context, service, child) {
return Text(service.fetchUserData());
},
),
);
}
}
Example: Using GetIt for DI
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
class MyService {
String getData() => 'Service Data';
}
void main() {
getIt.registerSingleton<MyService>(MyService());
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final service = getIt<MyService>();
return MaterialApp(
home: Scaffold(
body: Center(child: Text(service.getData())),
),
);
}
}
Note:
Provider is built into Flutter and is ideal for state management, while GetIt is a lightweight DI container that can be used for more complex dependency graphs.
8. Implementing MVVM Architecture
| Component | Description | Implementation |
|---|---|---|
| Model | Represents the data and business logic of the application. | class UserModel {<br> String name;<br> int age;<br><br> UserModel({required this.name, required this.age});<br>} |
| View | Represents the UI layer, typically built with Flutter widgets. | class UserView extends StatelessWidget {<br> final UserModel user;<br><br> const UserView({Key? key, required this.user}) : super(key: key);<br><br> @override<br> Widget build(BuildContext context) {<br> return Text('Name: ${user.name}, Age: ${user.age}');<br> }<br>} |
| ViewModel | Acts as a bridge between the Model and View, handling UI logic. | class UserViewModel extends ChangeNotifier {<br> UserModel _user = UserModel(name: '', age: 0);<br><br> UserModel get user => _user;<br><br> void updateUser(String name, int age) {<br> _user = UserModel(name: name, age: age);<br> notifyListeners();<br> }<br>} |
Example: Using ChangeNotifier for State Management
class MyHomePage extends StatelessWidget {<br> @override<br> Widget build(BuildContext context) {<br> return ChangeNotifierProvider(<br> create: (context) => UserViewModel(),<br> child: Consumer<UserViewModel>( <br> builder: (context, viewModel, child) {<br> return UserView(user: viewModel.user);<br> },<br> ),<br> );<br> }<br>}
Note: The
ChangeNotifier pattern is commonly used in Flutter for state management, especially with the Provider package.
Key Benefits of MVVM in Flutter
- Separation of Concerns: Clear separation between UI, business logic, and data.
- Testability: Easier to unit test business logic without UI dependencies.
- Maintainability: Changes in one layer don't directly affect others.
9. Implementing Clean Architecture
Example: Project Structure
lib/
core/
use_cases/
get_user.dart
repositories/
user_repository.dart
features/
auth/
domain/
models/
user.dart
use_cases/
login.dart
presentation/
screens/
login_screen.dart
widgets/
login_form.dart
data/
models/
user_response.dart
repositories/
auth_repository_impl.dart
| Layer | Description | Responsibility |
|---|---|---|
| Core Layer | lib/core/ |
Shared logic, use cases, and repositories |
| Features Layer | lib/features/ |
Feature-specific logic and UI |
| Data Layer | lib/features/*/data/ |
Concrete implementations of repositories |
Key Principles
- Separation of Concerns: Each layer has a distinct responsibility.
- Dependency Inversion: Higher layers depend on abstractions, not concrete implementations.
- Single Responsibility: Each use case or repository handles one responsibility.
Implementation Steps
- Create core use cases and repositories interfaces
- Implement data layer with concrete repository classes
- Inject dependencies using dependency injection (e.g.,
get_it) - Use use cases in presentation layer to manage state
Note: Clean Architecture in Flutter is often implemented using the
flutter_clean_architecture package or custom implementations with dependency injection.
Clean Architecture vs. MVC
- Clean Architecture: Separates business logic from UI, easier to test and maintain
- MVC: Tight coupling between UI and business logic, harder to scale
10. Using Service Locator Pattern
| Aspect | Description | Implementation |
|---|---|---|
| Service Registration | Registering services with the locator | final locator = getIt<ServiceLocator>();\nlocator.registerSingleton<MyService>(MyService()); |
| Service Retrieval | Fetching services by type | final service = locator<MyService>(); |
| Dependency Injection | Injecting services into widgets or classes | class MyWidget extends StatelessWidget {\n final MyService service = locator<MyService>();\n // Use service\n} |
Example: Service Locator Setup
import 'package:get_it/get_it.dart';\n\nfinal getIt = GetIt.instance;\n\nclass MyService {\n void doSomething() {\n print('Doing something...');\n }\n}\n\nvoid setupServiceLocator() {\n getIt.registerSingleton<MyService>(MyService());\n}
Note: The Service Locator pattern is useful for managing dependencies in larger applications but can make testing harder if overused.