Dart 3 Records and Patterns in Flutter: solved exercise

Dart 3 Records and Patterns in Flutter: solved exercise

Dart 3 introduced three major language features: records (anonymous structs with static typing), patterns (destructuring and pattern matching), and sealed classes (closed hierarchies that enable exhaustiveness checking). Together they transform how state and business logic are modeled in Flutter.

Problem statement

Implement a mini-app that demonstrates all three features:

  1. Records: function that returns a record with name, price, and stock.
  2. Pattern matching with switch expression: area() function accepting different shapes.
  3. Sealed classes: LoadState<T> to model async state (Loading, Success, Failure).
  4. List & map patterns: destructuring of lists and maps.
  5. Display all examples in a screen with a ListView of Card widgets.

Dependencies

Dart 3 only — already included in Flutter 3.10+. No extra dependencies.

1
2
3
dependencies:
  flutter:
    sdk: flutter

Full solution

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import 'package:flutter/material.dart';

// ══════════════════════════════════════════════════════════════════════════════
// 1. RECORDS — anonymous struct with static typing
// ══════════════════════════════════════════════════════════════════════════════

// Record with positional fields
typedef Point = (double x, double y);

// Record with named fields
typedef Product = ({String name, double price, int stock});

Product fetchProduct() =>
    (name: 'Flutter Course', price: 29.99, stock: 100);

// Function returning multiple values without creating a class
(String city, int population) getCapital(String country) {
  return switch (country) {
    'Spain'   => ('Madrid', 3_300_000),
    'France'  => ('Paris', 2_100_000),
    'Germany' => ('Berlin', 3_700_000),
    _         => ('Unknown', 0),
  };
}

// ══════════════════════════════════════════════════════════════════════════════
// 2. SEALED CLASSES — closed hierarchy with guaranteed exhaustiveness
// ══════════════════════════════════════════════════════════════════════════════

sealed class Shape {}

class Circle    extends Shape { final double radius;  Circle(this.radius); }
class Rectangle extends Shape { final double width, height; Rectangle(this.width, this.height); }
class Triangle  extends Shape { final double base, height;  Triangle(this.base, this.height); }

// switch expression is exhaustive with sealed — the compiler verifies it
double area(Shape shape) => switch (shape) {
  Circle(:var radius)                    => 3.14159 * radius * radius,
  Rectangle(:var width, :var height)     => width * height,
  Triangle(:var base, :var height)       => 0.5 * base * height,
};

// Generic async state with sealed class
sealed class LoadState<T> {}
class Loading<T> extends LoadState<T> {}
class Success<T> extends LoadState<T> { final T data; Success(this.data); }
class Failure<T> extends LoadState<T> { final String message; Failure(this.message); }

// ══════════════════════════════════════════════════════════════════════════════
// 3. PATTERNS — list and map destructuring
// ══════════════════════════════════════════════════════════════════════════════

String describeList(List<int> numbers) => switch (numbers) {
  []                              => 'Empty list',
  [var single]                    => 'One element: $single',
  [var first, var second]         => 'Two elements: $first and $second',
  [var first, ..., var last]      => 'Several: first $first, last $last',
};

String readConfig(Map<String, dynamic> config) {
  return switch (config) {
    {'mode': 'debug',   'version': var v} => 'DEBUG v$v',
    {'mode': 'release', 'version': var v} => 'RELEASE v$v',
    _ => 'Unknown configuration',
  };
}

// ══════════════════════════════════════════════════════════════════════════════
// Flutter app
// ══════════════════════════════════════════════════════════════════════════════

Future<void> main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dart 3 Features',
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.deepPurple),
      home: const Dart3Page(),
    );
  }
}

class Dart3Page extends StatelessWidget {
  const Dart3Page({super.key});

  @override
  Widget build(BuildContext context) {
    // ── 1. Records ────────────────────────────────────────────────────────────
    final product = fetchProduct();
    final (city, population) = getCapital('Spain'); // positional destructuring

    // ── 2. Shapes ─────────────────────────────────────────────────────────────
    final shapes = <Shape>[
      Circle(5),
      Rectangle(4, 6),
      Triangle(3, 8),
    ];

    // ── 3. LoadState ──────────────────────────────────────────────────────────
    final LoadState<String> state = Success('Data loaded successfully');

    // ── 4. Lists and maps ─────────────────────────────────────────────────────
    final listExamples = [
      describeList([]),
      describeList([42]),
      describeList([1, 2]),
      describeList([10, 20, 30, 40, 50]),
    ];

    final mapExample = readConfig({'mode': 'release', 'version': '2.0.0'});

    return Scaffold(
      appBar: AppBar(title: const Text('Dart 3: Records & Patterns')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // Named record
          _DemoCard(
            title: '📦 Named record fields',
            content: 'Product: ${product.name}\n'
                'Price: \$${product.price}\n'
                'Stock: ${product.stock}',
          ),
          // Positional record + destructuring
          _DemoCard(
            title: '🗺 Positional record + destructuring',
            content: 'Capital: $city\nPopulation: $population',
          ),
          // Sealed + switch expression
          _DemoCard(
            title: '🔷 Sealed class: shapes and areas',
            content: shapes.map((s) {
              final name = switch (s) {
                Circle()    => 'Circle',
                Rectangle() => 'Rectangle',
                Triangle()  => 'Triangle',
              };
              return '$name → area ${area(s).toStringAsFixed(2)}';
            }).join('\n'),
          ),
          // LoadState
          _DemoCard(
            title: '🔄 Sealed LoadState<T>',
            content: switch (state) {
              Loading()              => 'Loading...',
              Success(:var data)     => '✅ $data',
              Failure(:var message)  => '❌ $message',
            },
          ),
          // List patterns
          _DemoCard(
            title: '📋 List patterns',
            content: listExamples.join('\n'),
          ),
          // Map patterns
          _DemoCard(
            title: '🗂 Map patterns',
            content: mapExample,
          ),
        ],
      ),
    );
  }
}

class _DemoCard extends StatelessWidget {
  final String title;
  final String content;
  const _DemoCard({required this.title, required this.content});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title,
                style: const TextStyle(
                    fontWeight: FontWeight.bold, fontSize: 15)),
            const SizedBox(height: 8),
            Text(content, style: const TextStyle(fontFamily: 'monospace')),
          ],
        ),
      ),
    );
  }
}

Key concepts

ConceptDetail
(A, B)Positional record; access with $1, $2 or destructuring (var a, var b) = rec
({String name, int age})Named record; access with .name, .age
sealed classClosed class: only subclasses in the same file. Enables exhaustiveness in switch
switch expressionExpression (not statement) that returns a value; compiler requires all cases to be covered with sealed
:var fieldNamed object pattern: extracts field without repeating the name
[first, ..., last]List rest pattern: captures first and last element, ignores the middle
{'key': var v}Map pattern: extracts the value at key 'key'

Common mistakes

  • Non-exhaustive switch: with sealed classes, the compiler emits a warning if any subtype is missing. Only add a _ case if there is a genuinely valid default.
  • Records vs classes: records are immutable and have no methods. For business logic, a class is still better. Records shine for multiple return values and temporary data.
  • $1 vs .fieldName: positional records use $1, $2…; named records use .fieldName. Mixing them causes a compile error.
  • Flutter < 3.10: Dart 3 requires Flutter 3.10+. Verify with flutter --version.

Practical application

  • Multiple return values: (String, int) parseVersion(String v) avoids creating wrapper classes.
  • Async state: LoadState<T> sealed is a very clean pattern that replaces multiple booleans (isLoading, hasError).
  • Pattern matching in Navigator: switch (route) with sealed classes for type-safe routing.
  • Riverpod + sealed: combined with @riverpod, AsyncValue uses the same pattern internally.

Guided practice and next step

FAQ

Do records replace classes in Dart 3?

No. Records are for temporary data, multiple return values, and simple composition. Classes remain better when you need methods, inheritance, custom equality (though records have automatic structural equality), or complex JSON serialization.

What is the difference between a switch statement and a switch expression?

The switch expression (with =>) returns a value and is more concise. The switch statement (with { case: break; }) executes code. With Dart 3, the expression is preferred for mapping values; the statement for executing side effects.

Are sealed classes like enums?

They are complementary. Enums are for simple constant value sets. Sealed classes are for type hierarchies where each subtype can have its own fields. LoadState<T> with Success(data) and Failure(message) would not be possible with an enum.