Design Patterns
Irbisa · cheatsheetSeptember 13, 2026

Design Patterns

Singleton, Factory, Observer, Builder, Strategy, Command in Flutter

Senior Developer20 itemscompressed for a skim
  1. 01

    Implement and explain the Repository Pattern in Flutter.

    Hard

    The Repository Pattern abstracts data access behind an interface.

    // Abstract contract (Domain layer)
    abstract class WeatherRepository {
      Future<Weather> getWeather(String city);
      Stream<Weather> watchWeather(String city);
    }
    …
  2. 02

    Where do Singleton, Factory, Observer, Strategy and Builder show up in a Flutter codebase?

    Hard

    Flutter's own APIs are already full of GoF patterns: Stream and ChangeNotifier are Observer, ScrollPhysics is Strategy, and the Actions/Intent system is Command.

    // ── SINGLETON ─────────────────────────────────────
    class AppConfig {
      static AppConfig? _instance;
      late final String apiUrl;
      late final bool isDebug;
      AppConfig._();
    …
  3. 03

    Explain Dependency Injection in Flutter. Compare get_it vs injectable vs Riverpod.

    Hard

    Dependency Injection (DI) provides dependencies to a class from outside rather than creating them internally — enabling testability, flexibility, and loose coupling.

    // ── get_it (manual registration) ──────────────────
    import 'package:get_it/get_it.dart';
    
    final sl = GetIt.instance;
    
    Future<void> setupDi() async {
    …
  4. 04

    How do you apply the Adapter pattern to wrap an inconsistent third-party API?

    Medium

    An Adapter wraps a class with one interface and exposes it through another.

    // ── Domain interface — your app code talks to this ──
    abstract class Analytics {
      Future<void> track(String event, {Map<String, Object?> props});
      Future<void> identify(String userId, {Map<String, Object?> traits});
    }
    …
  5. 05

    How do you implement the Decorator pattern in Dart, and when do mixins or extensions cover the same need?

    Medium

    Decorator wraps an object to add behavior without changing the wrapped object's class.

    // ── Interface ──
    abstract class UserRepository {
      Future<User> getUser(int id);
      Future<void> updateUser(User user);
    }
    …
  6. 06

    How do you implement the Command pattern with undo/redo support?

    Medium

    A Command encapsulates an operation as an object with an execute() method (and optionally an undo()).

    sealed class Command {
      Future<void> execute();
      Future<void> undo();
    }
    
    // Concrete commands carry both the action and its inverse data
    …
  7. 07

    How does the State pattern compare with Dart 3 sealed classes?

    Hard

    Both express "this entity behaves differently in each of N modes".

    // ── 1) State pattern — rich, polymorphic behaviour ──
    abstract class TrafficLight {
      String get color;
      TrafficLight next();
      Duration get duration;
    }
    …
  8. 08

    What is the BLoC pattern, and how does it differ from the Observer pattern in general?

    Medium

    BLoC is the Observer pattern with three extra rules: inputs are named immutable events, outputs are immutable states, and exactly one class maps between them.

    // ── ChangeNotifier — plain Observer ──
    class CounterModel extends ChangeNotifier {
      int _value = 0;
      int get value => _value;
      void inc() { _value++; notifyListeners(); }
    }
    …
  9. 09

    When should you use the Service Locator pattern vs constructor injection?

    Hard

    A service locator hides dependencies behind a global registry; constructor injection puts them in the class's signature, where the compiler and the next reader can both see them.

    // ── Service locator ──
    import 'package:get_it/get_it.dart';
    final sl = GetIt.instance;
    
    void setup() {
      sl.registerLazySingleton<Dio>(() => Dio());
    …
  10. 10

    What does a Facade give you that a Repository does not, and where does one earn its place in a Flutter app?

    Medium

    A Facade is a single task-shaped entry point over several collaborating subsystems, holding no data and adding no rules of its own.

    // One call for the caller; four collaborators behind it
    class AttachmentFacade {
      AttachmentFacade(this._picker, this._permissions, this._compressor, this._uploads);
    
      final ImagePicker _picker;
      final PermissionService _permissions;
    …
  11. 11

    Functions are first-class in Dart, so when do you still write a Strategy interface instead of passing a callback?

    Medium

    A callback already is a Strategy when the behaviour is one operation with no state and no identity; anything beyond that is what the interface buys you.

    // ── Function is enough: one operation, no state ──
    typedef DiscountRule = double Function(Order order);
    
    double loyaltyDiscount(Order o) => o.customer.isGold ? 0.1 : 0.0;
    final total = applyRules(order, [loyaltyDiscount, (o) => o.items.length > 10 ? 0.05 : 0]);
    …
  12. 12

    How would you build a composable middleware chain in Dart, and when is that better than a list of if-statements?

    Hard

    Model each stage as a function that receives the request and the next stage, then fold the list into one callable pipeline.

    typedef Handler<Req, Res> = Future<Res> Function(Req request);
    typedef Middleware<Req, Res> = Handler<Req, Res> Function(Handler<Req, Res> next);
    
    Handler<Req, Res> pipeline<Req, Res>(
      List<Middleware<Req, Res>> stages,
      Handler<Req, Res> terminal,
    …
  13. 13

    Your widget tests pass one at a time but fail as a suite, and a singleton cache is still holding yesterday's user — what is wrong with the Dart singleton idiom?

    Medium

    The singleton is not the problem, its lifetime is: a static final instance lives as long as the isolate, so every test in a file shares one mutable object and nobody is allowed to destroy it.

    // ❌ Global lifetime: one instance per isolate, no owner, no teardown
    class LegacySessionCache {
      LegacySessionCache._();
      static final LegacySessionCache instance = LegacySessionCache._(); // lazy once, then forever
      User? user;
    }
    …
  14. 14

    Named arguments, cascades and copyWith already cover most of the GoF Builder, so when is a real builder class still worth writing in Dart?

    Medium

    Almost never for plain construction — named arguments plus copyWith already give you the readable, order-free, partially-specified construction Builder was invented for; a builder class earns its place only when construction accumulates over many calls or must be validated as a whole.

    // ── Named arguments already are the builder ──
    final theme = ThemeData(
      useMaterial3: true,
      colorSchemeSeed: Colors.indigo,
      visualDensity: VisualDensity.compact,
    );
    …
  15. 15

    A teammate overrides dispose() without calling super.dispose() and the release build still runs fine — what does Flutter's State lifecycle actually enforce, and when?

    Medium

    The State lifecycle is a Template Method: the framework owns the call order and you only fill in hooks, and the only thing guarding those hooks is @mustCallSuper — a static analysis rule for most of them and a debug-only assert for dispose.

    class _FeedPageState extends State<FeedPage> with WidgetsBindingObserver {
      late final ScrollController _scroll;
      StreamSubscription<Feed>? _sub;
      Feed? _feed;
    
      @override
    …
  16. 16

    Memory climbs after users open and close the same screen a hundred times, and every retaining path ends at an app-wide ChangeNotifier — what went wrong?

    Medium

    A ChangeNotifier holds strong references to its listeners, so a long-lived subject keeps every short-lived observer alive — and with a closure that captured this, the whole State, its element and its subtree.

    // ❌ Long-lived subject, short-lived observer, no real unsubscribe
    class _LeakyBadgeState extends State<CartBadge> {
      @override
      void initState() {
        super.initState();
        cart.addListener(() => setState(() {})); // closure captures this, kept forever
    …
  17. 17

    Your feed API tags each item with a type field and product ships a new kind every sprint — how do you build the right model, and what does an old client do with a type it has never seen?

    Hard

    Dispatch on the discriminator in exactly one factory, and make the unknown branch a real model rather than a throw — an app that crashes on a type it has never seen is an app you cannot fix without shipping a release.

    sealed class FeedItem {
      const FeedItem();
    
      factory FeedItem.fromJson(Map<String, dynamic> json) => switch (json['type']) {
            'article' => Article.fromJson(json),
            'poll' => Poll.fromJson(json),
    …
  18. 18

    A user removes their avatar, you call profile.copyWith(avatarUrl: null), and the old URL is still there — why, and how do you fix it properly?

    Hard

    Dart cannot distinguish an omitted named argument from one explicitly passed as null, so the usual `avatarUrl ??

    class LegacyProfile {
      const LegacyProfile({required this.id, required this.name, this.avatarUrl});
      final String id;
      final String name;
      final String? avatarUrl;
    …
  19. 19

    Your secure-storage wrapper compiles on mobile but flutter build web dies on dart:io — how do you ship one API with per-platform implementations?

    Hard

    Put the implementations behind one abstract interface and pick between them with a conditional export, because a kIsWeb check is a runtime branch while the web compiler still has to resolve every dart:io import in the file.

    // lib/src/secure_store_api.dart — the contract every branch must satisfy
    abstract interface class SecureStore {
      Future<void> write(String key, String value);
      Future<String?> read(String key);
    }
    …
  20. 20

    You render a document AST to widgets and also need plain text out of the same tree for search — Visitor, or a sealed class with two switches?

    Hard

    With Dart 3 the sealed switch is the default answer: it gives you the compiler-checked exhaustiveness Visitor was invented for, without double dispatch — Visitor is what you fall back to for hierarchies you do not own.

    sealed class Node {}
    
    final class Heading extends Node {
      Heading(this.level, this.text);
      final int level;
      final String text;
    …