Get Familiar with Dart 3, Make your Life Easier!
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
andb
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 toa
. - 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.
The following table shows incompatible combinations:
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
Articles
- Flutter October 2023 💙 Flutter Monthly
- Flutter September 2023 💙 Flutter Monthly
- Flutter August 2023 💙 Flutter Monthly
- Flutter July 2023 💙 Flutter Monthly
- 添加預覽影片到 App Store,提升品牌形象
- Fluttercon 2023 技術研討會
- Flutter 六月大小事
- Wow! Flutter runs on Apple Vision Pro!
- 這次 Flutter 3.10 與 Dart 3 又強大了多少?Google IO 告訴你
- Flutter Meetup #1 聚會有什麼?還有 Flutter 四月大小事!
- 提升開發效率的好物,Mason 讓你輕鬆撰寫自定義模板!
- 教你製作強大的 Rive 動畫,完成一隻 Flutter Dash,在 APP 跟它互動!
- Flutter 如何根據 Flavor 多環境載入對應的 Firebase Config
- Isolates 在 Flutter 3.7 & Dart 2.19 的升級,你該知道一下!
- 讓人驚艷的 Flutter Forward,釋出 Flutter 3.7 和 Dart 2.19
- 學會運用 Flutter Widgetbook,該管好自己和公司的元件庫了!
- 剛進入 Flutter 嗎?適合初學者食用,GetX 是否適合你呢!
- 你知道 Maestro 嗎,兼具人性的自動化測試框架,Flutter 品質就靠它了 — Part 1: 介紹與使用
- 教你為 Riverpod 2.0 撰寫 Flutter 測試 part.1
- 輕鬆了解 Isar NoSQL DB,用它來實作 Flutter 資料庫吧!
- Flutter 輕鬆實作 i18n,使用 easy_localization_generator 就對了
- Flutter CICD 使用 Gitlab Runner 和 App Center 實作 part.1
- 使用 CodeMagic 和 Firebase 實現 Flutter CICD
- 輕鬆完成Flutter開發環境,最新版!
About
- GitHub: chyiiiiiiiiiiii
- Twitter: yiichenhi
- Instagram: flutterluvr.yii
- Linkedin: yiichenhi
- Youtube: Yii
- Email: ab20803@gmail.com
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!
I hope it can help you. Welcome to follow me to get the upcoming article.