認識 Dart “Extension Types”,強化 Flutter 互通性開發
Extension Types in Dart 3.3
官方在文件開頭就提到:
“An extension type is a compile-time abstraction that “wraps” an existing type with a different, static-only interface.”
Extension Types 是 compile time abstraction,像是一個強大的類型包裝器。它的出現,是為了效能最佳化以及強化與 Native 程式碼的互動。身為零成本包裝器,在與原生平台溝通時,可以去除典型 Wrapper Class、Helper Class 會有的記憶體成本。
一般的 Class Wrapper 是在 run-time 運行,始終都會存在 Class 和 Object 的使用開銷,因此增加了記憶體使用與 GC 的處理成本。而當使用情境會在短時間生成非常多 Wrapper 實體時,就是一個應用的龐大負擔。
而 Extension Types 在編譯就確認它是某個類型的延伸,所以在 run-time 使用它時,這個抽象類型會消失,並以原有的 Representation Type 表現。因此對於應用來說,Extension Type 的使用沒有成本,是個非常良好的開發方式。
Extension Types 是 statifc JS interop 的主要幫手,可以輕鬆與現有 JS 類型的介面互動,同時確保不會產生實際的包裝器成本。
優點
1. 彈性的約束或擴充
允許強化現有類型,例如:int、String,添加屬性、函數等 API。
2. 更清晰的抽象
隱藏底層 Representation Type 的複雜性,進行更有意義的擴充,從而提高程式碼的可讀性和可維護性。
3. 方便與安全的互通性
使用自定義的 Dart 類型就像直接存取底層類型一樣,同時提供了類型安全性。對於與原生平台和其他語言的互通性非常有用,簡化了過程。
4. 增強的效能
避免為每個服務或特定的底層溝通建立 Wrapper Class,不會產生額外的記憶體開銷。非常適合效能敏感的場景,特別是在處理大型資料集或頻繁的物件操作時。
開發
Extension Type 本身在聲明時,預設擁有主要建構函數。
extension type MyId(int id) {}
void main(List<String> arguments) {
final id = MyId(1);
print(id); // 1
print(id.runtimeType); // int
}
在任何情況下,Extension Type 包裹的類型稱為 Representation Type,而它不是子類型,因此在一般情況下 Representation Type 與自定義的 Extension Type 不能互相賦值。
當沒有自定義新的屬性、函數介面時,什麼功能操作都沒有。限縮了 int
的原有行為
對於既有的類型、型別進一步擴展或約束,只暴露可使用的 API,進而避免一些不允許的操作。可以幫它加上一些我們需要且有意義的功能
extension type MyId(int id) {
operator >(MyId other) => id > other.id;
bool isBiggerThan(MyId other) => id > other.id;
}
void main(List<String> arguments) {
MyId safeId = MyId(200);
safeId + 10; // Compile error: No '+' operator.
safeId - 10; // Compile error: No '-' operator.
safeId > 10; // Compile error: Wrong type.
safeId > MyId(300); // ✅
int number = 100;
number = safeId; // Compile error: Wrong type.
number = safeId as int; // ✅ Cast to representation type.
safeId = number as MyId; // ✅ Cast to extension type.
print(safeId.isBiggerThan(MyId(300))); // false
}
可讀性也會因此提升,假設使用 int 乘載某個服務或 native api 互動後回傳的值,可以將它透過 Extension Type 給予特定名稱。因此在閱讀時能更快的了解表達意義 。
Extension Type 和 Representation Type 兩者可以直接相互轉換,使用 as
轉型。特別的是,他們不處於繼承關係卻可以強制轉換。
i = id as int; // ✅
i = -1;
id = i as Id; // ✅
以下方範例來看,我們可以將擴充類型作為一般的 Dart 類,可以實例化並呼叫自訂函數,不同的地方相當於 Dart 將其編譯為普通 int
。
extension type Wrapper(int i) {
void showValue() {
print('my value is $i');
}
}
void main() {
final wrapper = Wrapper(42);
wrapper.showValue(); // Prints 'my value is 42'
}
官方說明這對於與 Native 的互通性特別有用,可以直接使用原生類型,無需建立包裝器並耗費間接的成本,同時提供乾淨的 Dart API。
泛型
extension type MyList<T>(List<T> elements) {
void add(T value) => elements.add(value);
}
void main(List<String> arguments) {
MyList list = MyList<int>([1, 2]);
list.add(3);
final normalList = list as List<int>;
print(list); // [1, 2, 3]
print(normalList); // [1, 2, 3]
}
建構函數
多種建構函數的使用:
- 一般建構
- 命名建構
- 隱藏建構,使用與類別相同的私有建構函數語法
_
extension type Password._(String value) {
Password(this.value) {
assert(value.length >= 8);
if (value.length < 8) {
throw Exception('Password must be at least 8 characters long');
}
}
Password.random() : value = _generateRandomPassword();
static String _generateRandomPassword() => ...;
bool get isValid => value.length >= 8;
}
void main(List<String> arguments) {
// Implicit unnamed constructor.
Password password = Password('abcdefghijklmnopqrstuvwxyz'); // ✅
// Named constructor.
password = Password.random(); // ✅
password = Password('hello12'); // Exception: Password must be at least 8 characters long
password = 'hello' as Password; // ✅
}
記得,不能在隱性的主要建構函數使用 assert()
檢查或是其他操作,可以在裡面覆寫主要建構,在這時添加 assert
處理。這時候記得將原有的主要建構改為私有的命名函數
extension type Password._(String value) {
Password(this.value) {
assert(value.length >= 8);
if (value.length < 8) {
throw Exception('Password must be at least 8 characters long');
}
}
}
安全別名
使用 implements
允許 Extension Type 暴露底層類型,可以呼叫 Representation type 的所有成員,以及自定義的任何輔助 API。具有原本型別的能力,同時擁有別名和可受檢查的安全性。
extension type Height(double _) implements double {}
extension type Weight(double _) implements double {}
double calculateBmi(Height height, Weight weight) => weight / ( height * height);
void main() {
var height = Height(1.75);
var weight = Weight(65);
var bmi = calculateBmi(height, weight);
print(bmi); // 21.22448979591837
bmi = calculateBmi(1.64, 54.0); // ❌ compile-time error
bmi = calculateBmi(weight, height); // ❌ compile-time error
}
當拿它作為函數的參數時,不同別名雖然擁有共同能力,但是無法相容使用。這點就與
typedef
的別名寫法有所區別,typedef
無法受益於編譯器的類型檢查。
針對原有的 type 添加新的介面:
extension type MyId(int id) implements int {
MyId get value => this;
}
void main(List<String> arguments) {
final safeId = MyId(100);
safeId + 1; // 101
safeId - 1; // 99
safeId * 2; // 200
safeId / 2; // 50
safeId % 3; // 1
safeId.toString(); // '100'
int normalId = safeId; // 100
final safeId2 = safeId + normalId; // 200
final safeId3 = 10 + safeId; // 110
}
重新定義原有的功能行為,Extension Type 的成員完全取代同名的父類型成員,提供新的實現方式。
extension type MyId(int id) implements int {
bool get isEven => true;
}
void main(List<String> arguments) {
final myId = MyId(101);
print(myId.isEven); // true
}
可以使用 @redeclare 重新定義原有的功能行為,Extension Type 的成員完全取代同名的父類型成員,提供新的實現方式。
extension type MyId(int id) implements int {
@redeclare
String toString() => 'New version: ${toString()}';
}
其他情境
多類型的擴展
通常 Extension Types 都是擴展一個 type,當有多種資訊時可以搭配 Record 使用。
typedef UserInfo = ({String email, String password});
extension type User(UserInfo info) {
void printInfo() => print("Email: ${info.email}, Password: ${info.password}");
}
void main(List<String> arguments) {
final user = User(
(
email: 'extension@gmail.com',
password: 'types',
),
);
user.printInfo(); // Email: extension@gmail.com, Password: types
}
模擬測試數據
針對於測試環節,使用 Extension Type 也是沒問題,根據 Mock Class 我們只需微調寫法即可。差異是,一般的類別如果有沒有實作的介面,會出現編譯提醒,而 Extension Type 不會有。但好處是,針對多測試的情境下,使用 Extension Type 還是能以最省資源的方式去運作。
abstract final class Repository {
String getToken();
}
final class MyRepository implements Repository {
@override
String getToken() {
return 'hello world';
}
}
extension type MockRepository(Repository repository) implements Repository {
String getToken() {
return 'Testing';
}
}
void main(List<String> arguments) {
final myRepository = MyRepository();
final mockRepository = MockRepository(myRepository);
print(mockRepository.getToken()); // Testing
}
JSON 存取
不需要一般的 Data Class,除了節省記憶體之外,速度快。
final userMap = json.decode(r'''
{
"name": {
"first": "Yii",
"last": "Chen"
},
"email": "ab20803@gmail.com"
}
'''); // Map<String, dynamic>
extension type User(Map<String, dynamic> _) {
Name get name => _['name'] as Name;
String get email => _['email'] as String;
}
extension type Name(Map<String, dynamic> _) {
String get first => _['first'] as String;
String get last => _['last'] as String;
}
void main() {
final person = User(userMap);
print(person.name.first); // Yii
print(person.name.last); // Chen
print(person.email); // ab20803@gmail.com
print(person.email.length); // 17
}
Interoperability 互通性
目前 Extension Types 是 dart:js_interop
套件主要的開發方式,允許存取 JavaScript API 並使用慣用語法進行互動。其中定義了許多 JS 相關類型,例如:JSObject、JSAny,實現 Dart 與原生兩端的安全溝通。當然 C++ 等其他語言的開發也都能受益。
external
→ 關鍵字讓我們能存取外部功能、外部函數,常見的是來自另一種語言,所以在 Dart 互通性開發上都會看到。
@JS()
→ 如果 Dart 端想以不同名稱實現,或想要編寫多個 Dart API 指向相同的 JS API,可以定義互通的 JS API 名稱。
套件示範
flutter_soloud
是音頻播放套件,底層由 C++ 開發,提供高效、性能好的播放器。從原始碼摸索,當我們要執行播放功能時,由 player.cpp
裡的 play 函數開始,最後在 Dart 端透過 soloud.play()
執行,並取得音頻任務的 ID。
此 ID handle 由自定義的 SoundHandle 包裝,確保一定的可讀性與性能。
final soloud = SoLoud.instance;
await soloud.init();
final source = await soloud.loadAsset('path/to/asset.mp3');
SoundHandle soundHandle = await soloud.play(source); // id(int)
await soloud.stop(soundHandle);
await soloud.disposeSource(soundHandle);
以下快速看整個 API 運作的流程:
- 使用
player.cpp
的play()
取得 audio handle
2. 使用 bindings_player_ffi.dart
處理兩端的溝通,將拿到的 handle id 透過 SoundHandle 包裝
3. 在 Flutter 端,使用 soloud.dart 呼叫 play(),會得到一個結果是 Record type,將它的newHandle 拿出來,也就是我們要的 ID
4. ID 在 Dart 端使用 SoundHandle 進行包裝,是個有意義的 Extension Type。後續使用它進行其他的 Audio 控制
進階用法
工廠建構:Representation Type 上有另一種擴充類型。這允許跨多個擴充類型重複使用操作(類似於多重繼承)。
extension type Number(int i) {
const factory Number.zero() = Number2;
}
extension type Number2(int i) implements Number {
const Number2(int value) : this(i: value);
}
注意
Extension Type 是 compile-time 的包裝行為。在 run-time 時,沒有它的存在。此時任何類型的查詢或操作都適用於 Representation type。這也使得 Extension Type 成為不安全的抽象,因為始終可以在 run-time 時找到原本的表示類型並存取底層物件。
extension type Id(int value) {}
void idToInt() {
var id = Id(1);
// Run-time type of 'id' is representation type 'int'.
if (id is int) print(id.value); // 1
// Can use 'int' methods on 'id' at run time.
if (id case int x) print(x.toString()); // 1
switch (id) {
case int(:final isEven):
print("$id (${isEven ? "even" : "odd"})"); // 1 (odd)
}
}
void intToId() {
int i = 2;
if (i is Id) print("It is"); // It is
if (i case Id id) print("value: ${id.value}"); // value: 2
switch (i) {
case Id(:var value):
print("value: $value"); // value: 2
}
}
總結
- Extension Types 可以約束現有類型,限制原有的能力
- Extension Types 可以為現有類型添加新的介面,擁有新的能力
- 了解 Extension Types 的性質很重要,它只存在於 compile-time,在 run-time 會被忽略,並以 Representation Type 表現
- 雖然真正的 Class Wrapper 更安全。但權衡之下,Extension Type 可以在某些情況下節省成本,極大地提高性能,尤其是互通性開發的改善
Extensions 比較
- Extension Methods → 適合為現有類型添加簡單功能
- Extension Types → 強化現有類型,實現複雜場景和操作,優化與其他程式語言的互通性
Reference
- https://dart.dev/language/extension-types
- https://dart.dev/interop/js-interop/usage
- https://medium.com/dartlang/dart-3-3-325bf2bf6c13
- https://qiita.com/Cat_sushi/items/987e7eee469793369ef8
- https://qiita.com/Cat_sushi/items/87742dc3a886dd984f46
- https://ildysilva.medium.com/what-are-flutter-and-dart-extension-types-896eda0a3ddf
- https://www.youtube.com/watch?v=YHsi1Gfz5UU&ab_channel=imaNNeO
- https://www.youtube.com/watch?v=SyFNB81p-OY&t=3276s&ab_channel=FlutterUruguay
- https://www.youtube.com/watch?v=2TJIOpBDMnU&ab_channel=Prof.DiegoAntunes
Other Articles
- Flutter April 2024 💙 Flutter Monthly
- Flutter March 2024 💙 Flutter Monthly
- Flutter February 2024 💙 Flutter Monthly
- Flutter 3.19 & Dart 3.3 改版重點!
- Flutter January 2024 💙 Flutter Monthly
- Use Dart 3 to Improve Development Skills. More Examples and Tips.
- Flutter December 2023 💙 Flutter Monthly
- Flutter November 2023 💙 Flutter Monthly
- Get Familiar with Dart 3, Make your Life Easier!
- Flutter 3.16 & Dart 3.2 重點整理來了!
- 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