【文章翻譯】Announcing a free Flutter introductory course

【文章內容使用 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,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】How to Choose Which Flutter Animation Widget is Right for You?

【文章內容使用 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,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Flutter web: Navigating URLs using named routes

【文章內容使用 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,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Students: Join a Dart project for Google Summer of Code 2020

【文章內容使用 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,人們在那裡透過醒目顯示和回應這個故事來繼續對話。

【文章翻譯】Dart asynchronous programming: Streams

【文章內容使用 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,他製作了本文所依據的影片。

【文章翻譯】Dart declaration-site variance

【文章內容使用 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,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Life as a Dart intern

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

Dart 實習生生活

我不知道。

這三個字每天都在軟體工程師的腦海中浮現。工程工作由謎題和問題組成,所以不知道某件事只是另一個指標,表明工程師有成長的能力。然而,缺乏知識也會產生自我懷疑。

在今年秋季開始實習之前,我對程式語言、類型系統,甚至變異數(這是我整個專案)都不太了解。據我所知,變異數只是一個我需要計算平方根的值,這樣我才能得到標準差。程式語言和編譯器似乎是一種讓大多數開發人員感到神秘的黑魔法。

值得慶幸的是,我的經理是一位讀心者,他理解我的感受,並消除了我的緊張情緒。

_”所以,我要坦誠相待,這個專案會讓你完全陷入困境。你還什麼都不知道,但這沒關係,因為我希望你提出問題,很多問題,”_他解釋說。

直接進入一個我幾乎沒有任何背景知識的新專案是很可怕的,但我知道我得到了經理的支援。他給了我_嘗試_的自由,_犯錯_的自由。這對我來說意義重大。

與經理的快速交談開啟了我第一個月的工作:我在西雅圖弗里蒙特見到了我的團隊,並準備將自己投入到這個叫做_變異數_的深不可測的未知領域,您可以在Dart 宣告端變異數一文中閱讀相關內容。

我每週都會和經理一起參加變異數訓練營,他會提供我必須轉化為程式碼的理論。我記得我讀過我將要實作的變異數功能的提案,並對完成該專案所需的七個里程碑感到不知所措:(1) 掃描然後解析新的關鍵字,(2) 修改 kernel AST,(3) 在子類型演算法中支援變異數,(4) 加入錯誤和警告,(5) 變更上下限計算,(6) 變更類型推斷規則,以及 (7) 處理執行階段行為變更。

在變更 kernel(編譯器的中間表示)和子類型邏輯之間,我需要序列化和反序列化變異數註釋。執行階段行為被認為是一個延伸目標,但我幾乎無法理解它之前的六個任務。我不確定從哪裡開始或如何開始。

但情況好轉了。

我每天早上 9 點開始工作。炒雞蛋、香腸、羽衣甘藍和柳橙汁就是我所需要的。我準備好眯著眼睛看程式碼,想知道人們究竟是如何製作第一個編譯器的。

我經常同時處理不同的組件。這讓我保持忙碌,而且我喜歡組織目標。每天早上我都會建立一個新文件,列出我想完成的任務,將目標分解成可管理的任務。

A to-do list with most items crossed out (done!).
典型的待辦事項清單

到了晚上,丹麥奧胡斯的 Google 辦公室就開始活躍起來。我知道我必須與幾位來自丹麥的工程師合作,所以我經常寫下我的問題,最後完成並在西雅圖深夜發送出去。程式碼審查時間提前了 9 個小時,但幸運的是,時間差也讓我可以在程式碼被批評之前更頻繁地校對自己的工作。

團隊的批評提高了我判斷自己程式碼的能力,並思考不同實作方式的權衡。我的審查者指出了我忽略的地方,並詢問了關於變異數是什麼以及如何使用它的問題。我會修復我的程式碼,回答他們的問題,並確保在未來注意相同的問題。更重要的是,我會深入挖掘並問自己,他們是如何想到這個改進的?除了程式碼的修正版本之外,我可以從中學到什麼知識? 對我來說,重要的是我從每條評論中學習,而不是漫不經心地點擊方便的 完成 按鈕。

我的學習方式是透過提問。作為一個固執的軟體工程師,在咬牙切齒地研讀那些毫無意義的程式碼和勉強地找程式碼庫中的專家幫忙之間,總是一種微妙的平衡。後者幾乎總是能節省更多時間。我更喜歡分配相當多的時間獨立工作,在遇到新領域時做筆記並寫下問題,然後當我有 2 或 3 個問題和一個堅實的障礙時,再去詢問其他工程師。我意識到,與單獨花 3 個小時排除故障相比,詢問那些需要花費 3 個小時排除故障的問題,與其他人花 10 分鐘指導和解釋相比,我的效率要高得多。

工作場所的安全也會提高生產力。

對我來說,安全是以提問、開玩笑和與團隊共進午餐的形式出現的。當時鐘閃爍到 11:30 時,我知道接下來的半小時將充滿溫暖的食物和明亮的面孔。每天我都會聽到最精心設計的故事和想法:擁有 6 根手指的好處、收到一隻煮熟的龍蝦作為聖誕禮物、在 Factorio 中混合兩根不同的管道後數小時的清理工作……任何事情。我的團隊在我犯錯和取得成就的過程中都支援我。一個好的團隊會讓早上起床去上班變得容易得多。

午餐前後的時間也同樣愉快。我大部分時間都在閱讀程式碼和理解新概念。Dart 編譯器由許多大型的、不斷變化的組件組成,這意味著我需要深入研究幾個不同的程式碼庫。我喜歡有機會與負責靜態分析的前端團隊以及管理執行階段行為的後端團隊聯繫。看到這些組件如何協同工作以在 Dart 語言中建立新功能,真是太棒了。

實作和理論之間的裂痕是我專案中最具挑戰性的方面。並不是每天都需要將證明變成有形的程式碼。在處理變異數時,當我有問題時,這種裂痕就會浮出水面——有時我的詢問會將語言提案的組件與適當的實作交織在一起。我尋求答案的人要么知道哪個檔案包含子類型演算法,要么知道子類型變更如何影響賦值運算,但從來不知道兩者兼而有之。這感覺就像用幾塊膠合板和藝術家對最終結果的渲染來建造樹屋。

儘管如此,在 Dart 上工作仍然是非常有益和有趣的。我喜歡挑戰,尤其是知道除了變異數之外還有很多領域需要學習。Dart 過去是,現在仍然是,不斷發展以更好地支援使用該語言的開發人員。當出現關鍵問題並需要立即關注時,我看到團隊像調校良好的時鐘一樣運作。我傾聽團隊回顧以前的錯誤,確保未來的流程不再重複這些錯誤。

我在 Dart 團隊的實習提高了我的工程技能、溝通能力以及對自身能力的信心。它教會了我擁有一個友好但功能齊全的團隊的重要性。它教會我理論轉化為程式碼的許多方法,以及這樣做的必要解決問題的技巧。它教會我更多關於我自己的知識,以及我在工作中所珍視的東西。

感謝 Dart。下次見。


Dart 實習生生活 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Announcing Dart 2.7: A safer, more expressive Dart

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

宣佈 Dart 2.7:更安全、更具表現力的 Dart

今天,我們宣布 Dart 2.7 SDK 的穩定版本發布,它為開發人員提供了額外的功能。對於 Dart(我們針對任何平台上的快速應用程式進行客戶端優化的語言)來說,這是忙碌的一年。我們發布了六個新版本,其中包含數十項新功能。看到 Dart 社群使用這些功能是非常有益的,我們很高興最近的 GitHub Octoverse 報告將 Dart 列為 增長最快的語言(按貢獻者人數排名)。

Dart 2.7 新增了對擴展方法的支援,以及一個用於處理包含特殊字元的字串的新套件。我們對空安全(類型安全的可空和不可空類型)進行了更新,並在 DartPad 中提供了一個全新的空安全遊樂場體驗。在生態系統層級,pub.dev 有一個新的「喜歡」功能,用於向您喜歡的套件提供回饋。Dart 2.7 現在可以從 dart.dev 以 SDK 下載方式取得,它也內建於今天的 Flutter 1.12 版本 中。

擴展方法

Dart 2.7 新增了一個期待已久、功能強大的新語言特性:擴展方法。這些方法使您能夠向任何類型(即使是您無法控制的類型)添加新功能,並具有常規方法調用的簡潔性和自動完成體驗。

讓我們看一個小例子:新增對從字串解析整數和雙精度浮點數的支援。作為應用程式開發人員,我們無法更改 String 類別,因為它是在 dart:core 函式庫中定義的,但使用擴展方法,我們可以擴展它!定義此擴展後,我們可以在 String 上調用新的 parseInt 方法,就像該方法是在 String 類別本身上定義的一樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension ParseNumbers on String {
int parseInt() {
return int.parse(this);
}

double parseDouble() {
return double.parse(this);
}
}

main() {
int i = '42'.parseInt();
print(i);
}

擴展方法是靜態的

擴展方法是靜態解析和分派的,這表示您無法在類型為 dynamic 的值上調用它們。這裡的調用會在執行階段拋出異常:

1
2
3
4
dynamic d = '2';
d.parseInt();

→ Runtime exception: NoSuchMethodError

擴展方法與 Dart 的 類型推斷 配合良好,因此在以下變數 v 中,推斷類型為 String,並且 String 上的擴展可用:

1
2
var v = '1';
v.parseInt(); // Works!

因為擴展方法是靜態解析的,所以它們的速度與調用靜態函數或輔助方法一樣快,但調用語法更友好。

擴展可以具有類型變數

假設我們想要在 List 上定義一個擴展,用於獲取偶數索引處的元素。我們希望這個擴展適用於任何類型的列表,返回與輸入列表相同類型的新列表。我們可以透過使擴展泛型並將其類型參數應用於它擴展的類型和擴展方法來做到這一點:

1
2
3
4
5
extension FancyList<T> on List<T> {
List<T> get evenElements {
return <T>[for (int i = 0; i < this.length; i += 2) this[i]];
}
}

擴展方法實際上是擴展成員

我們將此功能稱為「擴展方法」,因為如果您在其他程式語言中使用過相應的語言功能,那麼這是熟悉的術語。但在 Dart 中,此功能更通用:它還支援使用新的 getter、setter 和運算子來擴展類別。在上面的 FancyList 範例中,evenElements 是一個 getter。以下是一個新增用於移位字串的運算子的範例:

1
2
3
4
5
extension ShiftString on String {
String operator <<(int shift) {
return this.substring(shift, this.length) + this.substring(0, shift);
}
}

社群中的絕佳範例

我們已經看到 Dart 社群中的許多開發人員試驗擴展方法。以下是一些我們目前看到的酷炫用法。

Jeremiah Ogbomo 建立了 time 套件,它在 num(整數和雙精度浮點數的基類)上使用擴展,以便輕鬆建立 Duration 物件:

1
2
3
4
5
6
7
8
// 透過 num 上的 `minutes` 擴展建立 Duration。
Duration tenMinutes = 10.minutes;

// 透過 num 上的 `hours` 擴展建立 Duration。
Duration oneHourThirtyMinutes = 1.5.hours;

// 使用 DateTime 上的 `+` 運算子擴展建立 DateTime。
final DateTime afterTenMinutes = DateTime.now() + 10.minutes;

Marcelo Glasberg 建立了一個 i18n(國際化)套件,它使用擴展方法來簡化字串本地化:

1
Text('Hello'.i18n) // 以英文顯示 Hello,以西班牙文顯示 Hola,依此類推。

Simon Leier 建立了 dartx 套件,其中包含許多核心 Dart 類型的擴展。一些範例:

1
2
3
4
5
var allButFirstAndLast = list.slice(1, -2);    // [1, 2, 3, 4]
var notBlank = ' .'.isBlank; // false
var file = File('some/path/testFile.dart');
print(file.name); // testFile.dart
print(file.nameWithoutExtension); // testFile

Brian Egan 正在使用擴展方法 更新 熱門的 RxDart 套件,以重新定義用於處理串流的 API。

安全的子字串處理

Dart 的標準 String 類別使用 UTF-16 編碼。這是程式語言中的常見選擇,尤其是那些同時提供在設備上原生運行和在 Web 上運行的支援的語言。

UTF-16 字串通常運作良好,並且編碼對開發人員是透明的。但是,在操作字串時,尤其是在操作使用者輸入的字串時,您可能會遇到使用者認為的字元與 UTF-16 中編碼為程式碼單位的字元之間的差異。讓我們看一個小例子,提取使用者輸入的字串的前三個字元:

1
2
3
4
5
var input = ['Resume'];
input.forEach((s) => print(s.substring(0, 3)));

$ dart main.dart
Res

到目前為止沒有問題;我們列印了輸入列表中字串的前三個字元,結果是 Res。現在讓我們考慮來自不同地區的使用者,他們可能會輸入包含重音符號、諺文(韓文)甚至表情符號組合的字串來表示「簡歷」的概念:

1
2
3
4
5
6
7
8
9
// 新的更長的輸入列表:
var input = ['Resume', 'Résumé', '이력서', '💼📃', 'Currículo'];

$ dart main.dart
Res
Ré
이력서
💼�
Cur

嗯,其中一些有效,但 Résumé 和 💼📃 元素發生了什麼事?對於 Résumé,為什麼我們得到一個「兩個字元」的字串?對於 💼📃,奇怪的問號是什麼意思?這裡的問題在於 Unicode 的黑暗角落。Résumé 中的重音 é 實際上是兩個程式碼點:一個 e 和一個 組合尖音符。而 📃,捲曲頁面 表情符號,是一個單獨的程式碼點,碰巧使用 U+d83d U+dcc3 的代理對進行編碼。困惑了嗎?

正如我們所說,您通常不需要擔心字元和程式碼點。如果您所做的只是接收、傳遞和處理整個字串,則內部編碼是透明的。但是,如果您需要迭代字串中的字元或操作字串的內容,則可能會遇到問題。好消息是 Dart 2.7 引入了一個新套件 characters 來處理這些情況。此套件支援將字串視為使用者感知字元的序列,也稱為 Unicode 字素叢集。使用 characters 套件,我們可以透過對縮短文字的程式碼進行少量更改來修復我們的程式碼:

1
2
3
4
5
// 之前:
input.forEach((s) => print(s.substring(0, 3)));

// 之後,使用 characters 套件:
input.forEach((s) => print(s.characters.take(3)));

首先,我們從 s 中的字串建立一個新的 Characters 實例(使用方便的 .characters 擴展方法)。然後,我們使用巧妙的 take() 方法來提取前 3 個字元。

此新套件的技術預覽版 可在 pub.dev 上取得。我們很樂意聽到您對此套件的想法。如果您發現任何問題,請 回報

空安全預覽

幾個月前,我們 宣布 了我們在 Dart 中支援空安全的意圖,新增了安全存取物件參考而不會觸發空參考異常的支援。今天,我們將為您提供預覽空安全靜態分析的方法。讓我們看一個小的激勵範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
Person('Larry', birthday: DateTime(1973, 03, 26)).describe();
Person('Sergey').describe();
}

class Person {
String firstName;
DateTime birthday;
Person(this.firstName, {this.birthday});

void describe() {
print(firstName);
int birthyear = birthday?.year;
print('Born ${DateTime.now().year - birthyear} years ago');
}
}

如果我們執行此程式碼,它會在描述第二個人時因空指標異常而崩潰,因為該個人沒有設定生日。我們犯了一個編碼錯誤:雖然我們確實預料到有些人透過在建構函數中將 birthday 欄位設為可選,並透過在 birthday?.year 中測試空生日來處理生日未知的情況,但我們忘記了處理 birthyear 為空的情況。

讓我們嘗試將此程式碼貼上到我們新的 空安全遊樂場 中,這是 DartPad 的一個特殊版本,其中包含空安全功能的靜態分析部分的技術預覽。即使不執行程式碼,我們也可以看到三個問題:

DartPad with null safety showing three analysis errors related to nulls

透過修復這些分析錯誤,我們可以開始利用空安全。嘗試在空安全遊樂場中進行以下編輯(最終得到 此安全程式碼):

  1. 若要宣告 birthday 可能為空,請將 DateTime birthday 更改為 DateTime? birthday
  2. 若要宣告當 birthday 為空時 birthyear 可能為空,請將 int birthyear 更改為 int? birthyear
  3. 將最後一個列印調用包裝在空測試中: if (birthyear != null) {…}

我們希望此範例能讓您很好地了解我們希望使用空安全的體驗。如前所述,這個遊樂場只是空安全部分的早期技術預覽,因為它正在構建中。我們正在努力完成 Dart SDK 中空安全的第一個測試版本。以下是我們正在為測試版本進行的工作:

  1. 完成可空和不可空參考的完整實作
  2. 將空安全整合到 Dart 的類型推斷和智慧提升中(例如,在賦值或空檢查後允許安全存取可空變數)
  3. 移植 Dart 核心函式庫 以宣告哪些類型是可空的,哪些類型是不可空的
  4. 加入一個遷移工具,它可以自動執行大多數移植 Dart 應用程式和套件的升級任務

一旦這項工作完成,我們將在測試版 SDK 中提供它,您可以開始在您的應用程式和套件中利用此功能。我們還計劃隨著新功能的實作而持續更新空安全遊樂場。

雖然我們確信許多開發人員會希望在空安全可用後立即使用它,但您可以在方便時進行遷移,在您準備好時選擇加入此功能。尚未選擇加入此功能的函式庫和套件將能夠依賴 已選擇加入 的函式庫,反之亦然。

在接下來的幾個月中,我們將會更多地討論空安全,包括關於如何為轉換做準備的更詳細建議。

在 pub.dev 上喜歡 👍 套件

今天在 pub.dev 上也推出了一個套件的全新喜歡功能。這引入了一個新的「人為信號」來表示您喜歡的套件。若要喜歡一個套件,只需點擊套件詳細資訊旁邊的豎起大拇指圖示:

pub.dev package detail page with new Like-feature voting button

目前,我們並未將喜歡次數納入我們的整體評分模型中,但我們計劃在以後的版本中這樣做。我們還計劃對我們的一般搜尋使用者介面和列表頁面進行視覺上的修改,以突顯套件的受歡迎程度。

謝謝

代表 Dart 團隊,我們要感謝您以及 Dart 社群中的每個人一直以來的支援!請繼續向我們提供回饋,並參與 Dart 討論和社群。沒有 Dart 社群的支援,我們將無法成為一個運作良好的開源專案。

2019 年對於 Dart 來說是令人難以置信的激動人心的一年,但我們並不止步於此。我們對 2020 年有著大膽的計劃,包括發布諸如 dart:ffi空安全 之類功能的穩定版本,以及引入新功能。我們邀請您立即開始使用 Dart 2.7。它可以從 dart.dev 取得,在今天的 Flutter 1.12 版本中,以及在最近 重新設計的 DartPad 上取得。


宣佈 Dart 2.7:更安全、更具表現力的 Dart 最初發佈在 Medium 上的 Dart 中,人們在那裡透過突出顯示和回應這個故事來繼續對話。

【文章翻譯】A brand new DartPad.dev with Flutter support

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

全新的 DartPad.dev,支援 Flutter

今天,我們宣布對 DartPad.dev 進行重大升級,它採用了全新的外觀和體驗,並支援熱門的 Flutter UI 工具包。DartPad 是我們的線上編輯器,可以直接在您的瀏覽器中運行 Dart 程式,現在它也可以運行 Flutter 應用程式了。

立即開始創作

要建立新的 Flutter 專案,請點擊「新增面板」按鈕,然後選擇 Flutter

這會在編輯器中加入一些起始程式碼並運行它。每當 DartPad 偵測到您正在使用 package:flutter 時,就會出現一個面板來顯示 UI。或者使用「範例」選單來尋找 Flutter 範例。

功能

Flutter 的所有核心函式庫,例如 cupertinomaterial,都可以使用,DartPad 可以顯示它們的文件。選擇一個符號即可在底部面板中查看文件。

DartPad 使用 Dart 格式化工具 (dartfmt) 格式化程式碼、提供輔助、提供修正、建議自動完成,並顯示錯誤和警告。

當您準備好分享您的程式碼片段時,您可以 建立 GitHub gist,並將 gist ID 放入像這樣的 URL 中:https://dartpad.dev/<GistID>。您可以在錯誤報告、StackOverflow 問題或您選擇的社群媒體平台上分享此連結。查看分享指南以了解更多詳細資訊。

您也可以將 DartPad 嵌入到頁面中,例如在此程式碼實驗室中。嵌入式 DartPad 特別適用於文章、程式碼實驗室和教學。(如果您有興趣在程式碼實驗室和教學中使用 DartPad,請查看本指南)。要了解更多關於將 DartPad 加到頁面的資訊,請查看嵌入指南

感謝!

DartPad 是 Dart 開源專案 的一部分。如果您提交了 DartPad 的 問題 或變更,感謝您使其變得更好。如果您想貢獻,請從 github.com/dart-lang/dart-pad 開始。要了解更多關於在 Web 上運行 Flutter 的資訊,請造訪 flutter.dev/web

我們迫不及待地想看看您會創造出什麼!


全新的 DartPad.dev,支援 Flutter 最初發佈在 Medium 的 Dart 上,人們在那裡透過醒目顯示和回應這個故事來繼續對話。

【文章翻譯】Announcing Dart 2.6 with dart2native: Compile Dart to self-contained, native executables

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

宣佈推出 Dart 2.6 和 dart2native:將 Dart 編譯成獨立的原生執行檔

Dart 已經為行動裝置和網路提供了 廣泛的編譯器集,用於建構經過生產最佳化的程式碼。這些靈活的編譯器使我們的框架合作夥伴能夠鎖定各種形式:在 Android 和 iOS 上的 Flutter 應用程式、在 網路桌面 上的 Flutter 應用程式、在 網路 上的 AngularDart 應用程式,以及在 嵌入式裝置 上的 Google 助理。

今天,我們宣佈推出 dart2native,它是我們現有編譯器集的擴展,能夠將 Dart 程式編譯成包含預先編譯機器碼的獨立執行檔。使用 dart2native,您可以在 macOS、Windows 或 Linux 上使用 Dart 建立命令列工具。此功能的宣佈圖片 是用此功能本身實現的 :-)

Dart Native 和 dart2native 編譯器

Dart 多年來一直支援 AOT(提前)編譯成原生機器碼,因此 Dart Native 是一項相當成熟的技術。然而,過去我們只透過 Flutter 在 iOS 和 Android 行動裝置上公開此功能。

使用 dart2native,我們正在擴展我們的原生編譯支援,以支援運行 macOS、Windows 和 Linux 的傳統桌面作業系統。由於使用 dart2native 建立的執行檔是獨立的,因此它們可以在未安裝 Dart SDK 的機器上運行。而且由於它們是使用 Dart 的 AOT 編譯器編譯的,因此執行檔只需幾毫秒即可開始運行。與其他 Dart 編譯器和運行時一樣,在編譯成原生程式碼時,Dart 中也提供了同一組豐富且一致的 核心函式庫

我們聽到了許多客戶要求桌面作業系統的 AOT 編譯——這是我們議題追蹤器中 評分第六高的議題——因此我們很高興能夠提供此功能。

如果您以前使用過 dart2aot,那麼從 2.6 開始,您將使用 dart2native。它提供了 dart2aot 功能的超集。

使用 dart2native 建構命令列應用程式

dart2native 編譯器是建構和部署 基於 Dart 的命令列應用程式 的絕佳選擇。這些應用程式通常使用諸如 dart:io(基本 I/O)、package:http(網路)和 package:args(參數解析)之類的函式庫。讓我們回顧一下將「hello, world」應用程式編譯成執行檔的基礎知識:

原始碼 hello.dart:

1
2
3
main() {
print('Hello Dart developers');
}

將 hello.dart 編譯成 hello 執行檔:

1
2
$ dart2native src/hello.dart -o hello
Generated: /Users/mit/hello

運行 hello 並測量執行時間:

1
2
3
4
5
6
$ time ./hello
Hello Dart developers

real 0m0.049s
user 0m0.018s
sys 0m0.020s

請注意命令如何啟動、列印到 stdout 並在僅 49 毫秒的組合時間內退出!

我們已經看到一些 Dart 開發人員嘗試使用 dart2native 開發命令列工具:

  • 來自 SASS(一種流行的 CSS 擴展工具)團隊的 Natalie 報告說,在將他們基於 Dart 的 SASS 實作切換到使用 dart2native 編譯後,它的效能現在可以與基於 C++ 的實作 LibSass 相媲美。
  • 來自 Dart DevRel 團隊的 Filip 使用 dart2native 重新編譯了他的 linkchecker 工具,並在檢查小型網站時發現 速度提高了 27 倍

透過 dart:ffi 與 C 程式碼互通

原生應用程式通常需要從周圍的作業系統存取原生功能。這些系統 API 通常在基於 C 的原生函式庫中公開,而 Dart 透過 dart:ffi 支援與這些函式庫的互通性,dart:ffi 是我們用於 C 互通 的新機制,我們在 Dart 2.5 中以預覽版推出。dart2native 編譯器與 dart:ffi 相容,因此您可以建立和編譯使用它的原生 Dart 應用程式。

一位團隊成員最近使用 dart:ffi 建立了一個用於控制台應用程式開發的 dart_console 函式庫,它具有獲取視窗尺寸、讀取和設定游標位置、管理顏色以及讀取鍵和控制序列等功能。使用 dart:ffi 的能力使 Dart 成為一種非常強大的控制台應用程式語言。

kilo:用不到 500 行 Dart 程式碼編寫的 7MB 程式碼編輯器

使用 Dart 核心函式庫、dart:ffi 和 dart_console 函式庫,我們可以建立非常有趣的控制台應用程式。dart_console 套件包含一個完整的 kilo 示範,這是一個僅用約 500 行 Dart 程式碼 編寫的控制台文字編輯器。kilo 這個名稱來自它的起源,kilo.c,它是一個用大約 1000 行 C 程式碼編寫的實作。

使用新的 dart2native 編譯器,我們可以輕鬆地打包它,最終得到一個 7MB 的獨立程式碼編輯器。以下是一個編譯編輯器的示範,然後使用編譯後的編輯器來編輯其自身的原始程式碼以修復錯誤:

用 Dart 編寫並使用 dart2native 編譯成執行檔的 kilo 編輯器正在編輯它自己的原始程式碼

使用 dart2native 建構服務

dart2native 編譯器的另一個潛在用途是小型服務——例如,支援使用 Flutter 編寫的前端應用程式的後端。近年來,一種日益增長的趨勢是使用在 無伺服器計算 上運行的服務。這些是完全託管的服務,可以自動擴展,包括從零擴展到零(不運行),由於它們僅在實際運行時才計費,因此有可能大大降低成本。Google Cloud 透過 Cloud Run 提供無伺服器計算。

對於無伺服器後端,服務快速啟動至關重要。傳統上,基於 Dart 的服務使用我們的 JIT(即時)編譯器運行,但 JIT 基礎的執行在啟動時延遲很高,因為程式碼需要先編譯和預熱才能開始執行。透過將服務的程式碼預先編譯為原生程式碼,您可以避免這種延遲並立即開始運行。此外,使用原生程式碼,您可以建立磁碟佔用空間小且獨立的 Dart 服務,從而大大減少 Dart 服務運行的容器的大小。Dart 開發人員 Paul Mundt 最近 記錄了他使用 dart2native 編譯器的經驗;他能夠將 Docker 映象的大小減少 91%,從使用 JIT 編譯程式碼的 220MB 減少到使用原生程式碼的僅 20MB!有關 伺服器端應用程式套件 的更多詳細資訊,請參閱我們的文件。

可用性

dart2native 編譯器從 2.6 版開始在 Dart SDK 中提供,今天開始可以從 dart.dev/get-dart 獲取。安裝 SDK 後,您應該會在 bin/ 目錄和 PATH 中看到新的編譯器。Dart.dev 上有 更多文件

如果您透過 Flutter 獲取 Dart SDK,請注意目前的 Flutter 版本對 dart2native 的支援不完整。我們建議您從 dart.dev/get-dart 安裝 Dart 2.6 SDK。

已知限制

此初始版本的 dart2native 編譯器有一些已知的限制,如下所示。您可以透過在我們的 GitHub 議題追蹤器中為議題添加「豎起大拇指」來讓我們知道哪些議題對您很重要。

  • 不支援交叉編譯(議題 28617):dart2native 編譯器僅支援為其運行的作業系統建立機器碼。因此,如果您想為所有三個作業系統建立執行檔,則需要在 macOS、Windows 和 Linux 上運行三次編譯器。執行此操作的一種方法是使用支援所有三個作業系統的 CI(持續整合)提供程式。
  • 不支援簽名(議題 39106):生成的執行檔使用與標準簽名工具(例如 codesign 和 signtool)不相容的格式。
  • 不支援 dart:mirrors 和 dart:developer(請參閱 Dart 核心函式庫)。

Dart 2.6 中的其他變化

Dart SDK 2.6 版還有一些其他變化。

我們在 Dart 2.5 中推出了 dart:ffi 的預覽版,這是我們用於 C 互通 的新機制。Dart 2.6 有一個新版本的 dart:ffi。這個新版本有許多重大 API 變更,以使我們的 API 更易於使用、提供更多類型安全,並提供方便的記憶體存取。有關其他詳細資訊,請參閱 Dart 2.6 變更日誌。透過這些變更,dart:ffi 進入測試階段,我們預計 API 變更的頻率會大大降低,並且預計總體穩定性會很高。請繼續透過 議題追蹤器 向我們提供回饋。

Dart 2.6 還包含一個令人興奮的新語言功能的預覽版,擴展方法。我們還有一些潤色和工具工作要完成此功能,但我們希望在下一個 Dart SDK 版本中正式推出它。到時候我們將會更多地談論擴展方法;現在,您可以閱讀有關 此功能背後的設計考量

後續步驟

下載 Dart 2.6 SDK(dart.dev/get-dart),使用 dart2native 建構一些酷炫的東西,然後告訴我們。如果您願意分享詳細資訊,請在此文章底部留言。我們很期待看到您建構的東西!


宣佈推出 Dart 2.6 和 dart2native:將 Dart 編譯成獨立的原生執行檔 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。