Dart Switch Expressions

Christian Findlay
5 min readMay 11, 2023

--

Dart Switch Expressions

Dart 3 adds a new feature called Switch Expressions. Dart is a multi-paradigm language that supports object-oriented, imperative, functional-style and declarative programming. Programmers have adopted it worldwide, primarily because of its simplicity, flexibility, and its seamless integration with the Flutter framework. The release of Dart 3 brought several new features and improvements around functional style programming, with the new switch expression being one.

Expressions Over Statements

Expressions and statements are two foundational concepts in coding. Expressions evaluate to a value. For example, they include arithmetic like 2 + 2 or more complex functions. Statements, in contrast, perform actions, such as assigning a value to a variable or controlling program flow. Expressions can be part of larger expressions, while statements can only execute an action and can't form part of larger expressions. This is why we consider statements part of imperative programming.

The preference for expressions over statements is characteristic of functional programming. They are more declarative. They emphasize data flow over action sequences, often leading to more concise and testable code. Imperative languages focus on statements and action sequences, which can be more straightforward but potentially lead to more complex code for intricate tasks. As languages evolve, the line between the two styles is getting blurry. Many languages, including Dart, incorporate more expressive features for more powerful and flexible programming.

What is a Switch Expression?

The traditional Dart switch statement is imperative and procedural. It lacks the capability to return a value directly. The new switch expression is functional in nature. It is an expression rather than a statement, which means it evaluates to a value. This transformation is an important shift as it introduces a new functional programming aspect to Dart, a feature that modernizes the language and offers developers more flexibility and power.

The switch statement is followed by a series of statements. But, the switch expression in Dart 3 uses a syntax similar to arrow functions to map case clauses directly to values. This makes the code more concise, readable, and less prone to errors. Here’s an example of how it works:

Try it live on Dartpad

void main() {
var dayOfWeek = 'Monday';
var dayNumber = switch (dayOfWeek) {
'Monday' => 1,
'Tuesday' => 2,
'Wednesday' => 3,
'Thursday' => 4,
'Friday' => 5,
'Saturday' => 6,
'Sunday' => 7,
_ => 10, //Default value
};
print(dayNumber);
}

Pattern Matching

Pattern matching is a powerful feature that checks if a given variable or object matches a specific pattern or structure. It’s a prevalent feature in functional programming languages like F#, Haskell, Rust, and Scala. You can use it to destructure complex data types, perform conditional execution, and write more readable and intuitive code. It enhances control flow and allows programmers to write cleaner, more efficient code with less error handling.

Dart 3 introduces pattern matching. The new switch expression in Dart 3 supports pattern matching. With pattern matching, each case clause in the switch expression can be an arbitrary pattern, and the expression to the right of the arrow => evaluates only if the pattern matches. You can use pattern matching with various types of patterns, including constant patterns, variable patterns, and structured patterns. This is more powerful than the switch statement, which only allows constant patterns.

This is a very simple example that matches on type:

Try it live on Dartpad

abstract class Animal {
String get name;
}

class Dog extends Animal {
@override
String get name => 'Spot';
}

class Cat extends Animal {
@override
String get name => 'Garfield';
}

void main() {
Animal pet = Dog();

final sound = switch (pet) {
(Dog d) => '${d.name}: Woof!',
(Cat c) => '${c.name}: Meow!',
_ => '...',
};
print(sound);
}

Exhaustiveness Checking

Exhaustiveness checking is a feature that gives compile-time errors if the switch expression does not cover all cases. For example, if you switch on a nullable Boolean (bool? b) without a case for null, you will get an error. However, a default case (_ or default) can cover all possible values, which makes any switch statement exhaustive.

Sealed types (another new Dart 3 feature) and enums are especially useful for switches because the compiler knows their possible values ahead of time, even without a default case. Applying the sealed modifier to a class enables exhaustiveness checking when switching over its subclasses. If we add a new subclass to a sealed class, the switch expression will be incomplete. Exhaustiveness checking helps flag this.

Try it live on Dartpad

import 'dart:math';

sealed class Shape {}

class Square extends Shape {
Square(this.length, this.width);
final double length;
final double width;
}

class Circle extends Shape {
Circle(this.radius);
final double radius;
}

double calculateArea(Shape shape) => switch (shape) {
Square(length: var l, width: var w) => l * w,
Circle(radius: var r) => pi * r * r
};

void main() {
Shape shape = Square(3, 3);
print(calculateArea(shape));
shape = Circle(3);
print(calculateArea(shape));
}

If you add this new class to the example, the code will fail to compile with an exhaustiveness error.

class Rectangle extends Shape {
Rectangle(this.length, this.width);
final double length;
final double width;
}

Matching On Records and Using Guard Clauses

Dart 3 also introduces records. Put simply, functions and expressions can return more than one value at a time. We can pattern match on records. This powerful feature allows us to destructure complex data types and perform conditional execution. We can use the when keyword to specify a guard clause. This Boolean expression must be true for the case to match.

Here is an example:

Try it live on Dartpad

(int a, int b) returnMulti() => (1, 2);

void main() {
var numbers = returnMulti();
var dayNumber = switch (numbers) {
(int a, int b) when a == 1 && b == 2 => 'One and Two',
(_, _) => 'Default'
};
print(dayNumber);
}

What Does This Mean For Flutter?

The new syntax makes widget composition even more concise and readable. It also makes it easier to handle complex state management. One example is when handling async snapshots. In this scenario, Dart 2 allowed us to use nested ternaries as expressions. But, the switch expression gives us a new option.

This is a flutter example. Notice that we can compose the entire app with a single expression, and the switch expression makes the async snapshot handling far less verbose.

Try it live on Dartpad

import 'package:flutter/material.dart';

void main() => runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: FutureBuilder(
future: Future<String>.delayed(
const Duration(seconds: 3), () => 'Hello World!'),
builder: (context, snapshot) => switch (snapshot) {
(AsyncSnapshot s) when s.hasData => Text(s.data!),
(AsyncSnapshot s) when !s.hasError =>
const CircularProgressIndicator.adaptive(),
(_) => const Text('Error'),
},
),
),
),
),
);

Conclusion

Dart 3’s new Switch Expression further adds functional programming to the language. It emphasizes expressions over statements and promotes concise, testable code. As Dart evolves, it merges the best of functional and imperative styles. This offers a versatile toolset for developers to craft efficient, robust applications. Dart signifies innovation and adaptability. This will further cement its place as a leading language for cross-platform application development.

Originally published at https://www.christianfindlay.com on May 11, 2023.

--

--

Christian Findlay

I am a freelance Flutter developer in Melbourne, Australia. I have a long background in software architecture with .NET and apps with Xamarin.