Get Familiar with Dart 3, Make your Life Easier!

Yii Chen
Flutter Taipei
Published in
10 min readNov 25, 2023

--

Smart using Dart 3 in Flutter world!

Dart 3 underwent a major upgrade with the release of Flutter 3.10, achieving 100% sound-null safety. This means that all properties and variables must declare whether they are nullable. The purpose of this enhancement is to make project compilation more efficient, increase speed, and enhance project stability, reducing unnecessary errors and crashes. Additionally, several language features such as Record, Pattern, and Class-Modifier were added, providing significant assistance in the development, simplifying, and improving readability in many scenarios.

This article aims to share the benefits of Dart 3, hoping to help you quickly understand and benefit from it. Therefore, I have prepared numerous practical examples, covering basic concepts, beginner to advanced usage, and sharing some fantastic techniques. Let’s dive into the main content!

Record

Record is an anonymous and immutable aggregate type that can store multiple objects into one. It is typically used with parentheses to define its properties (e.g., double lat, double lon). You can store Records in variables, put them in a List, use them as keys in a Map, or include other Records within a Record, providing versatile usage.

When using Records, there’s no need to create new Classes for individual processes at times. For instance, if a location has latitude and longitude, or a color has RGB values, Record can simplify handling such scenarios. It also helps in addressing the need for functions that require multiple return values. Let’s look at the following example:

Example 1:

  • In this example, the getLocation() function commonly returns latitude and longitude. Here, we use a Record to handle this and decide whether to customize the names based on readability. Externally, we can use (double, double) to represent the Record as an anonymous type.
  • A Record serves as an anonymous type, having two named parameters in this example. During the development, providing a pre-name is required.
  • Records can have both anonymous and named parameters, and anonymous parameters can be accessed by index, starting from 1 in sequential order.
// Function
(double Lat, double lon) getLocation(String name) => (25.034092, lon: 121.563956);

// Named-args
({double lat, double lon}) location;
location = (lat: 25.034092, Lon: 121.563956);

// Mixed
var person = ('Yii', isMale: true, '175');
print(person.$1);
print(person.isMale) ;
print(person.$2);

Example 2

In common Flutter scenarios, when an app requires a bottom menu, we often use the BottomNavigationBar. However, there’s no need to create a separate BottomNavigationBarItem class to save both the name and icon properties. Instead, you can directly use a Record. When writing UI code, you can access anonymous variables using their index.

List<(Widget, String)> items = <(Widget, String)>[
(const Icon(Icons.home), 'Home'),
(const Icon(Icons.search), 'Search'),
(const Icon(Icons.face), 'Profile'),
];

BottomNavigationBar(
items: items
.map
((Widget, String) item) =>
BottomNavigationBarItem(
icon: item.$1,
label: item.$2,
),
)
.toList(),
)

Comparison of Records

The identity of a Record depends on the fields it possesses, both anonymous and named. When two Records have the same structure, they are considered equal.

Example 3:

  • We can easily write tests to verify if two Records are the same. In the example, since the structures of a and b are the same, the test passes. However, c does not pass the test because it has only anonymous parameters, and it won't be considered equal to a.
  • When comparing Object lists that don’t implement the == operator, they won't be considered equal
test('Records equality', () {
// 1.
const ({int width, int height}) a = (width: 100, height: 200);
const ({int width, int height}) b = (width: 100, height: 200);

const (int width, int height) c = (100, 200);

expect(a, equals(b)); // Passed
expect(a, equals(c)); // Failed

// 2.
final complex = (1, 'dog', ['cat', 'pig']);
final complex2 = (1, 'dog', ['cat', 'pig']);
expect(complex, equals(complex2)); // Failed
});

Pattern Matching

Pattern Matching is responsible for checking if an object matches the expected structural format, allowing access to all or part of the attributes. It also involves destructuring and improving code readability. Understanding this through an example might make it clearer:

Example 4:

In this example, the requirement is to access the “command” field in a JSON object. Looking at the old style, we need to check if the JSON is a Map, its length, and if it has the correct key. Then, we check the types of individual fields and extract the values. This process involves multiple checks and has already written 10 lines of code — feeling a bit cumbersome, isn’t it?

Now, look at the Dart 3 new syntax. Through if-case matching, it checks if the structure of the JSON matches the expected fields (“command” and “value”) and if their types are String and int, respectively. It also ensures they are not null. This single line represents many conditions, and when they all match, you can confidently use the values. Destructuring is used here, so you can directly use the variables, and only 2 lines of code are needed.

final json = {'name': 'Amy', 'age': 30};

// Old
if (json is Map<String, Object?> &&
json.length == 2 &&
json.containsKey('name') &&
json.containsKey('age')) {
if (json['name'] is String && json['age'] is int) {
final name = json['name'] as String;
final age = json['age'] as int;
print('User $name is $age years old.');
}
}

// New
if (json case {'name': String name, 'age': int age}) {
print('$name is $age years old.');
} else if (json case {'name': 'Amy', 'age': int age}) {
print('Amy is $age years old.');
} else {
print('Error: json is not correct.');
}

Example 5: Destructuring Usage with Structure Matching

In this example, we’ll explore the usage of destructuring along with structure matching. If the structure matches, we can directly use the destructured values without the need to declare new variables. Let’s take a look.

final names = [
Person(name: 'Yii', age: 27),
Person(name: 'Andy', age: 30),
Person(name: 'Jay', age: 24),
]

final [yii, andy, jay] = names;
print(yii.toString());
print(andy.toString());
print(jay.toString());

Example 6: Lightweight Destructuring

In this example, we’ll explore a more lightweight way of destructuring, allowing you to directly use the named properties of the original Record. This involves destructuring the property values while declaring a getter with the same name as the properties.

const position = (x: 0, y: 2);
final (:x, :y) = position;
print('$x, $y');

Switch Expression

Switch expressions provide a more concise way to perform switch checks without the need for explicit case statements and return statements. It allows for a simplified syntax, incorporates pattern matching, and supports additional conditions use when for secondary checks. This enhances code readability and intuitiveness. Let's explore an example:

Example 7

This example demonstrates how you can directly provide content in UI code based on the change in a certain state, eliminating the need for multiple layers of if-else statements. Instead, it uses a switch to check the state. Suppose there is a tutorial page with 5 steps that can be navigated, and each step displays different text content.

  • You can see that the third line uses _, which usually represents ‘else’. The second condition checks if it is not the last page, referring to indexes 2 and 3 in this case.
  • Finally, using _ without any other conditions is essentially ‘else’ itself, corresponding to index 4, which is also the last page.
ElevatedButton(
onPressed: _goNext,
child: Text(
switch (_currentPage) {
0 => 'Start',
1 => 'Next',
_ when _currentPage != _LastPage => 'Next',
_ => 'Confirm',
},
),
)

Example 8

In this example, the states are ignored, and the main focus is on the second-level when conditions. Different content is displayed based on various scenarios. The final case serves as the ‘else’ condition for situations that don’t match any defined conditions.

switch (pageState) {
_ when pageState.isLoading => 'Loading..',
_ when pageState.content.isNotEmpty => state.content,
_ => 'Unknown Error',
}

Example 9

In this example, the scenario is to retrieve the second-to-last element from a List object. It returns a specified string if the condition is matched, and another string if it’s not.

  • In the first line, square brackets are used to represent a List. The first element is extracted using the spread operator, which represents zero or more elements in Dart.
  • The underscore _ is used to ensure there is at least one element at the end of the list.
  • The second element, the one we need, is assigned to a variable for later use.
  • In the second line, an exception is handled for scenarios where the list is empty or has only one element. If the list is empty, or there’s only one element, it returns ‘Need more numbers’
  • Finally, the result variable is used for further processing.
List<int> numbers = [1, 2, 3];

final result = switch (numbers) {
[..., final num, _] => 'Number is $num',
[] || [_] => 'Need more numbers',
};

print(result) ;

Class Modifier

Starting from Dart 3, various class modifiers are supported, allowing developers to precisely define the extensibility of classes. Different library files may have different restrictions, providing significant assistance for developers. The following introduces the modifiers:

  • base class: Only allows inheritance.
  • interface class: Only allows implementation.
  • final class: Prohibits inheritance, implementation, and mixing.
  • mixin class: A mixin class. Currently, regular classes are not allowed to act as mixins.
// Failed
class NormalClass {}
class FirstClass with NormalClass {}

// Passed
mixin class MixinClass {}
class SecondClass with MixinClass {}
  • sealed class: The compiler assists in checking when there is an error if a subclass is not handled.

To explore valid modifier combinations and usage, you can check the official Dart documentation, which provides a comprehensive list. It outlines whether each modifier allows construction, inheritance, implementation, mixin, and checking exhaustively.

https://github.com/dart-lang/language/blob/main/accepted/3.0/class-modifiers/feature-specification.md

The following table shows incompatible combinations:

https://dart.dev/language/modifier-reference

Example 10

This example uses sealed class, final class, and switch expression operations. The scenario involves making a network request and categorizing the response into two classes, Success and Failure.

The Response class is defined using sealed, with Success and Failure inheriting from Response, where the generic type represents our target type.

sealed class Response<T> {}

final class Success<T> extends Response<T> {
final T data;
Success({required this.data});
}

final class Failure<T> extends Response<T> {
final Exception exception;
Failure({required this.exception});
}

getPerson() is our request method, returning a Response<Person>. Pay attention to the middle part, after making the request, it checks the statusCode. If it's 200, the success is confirmed. It first parses the JSON into a Map, then obtains a Person object through fromJson(), and finally returns the Success subclass. For other statusCode values, it indicates failure, and it directly returns the Failure subclass.

Additionally, below is a new switch expression syntax provided, helping you review how to use it in practical scenarios.

Future<Response<Person>> getPerson({required int id}) async {
try {
final uri = Uri.parse('http://io.com/persons/' + id.toString());
final response = await http.get(uri);

// 1. Normal switch
switch (response.statusCode) {
case 200:
final data = json.decode(response.body);
return Success(data: Person.fromJson(data));
default:
return Failure(exception: Exception(response.reasonPhrase));
}

// 2. Switch expression
final result = switch (response.statusCode) {
200 => Success(data: Person.fromJson(json.decode(response.body))),
_ => Failure<Person>(exception: Exception(response.reasonPhrase)),
};

return result;
} on Exception catch (e) {
return Failure(exception: e);
}
}

After completing the request, when obtaining the Response object externally, we need to check whether it is a Success or Failure. Similarly, we use a switch expression for pattern matching. If it is a success, we can use the person object and return a string. If it's a failure, an error message is returned. Finally, the result is printed out.

final response = await getPerson(id: 1);

final result = switch (response) {
Success(data: final person) => person.toString(),
Failure(exception: final exception) => exception.toString(),
}

print(result);

Conclusion

We have briefly explained the usage of new things in Dart 3, such as Record, Pattern Matching, Class Modifier, etc., with 10 examples. You should know them more and be eager to develop your own projects with Dart 3. If you feel that you are still not satisfied, you can read my next article, where I will share more practical cases with you, let us enjoy it together!

In addition, if you want to watch the video and listen to the sound to learn, you can watch my sharing on Google IO Extended in Taipei, which explains the above examples. You are welcome to watch the video if you have time, and you will understand Flutter and Dart better. Here is the video link:

The video is in Chinese.

Chinese version on iThome

Day 2: 使用 Dart 3 改善我們的開發習慣,更多範例與技巧分享!

Reference

  1. https://github.com/dart-lang/language/tree/main/accepted/3.0
  2. https://dart.dev/language/modifier-reference
  3. https://medium.com/dartlang/a1f4b3a7cdda
  4. https://www.aloisdeniel.com/blog/dart-pattern-matching
  5. Pascal Welsch — Exploring Records and Patterns

Articles

About

Contribution

If you think the article is good, you can sponsor it, so that I have more motivation and enthusiasm to share my learning and life! Buy me a cup of coffee!

https://www.buymeacoffee.com/yiichenhi

I hope it can help you. Welcome to follow me to get the upcoming article.

--

--

Yii Chen
Flutter Taipei

Flutter Lover || Organizer FlutterTaipei || Writer, Speaker || wanna make Flutter strong in Taiwan. https://linktr.ee/yiichenhi