Skip to main content

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
abstract class SortStrategy {
  List<int> sort(List<int> data);
}
Concrete Strategies Implement the strategy interface with specific logic
class BubbleSort implements SortStrategy {
  @override
  List<int> sort(List<int> data) {
    // Bubble sort implementation
    return data..sort();
  }
}

class QuickSort implements SortStrategy {
  @override
  List<int> sort(List<int> data) {
    // Quick sort implementation
    return data..sort();
  }
}
Context Uses a strategy to perform the operation
class SortContext {
  late SortStrategy _strategy;

  void setSortStrategy(SortStrategy strategy) {
    _strategy = strategy;
  }

  List<int> sortData(List<int> data) {
    return _strategy.sort(data);
  }
}

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

  1. Create core use cases and repositories interfaces
  2. Implement data layer with concrete repository classes
  3. Inject dependencies using dependency injection (e.g., get_it)
  4. 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.

Was this article helpful?