0%

【文章內容使用 Gemini 1.5 Pro 自動產生】

介紹 Flutter 的 Google Fonts v 1.0.0!

Anthony RobledoMH Johnson 撰寫

Flutter 的 Google Fonts 最初於 2019 年 12 月的 Flutter Interact 上宣布。在社群的幫助下(感謝大家!),我們添加了許多新功能並提高了穩定性。在 Beta 測試取得成功後,它現在已準備好作為 Google Fonts 1.0.0 在 Android、iOS、Web 和 MacOS 上投入使用。

自 Beta 測試以來添加的功能:

  • 使用 GoogleFonts.asMap() 獲取所有支援字體的清單
  • 使用動態名稱的字體:GoogleFonts.getFont('Lato')
  • 使用校驗和驗證確保安全下載
  • 體驗效能提升,特別是在 Web 上
  • 使用 pubspec 資產預捆綁字體
  • 選項性地停用在執行時提取字體(在偵錯或預捆綁時有用)

Google Fonts 允許開發人員輕鬆地在他們的應用程式中嘗試和使用 fonts.google.com 上的任何字體。當應用程式準備發佈時,開發人員可以決定使用者是透過從 API 下載字體來接收字體,還是將字體預捆綁到應用程式套件中。

我們已將預捆綁字體所需的工作減至最少,並且使其與動態字體載入相容,因此如果您決定預捆綁,您無需更改程式碼。

請查看 入門指南,以了解如何透過 TextStyles 或 TextThemes 將字體包含到您的應用程式中。當您準備發佈時,請了解如何選項性地預捆綁字體並將字體許可添加到應用程式的 LicenseRegistry 中。

我們希望您發現此套件對您所有 Google Fonts 的需求都很有用。與往常一樣,請隨時留下回饋、提交議題 或開啟提議請求!


介紹 Flutter 的 Google Fonts v 1.0.0! 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動產生】

Flutter 2020 年春季更新

作者:Tim Sneath 和 Patrick Sosinski

對於我們 Google 的工程師、產品經理、使用者體驗研究員、技術作家和開發者關係工程師來說,過去幾個月充滿了各種挑戰;就像你們中的大多數人一樣,我們都在努力應對與平常非常不同的日常工作。儘管如此,作為一個開源專案,我們能夠繼續使用公開可用的工具進行開發。在有時具有挑戰性,當然也是新穎的工作環境的限制下,以及需要照顧家人的需求,我們正在繼續根據我們的春季積壓工作取得進展,並且也從「外部」學習了許多關於貢獻的知識!我們希望您也能取得進展,並保持健康和安全。

Flutter 的發展勢頭

我們繼續看到 Flutter 使用率的快速增長,自我們發佈以來,在過去的十六個月中,已有超過 兩百萬開發人員使用 Flutter。儘管遇到了這些前所未有的情況,但在 3 月份,我們看到 Flutter 的月度增長率為 10%,目前每月有近 50 萬開發人員使用 Flutter。

以下是一些其他有趣的統計數據:

  • 您中有 60% 使用 Windows 開發,27% 使用 macOS,13% 使用 Linux。
  • 您中有 35% 為新創公司工作,26% 是企業開發人員,19% 是自雇人士,7% 為設計公司工作。
  • 78% 的 Flutter 開發人員使用 stable channel,11% 使用 beta,11% 使用 dev 或 master。
  • Flutter 的前五大地區是印度、中國、美國、歐盟和巴西。
  • Play 商店中大約有 50,000 個 Flutter 應用程式,僅在過去一個月就上傳了近 10,000 個。
  • Flutter 應用程式中使用最多的框架套件是 httpshared_preferencesintlmetapath_providerpedantic
  • Flutter 應用程式中使用最多的第三方套件是 providerrxdartcached_network_imagesqflitefont_awesome_flutterflutter_launcher_icons

Flutter 在企業中的應用

Flutter 的使用率在企業客戶中特別快速增長,我們的研究繼續表明,能夠構建高度品牌化的體驗並支援多個平台是大型公司選擇 Flutter 的主要原因。一個最近的例子是 Nubank,它是亞洲以外最大的數字銀行,擁有超過 2000 萬客戶。在對應用程式開發的選擇進行了 詳細的調查和分析 後,Nubank 選擇了 Flutter,並且自那以後,他們能夠將其前端開發團隊統一到單一框架上,使他們能夠在 iOS 和 Android 上同時發佈新功能。

請查看下面的開發者故事,其中解釋了他們使用 Flutter 所看到的一些好處:

企業的一個常見需求是專業組件。我們正在與 SyncFusion 合作,他們的 Essential Studio 產品現在包含一系列 高品質的 Flutter 組件,包括圖表、PDF 操作和條碼生成。憑藉他們的 2020.1 版本,所有組件都開箱即用地支援 Android、iOS Web,並且他們現在有了一個 基於 Web 的控制項預覽

更新我們的發佈流程

最後,當我們展望我們的下一個穩定版本時,我們想分享一些我們正在對我們的發佈模型所做的更改,我們認為這些更改將進一步提高發佈版本的穩定性和可預測性。

今天的發佈流程旨在簡單且低維護。當我們還是一個規模較小的團隊和一個比較新的框架時,這個流程讓 Flutter 得益匪淺,但是隨著我們目前的規模,我們遇到了一些影響 Flutter 貢獻者和開發者的問題,包括:

  • 對於何時構建版本,以及版本中包含的程式碼,缺乏明確性
  • 缺乏分支測試導致熱修復版本出現回歸

從 4 月份的 Flutter 版本開始,我們將轉移到一個分支模型,其中包含一個用於 beta 和穩定版本發佈的穩定階段。我們現在將在每個月初為 beta 版本創建分支,並透過 cherry-picking 任何關鍵修復來穩定此版本。大約每季度一次,當前的 beta 分支將被提升到我們的穩定版本。如果有必要,我們將繼續在此版本上進行熱修復。我們的基礎架構現在支援針對分支進行測試,這意味著我們可以驗證 cherry-pick,並且會根據嚴重程度接受一些請求。

我們還藉此機會調整了 Flutter 和 Dart 發佈流程和通道。因此,Dart 添加了 beta 通道,並且未來發佈將同步(例如,Flutter beta 版本將包含一個 Dart beta 版本)。

如果您已經在發佈基於穩定通道的 Flutter 應用程式,我們鼓勵您在 beta 候選版本上測試您的應用程式,並透過報告任何問題來影響穩定版本發佈的品質。您也可以按照 Flutter wiki 上新的 Flutter cherry-pick 流程 在穩定通道上升級回歸或阻止錯誤。

我們認為,這個新流程既能提高我們對版本品質和可預測性的信心,也能提供一種更簡單的方法來將熱修復傳遞到穩定通道。

版本控制變更

作為此分支模型的一部分,我們對版本的版本控制方式進行了一些細微調整。

完整的技術細節可以在 Flutter 構建釋出通道 wiki 頁面上找到;以下是一個簡短的摘要:

  • 非穩定發佈版本 將在版本字串中使用 .pre 來註解,以表示其預發佈狀態。給定一個版本字串 x.y.z-n.m.pre,每次從 master 構建一個新的 dev 通道構建時,n 都會遞增。

    • 1.18.0–1.0.pre:master 移動到 1.18 後的第一個 dev 版本
    • 1.18.0–2.0.pre:從 master 上更近的點開始的下一個 dev 版本
  • beta 版本 將從 dev 版本點構建,如上所述。當我們對這些版本中的某個版本進行 cherry-pick 時,m 版本將遞增。例如,如果我們從 master 中選取第 15 個 dev 版本作為我們的 1.18 beta 版本,版本控制將如下所示:

    • 1.18.0–15.0.pre:初始的 beta 候選版本(與發佈到 dev 的版本相同)
    • 1.18.0–15.1.pre:在(現在的)beta 分支上進行一些 cherry-pick 的後續構建
    • 1.18.0–15.2.pre:第二次後續構建
  • 穩定版本 將被版本控制為 x.y.0。如果有必要,後續的熱修復版本將遞增修補程式號碼。x.y.1、x.y.2 等)

    • 1.18.0–15.4.pre:分支上的最後一個 beta 版本
    • 1.18.0:穩定版本,與 1.18.0–15.4-pre 相同的位元
    • 1.18.1:1.18.0 的潛在熱修復

接下來是什麼?

我們使用這個新版本控制模型的第一個版本將是 我們的下一個穩定版本,我們計劃在下週發佈。到時再來查看所有新功能的完整概述。

在此期間,請查看我們在過去幾週中發佈的一些其他公告。上週,我們宣布了 Flutter 的 CodePen 支援。我們很享受查看不同創建者在過去幾天中構建的筆刷。以下是一些我們最喜歡的:

如果您正在尋找 Flutter 學習資源,我們現在提供了一個 免費的線上 Flutter 入門培訓課程。這個由 Angela Yu 講授的十小時課程提供了教學和實驗室,幫助您開始您的 Flutter 旅程。

下週見。在此期間,請待在家中,保持健康!


Flutter 2020 年春季更新 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動產生】

動畫深探

去年我錄製了 Flutter 動畫系列中的其中一集,我想我會將相同的內容發佈給那些喜歡文字勝於視頻的人。

在這個系列的其他集中,我的同事們討論了在 Flutter 中建立動畫的所有實際方法。在我的那一集裡不是這樣。在這裡,您將學習如何以最不務實的方式實現動畫。(但同時您也會學到一些東西。)

讓我們從簡單輕鬆的東西開始:

什麼是運動?

你看,運動是一種錯覺。看看這個:

Filip 揮手的视频。

這是一個謊言。您實際上看到的是許多靜止的圖片以快速連續的方式顯示。這就是電影運作的方式。在電影中,這些單獨的圖片稱為幀,因為數位螢幕的運作方式類似,所以這裡也稱為幀。電影通常每秒顯示 24 幀。現代數位設備每秒顯示 60 到 120 幀。

因此,如果運動是謊言,那麼所有這些 AnimationFoo 和 FooTransition Widgets 到底在做什麼?當然,因為幀需要每秒構建高達 120 次,所以 UI 不能每次都重新構建。

還是可以?

事實上,Flutter 中的動畫只不過是在每一幀上重新構建 Widget 樹的一部分的方法。沒有特殊情況。Flutter 足夠快可以做到。

讓我們看看 Flutter 動畫的基礎構建塊之一:AnimatedBuilder。此 Widget 是一個 AnimatedWidget,它由 _AnimatedState 支援。在 State 的 initState() 方法中,我們正在監聽動畫(或者,如這裡所稱的 Listenable),當它更改其值時,我們會… 呼叫 setState()。

這段令人困惑的螢幕錄影只是顯示,我在前一段中所說的是真的。Animated Builder 確實會在每一幀上呼叫 setState()。

這就是了。Flutter 中的動畫只不過是快速連續地更改某個 Widget 的狀態,每秒 60 到 120 次。

我可以證明。這是一個從零到光速「動畫」的動畫。雖然它在每一幀上都更改文字,但從 Flutter 的角度來看,它只不過是另一個動畫。

讓我們使用 Flutter 的動畫框架從頭開始建立這個動畫。

通常,我們會使用 TweenAnimationBuilder Widget 或類似的東西,但在本文中,我們將忽略所有這些,而使用計時器、控制器和 setState。

計時器

讓我們先談談計時器。99% 的時間,您不會直接使用計時器。但是,我認為談談它仍然很有幫助,即使只是為了揭開它神秘的面紗。

計時器是一個針對每一幀呼叫函數的物件。

var ticker = Ticker((elapsed) => print('hello'));
ticker.start();

在本例中,我們會在每一幀中列印「hello」。誠然,這並不十分有用。

此外,我們忘記呼叫 ticker.dispose(),所以現在我們的計時器將永遠持續下去,直到我們關閉應用程式。

這就是為什麼 Flutter 會提供 SingleTickerProviderStateMixin,這個恰如其分的 Mixin 您在之前的幾個視頻中都有看到。

這個 Mixin 負責管理計時器的麻煩。只需將它添加到您的 Widget 的狀態中,現在您的狀態就默默地成為了 TickerProvider。

class _MyWidgetState extends State<MyWidget> 
with SingleTickerProviderStateMixin<MyWidget> {
  @override
Widget build(BuildContext context) {
return Container();
}
}

這意味著 Flutter 框架可以向您的狀態請求計時器。最重要的是,AnimationController 可以向狀態請求計時器。

class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
  @override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
  @override
Widget build(BuildContext context) {
return Container();
}
}

AnimationController 需要計時器才能運作。如果您使用 SingleTickerProviderStateMixin 或其近親 TickerProviderStateMixin,您可以直接將其提供給 AnimationController,這樣就完成了。

AnimationController

AnimationController 是您通常用於播放、暫停、反轉和停止動畫的工具。與純粹的「滴答」事件不同的是,AnimationController 在任何時候都會告訴我們動畫的 進展 到達了哪一點。例如,我們是否已經走了一半?我們是否已經走到了 99%?我們是否已經完成動畫?

通常,您會使用 AnimationController,然後使用 Curve 進行轉換,再透過 Tween 處理,最後在其中一個方便的 Widget(如 FadeTransition 或 TweenAnimationBuilder)中使用它。但是,出於教學目的,我們不會這樣做。相反,我們將直接呼叫 setState。

setState

在我們初始化 AnimationController 之後,我們可以為它添加一個監聽器。在這個監聽器中,我們會呼叫 setState。

class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
  @override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(_update);
}
  void _update() {
setState(() {
// TODO
});
}
  @override
Widget build(BuildContext context) {
return Container();
}
}

現在,我們可能應該有一個要設定的狀態。讓我們使用一個整數來保持簡單。而且不要忘記在我們的 build 方法中實際使用狀態,並根據控制器的當前值在我們的監聽器中更改狀態。

class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
  int i = 0;
  @override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(_update);
}
  void _update() {
setState(() {
i = (_controller.value * 299792458).round();
});
}
  @override
Widget build(BuildContext context) {
return Text('$i m/s');
}
}

此程式碼根據動畫的進度將值從零分配到光速。

執行動畫

現在,我們只需要告訴動畫它應該花多長時間完成,並啟動動畫。

class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
  int i = 0;
  @override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_controller.addListener(_update);
_controller.forward();
}
  void _update() {
setState(() {
i = (_controller.value * 299792458).round();
});
}
  @override
Widget build(BuildContext context) {
return Text('$i m/s');
}
}

Widget 在被添加到螢幕上的那一刻就會開始動畫。它會在 1 秒內從零到光速「動畫」。

釋放控制器

哦,別忘了釋放 AnimationController。否則您的應用程式中就會有記憶體洩漏。

class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin<MyWidget> {
AnimationController _controller;
  int i = 0;
  @override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_controller.addListener(_update);
_controller.forward();
}
  @override
void dispose() {
_controller.dispose();
super.dispose();
}
  void _update() {
setState(() {
i = (_controller.value * 299792458).round();
});
}
  @override
Widget build(BuildContext context) {
return Text('$i m/s');
}
}

也許直接使用內建的 Widget?

正如您所見,自己做所有事情並不好。使用 TweenAnimationBuilder 可以用更少的程式碼行實現相同的功能,而且不需要同時管理 AnimationController 和呼叫 setState。

class MyPragmaticWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: IntTween(begin: 0, end: 299792458),
duration: const Duration(seconds: 1),
builder: (BuildContext context, int i, Widget child) {
return Text('$i m/s');
},
);
}
}

總結

我們看到了計時器到底是什麼。我們看到了如何手動監聽 AnimationController。而且,我們看到了動畫在基本層面上只是快速連續地重新構建 Widget。您可以在任何幀上做任何您想做的事情。

本文是系列文章的一部分!請查看這裡的其他文章:


動畫深探 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

http://creativecommons.org/licenses/by/4.0/

【文章內容使用 Gemini 1.5 Pro 自動產生】

宣布 CodePen 支援 Flutter

今天,我們很興奮地宣布,CodePen,數百萬前端開發人員和設計人員的首選社群開發環境,將加入 對 Flutter 的支援!對於 Web 開發人員來說,CodePen 長期以來一直是分享設計探索、新技術和想法的絕佳場所。現在,隨著 Flutter 的引入,CodePen 使新的受眾能夠學習、分享和推廣他們的創造力。

用 CodePen 共同創辦人之一 Alex Vazquez 的話來說:

“多年來,CodePen 一直是 Flutter 和 Dart 程式設計語言的忠實粉絲。我們非常興奮 Flutter 現在支援行動裝置 Web。Flutter 社群正在快速發展,這就是我們很興奮地為 Flutter 提供自訂 CodePen 編輯器的原因!Flutter 正式成為 CodePen 社群的一員。我們迫不及待想看看您在 CodePen 上使用 Flutter 建立什麼。”

從 Flutter 誕生之日起,我們就將其設計為創意表達的畫布。看到越來越多人認可其設計能力,這令人鼓舞,從我們在以設計為中心的 Flutter Interact 活動 上宣布與 Adobe 和 SuperNova 的合作,到 Fast Company 將 Flutter 列為本世紀最重要的設計理念之一。有了基於 CodePen 的 Flutter 環境的新增,創意專業人員現在可以利用 CodePen 來發現藝術性的 Flutter 靈感、建立令人印象深刻的 Flutter 作品集,並向全世界展示驚人的 Flutter 想法!

CodePen 的 Flutter 編輯器建立在與目前為 DartPad(由 Flutter 和 Dart 團隊建立的基於 Web 的編輯器,最近更新以支援 Flutter)提供動力的相同後端服務 dart-services 之上。當我們建立 DartPad 時,我們將其設計為一種教育工具,幫助開發人員學習 Flutter 和 Dart,並彼此分享程式碼片段。我們特別決定將 dart-services 開源,以便像 CodePen 這樣的網站可以將其適配到新的場景和受眾。

DartPad 是一個出色的工具,可以用於快速在程式碼中測試想法,或與其他開發人員分享您的片段,並且對於再現(和提交)錯誤很有用。您也可以在 CodePen 中執行此操作,但是 CodePen 的優勢在於它擁有活躍的設計社群,您可以在其中分享、評論、宣傳、嘗試設計想法,並從其他設計人員那裡獲得輸入。CodePen 的 Flutter 編輯器更像是您的「右腦」,您用它來為創意表達和設計靈感製作原型,而 DartPad 更像是您的「左腦」,當您需要快速測試想法或編寫技術概念時,就會使用它。

“光效”動畫Mariano Zorrilla 製作

CodePen 上的 Flutter 編輯器

讓我們快速瀏覽一下 CodePen 上的 Flutter 編輯器。您可以從 頭開始 或從現有的 範本 建立新的 Flutter 筆記(CodePen 對「程式碼片段」的稱呼)。非常感謝我們尊貴的 Flutter 社群成員(aednlaxerayushnishaddiegoveloperdivyanshub024egorbelibovgskinnerTeammkiisoftorestesgaolinSlaxXxX 等等)為範本列表貢獻了一些很酷的範例。

讓我們從 GooeyEdge 範本 開始。如您所見,Flutter 程式碼在左側,Flutter 的 Web 輸出在右側。您可以使用滑鼠拖動視覺邊緣來玩弄這個互動式設計。

“Gooey 邊緣”動畫Grant Skinner 製作

您也可以更改 Flutter 程式碼,並看到它們相應地生效。例如,如果我們將頁面控制指標的顏色從「白色」更改為「藍色」(第 326 行),您會看到顏色在幾秒鐘內更新!每次您進行更改時,CodePen 會自動重新編譯您的程式碼。只需更新一行,等待幾秒鐘,新的輸出就會出現。

頁面控制指標更改為藍色

現在讓我們看看如果我引入語法錯誤會發生什麼。假設我不小心刪除第 1 行末尾的分號。我將立即看到一條紅色橫條警告語法錯誤。這使您能夠輕鬆地發現和糾正錯誤。

引入語法錯誤時的警告訊息

社群功能

我們特別喜歡 CodePen 的社群功能。建立新筆記或發現社群建立的 Flutter「筆記」後,您可以儲存、收藏、新增到合集中、在社群媒體上分享,甚至分叉以建立自己的版本!

嘗試一下!

我們希望 CodePen 上的這個新的 Flutter 遊樂場能夠讓您建立和展示您的酷炫 Flutter 動畫、想法、小品等等。請您在 Twitter 上使用 #FlutterPen 與我們分享您的設計。我們迫不及待想看看您會建立什麼!如果您錯過了上週關於 免費培訓 的公告,您可能需要查看一下。請密切關注:我們很快就會有更多消息。


宣布 CodePen 支援 Flutter 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動產生】

宣布免費 Flutter 入門課程

在我們共同度過當前全球危機的過程中,我們知道許多人正在尋找新的技能。我們想提供幫助,因此我們宣布與 App Brewery 合作,贈送 [Flutter 入門課程](https://www.appbrewery.co/courses/intro-to-flutter),在未來三個月內啟用課程的人可以免費獲得課程。此課程不需要任何 Flutter 經驗,我們希望這能讓您有機會為 iOS、Android 和 Web 建立新的開發技能。

[Flutter](http://flutter.dev) 是 Google 的 UI 工具包,用於從單一程式碼庫中為行動、Web 和桌面建立美麗、原生編譯的應用程式。全球超過 [百萬開發人員](https://youtu.be/REJDzio_h7o) 正在使用它,從個人和新創公司到阿里巴巴、Capital One 和 eBay 等大型公司。

我們免費贈送 [Flutter 開發入門](https://www.appbrewery.co/courses/intro-to-flutter) 課程,在未來三個月內啟用課程即可獲得終身免費存取權。課程涵蓋了 Flutter 開發的基本概念,教您如何使用 Dart 編碼,並引導您建立九個可在 iOS 和 Android 上運行的真實應用程式,即使您沒有程式設計經驗。

課程講師 Angela Yu 帶領您逐步完成引人入勝的線上課程,讓您獲得實際的動手操作經驗,成為一名成功的 Flutter 開發人員。課程包含 10 多小時的影片教學和實驗室,在此期間您將建立程式設計知識,並製作出真實世界的應用程式,例如「自行選擇冒險」遊戲和測驗應用程式。您甚至可以建立一個樂器!

我們預計將有很多人參加此課程,因此我們也與 [Very Good Ventures](https://verygood.ventures)(頂級 Flutter 開發機構之一)合作,為相關的 Discord 頻道提供諮詢支援,協助您解決問題。

在本課程結束時,您將準備好開始建立自己的 Flutter 應用程式,並朝成為一名成熟的 Flutter 開發人員邁進。如果您完成了本課程,那麼 [App Brewery](https://www.appbrewery.co/) 上還有更多模組可供您進一步學習。您將在課程結束時獲得結業證書,以及一些應用程式的作品集,這些應用程式將教您 Flutter 和 Dart 的基礎知識。

我們希望很快在課程中看到您,並希望您使用 #FreeFlutterCourse 分享您的進度。

在此註冊:https://www.appbrewery.co/courses/intro-to-flutter

Flutter 開發入門課程大綱


宣布免費 Flutter 入門課程 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動產生】

如何選擇適合您的 Flutter 動畫 Widget?

觀看此文章的影片版本,請點擊我們的 YouTube 影片

此文章最初由 Emily Fortuna 撰寫,並由她授權發佈。

您決定在您的 Flutter 應用程式中加入動畫,真是太棒了!問題是,有許多不同的動畫 Widget,因此弄清楚哪一個最適合您可能會讓人不知所措。幸運的是,本文旨在提供幫助!

我將引導您完成一系列關於您心目中動畫的問題,以幫助您確定應該如何建立它。要記住的另一件事是,核心 Flutter 函式庫中提供的動畫 Widget 非常低階。這表示如果您心目中有一個複雜的動畫,我建議您查看 pub.dev 上提供更高階介面的動畫套件。

請查看下面的決策樹,我將在本文中解釋它:

所有動畫決策的口袋流程圖。

廣泛來說,您可能希望在 Flutter 應用程式中加入兩種主要類型的動畫:繪圖型動畫和程式碼型動畫。

  • 程式碼型動畫 以 Widget 為中心,根植於標準佈局和樣式原語,例如行、列、顏色或文字樣式。這並不表示它們很無聊或很簡單,但它們的核心往往是增強特定現有 Widget 的外觀或轉場,而不是作為獨立的 Widget 存在。

  • 繪圖型動畫 相反,看起來像是有人畫出來的。它們通常是獨立的精靈,比如遊戲角色,或者涉及一些很難用純程式碼表達的轉換。

因此,您要問自己的第一個問題是:「我的動畫更像繪畫,還是更像是可以從 Flutter Widget 原語中構建出來的?」如果您的動畫更像繪畫,或者您與會提供向量或光柵圖像資產的設計團隊合作,那麼我建議您使用 Rive 或 Lottie 等第三方工具以圖形方式建立您的動畫,然後將其匯出到 Flutter。有幾個套件可以幫助您將這些資產加入到 Flutter 應用程式中。

否則,如果您的動畫涉及變更 Widget(例如變更顏色、形狀或位置),那麼您就可以撰寫一些 Flutter 程式碼!

明確或隱含?

Flutter 程式碼型動畫有兩種形式:隱含動畫和明確動畫。下一步是弄清楚您需要哪種類型。

隱含動畫 Widget 在值變更時進行動畫。

隱含動畫依賴於簡單地為某些 Widget 屬性設定一個新值,然後 Flutter 會負責將其從當前值動畫到新值。這些 Widget 非常易於使用,並且非常強大。您上面看到的所有動畫都是使用隱含動畫 Widget 完成的。當您想要動畫化某個東西時,隱含動畫是一個很好的起點。

明確動畫需要一個 AnimationController。它們之所以被稱為「明確」,是因為它們只有在被明確要求時才會開始動畫。您可以使用明確動畫來完成使用隱含動畫所能完成的所有事情,再加上一些額外的事項。令人感到困擾的是,您必須手動管理 AnimationController 的生命週期,因為它不是一個 Widget,這意味著將它放在一個有狀態的 Widget 中。因此,如果您能使用隱含動畫 Widget,那麼您的程式碼通常會更簡單。

有三個問題可以幫助您確定需要哪種類型的 Widget:您的動畫是否永遠重複?我這裡的「永遠」是指在某個螢幕上,或者只要某個條件為真,例如音樂正在播放。

您要問自己的第二個問題是,您的動畫中的值是否是不連續的?我這裡的「不連續動畫」的範例是這個不斷增大的圓形動畫。圓形會重複地不斷變大變小、變大變小。它永遠不會變大變小,然後再縮小。在本例中,圓形的大小是不連續的。

一個只會增長、永不縮小的圓形。它是一個不連續的動畫!

您要問自己的最後一個問題是,多個 Widget 是否以協調的方式一起進行動畫?例如:

多個方框一起進行動畫。

如果您對以上三個問題中的任何一個回答「是」,那麼您需要使用明確的 Widget。否則,您可以使用隱含的 Widget!在您決定是需要隱含的 Widget 還是明確的 Widget 之後,最後一個問題將引導您找到您需要的特定 Widget。

哪個 Widget?

問問自己,是否有符合我需求的內建 Widget?如果您正在尋找內建的隱含動畫 Widget,請搜索名為 AnimatedFoo 的 Widget,其中「Foo」是您想要動畫化的屬性,例如 AnimatedOpacity。此外,請查看 AnimatedContainer,因為它對於許多不同的隱含動畫來說是一個非常強大且通用的 Widget。

如果您找不到所需的內建隱含動畫,您可以使用 TweenAnimationBuilder 來建立自訂的隱含動畫。相反,如果您正在尋找內建的明確 Widget,它們通常被稱為 FooTransition,其中「Foo」是您正在動畫化的屬性,例如SlideTransition

如果您找不到相關的內建明確動畫,還需要問自己最後一個問題:我是否希望我的動畫是一個獨立的 Widget,還是另一個周圍 Widget 的一部分? 這個問題的答案主要取決於個人喜好。如果您想要一個獨立的自訂明確動畫,那麼您應該擴展 AnimatedWidget。否則,您可以使用 AnimatedBuilder

如果您遇到效能問題,還有一個最後的選項需要考慮,那就是使用 CustomPainter 進行動畫。您可以像使用 AnimatedWidget 一樣使用它,但是 CustomPainter 會直接繪製到 Canvas 上,而不會使用標準的 Widget 建立範式。當您正確使用它時,您可以建立一些整潔的、極其自訂的效果,或者節省效能。不過,如果使用不當,您的動畫可能會導致更多效能問題。因此,請小心,就像手動記憶體管理一樣,請確保您了解自己在做什麼,然後再在任何地方都使用共用指標。

結語

總之,您可以問自己一系列高階問題,這些問題可以引導您如何建立動畫。這些問題的順序會形成一個決策樹,用於確定哪個 Widget 或套件適合您的需求。如果您將這些終點摺疊起來,它們會形成一條線,大約從左到右表示難度。感謝您的閱讀,祝您使用第三方框架或套件(明確或隱含地)建立出色的 Flutter 動畫!

動畫 Widget 從最容易的...到最難的。


如何選擇適合您的 Flutter 動畫 Widget? 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動產生】

Flutter 網頁:使用命名路由導航 URL

命名路由可以用於導航 Flutter 行動應用程式中的頁面之間,但它們也適用於 Flutter 網頁應用程式中的 URL。本文將說明如何在您的應用程式中加入命名路由,以及如何自訂它們,以便在路由中進行模式比對。

定義命名路由

您可以透過在 MaterialApp 類別中定義命名路由來將它們加入到您的應用程式中。MaterialApp.routes 屬性包含一個映射,列出每個命名路由及其關聯的顯示 Widget。MaterialApp.initialRoute 屬性決定應用程式啟動時顯示的路由。因此,initialRoute 需要在 routes 屬性中定義。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/overview',
routes: {
'/overview': (context) => OverviewPage(),
},
);
}
}

為了保持程式碼的組織性,將命名路由放在靜態變數中是一個好習慣,例如,放在 Widget 本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class OverviewPage extends StatelessWidget {
static const String routeName = '/overview';

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Overview'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Overview Page',
style: TextStyle(fontSize: 20),
),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, ArticlePage.routeName);
},
child: Text('Go to article'),
),
],
),
),
);
}
}

接下來,使用現在定義為靜態變數的命名路由重新整理 MaterialApp.routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: OverviewPage.routeName,
routes: {
OverviewPage.routeName: (context) => OverviewPage(),
},
);
}
}

在頁面之間導航

若要從一個頁面導航到另一個頁面,只需將命名路由推入導航器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class OverviewPage extends StatelessWidget {
// ...

@override
Widget build(BuildContext context) {
return Scaffold(
// ...
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// ...
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, ArticlePage.routeName);
},
child: Text('Go to article'),
),
],
),
),
);
}
}

您可以在 DartPad 上查看此範例的完整互動式版本。如果您自己 構建和運行應用程式以供網頁使用,您也可以在網頁瀏覽器中鍵入 /#/overview。這會將名為 overview 路由推送到 Navigator,並帶您前往 OverviewPage Widget,如以下 GIF 所示:

在 DartPad 上查看完整的互動式範例。

動態 URL 的路由邏輯

您可能需要處理比本文中提到的更複雜的場景,例如在路由中進行模式比對以允許動態 URL。為了擴展此範例,假設您在概覽頁面上有很多不同的文章。對於每篇文章,您都希望能夠透過 URL 直接導航:

1
2
/#/article/a-very-interesting-article
/#/article/newsworthy-news

MaterialApp 中為所有文章定義命名路由的可擴展性並不好。對於此類動態情況,您需要做一些更自訂的操作。截至撰寫本文時,Flutter 的穩定版本為 v1.12,沒有 簡單的方法 可以做到這一點,但 新的 Navigator 有計畫加入對更進階路由的支援。

目前,您可以使用一個外部套件,例如 Fluro 套件 提供更多進階路由。它為您提供了路由中的萬用字元模式比對,以及對 URL 中查詢字串的解析。可能還有許多其他可用的套件,因此請在留言中留下您最喜歡的套件名稱。

如果您想挑戰自己,也可以透過使用 MaterialApp.onGenerateRoute 屬性來獲取動態路由。使用此屬性來編寫當命名路由不在 MaterialApp.routes 中時的路由邏輯。

對於每個路由,請使用 RegEx 模式定義一個 Path。如果命名路由與模式匹配,則返回關聯的 Widget。接下來,定義 Path 類別以支援該操作:

1
2
3
4
5
6
class Path {
final String pattern;
final Function builder;

const Path({required this.pattern, required this.builder});
}

對於概覽頁面和首頁路由,它非常簡單,看起來與您之前使用的類似。以下範例建立了一個 RegEx 模式,該模式與 slug(帶有連字號的小寫字母)匹配,用於查找相應的文章:

1
2
3
4
5
6
List<Path> paths = [
Path(
pattern: r'/article/(?<slug>[\w-]+)',
builder: (context, matches) => ArticlePage(slug: matches['slug']),
),
];

剩下的就是為 MaterialApp 建立一個 onGenerateRoute 函數。如果目前的命名路由(settings.name)在 paths 列表中定義,則返回關聯的 Widget。確保將 RegEx 中的任何命名匹配項傳遞進去(在此範例中為 slug)。如果找不到匹配項,只需返回 nullWidgetsApp.onUnknownRoute 將被呼叫以處理這些情況:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
onGenerateRoute: (settings) {
for (Path path in paths) {
final regExp = RegExp(path.pattern);
final match = regExp.firstMatch(settings.name);
if (match != null) {
return MaterialPageRoute(
builder: (context) => path.builder(context, match.groups),
);
}
}
return null;
},
);
}
}

確保在 MaterialApp 類別中定義 onGenerateRoute 函數;您已經使用 Flutter 實作了動態 URL,使用命名路由!您可以 在 DartPad 上查看完整的互動式範例

在 DartPad 上查看完整的互動式範例。

結論

無論您是選擇為路由編寫自己的自訂邏輯,還是僅使用 MaterialApp 中現有的路由支援,您在使用命名路由時,Flutter 網頁應用程式都可以在預設情況下獲得 URL 支援。實作命名路由還可以確保您將呈現邏輯與路由邏輯解耦,從而減少程式碼重複。

請在留言中告訴我您在應用程式中使用什麼解決方案,無論是編寫自己的自訂邏輯還是使用外部套件。

祝您編碼愉快!


Flutter 網頁:使用命名路由導航 URL 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

學生們:加入 Dart 專案,參與 2020 Google 暑期程式碼大賽!

Google Summer of Code logo

Google 暑期程式碼大賽 ( GSoC ) 是一個全球性的程式,旨在讓學生開發者參與到開源軟體開發中。Google 贊助學生在暑期三個月中,與一個指導性的開源組織合作進行一個程式設計專案。在過去的 15 年裡,已有超過 15,000 名學生參與了 Google 暑期程式碼大賽。

今年,我們很興奮地宣布,Dart 成為 2020 年 Google 暑期程式碼大賽的指導組織

您有興趣嗎?

現在就開始閱讀專案構想列表,找到符合您技能和興趣的專案。雖然正式申請將於 3 月 16 日開放,但您今天就可以與未來的導師討論和調整專案構想。

Dart 團隊預計只有足夠的導師來接受少數申請。因此,請務必查看 其他指導組織,以了解 2020 年 Google 暑期程式碼大賽。

如果您有關於 Dart 和 GSoC 的具體問題,請在我們 專用的郵件列表 中提問。

有關 Google 暑期程式碼大賽的更多資訊,請觀看以下影片。或閱讀 Google 暑期程式碼大賽學生指南

我們期待您的參與!


學生們:加入 Dart 專案,參與 2020 Google 暑期程式碼大賽! 最初發表於 Medium 的 Dart,人們在那裡透過醒目顯示和回應這個故事來繼續對話。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

Dart 非同步程式設計:資料流

一個顯示來自資料流資料的簡單 Flutter 應用程式

本文涵蓋了響應式程式設計的基礎之一:資料流,它們是 Stream 類型的物件。

如果您已閱讀 上一篇關於 Future 的文章,您可能記得每個 Future 代表一個它非同步傳遞的單一值(錯誤或資料)。資料流的工作原理類似,但不同的是,一個資料流可以隨著時間推移傳遞零個或多個值和錯誤。

本文是基於「Flutter in Focus」影片系列「Dart 中的非同步程式設計」的第三篇文章。第一篇文章 Isolates and event loops 涵蓋了 Dart 對背景工作的支援的基礎知識。第二篇 Futures 討論了 Future 類別。

如果您喜歡透過觀看或聆聽來學習,本文中的所有內容都包含在以下影片中。

如果您考慮單個值與相同類型的 迭代器 的關係,那麼 Future 與資料流的關係也是如此。

就像 Future 一樣,關鍵是在事前決定:「當資料準備就緒時,出現錯誤時以及資料流完成時該怎麼做。」

同樣 就像 Future 一樣,Dart 事件迴圈仍然在運行。

資料流與 Dart 事件迴圈一起工作。

例如,如果您使用 File 類別的 openRead() 方法從檔案中讀取資料,則該方法會返回一個資料流。

資料塊從磁碟讀取並到達事件迴圈。 Dart 函式庫會查看它們並說:「啊,有人在等待這個」,將資料加到資料流中,然後它會彈出到您的應用程式程式碼中。

當另一條資料到達時,它就會進入並出來。基於計時器的資料流、從網路通訊端串流資料 - 它們也與事件迴圈一起工作,使用時鐘和網路事件。

監聽資料流

讓我們來談談如何使用資料流提供的資料。假設您有一個類別,它會提供一個資料流,每秒鐘產生一個新的整數:1、2、3、4、5……

您可以使用 listen() 方法訂閱資料流。唯一的必需參數是一個函式。

每次資料流發出一個新值時,都會調用該函式並列印該值:

1
2
3
4
5
Data: 1
Data: 2
Data: 3
Data: 4
...

這就是 listen() 的工作原理。

重要資訊: 預設情況下,資料流設定為單一訂閱。它們會保留其值,直到有人訂閱,並且它們在其整個生命週期中只允許一個監聽器。如果您嘗試兩次監聽同一個資料流,則會收到例外狀況。

幸好,Dart 也提供廣播資料流。您可以使用 asBroadcastStream() 方法從單一訂閱資料流建立廣播資料流。廣播資料流的工作方式與單一訂閱資料流相同,但它們可以有多個監聽器,如果在資料準備就緒時沒有人在監聽,則該資料會被丟棄。

讓我們回到第一個 listen() 呼叫,因為還有一些事情要談。

正如我們前面提到的,資料流可以像 Future 一樣產生錯誤。透過在 listen() 呼叫中加入 onError 函式,您可以捕獲和處理任何錯誤。

還有一個 cancelOnError 屬性,預設值為 true,但可以設定為 false,以便即使在發生錯誤後也能保持訂閱。

您可以加入一個 onDone 函式,在資料流完成傳送資料時執行一些程式碼,例如檔案已完全讀取時。

結合所有四個參數 - onErroronDonecancelOnError 和必需的參數 (onData) - 您可以提前為任何情況做好準備。

提示: listen() 返回的小型訂閱物件本身有一些有用的方法。它是一個 StreamSubscription,您可以使用它來暫停、恢復甚至取消資料流。

使用和操作資料流

現在您已經知道如何使用 listen() 訂閱資料流並接收資料事件,我們可以討論一下是什麼讓資料流如此酷炫:操作它們。一旦您在資料流中獲得了資料,許多操作就會變得流暢而優雅。

回到之前的數字資料流,我們可以使用一個稱為 map() 的方法來獲取資料流中的每個值,並即時將其轉換為其他內容。為 map() 提供一個執行轉換的函式,它會返回一個新的資料流,其類型與函式的返回值相符。您現在擁有一個字串資料流,而不是整數資料流。您可以在最後加上一個 listen() 呼叫,為它提供 print() 函式,現在您就可以直接從資料流中列印字串,非同步地,在它們到達時。

有很多方法可以像這樣串在一起。例如,如果您只想列印偶數,則可以使用 where() 過濾資料流。為它提供一個測試函式,該函式為每個元素返回一個布林值,它會返回一個新的資料流,其中僅包含通過測試的值。

distinct() 方法是另一個好方法。如果您有一個使用 Redux 存放區的應用程式,該存放區會在 onChange 資料流中發出新的應用程式狀態物件。您可以使用 map() 將該狀態物件資料流轉換為應用程式一部分的視圖模型資料流。然後,您可以使用 distinct() 方法獲取一個資料流,該資料流會濾除連續相同的值(以防存放區發出對視圖模型中的資料子集沒有影響的變更)。然後,您可以在每次獲得新的視圖模型時監聽並更新 UI。

Dart 內建了許多其他方法,您可以使用它們來調整和修改您的資料流。此外,當您準備好進行更進階的操作時,在 pub.dev 上可以找到由 Dart 團隊維護的 async 套件。它有一些類別可以合併兩個資料流、快取結果,並執行其他類型的基於資料流的魔法。

試試 async 套件,了解更多基於資料流的魔法。

更多資料流魔法,請查看 stream_transform 套件

建立資料流

這裡有一個進階主題值得一提,那就是如何建立自己的資料流。就像 Future 一樣,大多數時候您將使用網路函式庫、檔案函式庫、狀態管理等為您建立的資料流。但您也可以使用 StreamController 建立自己的資料流。

讓我們回到我們一直在使用的 NumberCreator。以下是它的實際程式碼:

如您所見,它會持續計數,並使用計時器每秒鐘增加計數。然而,有趣的是資料流控制器。

StreamController 從頭開始建立一個全新的資料流,並讓您可以存取它的兩端。有資料流本身的末端,資料到達的地方。(我們在整篇文章中一直在使用它。)

1
Stream<int> get stream => _controller.stream;

然後是接收端,這是新資料被加入到資料流的地方:

1
_controller.sink.add(_count);

這裡的 NumberCreator 使用了它們兩個。當計時器關閉時,它會將最新的計數加入到控制器的接收端,然後使用公開屬性公開控制器的資料流,以便其他物件可以訂閱它。

使用資料流構建 Flutter Widget

現在我們已經介紹了建立、操作和監聽資料流,讓我們來談談如何在 Flutter 中使用它們來構建 Widget。

如果您看過之前的關於 Future 的影片,您可能記得 FutureBuilder。您為它提供一個 Future 和一個構建器方法,它會根據 Future 的狀態構建 Widget。

對於資料流,有一個類似的 Widget 稱為 StreamBuilder。為它提供一個資料流和一個構建器方法,它會在資料流發出新值時重建其子 Widget。

snapshot 參數是一個 AsyncSnapshot,就像 FutureBuilder 一樣。您可以檢查它的 connectionState 屬性,以查看資料流是否尚未傳送任何資料,或者它是否已完全完成。您可以使用 hasError 屬性來查看最新值是否為錯誤。當然,您也可以處理資料值。

最主要的是要確保您的構建器知道如何處理資料流所有可能的狀態。一旦您掌握了這一點,它就可以對資料流的任何操作做出反應。

總結

本文討論了資料流代表什麼、如何從資料流中獲取值、操作這些值的方法,以及 StreamBuilder 如何幫助您在 Flutter 應用程式中使用資料流值。

您可以從 Dart 和 Flutter 文件中了解更多關於資料流的資訊:

或者繼續觀看「Dart 中的非同步程式設計」系列中的下一個影片。它討論了 asyncawait,這是 Dart 提供的兩個關鍵字,可以幫助您保持非同步程式碼的簡潔和易讀性。

非常感謝 Andrew Brogdon,他製作了本文所依據的影片。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

Dart 宣告點變異數

A code snippet showing the contravariant variance modifier (`in`) in use.

宣告點變異數是我的 Dart 團隊實習專案,我在 Dart 實習生的生活 這篇文章中記錄了我在團隊中的個人經驗。作為 宣告點變異數功能 的主要實作者,我想分享可靠變異數的用法和好處。

我們將討論如何使用變異數、為什麼我們想要使用修飾符、該功能如何建構在不使用修飾符的類別之上,以及此功能為我們提供了哪些好處。

注意: 變異數的實作尚未完成。雖然您可以透過啟用實驗(說明如下)來試用它,但在最終確定之前,該功能可能會發生變化。

在深入探討 Dart 的宣告點變異數功能之前,我們將快速繞道討論變異數的含義以及它的使用方法。

什麼是變異數?

為了簡要介紹變異數,我們可以看看這個例子:

1
2
Iterable<int> integers = [1, 2, 3];
Iterable<Object> objects = integers; // 正確!

由此可見,整數的 Iterable 可以替換為物件的 Iterable,因為整數一個物件,並且可以在 Iterable 中的任何物件使用的地方使用。該語言允許這樣做,方法是說如果兩個相同泛型類型(例如這裡的 Iterable<int>Iterable<Object>)的類型引數(intObject)是子類型,則它們被視為子類型。這種子類型關係被視為協變

這很方便且合乎邏輯。這很有道理,也就是說,直到您查看方法參數的變異數。假設您想在 Dart 中建立一個 objectWriter

1
2
3
4
5
6
class Writer<T> {
void write(T object) {}
}

Writer<int> integerWriter = Writer<int>();
Writer<Object> objectWriter = integerWriter;

然後,您急切地讓您的 objectWriter 寫入一個字串,卻發現它產生了執行時錯誤。編譯器允許這段程式碼,但是當您執行它時,它會拋出一個異常。

1
type 'Writer<int>' is not a subtype of type 'Writer<Object>' in type cast

這是為什麼呢?對於逆變,子類型關係與協變相反。我們需要能夠向 objectWriter 寫入任何物件,但是從前面我們知道 objectWriter 實際上是一個偽裝的整數寫入器。

您需要知道的最後一個變異數類型是不變。不變的子類型關係意味著兩個不變的類型之間沒有子類型關係,除非它們是完全相同的類型。

1
2
3
4
5
6
7
class Invariant<T> {
T value;
Invariant(this.value);
}

Invariant<int> i = Invariant<int>(0);
Invariant<num> n = i; // 錯誤

Dart 中的變異數功能是什麼?

由於 Dart 團隊提議在語言中加入明確的變異數修飾符,我們將預覽一些預期的變化。

Dart 將具有可以應用於類別和混入中的類型參數的變異數修飾符。語法類似於 C# 中的變異數修飾符。

您可以分別使用關鍵字 outininout 來宣告協變、逆變和不變的類型參數。這與泛型類型一起使用,如下所示:

1
2
3
class Covariant<out T> {}
class Contravariant<in T> {}
class Invariant<inout T> {}

為什麼要為泛型類型定義明確的變異數?為什麼我們想要這個功能?

Dart 的靜態類型系統目前將所有類型參數視為協變。這對於泛型來說是正確且方便的,其中類型在安全協變的位置(例如返回類型)中使用。但是,當類型引數應該是逆變或不變時,這是錯誤的:

1
2
3
4
5
6
7
8
class Writer<T> {
void write(T object) {}
}

Writer<int> integerWriter = Writer<int>();
Writer<Object> objectWriter = integerWriter; // 靜態類型正確,但動態類型不正確

objectWriter.write("我是一個字串!"); // 執行時錯誤

當您使用 objectWriter 時,您期望能夠寫入任何物件。不幸的是,objectWriter 只寫入整數。編譯器不知道任何更好的方法,當您執行程式碼時,您會收到可怕的執行時錯誤。為了避免不健全,如果您以不安全的方式使用類型引數,Dart 會在執行時拋出錯誤。

幸運的是,加入變異數修飾符會將這種不正確的使用從執行時錯誤轉變為編譯時錯誤。

1
2
3
4
5
6
class Writer<in T> { // 加入 'in' 修飾符
void write(T object) {}
}

Writer<int> integerWriter = Writer<int>();
Writer<Object> objectWriter = integerWriter; // 編譯時錯誤

這樣好多了。早在您寫入 objectWriter.write("我是一個字串!"); 之前,編譯器就會通知您有問題。

現在,讓我們來看看使用變異數修飾符加入安全類型的參數會為您提供什麼。

成員中的類型參數

如果您使用 out 標記泛型類型參數,則如果您在方法或欄位中在不安全協變的位置(例如返回類型)中使用該類型,編譯器會發出靜態錯誤。同樣地,標記為 in 的類型參數只能在安全逆變的位置(例如方法參數類型)中使用。標記為 inout 的類型參數可以在任何地方使用。

以下是一些您可能會覺得有用的方法變異數位置錯誤和正確用法。相同的錯誤檢查也發生在混入中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Covariant<out T> {
T method1() => null; // 正確
void method2(T value) {} // 錯誤
T field1 = 42; // 錯誤
final T field2 = ''; // 錯誤
}

class Contravariant<in T> {
T method1() => null; // 錯誤
void method2(T value) {} // 正確
T field1; // 正確
T field2 = 42; // 正確
}

class Invariant<inout T> {
T method1() => null; // 正確
void method2(T value) {} // 正確
T field1; // 正確
T field2 = 42; // 正確
}

欄位中也可能會發出錯誤。

1
2
3
4
5
6
7
8
class Covariant<out T> {
T field1 = 42; // 錯誤
}

class Contravariant<in T> {
T field1; // 正確
T field2 = ''; // 正確
}

分配和子類型

編譯器報告的關於類型參數誤用的錯誤可以幫助泛型類別作者編寫正確的程式碼。另一半是幫助其他人正確使用類別的一組錯誤。可靠變異數修飾符帶來的變化之一是我們可以透過分配看到的子類型變化。

如果泛型類型參數是協變的,那麼當其類型引數是預期類型的類型引數的子類型時,您可以分配它。例如,您可以將 Reader<int> 分配給預期 Reader<Object> 的內容。

1
2
3
4
class Reader<out T> {}

Reader<int> integerReader = Reader<int>();
Reader<Object> objectReader = integerReader; // 正確

同樣地,如果泛型類型參數是逆變的,則當其類型引數是預期類型的類型引數的超類型時,允許分配。您可以將 Writer<Object> 分配給預期 Writer<int> 的內容,如下所示:

1
2
3
4
class Writer<in T> {}

Writer<Object> objectWriter = Writer<Object>();
Writer<int> integerWriter = objectWriter; // 正確

對於不變的參數,類型引數必須是相同的類型。

1
2
3
4
class Invariant<inout T> {}

Invariant<int> i = Invariant<int>();
Invariant<num> n = i; // 錯誤

介面繼承

所以您可能會問:「即使繼承舊的類別,我們也可以選擇使用變異數進行更強的編譯時檢查嗎?」好消息是您可以;但是,有一些限制。

out 參數只能繼承協變或具有預設 Dart 類型變異數的參數位置。

請記住,從傳統類別繼承的任何方法仍然可能是變異數不健全的,因此仍然可能導致執行時錯誤。否則,如果類型在不健全的位置,具有變異數修飾符的類型參數的子類別中的所有新方法都將發出錯誤。

1
2
3
4
5
6
7
8
// 傳統類別
class Legacy {
void method(Object o) {} // 預設協變
}
class Covariant<out T> extends Legacy {
T method1() => null; // 正確
void method2(T value) {} // 錯誤
}

in 參數只能繼承逆變的參數位置。

1
2
3
4
5
6
7
8
class Contravariant<in T> {
void method(T value) {} // 正確
}

class ContraSub<in T> extends Contravariant<T> {
void method2(T value) {} // 正確
T method1() => null; // 錯誤
}

inout 參數可以繼承所有參數位置。但是,定義為 inout 的參數只能由其他不變位置繼承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Invariant<inout T> {
void method(T value) {}
T method1() => null;
}

class InvariantSub<inout T> extends Invariant<T> {
void method2(T value) {} // 正確
T method1() => null; // 正確
}

class CovariantSub<out T> extends Invariant<T> { // 錯誤:不變只能被不變繼承
void method(T value) {}
}

如何提供關於變異數功能的回饋?

我們建議使用 最新的開發頻道 試用變異數功能。試用這個例子,以掌握變異數的工作原理以及它可以為您做些什麼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
final integerWriter = Writer<int>();
integerWriter.write(42);

final objectWriter = Writer<Object>(); // 錯誤,因為 Writer 現在是不變的
objectWriter.write("I'm a string!");

final covariantReader = Reader<int>();
final objectReader = covariantReader as Reader<Object>;
print(objectReader);

final contravariantWriter = Writer<Object>();
final intWriter = contravariantWriter as Writer<int>;
print(intWriter);
}

因為變異數功能仍在實作中,您需要設定一個實驗性標誌來啟用它:

1
dart --enable-experiment=variance variance_example.dart

我們感謝任何和所有回饋!您可以在 GitHub 問題 中告訴我們您的想法。

總結

現有的 Dart 泛型預設為協變,這使得開始編寫新類別和入門變得容易。然而,這意味著更多錯誤出現在執行時而不是編譯時。使用者還要付出額外的執行時檢查的成本。變異數背後的主要思想是在編譯時為使用者提供更多資訊的錯誤檢查。

變異數僅針對泛型類別和混入的參數定義。使用者可以透過在類型參數前面加入 inoutinout 關鍵字之一來使用變異數功能。

此外,具有這些修飾符的新泛型介面可以繼承沒有變異數修飾符的傳統介面。

宣告點變異數允許您獲得許多新的好處,包括:

  • 介面成員內的編譯時變異數位置檢查
  • 移除由向下和向上轉換引起的煩人的執行時錯誤
  • 根據宣告的變異數進行額外的子類型更改
  • 更具資訊性且易於存取的錯誤檢查

現在,您不必擔心 objectWriter 是否真的是任何物件的寫入器。您知道它是。


Dart 宣告點變異數 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。