Practical Design Patterns for Flutter Development

Majid Hajian - FlutterEngineering.io Author

  • Decoding the Role of Design Patterns
  • Factory Method Using Default Methods
  • Decorator by Chain Decorators
  • Execute Around Method Pattern
  • Strategy Pattern and Lightweight Strategies
  • Creating a Closed Hierarchy with Sealed
  • Summary

FlutterEngineering.io

FlutterEngineering.io

www.flutterengineering.io

www.flutterengineering.io

GDE and Author

flutterdesignpatterns.com

Design Pattern Chapter Reviewer

Mangirdas Kazlauskas

Decoding the Role of Design Patterns

FlutterEngineering.io

dealing with object creation

Such as Singleton, Factory method, prototype

1

Creational

focusing on class or object composition;

Such as Facade, Decorator, Adapter, Composite

2

Structural

which is about object interaction and responsibility;

Such as Command, Template method, State, Memento, Observer

3

Behavioral

FlutterEngineering.io

Design patterns establish a shared language that you and your teammates can use for more efficient communication.

If all you have is a hammer, everything looks like a nail.

 Decoding the Role of Design Patterns

Factory Method Using Default Methods

Decorator by Chain Decorators

Execute Around Method Pattern

Strategy Pattern and Lightweight Strategies

Creating a Closed Hierarchy with Sealed

Summary

FlutterEngineering.io

FlutterEngineering.io

FlutterEngineering.io

Factory Method

FlutterEngineering.io

Factory Method

abstract class PlatformWidgetFactory {
  Widget createWidget();
}

FlutterEngineering.io

Factory Method

class IOSWidgetFactory extends PlatformWidgetFactory {
  @override
  Widget createWidget() {
    return CupertinoButton(
      child: Text('iOS Button'),
      onPressed: () {},
    );
  }
}
class AndroidWidgetFactory extends PlatformWidgetFactory {
  @override
  Widget createWidget() {
    return ElevatedButton(
      child: Text('Android Button'),
      onPressed: () {},
    );
  }
}
class WebWidgetFactory extends PlatformWidgetFactory {
  @override
  Widget createWidget() {
    return TextButton(
      child: Text('Web Button'),
      onPressed: () {},
    );
  }
}

FlutterEngineering.io

Factory Method

void main() {
  PlatformWidgetFactory widgetFactory;

  if (Platform.isIOS) {
    widgetFactory = IOSWidgetFactory();
  } else if (Platform.isAndroid) {
    widgetFactory = AndroidWidgetFactory();
  } else {
    widgetFactory = WebWidgetFactory();
  }

  Widget myWidget = widgetFactory.createWidget();

  runApp(MyApp(home: Scaffold(body: Center(child: myWidget))));
}

FlutterEngineering.io

Factory Method with Default Method

abstract class WidgetCreator {
  // Factory method to be implemented by subclasses
  Widget createWidget(BuildContext context);

  // Core business logic that uses the factory method
  Widget build(BuildContext context) {
    final widget = createWidget(context);
    return Scaffold(
      appBar: AppBar(
        title: Text("Factory Method Example"),
      ),
      body: Center(child: widget),
    );
  }
}

FlutterEngineering.io

Factory Method with Default Method

abstract class CustomDialog {
  // Factory method to be implemented by subclasses	
  Widget create(BuildContext context);

  // Core business logic that uses the factory method
  Future<void> show(BuildContext context) => showDialog<void>(
        context: context,
        barrierDismissible: false,
        builder: create,
      );
}

FlutterEngineering.io

Factory Method with Default Method

class AndroidAlertDialog extends CustomDialog {
  @override
  Widget create(BuildContext context) {
    return AlertDialog(
      title: Text('ANdroid'),
      content: const Text('This is the material-style!'),
      actions: <Widget>[
        TextButton(
          onPressed: Navigator.of(context).pop,
          child: const Text('Close'),
        ),
      ],
    );
  }
}

FlutterEngineering.io

Factory Method with Default Method

class IosAlertDialog extends CustomDialog {
  @override
  Widget create(BuildContext context) {
    return CupertinoAlertDialog(
      title: Text('iOS'),
      content: const Text('This is the cupertino-style'),
      actions: <Widget>[
        CupertinoButton(
          onPressed: Navigator.of(context).pop,
          child: const Text('Close'),
        ),
      ],
    );
  }
}

FlutterEngineering.io

Factory Method with Default Method

AndroidAlertDialog().show();

IosAlertDialog().show();

 Decoding the Role of Design Patterns

 Factory Method Using Default Methods

Decorator by Chain Decorators

Execute Around Method Pattern

Strategy Pattern and Lightweight Strategies

Creating a Closed Hierarchy with Sealed

Summary

FlutterEngineering.io

FlutterEngineering.io

Decorator

FlutterEngineering.io

// Base Component
abstract class TextComponent {
  Widget build(BuildContext context);
}

// Concrete Component
class SimpleText extends TextComponent {
  final String text;

  SimpleText(this.text);

  @override
  Widget build(BuildContext context) {
    return Text(text);
  }
}

Decorator

FlutterEngineering.io

FlutterEngineering.io

// Decorator
abstract class TextDecorator extends TextComponent {
  TextDecorator(this.decoratedText);
  
  final TextComponent decoratedText;
}

Decorator

FlutterEngineering.io

// Concrete Decorators
class BorderText extends TextDecorator {
  BorderText(super.decoratedText);

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        border: Border.all(color: Colors.black),
      ),
      child: decoratedText.build(context),
    );
  }
}

Decorator

FlutterEngineering.io

// Concrete Decorator
class PaddingText extends TextDecorator {
  PaddingText(super.decoratedText);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: decoratedText.build(context),
    );
  }
}

Decorator

FlutterEngineering.io

// Concrete Decorator
class BackgroundText extends TextDecorator {
  BackgroundText(super.decoratedText);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellowAccent,
      child: super.build(context),
    );
  }
}

Decorator

FlutterEngineering.io

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    TextComponent text = SimpleText('Hello, World!');
    TextComponent borderedText = BorderText(text);
    TextComponent paddedText = PaddingText(borderedText);

    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: paddedText.build(context),
        ),
      ),
    );
  }
}

Decorator

Decorator by Chain Decorators

class ChainedTextDecorator extends TextComponent {
  ChainedTextDecorator(this.baseComponent, this.decorators);
  
  final TextComponent baseComponent;
  final List<TextDecorator Function(TextComponent)> decorators;

  @override
  Widget build(BuildContext context) {
    TextComponent currentComponent = baseComponent;

    // Apply each decorator in sequence
    for (final decorator in decorators) {
      currentComponent = decorator(currentComponent);
    }

    return currentComponent.build(context);
  }
}

FlutterEngineering.io

Decorator by Chain Decorators

    final TextComponent chainedText = ChainedTextDecorator(
      SimpleText('Hello, World!'),
      [
        (widget) => PaddingText(widget, padding: EdgeInsets.all(16.0)),
        (widget) => BorderText(widget, color: Colors.blue, width: 3.0),
        (widget) => BackgroundText(widget, backgroundColor: Colors.yellow[100]!),
      ],
    );

FlutterEngineering.io

Decorator by Chain Decorators

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // ...
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: chainedText.build(context), 
        ),
      ),
    );
  }
}

FlutterEngineering.io

 Decoding the Role of Design Patterns

 Factory Method Using Default Methods

 Decorator by Chain Decorators

Execute Around Method Pattern

Strategy Pattern and Lightweight Strategies

Creating a Closed Hierarchy with Sealed

Summary

FlutterEngineering.io

FlutterEngineering.io

Execute Around Method Pattern

FlutterEngineering.io

Execute Around Method Pattern

FlutterEngineering.io

class LoadingManager {
  LoadingManager(this.context);
  
  final BuildContext context;
  
}

Execute Around Method Pattern

class LoadingManager {
  // ...
  
  Future<void> executeAround(Future<void> Function() action) async {
    try {
      // Setup: Show the loading indicator
      _showLoadingIndicator();

      // Main Action: Execute the passed asynchronous function
      await action();
    } finally {
    
      // Cleanup: Hide the loading indicator
      _hideLoadingIndicator();
    }
  }
  
}

FlutterEngineering.io

Execute Around Method Pattern

class LoadingManager {
  // ...
  
  void _showLoadingIndicator() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return Center(child: CircularProgressIndicator());
      },
    );
  }
  void _hideLoadingIndicator() {
    Navigator.of(context).pop(); 
  }
}

FlutterEngineering.io

Execute Around Method Pattern

class NetworkRequestExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            final loadingManager = LoadingManager(context);
            await loadingManager.executeAround(() async {
              // Simulate a network request
              await Future.delayed(Duration(seconds: 2));
              print('Network request completed');
            });
          },
          child: Text('Perform Network Request'),
        ),
      ),
    );
  }
}

FlutterEngineering.io

 Decoding the Role of Design Patterns

 Factory Method Using Default Methods

 Decorator by Chain Decorators

 Execute Around Method Pattern

Strategy Pattern and Lightweight Strategies

Creating a Closed Hierarchy with Sealed

Summary

FlutterEngineering.io

FlutterEngineering.io

Strategy Pattern and Lightweight Strategies

FlutterEngineering.io

Strategy Pattern and Lightweight Strategies

FlutterEngineering.io

abstract class PaymentStrategy {
  void pay(double amount);
}

Strategy Pattern and Lightweight Strategies

class CreditCardPayment implements PaymentStrategy {
  @override
  void pay(double amount) {
    print('Processing credit card payment of \$${amount.toStringAsFixed(2)}');
    // Implement credit card payment logic
  }
}

class PayPalPayment implements PaymentStrategy {
  @override
  void pay(double amount) {
    print('Processing PayPal payment of \$${amount.toStringAsFixed(2)}');
    // Implement PayPal payment logic
  }
}

class CryptoPayment implements PaymentStrategy {
  @override
  void pay(double amount) {
    print('Processing cryptocurrency payment of \$${amount.toStringAsFixed(2)}');
    // Implement cryptocurrency payment logic
  }
}

FlutterEngineering.io

Strategy Pattern and Lightweight Strategies

class PaymentContext {
  PaymentStrategy? _strategy;

  void setStrategy(PaymentStrategy strategy) {
    _strategy = strategy;
  }

  void executePayment(double amount) {
    _strategy?.pay(amount);
  }
}

FlutterEngineering.io

Strategy Pattern and Lightweight Strategies

class _PaymentExampleState extends State<PaymentExample> {
  final PaymentContext paymentContext = PaymentContext();
  String selectedMethod = 'Credit Card';

  final Map<String, PaymentStrategy> strategies = {
    'Credit Card': CreditCardPayment(),
    'PayPal': PayPalPayment(),
    'Cryptocurrency': CryptoPayment(),
  };

  void _selectPaymentMethod(String method) {
    setState(() {
      selectedMethod = method;
      paymentContext.setStrategy(strategies[method]!);
    });
  }

  void _pay(double amount) {
    paymentContext.executePayment(amount);
  }
//  ...
 }

FlutterEngineering.io

Strategy Pattern and Lightweight Strategies

child: Column(
          children: [
            ...strategies.keys.map((method) {
              return Padding(
                padding: EdgeInsets.all(16),
                child: ElevatedButton(
                  onPressed: () => _selectPaymentMethod(method),
                  child: Text('Pay with $method'),
                ),
              );
            }).toList(),
            TextButton(
              onPressed: () => _pay(100.00),
              child: Text('Proceed to Pay \$100.00'),
            ),
          ],
        ),

FlutterEngineering.io

 Decoding the Role of Design Patterns

 Factory Method Using Default Methods

 Decorator by Chain Decorators

 Execute Around Method Pattern

 Strategy Pattern and Lightweight Strategies

Creating a Closed Hierarchy with Sealed

Summary

FlutterEngineering.io

FlutterEngineering.io

Creating a Closed Hierarchy with Sealed

FlutterEngineering.io

Exhaustive?

Creating a Closed Hierarchy with Sealed

sealed class NetworkState {}

FlutterEngineering.io

Creating a Closed Hierarchy with Sealed

class LoadingState extends NetworkState {}

class SuccessState<T> extends NetworkState {
  SuccessState(this.data);
  final T data;
}

class ErrorState extends NetworkState {
  ErrorState(this.errorMessage);
  final String errorMessage;
}

class EmptyState extends NetworkState {}

FlutterEngineering.io

Creating a Closed Hierarchy with Sealed

class ApiService {
  Future<NetworkState> fetchData() async {
    await Future.delayed(Duration(seconds: 2));

    // Simulate different outcomes
    final success = true;

    if (success) {
      final data = ['Item 1', 'Item 2', 'Item 3'];
      if (data.isEmpty) {
        return EmptyState();
      } else {
        return SuccessState(data);
      }
    } else {
      return ErrorState('Failed to fetch data.');
    }
  }
}

FlutterEngineering.io

Creating a Closed Hierarchy with Sealed

		builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasData) {
            final state = snapshot.data!;
            return Center(
              child: switch (state) {
                LoadingState() => CircularProgressIndicator(),
                SuccessState dataState => ListView.builder(),
                ErrorState errorState => Column(),
                EmptyState() => Text()
              },
            );
          } else {
            return Center(child: Text('Something went wrong.'));
          }
        },

FlutterEngineering.io

Creating a Closed Hierarchy with Sealed

		builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasData) {
            final state = snapshot.data!;
            return Center(
              child: switch (state) {
               
                SuccessState dataState => ListView.builder(),
                ErrorState errorState => Column(),
                EmptyState() => Text()
              },
            );
          } else {
            return Center(child: Text('Something went wrong.'));
          }
        },

FlutterEngineering.io

 Decoding the Role of Design Patterns

 Factory Method Using Default Methods

 Decorator by Chain Decorators

 Execute Around Method Pattern

 Strategy Pattern and Lightweight Strategies

 Creating a Closed Hierarchy with Sealed

Summary

FlutterEngineering.io

FlutterEngineering.io

Patterns as a communication tool, not only a design tool

Focus on Solving Problem Reasonably

 Decoding the Role of Design Patterns

 Factory Method Using Default Methods

 Decorator by Chain Decorators

 Execute Around Method Pattern

 Strategy Pattern and Lightweight Strategies

 Creating a Closed Hierarchy with Sealed

 Summary

FlutterEngineering.io

FlutterEngineering.io

Thank You!

bit.ly/4cOcLWE

  1. flutterengineering.io
  2. github.com/mhadaily/flutterengineering_examples/

Slides

Made with Slides.com