SkillByAIOpen interactive version →

Lesson 24 / 25

Dart Patterns You Will Use in Flutter

Connect Dart language features to everyday Flutter code.

The language inside the UI framework

Flutter code is Dart code, and several language features appear constantly. Named parameters with required define widget APIs (Text('Hi', style: ...)). const constructors let Flutter reuse widget instances and skip rebuilding them, so prefer const widgets wherever possible. Immutable classes with final fields and copyWith methods model state that changes by replacement rather than mutation, which state-management libraries (Provider, Riverpod, Bloc) rely on. Sealed classes and switch expressions model UI states (Loading, Loaded, Failed) with compile-time exhaustiveness. Collection if/for and spread build widget lists declaratively. Futures and Streams drive FutureBuilder and StreamBuilder, and async callbacks handle button taps. late fields are initialised in initState, and subscriptions and controllers must be cancelled or disposed in dispose. Isolates (compute or Isolate.run) keep heavy parsing off the UI thread so animations stay smooth. The existing Flutter course on this site covers widgets, layout and state management in depth.

An immutable state model with copyWith and a sealed UI state

Typical Dart shapes in a Flutter app, independent of any state library.

class CartState {
  const CartState({this.items = const [], this.couponCode});
  final List<String> items;
  final String? couponCode;

  CartState copyWith({List<String>? items, String? couponCode}) => CartState(
        items: items ?? this.items,
        couponCode: couponCode ?? this.couponCode,
      );

  CartState addItem(String sku) => copyWith(items: [...items, sku]);   // new state, no mutation
}

sealed class ProductsState {}
class ProductsLoading extends ProductsState {}
class ProductsLoaded extends ProductsState {
  ProductsLoaded(this.titles);
  final List<String> titles;
}
class ProductsFailed extends ProductsState {
  ProductsFailed(this.message);
  final String message;
}

String render(ProductsState state) => switch (state) {   // in Flutter this would return widgets
      ProductsLoading() => 'Loading...',
      ProductsLoaded(:final titles) when titles.isEmpty => 'No products yet',
      ProductsLoaded(:final titles) => titles.join(', '),
      ProductsFailed(:final message) => 'Error: $message',
    };

void main() {
  const empty = CartState();
  final cart = empty.addItem('pen').addItem('ink');
  print('${empty.items.length} -> ${cart.items.length}');   // 0 -> 2
  print(render(ProductsLoaded(['Notebook', 'Pen'])));
}

copyWith cannot clear a nullable field this way

With couponCode ?? this.couponCode, passing null keeps the old value, so you cannot remove a coupon. Packages such as freezed generate copyWith methods that distinguish "not passed" from "set to null".

Quick check: Why are const constructors valuable in Flutter widget trees?

  • Identical const widgets are canonicalised, so Flutter can reuse them and skip unnecessary rebuild work
  • They make widgets mutable
  • They enable hot reload
  • They are required for every widget
Answer

Identical const widgets are canonicalised, so Flutter can reuse them and skip unnecessary rebuild work — const instances are created once and shared, reducing rebuild cost.