Design Patterns in Flutter

Mobterest Studio
20 min readJun 3, 2023

--

Design patterns in Flutter

Design pattern is a reusable solution to a commonly occurring problem in software design and development.

Benefits of Design patterns:

  1. Reusability: Design patterns encapsulate reusable solutions, allowing developers to apply proven approaches to similar design problems. This reduces the need for reinventing solutions and promotes code reusability.
  2. Maintainability: Design patterns promote a clear and organized code structure, making it easier to understand and maintain. They provide a common language and framework for developers, enabling better collaboration and communication within teams.
  3. Scalability: Design patterns help in building scalable software systems by providing guidelines for managing complexity and allowing for easy extension or modification of existing code without breaking the system.
  4. Flexibility: Design patterns enable developers to build flexible software that can adapt to changing requirements. They decouple components and promote loose coupling, making it easier to modify or replace individual elements of the system without affecting other parts.
  5. Performance: Some design patterns focus on optimizing performance and resource utilization. They provide techniques for efficient object creation, caching, lazy loading, and other performance-related considerations.

Let’s delve into some of the most commonly used design patterns in the Flutter framework and understand how they can help us build robust and maintainable Flutter apps.

  1. Singleton Pattern:

The Singleton pattern ensures that there is only one instance of a class throughout the application. In Flutter, this pattern is commonly used for managing global state or resources that need to be accessed from multiple parts of the app. The Provider package, built on top of the InheritedWidget, is an excellent example of a Singleton pattern implementation in Flutter. It allows for efficient state management by providing a single instance of a provider class that can be accessed by widgets across the widget tree.

Here’s an example of how you can implement the Singleton pattern in Flutter for a to-do app:

class TaskManager {
static TaskManager _instance;

factory TaskManager() {
if (_instance == null) {
_instance = TaskManager._();
}
return _instance;
}

TaskManager._();

List<Task> tasks = [];

void addTask(Task task) {
tasks.add(task);
}

void removeTask(Task task) {
tasks.remove(task);
}
}

class Task {
final String title;
final String description;

Task({required this.title, required this.description});
}

In the example above, TaskManager is implemented as a singleton class that manages the tasks in a to-do app. The class has a private constructor to prevent direct instantiation from outside the class. The factory constructor TaskManager() checks if an instance of TaskManager already exists, and if not, creates a new instance. The factory constructor ensures that there is only one instance of TaskManager throughout the application.

You can then use the TaskManager to manage tasks within your to-do app. For example, to add a task, you can call TaskManager().addTask(task):

Task newTask = Task(title: "Buy groceries", description: "Milk, eggs, bread");
TaskManager().addTask(newTask);

By using the Singleton pattern, you can ensure that there is only one instance of TaskManager throughout the app, allowing you to manage tasks consistently across different parts of the application.

2. Builder Pattern:

The Builder pattern separates the construction of complex objects from their representation, making the process more flexible and readable. In Flutter, the Builder pattern is often used to create widgets with a fluent API. The ListView.builder and GridView.builder constructors are examples of the Builder pattern in action. They allow developers to efficiently build lists or grids by providing a builder function that constructs individual widgets on-demand as the user scrolls through the list or grid.

Here’s an example of how you can implement the Builder pattern in Flutter for a to-do app:

class Task {
final String title;
final String description;

Task({required this.title, required this.description});
}

class TaskGrid extends StatelessWidget {
final List<Task> tasks;

TaskGrid({required this.tasks});

@override
Widget build(BuildContext context) {
return GridView.builder(
itemCount: tasks.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return buildTaskCard(tasks[index]);
},
);
}

Widget buildTaskCard(Task task) {
return Card(
child: Column(
children: [
Text(task.title),
Text(task.description),
],
),
);
}
}

In the example above, TaskGrid is a Flutter widget that displays a grid of tasks using GridView.builder. It follows the Builder pattern by separating the construction of individual task cards (widgets) from their representation in the grid.

The GridView.builder widget is used to construct the grid, and its itemBuilder parameter takes a builder function that is responsible for constructing the individual task cards. The itemBuilder function is called for each item in the tasks list and is passed the current index. Inside the itemBuilder, we call the buildTaskCard function to construct the task card widget for that particular task.

The buildTaskCard function takes a Task object and returns a Card widget containing the task's title and description. This function represents the builder part of the Builder pattern, encapsulating the creation and configuration of the task card widget.

By using the Builder pattern with GridView.builder, we separate the construction of individual task cards from their representation in the grid, making the code more modular and easier to maintain.

3. Factory Pattern

The Factory pattern is a creational pattern that provides an interface for creating objects but allows subclasses to decide which class to instantiate.

Here’s a simpler example of how you can implement the Factory pattern in Flutter for a to-do app

enum TaskType {
personal,
work,
shopping,
}

class Task {
final String title;
final String description;
final TaskType type;

Task({required this.title, required this.description, required this.type});

factory Task.createPersonal({required String title, required String description}) {
return Task(title: title, description: description, type: TaskType.personal);
}

factory Task.createWork({required String title, required String description}) {
return Task(title: title, description: description, type: TaskType.work);
}

factory Task.createShopping({required String title, required String description}) {
return Task(title: title, description: description, type: TaskType.shopping);
}
}

we have a Task class representing a task in the to-do app. The class has a constructor that takes title, description, and type parameters.

We also have three factory methods: createPersonal, createWork, and createShopping. Each factory method creates and returns a Task object with the specified parameters and sets the appropriate TaskType.

To create instances of tasks, you can use the factory methods like this:

Task personalTask = Task.createPersonal(title: 'Personal Task', description: 'This is a personal task');
Task workTask = Task.createWork(title: 'Work Task', description: 'This is a work-related task');
Task shoppingTask = Task.createShopping(title: 'Shopping Task', description: 'This is a shopping task');

4. Observer Pattern

The Observer pattern establishes a one-to-many dependency between objects, so that when one object changes its state, all its dependents are notified and updated automatically. In Flutter, the Observer pattern is commonly used for state management and reactivity. Libraries like Provider and Bloc implement the Observer pattern, allowing widgets to subscribe to changes in the application state and automatically rebuild when necessary.

Here’s an example of how you can implement the Observer pattern in Flutter for a to-do app:

import 'dart:async';

class Task {
String title;
bool completed;

Task({required this.title, this.completed = false});
}

class TaskManager {
StreamController<Task> _taskController = StreamController<Task>.broadcast();

Stream<Task> get taskStream => _taskController.stream;

void addTask(Task task) {
// Perform any additional logic if needed
_taskController.add(task);
}

void updateTaskCompletion(Task task, bool completed) {
// Perform any additional logic if needed
task.completed = completed;
_taskController.add(task);
}

void dispose() {
_taskController.close();
}
}

class TaskWidget extends StatefulWidget {
final Task task;
final TaskManager taskManager;

TaskWidget({required this.task, required this.taskManager});

@override
_TaskWidgetState createState() => _TaskWidgetState();
}

class _TaskWidgetState extends State<TaskWidget> {
@override
void initState() {
super.initState();
widget.taskManager.taskStream.listen((updatedTask) {
if (updatedTask == widget.task) {
setState(() {
// Update the widget state based on the task changes
});
}
});
}

@override
Widget build(BuildContext context) {
// Build the task widget
return ListTile(
title: Text(widget.task.title),
trailing: Checkbox(
value: widget.task.completed,
onChanged: (value) {
widget.taskManager.updateTaskCompletion(widget.task, value!);
},
),
);
}

@override
void dispose() {
widget.taskManager.dispose();
super.dispose();
}
}

5. Decorator Pattern

The Decorator pattern allows behavior to be added to an object dynamically, without changing its original class. In Flutter, the Decorator pattern is employed through the concept of mixins. Mixins are classes that provide additional functionality to other classes, without requiring inheritance. They can be used to add features like animation, gesture recognition, or data parsing to existing widgets or classes, making them more flexible and reusable.

Here’s an example of how you can implement the Decorator pattern in Flutter for a to-do app using mixins:

abstract class Task {
String get title;
String get description;
}

class SimpleTask implements Task {
@override
final String title;
@override
final String description;

SimpleTask({required this.title, required this.description});
}

mixin Decorator implements Task {
Task task;

Decorator(this.task);

@override
String get title => task.title;

@override
String get description => task.description;
}

class PriorityTaskDecorator with Decorator {
final String priority;

PriorityTaskDecorator(Task task, this.priority) : super(task);

@override
String get title => '${task.title} [$priority]';

@override
String get description => task.description;
}

class DueDateTaskDecorator with Decorator {
final DateTime dueDate;

DueDateTaskDecorator(Task task, this.dueDate) : super(task);

@override
String get title => task.title;

@override
String get description => '${task.description} - Due Date: $dueDate';
}

In the example above, we have an abstract Task class representing a task in the to-do app. It has two properties: title and description.

The SimpleTask class is a concrete implementation of the Task interface. It represents a basic task without any additional decorations.

The Decorator mixin is used to define the base decorator functionality. It implements the Task interface and holds a reference to the original task object. The mixin overrides the title and description getters to provide the decorated versions.

The PriorityTaskDecorator and DueDateTaskDecorator classes are concrete decorators that extend the Decorator mixin. They add additional functionality or modify the behavior of the underlying task. For example, the PriorityTaskDecorator adds a priority label to the task's title, while the DueDateTaskDecorator adds the due date to the task's description.

Here’s an example of how you can use the decorators:

void main() {
Task task = SimpleTask(title: 'Buy groceries', description: 'Milk, eggs, bread');

Task priorityTask = PriorityTaskDecorator(task, 'High');
print(priorityTask.title); // Output: Buy groceries [High]
print(priorityTask.description); // Output: Milk, eggs, bread

Task dueDateTask = DueDateTaskDecorator(task, DateTime.now());
print(dueDateTask.title); // Output: Buy groceries
print(dueDateTask.description); // Output: Milk, eggs, bread - Due Date: 2023-06-02 12:00:00.000
}

In the example above, we create a SimpleTask and then apply decorators to it using the PriorityTaskDecorator and DueDateTaskDecorator. Each decorator adds additional functionality or modifies the task's properties.

The output shows how the decorators affect the task’s title and description based on their specific modifications.

By using the Decorator pattern with mixins, you can dynamically add or modify the behavior of objects at runtime without changing their underlying classes. This allows for flexible and reusable composition of objects with different combinations of features or decorations.

6. Strategy Pattern:

The Strategy pattern defines a family of interchangeable algorithms, encapsulating each one and making them interchangeable at runtime.

Here’s an example of how you can implement the Strategy pattern in Flutter for a to-do app:

class Task {
final String title;
final DateTime dueDate;
final int priority;

Task({required this.title, required this.dueDate, required this.priority});
}

typedef TaskSortingStrategy = List<Task> Function(List<Task> tasks);

List<Task> sortByDueDate(List<Task> tasks) {
tasks.sort((a, b) => a.dueDate.compareTo(b.dueDate));
return tasks;
}

List<Task> sortByPriority(List<Task> tasks) {
tasks.sort((a, b) => a.priority.compareTo(b.priority));
return tasks;
}

class TaskManager {
List<Task> _tasks = [];
TaskSortingStrategy _sortingStrategy;

TaskManager(this._sortingStrategy);

void addTask(Task task) {
_tasks.add(task);
}

void setSortingStrategy(TaskSortingStrategy strategy) {
_sortingStrategy = strategy;
}

List<Task> getSortedTasks() {
return _sortingStrategy(_tasks);
}
}

In this simplified example, we have a Task class representing a task in the to-do app, with properties like title, dueDate, and priority.

The TaskSortingStrategy is a function type that defines the strategy interface for sorting tasks. It takes a list of tasks as input and returns the sorted list.

We have two sorting functions, sortByDueDate and sortByPriority, which implement the sorting logic based on the due date and priority of tasks, respectively.

The TaskManager class manages tasks and sorting strategies. It has a list of tasks _tasks and a reference to a TaskSortingStrategy _sortingStrategy. The addTask method adds a task to the list, the setSortingStrategy method allows changing the sorting strategy at runtime, and the getSortedTasks method returns the sorted list of tasks based on the current sorting strategy.

Here’s an example of how you can use the Strategy pattern in the to-do app:

void main() {
TaskManager taskManager = TaskManager(sortByDueDate);

Task task1 = Task(title: 'Task 1', dueDate: DateTime(2023, 6, 1), priority: 2);
Task task2 = Task(title: 'Task 2', dueDate: DateTime(2023, 6, 2), priority: 1);
Task task3 = Task(title: 'Task 3', dueDate: DateTime(2023, 6, 3), priority: 3);

taskManager.addTask(task1);
taskManager.addTask(task2);
taskManager.addTask(task3);

List<Task> sortedTasks = taskManager.getSortedTasks();
for (Task task in sortedTasks) {
print('Task: ${task.title}, Due Date: ${task.dueDate}, Priority: ${task.priority}');
}
}

In this example, we create a TaskManager instance with sortByDueDate as the initial sorting strategy. We add three tasks to the TaskManager.

To switch to a different sorting strategy, you can use taskManager.setSortingStrategy(sortByPriority).

The output will display the tasks sorted based on the selected sorting strategy.

7. Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together by providing a common interface that both parties understand.

Here’s an example of how you can implement the Adapter pattern in Flutter for a to-do app:

// External Library
class ExternalTask {
final String name;
final String description;

ExternalTask({required this.name, required this.description});
}

// Adapter Interface
abstract class TaskAdapter {
String getTitle();
String getDescription();
}

// Adapter
class ExternalTaskAdapter implements TaskAdapter {
final ExternalTask externalTask;

ExternalTaskAdapter(this.externalTask);

@override
String getTitle() {
return externalTask.name;
}

@override
String getDescription() {
return externalTask.description;
}
}

// Client Code
class Task {
final String title;
final String description;

Task({required this.title, required this.description});
}

void main() {
// External Task from an external library
ExternalTask externalTask = ExternalTask(name: 'External Task', description: 'External Task Description');

// Create an Adapter
TaskAdapter adapter = ExternalTaskAdapter(externalTask);

// Use the Adapter as a regular Task object
Task task = Task(title: adapter.getTitle(), description: adapter.getDescription());

print('Task: ${task.title}');
print('Description: ${task.description}');
}

In the example above, we have an ExternalTask class representing a task from an external library. It has properties name and description.

The TaskAdapter interface defines the methods getTitle() and getDescription() that the adapter needs to implement. It acts as a bridge between the external library and our application.

The ExternalTaskAdapter class implements the TaskAdapter interface. It takes an ExternalTask object as input and provides the necessary adaptations to make it compatible with our Task class. It internally delegates the calls to the corresponding methods of the ExternalTask.

In the client code, we create an instance of ExternalTask from the external library. Then, we create an adapter ExternalTaskAdapter with the ExternalTask object. The adapter implements the TaskAdapter interface.

We can now use the adapter as a regular Task object in our application. We create a Task instance, passing the adapted title and description using the adapter's methods.

Finally, we print the title and description of the Task object, which shows that the adapter successfully converts the external task to our internal Task representation.

The Adapter pattern allows us to adapt incompatible classes/interfaces to make them work together seamlessly. In this case, the ExternalTaskAdapter acts as a bridge between the external library's ExternalTask and our internal Task class.

8. Template Pattern

The Template pattern is a behavioral design pattern that allows you to define the skeleton or structure of an algorithm in a superclass, while letting subclasses provide specific implementations of certain steps of the algorithm. It promotes code reusability and provides a way to define a common structure while allowing flexibility in the implementation details.

Here’s an example of how you can implement the template pattern in Flutter for a to-do app:

Consider a scenario where you want to define a template for saving tasks to a database. The template method will handle the common steps of saving, such as opening the database connection and closing it, while the subclasses will provide the specific implementation for inserting the task into the database.

// Abstract Task Repository
abstract class TaskRepository {
void saveTask(String task);

void openConnection();

void closeConnection();

void saveToDatabase(String task);

// Template Method
void saveTaskToDatabase(String task) {
openConnection();
saveToDatabase(task);
closeConnection();
}
}

// Concrete Task Repository
class FirebaseTaskRepository extends TaskRepository {
@override
void openConnection() {
print('Opening connection to Firebase Database...');
}

@override
void closeConnection() {
print('Closing connection to Firebase Database...');
}

@override
void saveToDatabase(String task) {
print('Saving task to Firebase Database: $task');
// Implementation for saving task to Firebase
}
}

// Client Code
void main() {
TaskRepository taskRepository = FirebaseTaskRepository();

taskRepository.saveTaskToDatabase('Task 1');
}

In the example above, we have an abstract class TaskRepository representing the template for saving tasks to a database. It defines the template method saveTaskToDatabase that encapsulates the common steps for saving a task.

The abstract class also declares abstract methods openConnection, closeConnection, and saveToDatabase to be implemented by the concrete subclasses.

We then have a concrete class FirebaseTaskRepository that extends TaskRepository and provides the specific implementation for saving tasks to a Firebase database.

In the client code, we create an instance of FirebaseTaskRepository and call the saveTaskToDatabase method, which internally calls the template method. The template method handles the common steps of opening the connection, saving the task to the database, and closing the connection.

The output of the example will show the execution of the template method with the specific steps implemented in the FirebaseTaskRepository class.

9. Composite Pattern

The Composite pattern is a structural design pattern that allows you to treat individual objects and groups of objects uniformly, forming a tree-like hierarchical structure. It lets clients treat individual objects and compositions of objects uniformly, simplifying the client’s code and enabling recursive operations on the entire structure.

// Abstract Component
abstract class TaskComponent {
void add(TaskComponent component);
void remove(TaskComponent component);
void display();
}

// Leaf Component: Individual Task
class Task implements TaskComponent {
final String name;

Task(this.name);

@override
void add(TaskComponent component) {
throw UnimplementedError('Cannot add to an individual task');
}

@override
void remove(TaskComponent component) {
throw UnimplementedError('Cannot remove from an individual task');
}

@override
void display() {
print('Task: $name');
}
}

// Composite Component: Task Category
class TaskCategory implements TaskComponent {
final String name;
final List<TaskComponent> tasks;

TaskCategory(this.name) : tasks = [];

@override
void add(TaskComponent component) {
tasks.add(component);
}

@override
void remove(TaskComponent component) {
tasks.remove(component);
}

@override
void display() {
print('Category: $name');
for (var task in tasks) {
task.display();
}
}
}

// Client Code
void main() {
TaskComponent personalTasks = TaskCategory('Personal Tasks');
TaskComponent workTasks = TaskCategory('Work Tasks');

personalTasks.add(Task('Buy groceries'));
personalTasks.add(Task('Pay bills'));

workTasks.add(Task('Prepare presentation'));
workTasks.add(Task('Attend meeting'));

TaskComponent allTasks = TaskCategory('All Tasks');
allTasks.add(personalTasks);
allTasks.add(workTasks);

allTasks.display();
}

In the example above, we have an abstract class TaskComponent that serves as the common interface for both individual tasks and task categories. It declares methods like add, remove, and display that are implemented by both the leaf components (individual tasks) and the composite components (task categories).

The Task class represents an individual task, while the TaskCategory class represents a category that can contain multiple tasks. The TaskCategory class has a list of TaskComponent objects to store its child components.

In the client code, we create instances of Task and TaskCategory to represent individual tasks and task categories. We add tasks to the task categories using the add method. We can also remove tasks using the remove method if needed.

We create a root TaskCategory called allTasks and add the personalTasks and workTasks categories to it. Finally, we call the display method on the root component to display the hierarchical structure of tasks and categories.

The output of the example will show the hierarchical structure of tasks and categories, demonstrating how the Composite pattern allows you to treat individual tasks and task categories uniformly.

10. State Pattern

The State pattern is often used in combination with the StatefulWidget and State classes to manage the state and behavior of widgets dynamically.

The StatefulWidget class represents the context or container for managing the state, while the State class represents different states and handles their behaviors. When the state changes, the StatefulWidget updates its reference to the new State object, causing the widget to rebuild and reflect the new state and behavior.

Let’s consider a scenario where you want to implement different behaviors for a task based on its completion status. We can use the State pattern to manage the task state and dynamically update its appearance and behavior.

import 'package:flutter/material.dart';

// Task Widget
class TaskWidget extends StatefulWidget {
@override
_TaskWidgetState createState() => _TaskWidgetState();
}

// Task State
class _TaskWidgetState extends State<TaskWidget> {
bool _isComplete = false;

void toggleCompletionStatus() {
setState(() {
_isComplete = !_isComplete;
});
}

@override
Widget build(BuildContext context) {
return ListTile(
title: Text('Task'),
leading: Icon(_isComplete ? Icons.check_box : Icons.check_box_outline_blank),
onTap: toggleCompletionStatus,
);
}
}

// Main App
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('To-Do App')),
body: ListView(
children: [
TaskWidget(),
TaskWidget(),
],
),
),
));
}

In the example above, we have a TaskWidget representing a task item in the to-do app. The TaskWidget is a StatefulWidget, which means it has a mutable state that can change over time.

The _TaskWidgetState class represents the state of the TaskWidget. It contains a boolean _isComplete variable to track the completion status of the task. The toggleCompletionStatus method is called when the task is tapped, and it updates the state by toggling the _isComplete variable using setState.

In the build method of the _TaskWidgetState, we use the current state _isComplete to determine the appearance and behavior of the task widget. It displays a ListTile with a title, leading checkbox icon (either checked or unchecked based on the completion status), and an onTap callback to toggle the completion status.

In the main app, we create a MaterialApp with a Scaffold and a ListView containing multiple instances of TaskWidget to represent multiple tasks. Tapping on a task will toggle its completion status, and the widget will rebuild to reflect the updated state.

11. Facade Pattern

The Facade pattern is a structural design pattern that provides a simplified interface or a higher-level abstraction layer for accessing complex subsystems or performing multiple operations. It hides the complexities and details of the underlying implementation and provides a simple and straightforward interface for clients to interact with.

Let’s consider a scenario where you want to provide a simplified interface to handle tasks. We can use the Facade pattern to create a ToDoFacade class that encapsulates the complex operations related to task management.

// Complex Subsystem: TaskManager
class TaskManager {
void addTask(String taskName) {
print('Task added: $taskName');
}

void completeTask(String taskName) {
print('Task completed: $taskName');
}

void deleteTask(String taskName) {
print('Task deleted: $taskName');
}
}

// Facade: ToDoFacade
class ToDoFacade {
final TaskManager _taskManager;

ToDoFacade() : _taskManager = TaskManager();

void addTask(String taskName) {
_taskManager.addTask(taskName);
}

void completeTask(String taskName) {
_taskManager.completeTask(taskName);
}

void deleteTask(String taskName) {
_taskManager.deleteTask(taskName);
}
}

// Client Code
void main() {
ToDoFacade toDoFacade = ToDoFacade();

// Using the simplified interface provided by the facade
toDoFacade.addTask('Buy groceries');
toDoFacade.completeTask('Buy groceries');
toDoFacade.deleteTask('Buy groceries');
}

In the example above, we have a complex subsystem represented by the TaskManager class. It provides methods to add, complete, and delete tasks.

The ToDoFacade class serves as the facade, encapsulating the complexity of the TaskManager subsystem. It provides a simplified interface for clients to perform task management operations. The facade directly interacts with the TaskManager to execute the corresponding operations.

In the client code, we create an instance of ToDoFacade and use its simplified interface to add, complete, and delete tasks. The client code doesn't need to interact directly with the TaskManager class, as the facade handles the complexity internally.

The output of the example will show the execution of the task management operations, but the clients only interact with the simplified interface provided by the facade.

12. Facade Pattern

The Interpreter pattern is a behavioral design pattern that parses and interprets textual expressions or commands. It involves defining a grammar, creating classes that represent different expressions or elements of the grammar, and implementing an interpreter that evaluates the expressions based on the given grammar.

Let’s consider a scenario where you want to interpret and execute commands for managing tasks. We can use the Interpreter pattern to parse and execute simple textual commands to perform operations like adding, completing, or deleting tasks.

// Context: TaskContext
class TaskContext {
List<String> tasks = [];

void addTask(String taskName) {
tasks.add(taskName);
print('Task added: $taskName');
}

void completeTask(String taskName) {
tasks.remove(taskName);
print('Task completed: $taskName');
}

void deleteTask(String taskName) {
tasks.remove(taskName);
print('Task deleted: $taskName');
}
}

// Abstract Expression: Expression
abstract class Expression {
void interpret(TaskContext context);
}

// Concrete Expression: AddExpression
class AddExpression implements Expression {
final String taskName;

AddExpression(this.taskName);

@override
void interpret(TaskContext context) {
context.addTask(taskName);
}
}

// Concrete Expression: CompleteExpression
class CompleteExpression implements Expression {
final String taskName;

CompleteExpression(this.taskName);

@override
void interpret(TaskContext context) {
context.completeTask(taskName);
}
}

// Concrete Expression: DeleteExpression
class DeleteExpression implements Expression {
final String taskName;

DeleteExpression(this.taskName);

@override
void interpret(TaskContext context) {
context.deleteTask(taskName);
}
}

// Client Code
void main() {
TaskContext context = TaskContext();

List<Expression> expressions = [
AddExpression('Buy groceries'),
CompleteExpression('Buy groceries'),
DeleteExpression('Buy groceries'),
];

for (Expression expression in expressions) {
expression.interpret(context);
}
}

In the example above, we have a TaskContext class that represents the context or environment for executing commands related to task management. It provides methods like addTask, completeTask, and deleteTask to perform the corresponding operations.

The Command interface defines the abstract expression for interpreting commands. It declares the interpret method, which takes the TaskContext as a parameter and is implemented by concrete command classes.

The concrete command classes, such as AddCommand, CompleteCommand, and DeleteCommand, represent specific commands for adding, completing, and deleting tasks. They implement the interpret method and perform the corresponding operations on the TaskContext.

In the client code, we create an instance of TaskContext and define a list of commands. We iterate over the commands and call the interpret method on each command, passing the TaskContext as the context. This interprets and executes the commands, performing the corresponding operations on the tasks.

The output of the example will show the execution of the commands, such as adding, completing, and deleting tasks

12. Iterator Pattern

The Iterator pattern is a behavioral design pattern that provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. It decouples the traversal logic from the aggregate object, making it easier to iterate over the elements in a uniform manner.

Let’s consider a scenario where you want to iterate over a collection of tasks. We can use the Iterator pattern to provide a standardized way to traverse and access the tasks in the collection.

// Aggregate: TaskCollection
class TaskCollection {
List<String> tasks = [];

void addTask(String taskName) {
tasks.add(taskName);
}

TaskIterator getIterator() {
return TaskIterator(this);
}
}

// Iterator: TaskIterator
class TaskIterator {
final TaskCollection collection;
int currentIndex;

TaskIterator(this.collection) : currentIndex = 0;

bool hasNext() {
return currentIndex < collection.tasks.length;
}

String next() {
if (hasNext()) {
final task = collection.tasks[currentIndex];
currentIndex++;
return task;
}
return null;
}
}

// Client Code
void main() {
TaskCollection collection = TaskCollection();
collection.addTask('Task 1');
collection.addTask('Task 2');
collection.addTask('Task 3');

TaskIterator iterator = collection.getIterator();
while (iterator.hasNext()) {
final task = iterator.next();
print(task);
}
}

In the example above, we have a TaskCollection class that represents the aggregate object or collection of tasks. It contains a list of tasks and provides a method addTask to add tasks to the collection. The getIterator method returns an instance of the TaskIterator to iterate over the tasks.

The TaskIterator class implements the iterator interface and holds a reference to the TaskCollection. It keeps track of the current index while traversing the tasks. The hasNext method checks if there are more tasks to iterate, and the next method returns the next task and advances the current index.

In the client code, we create an instance of TaskCollection and add some tasks to it. We then obtain an iterator from the collection using the getIterator method. We can then use the iterator to traverse the tasks using a while loop. The hasNext method is used to check if there are more tasks, and the next method is used to retrieve the next task.

The output of the example will show the tasks being printed in sequential order:

Task 1
Task 2
Task 3

13. Visitor Pattern

The Visitor pattern is a behavioral design pattern that allows adding new operations to existing classes without modifying their structure. It separates the operations or algorithms from the objects on which they operate, providing a way to add new operations without modifying the classes themselves.

Let’s consider a scenario where you have different types of tasks (e.g., personal tasks, work tasks) and you want to perform different operations on each type of task. We can use the Visitor pattern to define a visitor interface and concrete visitor classes to perform specific operations on the tasks.

// Element: Task
abstract class Task {
void accept(TaskVisitor visitor);
}

// Concrete Element: PersonalTask
class PersonalTask implements Task {
void accept(TaskVisitor visitor) {
visitor.visitPersonalTask(this);
}
}

// Concrete Element: WorkTask
class WorkTask implements Task {
void accept(TaskVisitor visitor) {
visitor.visitWorkTask(this);
}
}

// Visitor: TaskVisitor
abstract class TaskVisitor {
void visitPersonalTask(PersonalTask task);
void visitWorkTask(WorkTask task);
}

// Concrete Visitor: TaskPrinterVisitor
class TaskPrinterVisitor implements TaskVisitor {
void visitPersonalTask(PersonalTask task) {
print('Processing personal task');
// Perform specific operations for personal tasks
}

void visitWorkTask(WorkTask task) {
print('Processing work task');
// Perform specific operations for work tasks
}
}

// Client Code
void main() {
List<Task> tasks = [
PersonalTask(),
WorkTask(),
PersonalTask(),
];

TaskVisitor visitor = TaskPrinterVisitor();

for (Task task in tasks) {
task.accept(visitor);
}
}

In the example above, we have an abstract Task class that represents the element in the Visitor pattern. It declares an accept method that accepts a TaskVisitor as an argument.

The concrete PersonalTask and WorkTask classes implement the Task interface and provide their own implementations of the accept method. They call the appropriate visit method of the visitor, passing themselves as an argument.

The TaskVisitor interface declares visit methods for each type of task. In this example, we have visitPersonalTask and visitWorkTask methods.

The concrete TaskPrinterVisitor class implements the TaskVisitor interface and provides specific implementations for each visit method. In this example, it simply prints a message indicating the type of task being processed.

In the client code, we create a list of tasks, including PersonalTask and WorkTask objects. We also create an instance of the TaskPrinterVisitor visitor.

We then iterate over the tasks and call the accept method on each task, passing the visitor as an argument. This invokes the appropriate visit method in the visitor based on the type of task.

The output of the example will show messages indicating the type of task being processed:

Processing personal task
Processing work task
Processing personal task

Understanding and utilizing design patterns in Flutter empowers developers to make informed design decisions, improve code quality, and create scalable and maintainable applications. It allows for better separation of concerns, easier testing, and flexibility to accommodate future changes.

They serve as powerful tools in Flutter mobile development, enabling developers to build well-structured, maintainable, and extensible applications while adhering to best practices and industry standards. By leveraging the right design patterns, developers can optimize their development process, enhance code quality, and deliver exceptional user experiences.

👏🏽 Give this story a CLAP

👉🏽 Subscribe for upcoming articles

💰 Access Free Mobile Development tutorials

🔔 Follow for more

See you on next article 👋

--

--

No responses yet