All Design Patterns in Flutter: A Detailed Explanation

Flutter, Google’s open-source UI toolkit, allows developers to build natively compiled applications for mobile, web, and desktop from a single codebase. Design patterns, which are reusable solutions to common software design problems, are a vital part of crafting maintainable and scalable Flutter applications. In this blog post, we’ll dive deep into various design patterns and their implementation in Flutter.


1. Creational Design Patterns

a. Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. In Flutter, this is often used for managing shared resources like API clients or databases.

Example:

class ApiClient {
  static final ApiClient _instance = ApiClient._internal();

  factory ApiClient() {
    return _instance;
  }

  ApiClient._internal();

  void fetchData() {
    // Logic to fetch data
  }
}

void main() {
  var apiClient = ApiClient();
  apiClient.fetchData();
}

b. Factory Pattern

The Factory pattern defines a method for creating objects without specifying their exact class. In Flutter, this is often used when the exact type of the widget or object is determined at runtime.

Example:

abstract class Shape {
  void draw();
}

class Circle implements Shape {
  @override
  void draw() => print('Drawing Circle');
}

class Square implements Shape {
  @override
  void draw() => print('Drawing Square');
}

class ShapeFactory {
  static Shape getShape(String type) {
    if (type == 'circle') return Circle();
    if (type == 'square') return Square();
    throw Exception('Unknown shape type');
  }
}

void main() {
  Shape shape = ShapeFactory.getShape('circle');
  shape.draw();
}

2. Structural Design Patterns

a. Adapter Pattern

The Adapter pattern acts as a bridge between two incompatible interfaces. In Flutter, it’s useful when integrating third-party libraries.

Example:

class SquarePeg {
  final double width;
  SquarePeg(this.width);
}

class RoundHole {
  final double radius;
  RoundHole(this.radius);

  bool fits(SquarePegAdapter peg) => peg.getRadius() <= radius;
}

class SquarePegAdapter {
  final SquarePeg peg;
  SquarePegAdapter(this.peg);

  double getRadius() => peg.width * (1.414 / 2); // Convert square width to diagonal
}

void main() {
  var hole = RoundHole(5);
  var squarePeg = SquarePeg(5);
  var adapter = SquarePegAdapter(squarePeg);

  print(hole.fits(adapter)); // true or false based on compatibility
}

b. Decorator Pattern

The Decorator pattern dynamically adds responsibilities to an object. In Flutter, this is often seen with widget composition.

Example:

Widget decoratedContainer() {
  return Container(
    padding: EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(8),
      boxShadow: [
        BoxShadow(
          color: Colors.black26,
          blurRadius: 4,
          offset: Offset(2, 2),
        )
      ],
    ),
    child: Text(
      'Decorated Text',
      style: TextStyle(color: Colors.white, fontSize: 16),
    ),
  );
}

3. Behavioral Design Patterns

a. Observer Pattern

The Observer pattern defines a one-to-many dependency between objects, such that when one object changes state, all its dependents are notified. In Flutter, this is commonly implemented using ChangeNotifier and Provider.

Example:

import 'package:flutter/material.dart';

class Counter extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => Counter(),
      child: MaterialApp(
        home: CounterScreen(),
      ),
    );
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Observer Pattern')),
      body: Center(
        child: Consumer<Counter>(
          builder: (context, counter, _) => Text(
            'Count: ${counter.count}',
            style: TextStyle(fontSize: 24),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<Counter>().increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

b. Command Pattern

The Command pattern encapsulates a request as an object, allowing for parameterization and queuing of requests. In Flutter, this is useful for undo/redo functionality.

Example:

abstract class Command {
  void execute();
}

class Light {
  void on() => print('Light is ON');
  void off() => print('Light is OFF');
}

class LightOnCommand implements Command {
  final Light light;
  LightOnCommand(this.light);

  @override
  void execute() => light.on();
}

class LightOffCommand implements Command {
  final Light light;
  LightOffCommand(this.light);

  @override
  void execute() => light.off();
}

void main() {
  var light = Light();
  var lightOn = LightOnCommand(light);
  var lightOff = LightOffCommand(light);

  lightOn.execute();
  lightOff.execute();
}

Conclusion

Design patterns are indispensable for writing clean, maintainable, and scalable Flutter applications. By mastering these patterns, you can tackle complex problems with elegance and efficiency. Whether you’re building small widgets or large enterprise applications, applying the right design patterns will make your Flutter development journey smoother and more productive.

What are your favorite design patterns in Flutter? Share your thoughts in the comments below!