Use Dart 3 to Improve Development Skills. More Examples and Tips.

Yii Chen
Flutter Taipei
Published in
7 min readJan 20, 2024

Smart using Dart 3 in Flutter world!

When Dart 3 introduced powerful features like Record and Pattern, it became an attractive language, well worth spending time with to effortlessly and quickly write quality code in projects.

Here are 8examples to deepen your understanding of Dart. I recommend practicing with IDE while going through them and printing out the results. I believe you’ll gain a better sense of the language. Every moment is crucial. Let’s not waste any more time. Follow along as I delve into the examples below:

Example 1

With Dart 3, Pattern Matching and Destructuring can be easily combined when working with the spread operator .... It effortlessly handles list operations, making the code quick and straightforward. In the example below, we access the first and last elements while ignoring the ones in between.

In this scenario, it’s essential to ensure there are at least two elements to avoid errors. The first and last variables are not null, so having only one element would result in an error.

final [first, ..., last] = [2, 4, 6, 8, 10];
debugPrint('$first, $last'); // 2, 10

final [first, ..., last] = [2, 4];
debugPrint('$first, $last'); // 2, 4

final [first, ..., last] = [2];
debugPrint('$first, $last'); // throw exception

Example 2

This example demonstrates a tip in Pattern Matching called if-case matching. It allows us to perform concise and quick checks on nullable variables, also known as Null Guard. The compiler ensures that the variable is not within the null range. This technique is beneficial not only in writing business logic but also in the UI code. The following outlines the original approach and the new way:

Old: When the variable is not in the same block, and we want to ensure it is not null, we need to use an if statement. The drawback is that even after confirming non-null status, a default value needs to be provided when accessing the variable.

New: Using if-case matching, we can determine whether the variable is null, and we can also customize the variable name. This validation can be achieved in a single line, making the code concise and elegant.

The third section provides an example of allowing the variable to be null, depending on the specific requirements.

int? age;

void main() {
// Old
if (age != null) {
printAge(age ?? 0);
}

// New, check value is not nullable.
if (age case final int age) {
printAge(age);
}

// New, allow nullable value
if (age case final int? age) {
printAge(age ?? 0);
}
}

void printAge(int age) {
debugPrint('Age is $age.');
}

Example 3

This example uses Record, Pattern Matching, and Switch Expression along with enum for conditional checks. When dealing with multiple variables that require evaluation, avoiding nested if-else structures is crucial to prevent code lengthiness. By wrapping multiple variables with Record and listing only the specific conditions you encounter within, the code becomes concise and highly readable. Any other scenarios can be represented with an underscore (_) for simplicity.

enum AccountType {
vip,
member,
guest,
}

void main() {
bool isAuthenticated = true;
bool isPaid = true;

final type = switch ((isAuthenticated, isPaid)) {
(true, true) => AccountType.vip,
(true, false) => AccountType.member,
(_, _) => AccountType.guest,
};

debugPrint("This account is ${type.name}");
}

Example 4

In this example, typedef is employed to create a convenient Record. Sometimes, all that is needed is a simple class to store some properties and enable object comparison based on these properties, with equatable functionality. Using typedef in conjunction with Record provides a good approach to mimic class-like behavior.

It’s important to note that Record itself is not a class; as long as the fields and representations are the same, they are treated as the same type.

typedef Student = ({String name, int number});

extension StudentExtension on Student {
void sayHi() {
debugPrint('Hi, I am ${this.name}. No.$number');
}
}

void main() {
const Student student = (name: 'Yii', number: 1);
student.sayHi();
}

Example 5

Class Equality with Record achieves the same functionality as the equatable package, making it easier to compare class objects for equality. This boring task is improved through Dart 3.

Old:

In the operator method, each property needs to be compared individually, resulting in lengthy code. The same applies to the hashCode getter.

@override
bool operator ==(covariant Location other) {
if (identical(this, other)) return true;

return other.country == country && other.id == id;
}

@override
int get hashCode => Object.hashAll([country, id]);

New:

With Record, the development transforms. The comparison part is handled through a _equality() method, a feature of Record. Writing the operator hashCode becomes a straightforward task, enhancing code conciseness.

class Student {
Student({
required this.name,
required this.number,
});

final String name;
final int number;

(String, int) _equality() => (name, number);

@override
bool operator ==(covariant Student other) {
if (identical(this, other)) return true;

return other._equality() == _equality();
}

@override
int get hashCode => _equality().hashCode;
}

Example 6

In certain situations, reading data from a Map and parsing it can be cumbersome. The old approach involves multiple layers of type and null checks before extracting the content of specific fields. This method is likely not favored by many due to its complexity.

Old

final dynamic json = {
'data': [
{'name': 'Andyy'},
{'name': 'Anby'},
{'name': 'Ancy'},
{'name': 'Andy'},
]
};

bool hasAndy = false;

if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final item in data) {
if (item is Map) {
if (item.containsKey('name') && item['name'] == 'Andy') {
hasAndy = true;
}
}
}
}
}

print(hasAndy); // true

New

Improved Approach using Dart 3. In this enhanced approach, Dart 3 features such as Switch Expression, Pattern Matching, and Destructuring are employed to effortlessly parse a Map, significantly improving readability.

final hasAndy = switch (json) {
{'data': List items} => items.any((element) => switch (element) {
{'name': 'Andy'} => true,
_ => false,
}),
_ => false,
};

print(hasAndy); // true

Example 7

This example aims to parse an array to obtain the RGB values of a color. Initially, we need to check the length, and then verify if the elements are non-null strings before accessing the values.

Old

final rgb = [255, 255, 255];

if (rgb.length == 3) {
final red = rgb[0];
final green = rgb[1];
final blue = rgb[2];

if (red is int && green is int && blue is int) {
debugPrint('$red, $green, $blue');
}
}

New

A more concise solution using if-case matching and destructuring. This approach ensures comfortable access to the data when the conditions are met, providing a more pleasant coding experience.

final rgb = [255, 255, 255];

if (rgb case [int red, int green, int blue]) {
debugPrint('$red, $green, $blue');
}

Example 8

This example has been available in Dart even before Dart 3, but it’s not widely known. The example shows that variables, even those declared as non-nullable, don’t necessarily need to be initialized with late or default values. Dart automatically checks whether each scenario is handled during the process, ensuring that the variable doesn’t violate its intended behavior. In the following code, a try-catch block is used after the variable, and the value is assigned within the try block. If there’s an error, a new exception is thrown, and Dart still knows that the usage of the price variable later on won’t result in an error, as the compiler has given assurance based on the expected scenarios.

Two situations can be used like this:

  1. Both try and catch perform assignments
  2. try assignment catch throws an exception and prevents the program from continuing
int price;

try {
price = 20;
} catch (error) {
// 1.
price = -1;
// 2.
throw Exception(error);
}

debugPrint(price.toString());

This can also be used for if-else situations. Assuming that both true and false situations have values ​​assigned or exceptions are thrown, it will compile normally. Dart already knows that there will be no problem, so the variables do not need late and default values.

int getPrice(bool canKnowPrice) {
int price;

if (canKnowPrice) {
price = 20;
} else {
// 1.
price = -1;
// 2.
throw Exception('Something wrong.');
}

return price;
}

The difference between the above usage and the late usage is that if we use late to declare price, it means that we guarantee that the variables will be initialized and assigned, so the compiler will not help check, if you do not write a test to ensure that there is no problem, you The program may report an error and the APP may stuck.

Conclusion

The above examples are all situations encountered in daily development. Dart 3 has endless power, but you may not be able to control it. We can only become familiar with it through daily practice, strengthen our awareness, and slowly be able to write high-quality code. It is also recommended that everyone read the Dart official website and Github repo to learn new usages to improve your own and your company’s projects!

Chinese version on iThome

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

Reference

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