Dart 3 has been described as the largest Dart release to date.
This release introduces some major features such as:
- patterns and records
- enhanced switch and if-case statements
- destructuring
- sealed classes and other class modifiers
These features were first announced at Flutter Forward, and I am so happy that we can use them now that Flutter 3.10 and Dart 3.0 have been officially released.
In practice, Dart 3.0 allows us to write more expressive and elegant code, but there is a bit of a learning curve if you want to make the most of it.
And since there is so much to cover, I’ve decided to write a new series of articles showing how to use each feature in detail, along with examples and practical use cases that you may encounter in the real world.
And as far as this article goes, I want to give you a little taste of what Dart 3.0 has to offer.
In other words, this article is the appetizer. And the upcoming ones will be the main course (and dessert 🍰).
Ready? Let’s go! 🎯3️⃣
Enabling Dart 3 in your Flutter project
To use the new features, you'll need to enable Dart 3.0 in your pubspec.yaml
file:
environment:
sdk: '>=3.0.0 <4.0.0'
Dart 3.0 is enabled by default when you create a new project with Flutter 3.10.
With that taken care of, let's learn about the new language features. 🔥
Introduction to Records in Dart 3
Records are very versatile and can be used in many different scenarios.
For example, imagine you’re making a network request and getting some JSON data which looks like this:
final json = {'name': 'Andrea', 'age': 38, 'height': 184};
If you use the json
variable as is, you lose type safety because Dart will infer it to be a Map<String, dynamic>
.
To improve things, you can declare a Person
class and implement the deserialization logic inside a factory constructor:
class Person {
const Person({required this.name, required this.age, required this.height});
final String name;
final int age;
final int height;
factory Person.fromJson(Map<String, dynamic> json) {
// * error handling code missing for simplicity
return Person(
name: json['name'],
age: json['age'],
height: json['height'],
);
}
}
This works but it’s a bit verbose.
With Dart 3.0, there is a quicker way and you can use a record type to represent your data. This allows you to create functions with multiple return values:
(String, int, int) getPerson(Map<String, dynamic> json) {
return (
json['name'],
json['age'],
json['height'],
);
}
You can then assign the result to a record variable of type (String, int, int)
and access each field like this:
final person = getPerson(json);
print(person.$1); // 'Andrea'
print(person.$2); // 38
print(person.$3); // 184
Alternatively, you can destructure the returned value like this:
final (name, age, height) = getPerson(json);
print(name); // 'Andrea'
print(age); // 38
print(height); // 184
But there’s more. 👇
Positional vs named arguments
In the example above, the record type only used positional arguments (in other languages, this is known as a tuple).
But Dart records can use named arguments too.
This means that if we wish, we can change the return type of our function:
// same example as above, now using named arguments
({String name, int age, int height}) getPerson(Map<String, dynamic> json) {
return (
name: json['name'],
age: json['age'],
height: json['height'],
);
}
As a result, we can obtain and print the record values like this:
final person = getPerson(json);
print(person.name); // 'Andrea'
print(person.age); // 38
print(person.height); // 184
And here’s the equivalent code using destructuring:
// * we'll cover the : syntax in a follow up article
final (:name, :age, :height) = getPerson(json);
print(name); // 'Andrea'
print(age); // 38
print(height); // 184
Overall, think of records as a lightweight alternative to classes.
Records are “more” type safe than Map
or List
because they let you specify how many values there are, along with their type.
They support both named and positional arguments (or a combination of both), just like regular function arguments.
And as we have seen, you can use destructuring to simplify your code even more.
But if you want to use records correctly and prevent syntax errors, there are some rules to follow. So I’ll cover them in more detail in an upcoming article.
Introduction to Switch Expressions and Patterns
Suppose we’re writing a 2D game where the player can move up, down, left, and right.
This is easily represented as an enum:
enum Move { up, down, left, right }
Now, suppose we want to calculate certain offset (as a pair of x-y coordinates) depending on the current move.
Before Dart 3, we may have implemented the required code like this:
enum Move {
up,
down,
left,
right;
Offset get offset {
switch (this) {
case up:
return const Offset(0.0, 1.0);
case down:
return const Offset(0.0, -1.0);
case left:
return const Offset(-1.0, 0.0);
case right:
return const Offset(1.0, 0.0);
}
}
}
All those case
and return
keywords sure make my eyes tired. 🥱
But with Dart 3, we get switch expressions, and we can rewrite the code above in a much more concise way:
enum Move {
up,
down,
left,
right;
Offset get offset => switch (this) {
up => const Offset(0.0, 1.0),
down => const Offset(0.0, -1.0),
left => const Offset(-1.0, 0.0),
right => const Offset(1.0, 0.0),
};
}
Wanna get the movement on the x-axis only? Then leverage pattern matching by using logical operators inside the switch:
double get xAxisMovement => switch (this) {
up || down => 0.0, // logical OR operator with pattern matching
left => -1.0,
right => 1.0,
};
You can do so much more with switch expressions in Dart 3.
So I’ll be covering them in detail in a separate article. 👍
Introduction to Sealed Classes
Sealed classes help you check for exhaustiveness so that you can handle all possible cases.
This is particularly important when handling exceptions in your code.
For example, you could use sealed classes to define all possible auth exceptions that could be returned by your backend:
sealed class AuthException implements Exception {}
class EmailAlreadyInUseException extends AuthException {
EmailAlreadyInUseException(this.email);
final String email;
}
class WeakPasswordException extends AuthException {}
class WrongPasswordException extends AuthException {}
class UserNotFoundException extends AuthException {}
This lets you handle each possible exception type with a switch expression:
String describe(AuthException exception) {
return switch (exception) {
EmailAlreadyInUseException(email: final email) =>
'Email already in use: $email',
WeakPasswordException() => 'Password is too weak',
WrongPasswordException() => 'Wrong password',
UserNotFoundException() => 'User not found',
};
}
The code above only works because the AuthException
class is sealed.
If it weren’t, we would get a non_exhaustive_switch_expression
error:
But if we mark the base class as sealed
, the compiler knows that we’ve handled all possible cases.
Declaring a sealed class has two important implications:
- The class becomes abstract (you can’t create a concrete instance of it)
- All subclasses must be defined in the same library (file).
There is more to sealed classes than what I have presented here, so stay tuned for a more in-depth article about them. 🙂
Introduction to Class Modifiers in Dart 3
Before Dart 3, only two class modifiers were available: abstract
and mixin
.
But with the new release, we get six:
abstract
base
final
interface
sealed
mixin
Conveniently, the Dart website has a new page explaining how all these modifiers work:
I may cover these modifiers more in detail in the future, but for now, the official page is a good starting point.
Dart 3: Next Steps
With this article, I wanted to give you a glimpse of what you can do with Dart 3.
Hopefully, I’ve inspired you to try out the new language features in your projects.
But there is a lot more to unpack, including if-case statements, how to do pattern matching with nullable values, and other advanced use cases. And unless you have a good grasp of how they work, you may struggle.
For this reason, I’ll publish a whole series of articles diving deeper into all the new language features.
These articles will give you more confidence and a solid understanding of how to use Dart 3 effectively in your own apps.
If you don’t want to miss them, sign up for my newsletter. 👇