Pattern matching in Dart-Flutter: A Developer’s Playground
Pattern matching and destructuring:
- Patterns are a new and powerful feature of Dart 3.0 that allows you to express how data should be shaped and extract specific parts from it in a readable way.
- They offer two main functionalities: A pattern may match a value or destructure a value.
1. Matching value:
- A pattern defines the characteristics of a value it can match against.
- It determines whether a value fits a specific type: Value matching with patterns.
- This feature streamlines conditional logic and replaces lengthy if-else if statements with patterns.
// if, else if
if (user is Admin) {
return AdminDashboard();
} else if (user is VerifiedUser) {
return UserHome();
} else {
return WelcomeScreen();
}
// Type Matching
switch (user) {
case Admin admin:
return AdminDashboard(admin);
case VerifiedUser verifiedUser:
return UserHome(verifiedUser);
default:
return WelcomeScreen();
}
2. Destructuring
Remember the days of manually extracting properties from complex data structures, line by line? Those days are fading fast with the power of destructuring
- When a pattern matches a value, it can simultaneously extract and bind specific parts of that value to variables.
- This eliminates the need for manual checks and extraction, making your code cleaner and less prone to errors.
final user = {'id': 1, 'name': 'Happy', 'email': 'xyz@gmail.com'};
// Before:
final userId = user['id'];
final userName = user['name'];
final userEmail = user['email'];
// Now: Using Patterns
// Here uID, uName and uEmail are new variable declarations
// which holds respective values of [user] data
// For more, see Record and Object pattern type below
final {'id':uId, 'name':uName, 'email':uEmail} = user;
print("$uId - $uName - $uEmail");
That’s all! In a single line, you’ve extracted each property and assigned it to its corresponding variable. It’s concise, readable, and efficient.
Here is another example:
// This case pattern matches and destructures a two-element list
// whose first element is 'a' or 'b':
var list = ['a', 'x'];
switch (list) {
// 1. Check if list is type of List
// 2. Check if the 1st element is 'a' or 'b'
// 3. And store 2nd element to c of var type.
case ['a' || 'b', var c]:
print(c); // Print x
}
Pattern types:
- Just like mathematical operators, patterns also have a hierarchy of precedence—a set of rules that determine which patterns are evaluated first.
- Precedence Matters: The order in which patterns are evaluated can significantly impact the behavior of your code.
- Parenthesized Power: Use parentheses to explicitly control precedence, similar to how you use them with operators.
- Below is a list of the pattern types in ascending order of precedence:
Logical-or: subpattern1 || subpattern2
switch (color) {
case Color.red || Color.yellow || Color.blue:
}
Logical-and: subpattern1 && subpattern2
switch (shape) {
case Shape.circle && != null:
}
Relational: ==
, !=
, <
, >
, <=
, and>=
.
String asciiCharType(int char) {
const space = 32;
const zero = 48;
const nine = 57;
//Switch expression
return switch (char) {
< space => 'control',
== space => 'space',
> space && < zero => 'punctuation',
>= zero && <= nine => 'digit',
_ => ''
// _ denotes Default case
};
}
Cast: foo as String
(num, Object) record = (1, 's'); // Record Type
var (i as int, s as String) = record; // Pattern Destructuring
Null — Check: subpattern?
String? maybeString = 'Hello, Good Morning';
switch (maybeString) {
// Matches if maybeString != null.
// Skip the case if its null
case var s?:
print(s);
}
Null-assert: subpattern!
List<String?> row = ['user', null];
switch (row) {
case ['user', var name!]: // Throws if the matched value is null.
}
Constant: 123, null, 'string', math.pi, SomeClass.constant, const Thing(1, 2), const (1 + 2)
switch (number) {
case 1: // Matches if 1 == number.
}
Variable: var bar, String str, final int _
switch ((1, 2)) {
// Match if switch type is record and then match individual variable type
// a and b are variable patterns that bind to 1 & 2 respectively.
case (var a, var b):
print("Sum is: ${a+b}");
// 'a' and 'b' are in scope in the case body.
}
switch ((1, 2)) {
// Does not match internal type.
case (int a, String b): print(a);
// Do match.
case (num a, int b): print(a);
}
Identifier: foo, _
//1. Declaration:
var (a, b) = (1, 2);
//3. Assignment:
(a, b) = (3, 4);
//3. Matching
const c = 1;
switch (2) {
case c:
print('match $c');
default:
print('no match'); // Prints "no match".
}
//4. Wildcard
case [_, var y, _]: print('The middle element is $y');
Parenthesized: (subpattern)
var x = true;
var y = true;
var z = false;
x || y && z => 'matches true', //logical-and patterns have higher precedence than logical-or
(x || y) && z => 'matches false',
List: [subpattern1, subpattern2]
var obj = ['a', 'b'];
const a = 'a';
const b = 'b';
switch (obj) {
// Matches obj first if obj is a List with 2 fields
// then if its fields match the constant subpatterns 'a' and 'b'.
case [a, b]:
print('$a, $b');
}
Rest element: List patterns can contain one rest element (…) which allows matching lists of arbitrary lengths.
var [a, b, ..., c, d] = [1, 2, 3, 4, 5, 6, 7];
print('$a $b $c $d'); // Prints "1 2 6 7"
var [a, b, ...rest, c, d] = [1, 2, 3, 4, 5, 6, 7];
print('$a $b $rest $c $d'); // Prints "1 2 [3, 4, 5] 6 7".
var [a, d] = [1];
print('$a $d'); // Throw run time error as array length does
var [a, ...restEle] = [1];
print('$a $restEle'); // Prints "1 []".
Map: {"key": subpattern1, someConst: subpattern2}
var mapVar = {'a': 10, 'b': 20};
switch(mapVar){
// Does not match as c key is not matched
case {'a': var a, 'c': var c}: print("a is $a");
// Do match
case {'b': var b}: print("b is $b");
}
Record: (subpattern1, subpattern2), (x: subpattern1, y: subpattern2)
var (latitude: lat, longitude: bar) = (latitude: 10.789, longitude: -8.856);
print("$lat $long"); // 10.789 -8.856
var (untyped: untypedVar, typed: int typedVar) = record;
var (latitude: lat, longitude: double long) = (latitude: 10.789, longitude: -8.856);
//---------------------------------------------
var (:untypedVar, :int typedVar) = record;
var (:lat, :double long) = (lat: 10.789, long: -8.856);
//---------------------------------------------
switch (record) {
case (untyped: var untypedVar, typed: int typedVar):
case (:var untypedVar, :int typedVar):
}
// Record pattern with null-check and null-assert subpatterns:
switch (record) {
case (checked: var checked?, asserted: var asserted!):
case (:var checked?, :var asserted!):
}
// Record pattern with cast subpattern:
var (untyped: untypedVar as int, typed: typedVar as String) = record;
var (:untypedVar as int, :typedVar as String) = record;
Object: SomeClass(x: subpattern1, y: subpattern2)
switch (shape) {
// Matches if shape is of type Rect, and then against the properties of Rect.
case Rect(width: var w, height: var h):
}
// Binds new variables x and y to the values of Point's x and y properties.
var Point(:x, :y) = Point(1, 2);
class Candidate{
Candidate({
required this.name,
required this.yearsExperience,
});
final String name;
final int yearsExperience;
}
//Different ways for Pattern matches and destructure in Object type
var candidate = Candidate(name: "Happy", yearsExperience: 2);
var Candidate(name: cName, yearsExperience: int uYears) = candidate;
var Candidate(:name, :int yearsExperience) = candidate;
switch (candidate) {
case Candidate(name: var uName, yearsExperience: int uYears):
case Candidate(:String name, :int yearsExperience):
}
switch (candidate) {
case Candidate(name: var uName?, yearsExperience: var uYears!):
case Candidate(:var name?, :var yearsExperience!):
}
var Candidate(name: uName as String, yearsExperience: uYears as int) = candidate;
var Candidate(:name as String, :yearsExperience as int) = candidate;
Object patterns don’t require the pattern to match the entire object. If an object has extra fields that the pattern doesn’t destructure, it can still match.
var Candidate(:name) = Candidate("Happy", 10);
print(name);
Wildcard: _
var list = [1, 2, 3];
var [_, two, _] = list;
switch (record) {
case (int _, String _):
print('First field is int and second is String.');
}
Patterns in several places in the Dart language
1. Local variable declarations:
- The pattern matches the value on the right of the declaration. Once matched, it destructures the value and binds it to new local variables.
Note: A pattern variable declaration must start with either var
orfinal
, followed by a pattern.
// Declares new variables a, b, and c.
var (a, [b, c]) = ('str', [1, 2]);
2. Variable assignment:
- A variable assignment pattern falls on the left side of an assignment. First, it destructures the matched object. Then it assigns the values to existing variables instead of binding new ones.
//swap the values of two variables without declaring temporary one:
var (a, b) = ('10', '20');
(b, a) = (a, b); // Swap.
print('$a $b'); // Prints "20 10".
3. for and for-in loops
// Example 1: List
List<Candidate> candidates = [
Candidate('Alice', 3),
Candidate('Bob', 5),
Candidate('Charlie', 2)
];
//Method 1: Before
for (Candidate candidate in candidates) {
print('${candidate.name} has ${candidate.yearsExperience} of experience.');
}
//Method 2: Now: Using Pattern
for (final Candidate(:name, :yearsExperience) in candidates) {
print('$name has $yearsExperience of experience.');
}
// Example 2: Map
Map<String, int> hist = {
'a': 23,
'b': 100,
};
for (var MapEntry(key: key, value: count) in hist.entries) {
print('$key occurred $count times');
}
for (var MapEntry(:key, value: count) in hist.entries) {
print('$key occurred $count times');
}
3. if-case
- Dart
if
statements supportcase
clauses followed by a pattern:
final pair = [10, 20];
if (pair case [int x, int y]){
return Point(x, y); // Point(10,20)
}
4. Switch case:
//Switch statement:
switch (charCode) {
case slash || star || plus || minus: // Logical-or pattern
token = operator(charCode);
case comma || semicolon: // Logical-or pattern
token = punctuation(charCode);
case >= digit0 && <= digit9: // Relational and logical-and patterns
token = number();
default:
throw FormatException('Invalid');
}
//Switch Expression:
token = switch (charCode) {
slash || star || plus || minus => operator(charCode),
comma || semicolon => punctuation(charCode),
>= digit0 && <= digit9 => number(),
_ => throw FormatException('Invalid')
};
5. Guard clause:
- To set an optional guard clause after a
case
clause, use the keywordwhen
. - Guards evaluate an arbitrary boolean expression after matching. This allows you to add further constraints on whether a case body should execute.
// Switch statement:
switch (pair) {
case (int a, int b) when a > b:
// ^^^^^^^^^ Guard clause.
body;
}
// Switch expression:
var value = switch (pair) {
(int a, int b) when a > b => body,
// ^^^^^^^^^^^ Guard clause.
}
// If-case statement:
if (something case (int a, int b) when a > b) {
// ^^^^^^^^^^ Guard clause.
body;
}
6. Control flow in collection literals
var nav = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];
7. Validating incoming JSON
- Map and list patterns work well for destructuring key-value pairs in JSON data:
var json = {'user': ['Lily', 13]};
var {'user': [name, age]} = json;
print("$name is $age years old"); // Lily is 13 years old
final json = {'user': ['Happy', 23];
//Before: Without patterns, validation is verbose:
if (json is Map<String, Object?> &&
json.length == 1 &&
json.containsKey('user')) {
var user = json['user'];
if (user is List<Object> &&
user.length == 2 &&
user[0] is String &&
user[1] is int) {
var name = user[0] as String;
var age = user[1] as int;
print('User $name is $age years old.');
}
}
//Now: With Pattern: less verbose method of validating
if (json case {'user': [String name, int age]}) {
print('User $name is $age years old.');
}
Wrap
Pattern matching is like a secret decoder for code, making it easier to understand and work with. It’s like having a special tool that lets you break down complex data into simpler pieces, write more organized if statements, and catch errors before they happen.
So, whether you’re a coding pro or just starting out, don’t forget to add pattern matching to your toolbox! It’s like a magic wand for writing cleaner, more reliable code, and it’ll help you create awesome things in Dart 3.0 and beyond.
Thank you…
References:
Other Account: Happy Makadiya
Banner Credit: Kaival Patel