0%

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

AngularDart 5 IDE 工具發佈公告

Dart 是一種我們熟知且喜愛的靜態類型語言,而 Dart 2 的類型將更加嚴格。這就是為什麼它成為 Angular 框架如此出色的平台的原因,在 Angular 框架中,組件大多以靜態方式連結到模板中,以建立高效能的 UI。

因此,為了進一步改善這種高效且安全的開發體驗,我們宣布推出新的 IDE 工具,以在您的模板中保留類型安全!它支援 AngularDart 5,並且與 IntelliJ/WebStorm 開箱即用。它也可以設定為在 VSCode、vim 等環境中工作。

如果您已經迫不及待,現在可以直接跳到 設定,或者繼續瀏覽我們新的 IDE 整合所提供的功能!

錯誤

新的分析插件將在您的模板中為您找到許多類型錯誤。運算式會根據您使用的指令、它們包含的輸入以及您繫結的參考(#foo、let item of、…)進行驗證。

這裡有一個拼寫錯誤的成員,因此您不必扮演人工拼寫檢查器的角色!

組件輸入上的類型不匹配也不再是問題。

我們還可以提供與 $event 變數類型相關的錯誤,

我們還會檢查您在指令中嵌入的內容。這是一個我們不僅可以捕捉類型錯誤,還可以捕捉無效程式碼的例子。

我們還可以繼續!我們可以捕捉到許多其他類型的錯誤,無論是在模板中還是在組件定義中。

除了在編輯器中突出顯示外,完整的錯誤列表還會顯示在 Dart Analysis 面板中。

自動完成

我們不僅僅停留在驗證上!雖然大部分工作是讓我們的分析能夠高效能運作,並且在編譯器方面完全正常運作,但大部分價值來自於使用已解析的模板狀態,在您的模板中提供您常用的 Dart 自動完成的優點:

但我們可以完成比普通 Dart 成員更多的內容——輸入、輸出和 HTML 標籤呢?

標籤?可以。

屬性?可以。

星號?可以。

星號內的屬性?可以。

額外加分:在組件嵌入中建議 帶有 屬性的標籤!

嵌入是 Angular 中一個眾所周知的進階主題,因此如果您認為您知道我們建議的是什麼,請為您非常了解 AngularDart 而自豪。

可以這麼說,使用我們的插件後,組件 API 更容易理解了!

導航

這裡的支援因編輯器而異,但在 IntelliJ 中,您可以點擊 Dart 運算式的各個部分。在其他編輯器中,您甚至可以導航更多實體,例如標籤和輸入/輸出。

如何使用

確保您使用的是 AngularDart 5 beta 版本,以及 Dart 2.0.0-dev.31 或更新版本。

只需將此加入到您的 analysis_options.yaml 檔案中:

1
2
3
analyzer:
plugins:
- angular

然後重新啟動您的 IDE。請注意,啟動插件可能需要幾秒鐘的時間。它首先必須為您下載原始程式碼和依賴項,並且第一次分析會比後續分析慢。

您也可以在我們的 分析器插件範例專案 中試用它。

如果您使用的是 IntelliJ,這就是您需要做的所有事情。對於其他編輯器,它們可能不會在沒有額外工作的情況下在 HTML 上運行我們的插件——例如,VSCode 需要一個 標誌

回饋和更多

我們的插件原始程式碼位於 Github 上,其中包含更多關於支援哪些功能的資訊,以及如果您有任何問題或遇到任何錯誤,可以在哪裡提交問題。

新的 IDE 工具將成為 AngularDart (v5) 的下一個穩定版本的一部分。我們非常興奮能夠在您編寫 Angular 網頁應用程式時為您提供更高的生產力!


AngularDart 5 IDE 工具發佈公告 最初發佈於 Medium 上的 Dart,人們在那裡繼續透過醒目顯示和回應這個故事來進行對話。

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

宣布 Dart 官方 gRPC 支援

gRPC 是一個高效能、開源的 RPC 框架。它提供簡單的服務定義,建立在 http/2 之上,並支援雙向串流和完全整合的可插拔身份驗證。 gRPC 框架支援 多種語言,我們很高興地宣布 Dart 語言的支援現在已進入 Beta 階段!Dart gRPC 支援適用於 1.24.3 或更高版本的 Dart SDK,目前支援 FlutterVM/伺服器 平台。

建立伺服器和撰寫 gRPC 服務定義

gRPC 服務通常使用 Protocol Buffers v3 描述其端點和資料序列化。以下是一個小型範例服務定義,它定義了一個名為「Greeter」的服務,其中包含一個名為「SayHello」的 rpc 訊息(兩條訊息中的數字「1」指定了 訊息欄位 的唯一 ID):

1
2
3
4
5
6
7
8
9
10
11
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

定義服務後,您可以自動產生伺服器的框架:

1
protoc --dart_out=grpc:generated -Iprotos protos/greeter.proto

這會在 generated 目錄中產生一個 GreeterServiceBase 類別,然後您可以將其子類別化以加入實際的服務實作:

1
2
3
4
5
class GreeterService extends GreeterServiceBase {
@override
Future<HelloReply> sayHello(ServiceCall call, HelloRequest request) =>
Future.value(HelloReply()..message = 'Hello, ${request.name}!');
}

使用 gRPC 用戶端呼叫伺服器

當我們產生上面的服務 stub 時,protoc 編譯器也產生了一個用戶端函式庫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'package:grpc/grpc.dart';
import 'package:helloworld/greeter.pbgrpc.dart';

Future<void> main() async {
final channel = ClientChannel('localhost',
port: 50051,
options: const ChannelOptions(credentials: ChannelCredentials.insecure()));
final stub = GreeterClient(channel);

final name = 'Michael';
final request = HelloRequest()..name = name;
try {
final response = await stub.sayHello(request);
print('Greeter client received: ${response.message}');
} catch (e) {
print('Caught error: $e');
}
await channel.shutdown();
}

這樣,從用戶端層呼叫服務就很簡單了:

當運行時,這將列印以下輸出:

1
Greeter client received: Hello, Michael!

後續步驟

要開始在 Dart 中使用 gRPC,請查看我們新的 Dart gRPC 快速入門,它將引導您運行和擴展 Greeter 範例。接下來,查看 Dart gRPC 教程

如果您遇到任何問題,請 提交 issue。我們也很樂意聽到您對任何您希望看到的變更或新增的回饋;例如,我們已經聽到了一些支援 gRPC-Web 協定 的請求。提供快速回饋的一種具體方法是透過按下 issue 最上方評論中的 GitHub 豎起大拇指 👍 按鈕來「投票」給 issue。

我們期待看到您使用 Dart 的 gRPC 建立的成果!

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

Dart 的不可變性介紹

不可否認,不可變性是程式設計中的一個熱門話題,尤其是在前端程式設計中。像 Immutable.js 這樣的函式庫和其他概念,像是單向資料流,都認為當資料在您不知不覺中沒有發生變化時,更容易理解資料:

在物件導向和函數式程式設計中,不可變物件(不可更改物件)是指在建立後其狀態無法修改的物件。這與可變物件(可更改物件)相反,後者可以在建立後進行修改。

那麼,Dart 呢?我們有一些概念非常適合不可變性和內建的不可變物件,從 const 修飾符和 const 建構函式開始。不要與 ES6 中的 const 混淆,後者只是一個不可變的繫結:

在 Dart 中,const 既是一個不可變的繫結,也 一個不可變的物件:

所有字面量(NullStringintdoublenumboolMapListSymbol)都可以是 const,並且可以使用 const 建構函式建立使用者類型:

讓我們回顧一下 - const 實例既是不可變的繫結,並且在語言層面上被 強制 為深度不可變的 - 並且在編譯時被 規範化 - 也就是說,任何兩個實例都被認為是等效的,並且在運行時只由一個實例表示。例如,以下程式碼相當便宜 - 它在執行時只分配一個實例:

想了解更多嗎?閱讀關於 final 和 const 的 Dart 語言導覽

使用 package:meta 進行進一步的靜態檢查

當然,const 有點限制 - 您必須能夠在編譯時建立一個類別 - 因此您不能例如讀取資料庫並在執行時建立 const 物件。我們最近在 package:meta 中引入了 @immutable 註釋:

您可以使用此註釋來幫助確保開發人員保持您的類別深度不可變。它不會像 const 那樣被規範化,但仍然對開發人員很有幫助。

希望這是一個很好的不可變性介紹。如果您想了解更多關於 Dart 或不可變性的資訊,請在評論中或 Twitter 上告訴我。


Dart 的不可變性介紹 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

演進 Dart REPL 概念驗證

Dart REPL 允許您在 互動式 shell 中評估 Dart 表達式和語句。自從我 第一次發佈關於 Dart REPL 的文章 以來已經有一段時間了(您不需要閱讀它就能享受這篇文章),而且仍然缺少許多功能。特別是,動態導入和對頂級宣告的支援將非常有用,所以讓我們來看看如何支援它們。

免責聲明:我的確在 Google 工作,但這篇文章是關於一個個人專案。我不在 Dart 團隊或相關團隊。本文僅包含我個人的淺見。

tl;dr:關於如何運行 Dart REPL 的程式碼和說明可以在 https://github.com/BlackHC/dart_repl 找到。

熱重載

對於 Flutter,Dart VM 中加入了一項很酷的新功能:熱重載。Dart DevSummit 上有一個有趣的 YouTube 影片,詳細解釋並展示了它:

熱重載允許您在程式運行時更改程式碼。Dart VM 將擷取您所做的更改,並嘗試應用它們,同時保持一切正常運行。如果不能,它會告訴您原因。這非常酷!本著駭入 Dart 以做偉大的事情的精神,讓我們思考如何使用它來實作新功能。

為什麼我們不能在目前版本的 REPL 中導入新的函式庫?

REPL 使用 Dart 的 VM 服務來評估表達式。遺憾的是,導入函式庫在 Dart 中不是表達式,因此我們不能僅在該上下文中評估它。但是,我們可以在 REPL 的沙盒運行時更改其程式碼以導入新的函式庫,然後我們可以觸發熱重載來更新 REPL。這樣可行嗎?實際上可行 \o/

具有運行時導入的 Dart REPL

但是等等:我們透過 VM 服務評估表達式無法做的另一件事是建立新的類別和函數。事實上,由於這個原因,Dart 的任何頂級宣告都不能透過評估表達式來執行。

我們如何允許頂級宣告?

當然,我們可以使用上面描述的相同想法來加入新的類別或全域函數。但是,任何使用過 IPython 或類似工具一段時間的人都知道,您往往會在迭代程式碼時經常重新宣告相同的類別或函數。在您試用程式碼時,您會一遍又一遍地重新執行略微修改過的相同程式碼版本。

如果我們只是將這些宣告加入到我們的沙盒 Dart 函式庫中,則需要我們記錄在檔案中何時何地宣告了什麼,以便在您迭代它時更新宣告。這需要大量的邏輯和聰明的程式碼。遺憾的是,如果對類別的更改與舊程式碼或其他宣告不相容,它也容易損壞。這將阻止 REPL 熱重載,並迫使用戶重新啟動它 :( 這聽起來很複雜且脆弱:我認為這不是一個成功的組合!

Spike 和鏈

相反,如果我們可以多次重新定義相同的頂級宣告而不會發生重新宣告衝突怎麼辦?這在 Dart 中可能嗎?當然可以!但不是在同一個函式庫中 :) Dart 允許您導入一個函式庫,然後宣告一個遮蔽現有宣告的類別、函數或全局變數。

在此範例中,b.dart 的 MyClass 遮蔽 a.dart 的版本不會有任何抱怨,因為它們位於不同的函式庫中,並且 b.dart 中的本地宣告優先於從 a.dart 導入的宣告。

一般來說,當您宣告一個變數來隱藏來自外部作用域的另一個變數時,就會發生遮蔽。例如:

我們可以使用這個嗎?為了研究它,我實作了一個快速 spike 在這裡。它不會產生任何程式碼。相反,這是一個非常愚蠢的範例,用於確保我們認為可行的方法實際上可行。如果花費大量時間使用程式碼生成來實作它,卻發現它永遠不可能工作,那將會令人沮喪!這是它的要點:

這確實可行!我們可以建立一個相互導入的函式庫鏈(並且也相互匯出,因為否則符號將不會在任何地方都可用)。然後,用戶可以根據需要重新定義符號。顯然,這可能導致舊程式碼引用被遮蔽的符號,這可能會使事情稍微混亂,但至少它不會損壞。任何使用過 IPython 或類似工具的人也都學會了忍受它。它不可能那麼糟糕。

上圖顯示了它是如何工作的:當我們加入新的頂級宣告時,會建立新的「單元格」(Dart 函式庫),這些單元格導入(並匯出)前一個單元格。最終單元格被導入到沙盒函式庫中,該函式庫用作普通 Dart 表達式和語句的執行環境。沙盒檔案被就地編輯,然後使用熱重載重新載入。

工作流程願景

此外,如果您想在不遮蔽任何內容的情況下連續更新程式碼,這也是可能的:熱重載已經允許在普通 Dart 程式中使用此工作流程。您可以在 REPL 中執行相同的操作。您可以編輯您的神奇 Dart 函式庫 amazing_dart_library.dart 並將其導入 REPL,試用它,並且在您這樣做的同時,您可以在您選擇的編輯器中編輯程式碼,並讓 REPL 在您想要時透過調用 reload() 熱重載程式碼。兩全其美 \o/

我們如何在實踐中實作它?

好吧,我們在這裡駭入 Dart,所以讓我們看看:熱重載尚未得到 vm_service_client 的支援,因為它是一個如此新的功能,並且 服務規範 尚未完全完成。我開始為 Natalie(維護者)撰寫 一個 pull request 來加入對它的支援,但實際上,正如我的同事所知:生產品質的程式碼不是我的專長,尤其不是在我的業餘時間(對不起,Natalie!)。但是,這並不會阻止我們的駭客冒險。

Pub,Dart 的套件管理系統,不僅支援自動版本約束解析和套件的集中式儲存庫,還允許您 使用本地套件或直接依賴 GitHub。通常不建議這樣做,因為您會失去許多使 pub 變得偉大的東西,但在這裡它可以工作:我只是將 vm_service_client fork 到我自己的 GitHub clone 中,並進行了必要的更改。您可以在 https://github.com/BlackHC/vm_service_client/tree/reload_sources_poc 找到程式碼。之後,我更改了 Dart REPL 的 pubspec.yaml 以連結到我的 GitHub clone,而不是官方版本:

就是這樣!在終端機中簡單的 pub get 現在會更新 Dart REPL 以使用 fork 的版本。

這使得試驗任何東西都非常容易:您可以 fork 其他套件來試用,並且輕鬆地依賴它們。而且很酷的是,我可以發佈它,當您使用 pub 為自己下載 REPL 時,它也會從 GitHub 獲取程式碼。非常容易駭入,但也可以分享!(即使通常不建議用於生產套件 :) )

主要的一點是有趣的邏輯是單元格生成器,它實作了一個非常簡單的模板機制,與我們上面討論的一致:

當需要新的導入時,會從 REPL 中調用熱重載功能:

差不多就是這樣!您可以在 pull request 中查看所有更改:https://github.com/BlackHC/dart_repl/pull/2
我承認程式碼有點駭客,而且不整潔。在 pull request 中,REPL 和沙盒之間的消息傳遞也有一些無關的包裝程式碼。遺憾的是,這有點掩蓋了主要的更改。我需要看看我們如何重構所有這些,讓它再次變得更整潔…但有時,快速讓事情運行起來比撰寫最好的程式碼和 pull request 更容易。對此感到抱歉!

帶有頂級宣告的 Dart REPL

Dart REPL 的原始碼可以在 https://github.com/BlackHC/dart_repl 找到。除了支援頂級宣告之外,我還加入了對內建 importloadPackagereload 命令的支援。(請注意:loadPackage 需要即將發佈的 Dart SDK 1.24 開發構建版本。否則它就是一個無操作。)這些內建命令都是使用熱重載的簡單擴展。最後,為了從您的本地 pub 快取中載入新的套件,我使用了優秀的 pub_cache 套件。

要試用它(並假設您已經安裝了 Dart SDK),只需運行:

1
2
pub global activate dart_repl
pub global run dart_repl

感謝您閱讀到本文的結尾!請告訴我您的想法 :)

乾杯,
Andreas


演進 Dart REPL 概念驗證 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

從零到一學習 Flutter,第二部分

如何在跨平台行動應用程式的環境中製作複合圖形物件的動畫?加入一位熱衷於概念挖掘者的行列,學習如何將 tween 概念應用於結構化值的動畫,例如長條圖。提供完整的程式碼範例,電池已包含在內。

您如何進入一個新的程式設計領域?實驗顯然是關鍵,學習和模仿更有經驗的同行編寫的程式也是如此。我個人喜歡用概念挖掘來補充這些方法:試圖從基本原則出發,識別概念,探索它們的優勢,刻意尋求它們的指導。這是一種理性主義的方法,它不能獨立存在,但它會激發智慧,並可能更快地引導您獲得更深層次的見解。

這是 Flutter 及其 Widget 和 tween 概念介紹的第二部分,也是最後一部分。在 第一部分 的結尾,我們得到了一個 Widget 樹,其中包含各種佈局和狀態處理 Widget,

  • 一個用於使用自訂、動畫感知繪圖程式碼繪製單個長條的 Widget,
  • 一個用於啟動長條高度動畫變化的浮動動作按鈕 Widget。

動畫是使用 BarTween 實現的,我聲稱 tween 概念可以擴展以處理更複雜的情況。在第二部分中,我將通過將設計推廣到具有更多屬性的長條,以及包含各種配置的多個長條的長條圖來實現這一主張。

讓我們從為單個長條添加顏色開始。我們在 Bar 類別的 height 欄位旁邊添加一個 color 欄位,並更新 Bar.lerp 以對它們進行 lerp。這種模式很典型:

通過對應的組件進行 lerp 來在複合值之間進行 lerp。

回想一下第一部分,「lerp」是「線性插值」的縮寫。

請注意這裡靜態 lerp 方法慣用語法的效用。沒有 Bar.lerplerpDouble(在道德上是 double.lerp)和 Color.lerp,我們就必須通過為高度建立一個 Tween<double> 和為顏色建立一個 Tween<Color> 來實現 BarTween。這些 tween 將是 BarTween 的實例欄位,由其建構函式初始化,並在其 lerp 方法中使用。我們將在 Bar 類別之外多次複製關於 Bar 屬性的知識。我們的程式碼的維護者可能會覺得這不太理想。

為長條高度和顏色製作動畫。

為了在我們的應用程式中使用彩色長條,我們將更新 BarChartPainter 以從 Bar 獲取長條顏色。在 main.dart 中,我們需要能夠建立一個空的 Bar 和一個隨機的 Bar。我們將為前者使用完全透明的顏色,為後者使用隨機顏色。顏色將取自一個簡單的 ColorPalette,我們將在它自己的檔案中快速介紹它。我們將使 Bar.emptyBar.random 成為 Bar 上的工廠建構函式(程式碼清單)。

長條圖涉及各種配置中的多個長條。為了慢慢引入複雜性,我們的第一個實作將適用於顯示固定類別集的數量的長條圖。範例包括每週的訪客數或每季的銷售額。對於此類圖表,將資料集更改為另一週或另一年不會更改使用的類別,只會更改每個類別顯示的長條。

我們這次將首先更新 main.dart,用 BarChart 替換 Bar,用 BarChartTween 替換 BarTween程式碼清單)。

為了讓 Dart 分析器滿意,我們在 bar.dart 中建立 BarChart 類別,並使用固定長度的 Bar 實例列表來實現它。我們將使用五個長條,每個長條代表一週中的一天。然後,我們需要將建立空實例和隨機實例的職責從 Bar 移動到 BarChart。對於固定類別,一個空的長條圖合理地被視為一個空長條的集合。另一方面,讓隨機長條圖成為隨機長條的集合會使我們的圖表變得相當像萬花筒。相反,我們將為圖表選擇一個隨機顏色,並讓每個長條(仍然是隨機高度)繼承該顏色。

BarChartPainter 將可用寬度均勻分佈在各個長條之間,並使每個長條佔用其可用寬度的 75%。

固定類別長條圖。

請注意 BarChart.lerp 是如何根據 Bar.lerp 實現的,動態重新生成列表結構。固定類別長條圖是複合值,對其進行直接的組件式 lerp 是有意義的,就像對具有多個屬性的單個長條一樣。

這裡有一個模式在起作用。當 Dart 類別的建構函式接受多個參數時,您通常可以單獨對每個參數進行 lerp,並且組合也會看起來不錯。而且您可以任意嵌套此模式:儀表板將通過對其組成長條圖進行 lerp 來進行 lerp,而長條圖將通過對其長條進行 lerp 來進行 lerp,而長條將通過對其高度和顏色進行 lerp 來進行 lerp。顏色是通過對其 RGB 和 alpha 組件進行 lerp 來進行 lerp 的。在這個遞迴的葉子上,我們對數字進行 lerp。

具有數學傾向的人可能會表達這一點,即 lerp 與結構的可交換性,因為對於複合值 C( x, y),我們有

lerp( C( x 1, y 1), C( x 2, y 2), t) == C( lerp( x 1, x 2, t), lerp( y 1, y 2,  t))

正如我們所見,這可以很好地從兩個組件(長條的高度和顏色)推廣到任意多個組件(固定類別長條圖的 n 個長條)。

然而,在某些情況下,這幅美麗的畫面會崩潰。我們可能希望在兩個並非以完全相同的方式組成的值之間製作動畫。舉一個簡單的例子,考慮從一個包含五個工作日資料的長條圖到一個包含週末的圖表之間的動畫。

您可能會很容易地想出幾種不同的臨時解決方案,然後可能會詢問您的 UX 設計師在它們之間進行選擇。這是一種有效的方法,儘管我認為在討論過程中牢記這些不同解決方案的共同基本結構是有好處的:tween。回想一下第一部分:

通過在動畫值從零到一的過程中描繪所有 T 的空間中的路徑來為 T 製作動畫。使用 Tween 對路徑進行建模。

與 UX 設計師要回答的核心問題是:五個長條的圖表和七個長條的圖表之間的中間值是什麼?一個顯而易見的選擇是使用六個長條,但我們需要比這更多的中間值才能使動畫平滑地進行。我們需要以不同的方式繪製長條,超越等寬、均勻間距、適合 200 像素的領域。換句話說,T 值的空間必須被推廣。

通過將具有不同結構的值嵌入到更通用的值空間中來對它們進行 lerp,將兩個動畫端點和所有需要的中間值都包含在內。

我們可以分兩步完成這項工作。首先,我們將 Bar 推廣到包含其 x 坐標和寬度作為屬性:

其次,我們使 BarChart 支援具有不同長條數的圖表。我們的新圖表將適用於長條 i 代表產品發佈後第 i 天的銷售額等系列中的第 i 個值的資料集。作為程式設計師計數,任何此類圖表都涉及一個長條,用於每個整數值 0.._n_,但長條數 n 可能在不同圖表之間有所不同。

考慮兩個分別有五個和七個長條的圖表。它們五個共同類別 0..5 的長條可以像我們上面看到的那樣組合地進行動畫製作。索引為 5 和 6 的長條在另一個動畫端點中沒有對應的長條,但由於我們現在可以自由地為每個長條指定其自己的位置和寬度,我們可以引入兩個不可見的長條來扮演這個角色。視覺效果是長條 5 和 6 在動畫進行時逐漸變為最終外觀。以相反的方向製作動畫,長條 5 和 6 將會縮小或淡出到不可見。

通過 lerping 相應的組件在複合值之間進行 lerp。如果一個端點中缺少一個組件,請在其位置使用一個不可見的組件。

通常有多種方法可以選擇不可見的組件。假設我們友好的 UX 設計師決定使用零寬度、零高度的長條,其 x 坐標和顏色繼承自它們的可見對應物。我們將向 Bar 添加一個方法,用於建立給定實例的這種摺疊版本。

將上述程式碼整合到我們的應用程式中涉及為此新設定重新定義 BarChart.emptyBarChart.random。一個空的長條圖現在可以合理地被視為包含零個長條,而一個隨機的長條圖可能包含一個隨機數量的長條,所有長條都具有相同的隨機選擇的顏色,並且每個長條都具有隨機選擇的高度。但由於位置和寬度現在是 Bar 定義的一部分,因此我們需要 BarChart.random 也指定這些屬性。向 BarChart.random 提供圖表 Size 參數,然後減輕 BarChartPainter.paint 的大部分計算似乎是合理的(程式碼清單)。

Lerp 到/從不可見的長條。

敏銳的讀者可能已經注意到我們上面對 BarChart.lerp 的定義中存在潛在的效率低下問題。我們建立摺疊的 Bar 實例只是為了將它們作為參數提供給 Bar.lerp,並且對於動畫參數 t 的每個值都會重複發生這種情況。以每秒 60 幀的速度,即使對於相對較短的動畫,這也可能意味著大量 Bar 實例被饋送到垃圾收集器。有一些替代方案:

  • 通過在 Bar 類別中只建立一次摺疊的 Bar 實例,而不是在每次調用 collapsed 時都建立一次,可以重複使用它們。這種方法在這裡有效,但並不普遍適用。
  • 可以由 BarChartTween 代為處理重複使用,方法是讓其建構函式建立一個 BarTween 實例列表 _tween,在建立 lerp 長條圖時使用:(i) => _tweens[i].lerp(t)。這種方法打破了始終使用靜態 lerp 方法的慣例。在靜態 BarChart.lerp 中沒有涉及物件來儲存動畫期間的 tween 列表。相比之下,BarChartTween 物件非常適合於此。
  • 可以使用 null 長條來表示摺疊的長條,假設 Bar.lerp 中有適當的條件邏輯。這種方法很巧妙且高效,但確實需要一些小心以避免取消引用或誤解 null。它通常用於 Flutter SDK 中,其中靜態 lerp 方法傾向於接受 null 作為動畫端點,通常將其解釋為某種不可見的元素,例如完全透明的顏色或零大小的圖形元素。作為最基本的例子,lerpDoublenull 視為零,除非兩個動畫端點都是 null

下面的程式碼片段顯示了我們按照 null 方法編寫的程式碼:

我認為可以公平地說,Dart 的 ? 語法非常適合這項任務。但請注意,使用摺疊(而不是例如透明)長條作為不可見元素的決定現在被埋藏在 Bar.lerp 的條件邏輯中。這就是我之前選擇看似效率較低的解決方案的主要原因。與往常一樣,在效能與可維護性的問題上,您的選擇應基於測量結果。

在我們能夠完全概括地處理長條圖動畫之前,還有一步要做。考慮一個使用長條圖來顯示給定年份按產品類別劃分的銷售額的應用程式。使用者可以選擇另一

年,然後應用程式應動畫到該年份的長條圖。如果兩個年份的產品類別相同,或者恰好相同,只是其中一個圖表中右側顯示了一些額外的類別,我們可以使用我們現有的程式碼。但是,如果公司在 2016 年擁有產品類別 A、B、C 和 X,但在 2017 年停止使用 B 並引入了 D,該怎麼辦?我們現有的程式碼將如下所示製作動畫:

1
2
3
4
5
2016  2017
A -> A
B -> C
C -> D
X -> X

動畫可能很漂亮且流暢,但對使用者來說仍然會感到困惑。為什麼?因為它沒有保留語義。它將代表產品類別 B 的圖形元素轉換為代表類別 C 的圖形元素,而 C 的圖形元素則移動到其他位置。僅僅因為 2016 年的 B 恰好繪製在 2017 年的 C 後來出現的相同位置並不意味著前者應該變形為後者。相反,2016 年的 B 應該消失,2016 年的 C 應該向左移動並變形為 2017 年的 C,而 2017 年的 D 應該出現在它的右側。我們可以使用書中最古老的演算法之一來實現這種混合:合併排序列表。

通過對語義上對應的組件進行 lerp 來在複合值之間進行 lerp。當組件形成排序列表時,合併演算法可以使此類組件處於同等地位,根據需要使用不可見的組件來處理單側合併。

我們只需要使 Bar 實例在線性順序中相互可比較即可。然後我們可以如下所示合併它們:

具體來說,我們將為每個長條分配一個排序鍵,形式為整數等級屬性。然後,等級也可以方便地用於從調色板中為每個長條分配顏色,從而使我們能夠在動畫演示中跟蹤各個長條的移動。

現在,隨機長條圖將基於要包含的等級的隨機選擇(程式碼清單)。

任意類別。基於合併的 lerp。

這很有效,但可能不是最有效的解決方案。我們在 BarChart.lerp 中重複執行合併演算法,每次都是針對 t 的每個值。為了解決這個問題,我們將實現前面提到的將可重複使用的資訊儲存在 BarChartTween 中的想法。

我們現在可以移除靜態 BarChart.lerp 方法。

讓我們總結一下到目前為止我們所學到的關於 tween 概念的知識:

通過在動畫值從零到一的過程中描繪所有 T 的空間中的路徑來為 T 製作動畫。使用 Tween 對路徑進行建模。

根據需要推廣 T 概念,直到它包含所有動畫端點和中間值。

通過對應的組件進行 lerp 來在複合值之間進行 lerp。

  • 對應關係應基於語義,而不是偶然的圖形共置。
  • 如果一個動畫端點中缺少一個組件,請在其位置使用一個不可見的組件,該組件可能源自另一個端點。
  • 當組件形成排序列表時,使用合併演算法使語義上對應的組件處於同等地位,根據需要引入不可見的組件來處理單側合併。

考慮使用靜態 Xxx.lerp 方法實現 tween,以方便在複合 tween 實現中重複使用。如果在對單個動畫路徑的 Xxx.lerp 的調用中發生大量重新計算,請考慮將計算移動到 XxxTween 類別的建構函式中,並讓其實例託管計算結果。

有了這些見解,我們終於可以為更複雜的圖表製作動畫了。我們將快速連續地製作堆疊長條圖、分組長條圖和堆疊 + 分組長條圖:

  • 堆疊長條圖用於類別是二維的資料集,並且將長條高度表示的數值相加是有意義的。一個例子可能是每個產品和地理區域的收入。按產品堆疊可以輕鬆比較全球市場中的產品效能。按區域堆疊顯示哪些區域很重要。
堆疊長條圖。
  • 分組長條圖也用於具有二維類別的資料集,但在這種情況下,堆疊長條沒有意義或不可取。例如,如果數值是每個產品和區域的市場份額百分比,則按產品堆疊沒有意義。即使在堆疊有意義的情況下,分組也可能更可取,因為它可以更輕鬆地同時跨兩個類別維度進行定量比較。
分組長條圖。
  • 堆疊 + 分組長條圖支援三維類別,例如每個產品、地理區域和銷售管道的收入。
堆疊 + 分組長條圖。

在所有三個變體中,都可以使用動畫來視覺化資料集的變化,從而引入額外的維度(通常是時間),而不會使圖表變得混亂。

為了使動畫有用而不仅仅是漂亮,我们需要确保我们只在语义上对应的组件之间进行 lerp。因此,用于表示 2016 年特定产品/区域/管道的收入的条形段应变形为表示 2017 年相同产品/区域/管道收入的条形段(如果存在)。

可以使用合并算法来确保这一点。正如您可能从前面的讨论中猜到的那样,合并将在多个级别上进行,反映类别的维度。我们将在堆叠图中合并堆叠和条形,在分组图中合并组和条形,并在堆叠 + 分组图中合并所有三个。

为了在不大量重复代码的情况下实现这一点,我们将合并算法抽象为一个通用实用程序,并将其放在它自己的文件中 tween.dart 中:

MergeTweenable<T> 接口精確地捕獲了通過合併建立兩個 T 的排序列表的 tween 所需的內容。我們將使用 BarBarStackBarGroup 實例化類型參數 T,並使所有這些類型都實現 MergeTweenable<T>

堆疊分組堆疊 + 分組 實作已編寫為可以直接比較。我鼓勵您使用程式碼進行試驗:

  • 更改 BarChart.random 建立的組、堆疊和長條的數量。
  • 更改調色板。對於堆疊 + 分組長條,我使用了單色調色板,因為我認為這樣看起來更好。您和您的 UX 設計師可能不同意。
  • BarChart.random 和浮動動作按鈕替換為年份選擇器,並從實際資料集中建立 BarChart 實例。
  • 實現水平長條圖。
  • 實現其他圖表類型(餅圖、折線圖、堆疊面積圖)。使用 MergeTweenable<T> 或類似方法為它們製作動畫。
  • 添加圖表圖例和/或標籤和軸,然後也為它們製作動畫。

最後兩個要點的任務非常具有挑戰性。祝您玩得開心。


從零到一學習 Flutter,第二部分 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

讓 Dart 網頁應用程式具備離線功能:只需 3 行程式碼

您是否曾經嘗試載入網頁應用程式(例如遊戲或測量轉換器),卻因為網路斷線而無法使用?這是一個糟糕的體驗,但幸運的是,我們擁有讓此類應用程式可供使用者使用的技術。

對於大多數應用程式和遊戲,只需 3 行 Dart 程式碼和在終端機中輸入 1 個指令即可完成。在這篇簡短的文章中,我將引導您完成這些步驟,並確保您可以隨時玩 Pop, Pop, Win!

[Pop, Pop, Win!](https://dart-lang.github.io/sample-pop_pop_win/) - Dart 中的 [踩地雷實作](https://github.com/dart-lang/sample-pop_pop_win/)

Service Worker

Service Worker 是一個在背景執行的 JavaScript 檔案。它可以控制與之關聯的網頁或網站,攔截和修改導航和資源請求,並以非常精細的方式快取資源。

它是一種非侵入式網頁技術:如果瀏覽器 支援 Service Worker,它們可以改善使用者體驗,但網站即使沒有它們也可以正常運作(使用預設的網頁行為)。這是一個有用的屬性,可以啟用**漸進式網頁應用程式 (PWA)**,您可以為大多數使用者提供更進階的功能,同時確保其餘使用者不被鎖定。

作為背景處理執行緒,Service Worker 可以幫助:

  • 離線模式(在網路斷線時從快取中擷取資源)
  • 快取策略(用於近乎即時的快取回應,稍後可以使用新內容進行更新)
  • 推播通知(如在行動應用程式中)
  • 訊息傳遞(如果應用程式在多個分頁中開啟)

對於我們的離線遊戲體驗來說,重要的功能是:我們希望玩 Pop, Pop, Win!,而不是看到這隻恐龍:

有趣的事實:您可以透過按下向上箭頭鍵來玩這隻恐龍。

使用 Dart 建立漸進式網頁應用程式

支援離線模式大致需要以下步驟:

  1. 確定要將哪些資源放入快取以供離線使用。
  2. 建立一個 Service Worker 來準備這些資源的快取。
  3. 註冊 Service Worker,以便可以從離線快取中提供後續請求(以防網路斷線)。
  4. 在該 Service Worker 中,使用 URL 預先填入離線快取,並從快取或網路中處理適當的擷取請求。
  5. 確保 Service Worker 偵測到應用程式或靜態資源的變更,並將新版本放入快取中。

雖然上面的列表可能聽起來有點嚇人,但 Dart 中有一個 pwa 套件可以為我們完成大部分工作,提供高階 API 並自動化大部分工作。

應用程式中的變更

在您的 pubspec.yaml 中匯入 pwa 套件:

1
2
dependencies:
pwa: ^0.1.2

執行 pub get 後,將客戶端新增到您的 web/main.dart 中:

1
2
3
4
5
6
import 'package:pwa/client.dart' as pwa;

main() {
// 註冊 PWA ServiceWorker 以進行離線快取。
new pwa.Client();
}

上面的程式碼透過註冊 Service Worker(我們將在下一步中建立)來處理上面列表中的第 3 項。目前我們沒有將 Client 實例用於其他任何用途,但隨著 pwa 套件獲得新功能,它可能會用於其他用途。

自動生成的漸進式網頁應用程式

pwa 套件提供程式碼生成,處理上面列表中的第 1-2 項和第 4-5 項。為了確保正確使用快取(包括填入和清空快取),請使用以下工作流程:

  1. 建置您的 Web 應用程式,所有靜態資源都位於 build/web 中:
    pub build
  2. 運行程式碼產生器以掃描(或重新掃描)您的離線資源:
    pub run pwa
  3. 再次建置您的專案,因為您需要編譯(新的)pwa.dart 檔案:
    pub build

這些步驟會產生一個名為 lib/pwa/offline_urls.g.dart 的檔案,其中包含要快取的離線 URL 列表。.g.dart 副檔名表示該檔案是生成的,可能會被 pwa 的程式碼產生器工具自動覆蓋。

第一次執行時,此工作流程會產生 web/pwa.dart 檔案,其中包含具有合理預設值的 Service Worker。您可以修改此檔案(例如,自訂離線 URL 或使用高階 API),因為程式碼產生器不會再次更改或覆蓋它。

注意事項

雖然 Dartium 非常適合大多數 Web 開發,但目前很難與 Service Worker 一起使用。我們建議改用 Chrome 或 Firefox。

快取失效是電腦科學中最難的問題之一。底層的 Web Cache API 提供了一些保證,pwa 函式庫也盡力妥善處理邊緣情況,但不要將快取視為任何真正重要內容的可靠儲存空間。在快取可用時使用它,在不可用時優雅地失敗。

試試看

您現在可以部署應用程式的新版本。或者嘗試 離線 Pop, Pop, Win! 遊戲

開啟遊戲並玩一輪後,關閉您的 Wi-Fi 或拔掉網路線,然後重新載入(或重新輸入 URL)。如果您使用的是 Chrome 或 Firefox,您的遊戲應該可以正常執行。祝您好運,玩得開心!


讓 Dart 網頁應用程式具備離線功能:只需 3 行程式碼 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

使用 AngularDart 整合 Google 地圖

本文將介紹如何將 Google 地圖整合到 AngularDart 應用程式中。該應用程式本身非常簡單:它計算地圖上兩個選定標記之間的大圓距離(球體表面上的最短距離)。

在此過程中,您將:

  • 註冊您自己的 Google 地圖 API 金鑰。
  • 建立一個基本的 Angular 網頁應用程式。
  • 將 Dart 與 Google 地圖 JavaScript API 整合,並處理地圖互動。
  • 學習一些 Angular 元件的優化技巧。

本文的篇幅大約是程式碼的四倍。如果您願意,可以直接查看完整的原始碼和最終的Demo

Demo 的螢幕截圖

Google 地圖 JavaScript API

Google 地圖 JavaScript API允許開發人員在其網站上嵌入和整合 Google 地圖。您可以使用自己的圖像、數據和處理來自訂顯示的地圖內容和處理方式。

要開始使用 Google 地圖 API 進行開發,您必須註冊一個免費的 API 金鑰,該金鑰允許合理的使用量。隨著您的應用程式獲得更多關注,您可以將其升級到付費方案。

訪問JavaScript API 頁面,然後在頁面頂部點擊「取得金鑰」。為您的專案建立名稱,然後點擊「建立並啟用 API」:

您的金鑰將在幾秒鐘內啟用並準備使用:

記下您的 API 金鑰。我們將在擷取地圖 JavaScript 函式庫時使用它:

1
https://maps.googleapis.com/maps/api/js?key=您的金鑰

在 JavaScript 中,要建立地圖實例並在其上放置標記,我們將使用以下程式碼:

1
2
3
4
5
6
7
8
9
10
var hostElement = document.getElementById('map-id');
var map = new google.maps.Map(hostElement, {
zoom: 2,
center: {lat: 47.4979, lng: 19.0402}
});
var marker = new google.maps.Marker({
position: {lat: 47.4979, lng: 19.0402},
map: map,
label: 'A'
});

稍後您將看到,Dart 程式碼將非常相似(具有 Dart 的所有額外好處)。

AngularDart 應用程式

開始使用 AngularDart 應用程式的最簡單方法是遵循入門指南,並在 WebStorm 或 IntelliJ IDEA 的社群版中建立新專案

如果您使用的是不同的編輯器,或者您只想遵循我們的範例程式碼的結構,以下是最低要求。

在 pubspec 檔案 pubspec.yaml 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: google_maps_angular_dart
version: 0.0.1
description: 整合 Google 地圖的 Angular 應用程式

environment:
sdk: '>=1.19.0 <2.0.0'

dependencies:
angular2: ^2.2.0
google_maps: ^3.0.0

dev_dependencies:
dart_to_js_script_rewriter: ^1.0.1

transformers:
- angular2:
platform_directives:
- 'package:angular2/common.dart#COMMON_DIRECTIVES'
platform_pipes:
- 'package:angular2/common.dart#COMMON_PIPES'
entry_points: web/main.dart
- dart_to_js_script_rewriter

在主機頁面 web/index.html 中:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<!-- 其他標題 -->
<script src="https://maps.googleapis.com/maps/api/js?key=您的金鑰"></script>
<script defer src="main.dart" type="application/dart"></script>
</head>
<body>
<map-control></map-control>
</body>
</html>

在應用程式的入口點 web/main.dart 中:

1
2
3
4
5
6
import 'package:angular2/platform/browser.dart';
import 'package:google_maps_angular_dart/component/map_control.dart';

void main() {
bootstrap(MapControl);
}

<map-control> 元件的 Dart 檔案 lib/component/map_control.dart 中:

1
2
3
4
5
6
7
8
9
import 'package:angular2/core.dart';

@Component(
selector: 'map-control',
template: '{{distance}}',
)
class MapControl {
String distance = '尚未計算距離';
}

上面的程式碼不會做太多事情,但這是一個開始,您可以使用 pub serve 執行它,然後在 Dartium 中打開頁面:

1
2
3
4
5
$ pub serve
Loading source assets...
Loading angular2 and dart_to_js_script_rewriter transformers...
Serving google_maps_angular_dart web on http://localhost:8080
Build completed successfully

Google 地圖整合

要開始 Google 地圖整合,請將以下 script 標籤放入您的 web/index.html 中。請注意,您需要設定您的 API 金鑰:

1
<script src="https://maps.googleapis.com/maps/api/js?key=[您的金鑰]"></script>

幸運的是,pub 上有一個現成的 Google 地圖 Dart 套件。將其加入到您的 pubspec.yaml 中:

1
2
3
dependencies:
angular2: ^2.2.0
google_maps: ^3.0.0

然後執行 pub get以下載套件。

我們需要在元件模板中為地圖區域建立一個主機元素。我們將從基本樣式開始,並使用 #mapArea 錨點在下一步中識別元素:

1
<div style="width: 300px; height: 300px" #mapArea>[map]</div>

在元件程式碼中,我們可以注入元素的參考,如下所示:

1
2
@ViewChild('mapArea')
ElementRef mapAreaRef;

MapControl 類別建立後,元素參考不會立即提供,因此我們需要連接到 Angular 的生命週期回調:

1
2
3
4
5
6
7
class MapControl implements AfterViewInit {

@override
void ngAfterViewInit() {
// mapAreaRef 現在可用
}
}

提示:使用 IDE 為您撰寫方法主體。例如,在 IntelliJ 中,按 CMD + N(或 CTRL + N),然後選擇「實作方法…」選單項。它將允許您選擇缺少的方法,您只需要擔心方法主體:

如下面的程式碼所示,Dart API 與 JavaScript API 非常相似,額外的好處是它經過類型檢查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MapControl implements AfterViewInit {
// ...

@override
void ngAfterViewInit() {
GMap map = new GMap(
mapAreaRef.nativeElement,
new MapOptions()
..zoom = 2
..center = new LatLng(47.4979, 19.0402)
// ^ 布達佩斯,匈牙利
);
new Marker(new MarkerOptions()
..map = map
..position = new LatLng(47.4979, 19.0402)
..label = 'A');
}
}

類型檢查好處的一個例子是,當我們監聽事件時,我們不需要考慮如何存取屬性。例如,IDE 可以幫助我們找到包含標記位置的 MouseEvent 屬性 (latLng)。

以下程式碼回應拖動標記:

1
2
3
marker.onDrag.listen((MouseEvent event) {
print('拖動時的新位置:${event.latLng}');
});

在地圖上擷取點擊事件類似:

1
2
3
map.onClick.listen((MouseEvent event) {
print('使用者點擊的位置:${event.latLng}');
});

整合在一起

為了測量兩個坐標之間的距離,我們將在地圖上追蹤兩個標記。使用者應該能夠拖動標記或透過點擊放置它們。

我們需要將地圖和標記作為欄位進行追蹤:

1
2
3
GMap _map;
Marker _aMarker;
Marker _bMarker;

更新初始化以儲存地圖參考並註冊點擊處理程式。點擊處理程式會更新標記位置和距離:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@override
void ngAfterViewInit() {
_map = new GMap(
mapAreaRef.nativeElement,
new MapOptions()
..zoom = 2
..center = new LatLng(47.4979, 19.0402)
// ^ 布達佩斯,匈牙利
);
_map.onClick.listen((MouseEvent event) {
_updatePosition(event.latLng);
_updateDistance();
});
}

這是點擊處理程式第一部分的程式碼:

1
2
3
4
5
6
7
8
9
10
void _updatePosition(LatLng position) {
if (_aMarker == null) {
_aMarker = _createMarker(_map, 'A', position);
} else if (_bMarker == null) {
_bMarker = _createMarker(_map, 'B', position);
} else {
_aMarker.position = _bMarker.position;
_bMarker.position = position;
}
}

標記實例化的程式碼與前面的範例類似:

1
2
3
4
5
6
7
8
9
10
11
Marker _createMarker(GMap map, String label, LatLng position) {
final Marker marker = new Marker(new MarkerOptions()
..map = map
..draggable = true
..label = label
..position = position);
marker.onDrag.listen((MouseEvent event) {
_updateDistance();
});
return marker;
}

借助 dart:math 中的工具函數,我們能夠處理大圓距離計算的數學運算,並在我們的 distance 欄位中設定值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// 地球半徑,單位為公里。
const int radiusOfEarth = 6371;

double _toRadian(num degree) => degree * PI / 180.0;

void _updateDistance() {
if (_aMarker == null || _bMarker == null) return;
LatLng a = _aMarker.position;
LatLng b = _bMarker.position;
double dLat = _toRadian(b.lat - a.lat);
double sLat = pow(sin(dLat / 2), 2);
double dLng = _toRadian(b.lng - a.lng);
double sLng = pow(sin(dLng / 2), 2);
double cosALat = cos(_toRadian(a.lat));
double cosBLat = cos(_toRadian(b.lat));
double x = sLat + cosALat * cosBLat * sLng;
double d = 2 * atan2(sqrt(x), sqrt(1 - x)) * radiusOfEarth;
distance = '${d.round()} 公里';
}

您是否曾經想過您住的地方距離親戚、著名地點或地標有多遠?現在是時候試用該應用程式並親自檢查一下了。事實證明,我家距離山景城的 Google 總部 9815 公里:

加州山景城 Googleplex 與作者在匈牙利布達佩斯的住所之間的距離

優化應用程式

此時,我們 Angular 應用程式中的 Google 地圖整合已完成。我們正在監聽和回應地圖事件,並在地圖上建立物件。在最後一部分中,我們將實作一些功能,為我們的 Demo 增添一些美好的修飾。

分離模板和樣式

將複雜的 UI 模板放在單獨的 .html 檔案中是一個好習慣。我們可以用 CSS 樣式做同樣的事情:

1
2
3
4
5
@Component(
selector: 'map-control',
templateUrl: 'map_control.html',
styleUrls: const <String>['map_control.css'])
class MapControl implements AfterViewInit {

地圖元素有一個 CSS 類別 map-area

1
<div class="map-area" #mapArea>[map]</div>

我們的 CSS 檔案可以像這樣簡單:

1
2
3
4
5
.map-area {
width: 500px;
height: 400px;
margin: 10px;
}

處理距離單位

有些人精通公里 ↔ 英里的轉換,但我們其他人想要一個下拉選單,我們可以在其中選擇距離單位。在 HTML 模板中,下拉選單可以是一個簡單的 <SELECT> 元素,模型綁定到 unit 欄位。

1
2
3
4
5
<label>單位:</label>
<select [(ngModel)]="unit">
<option value="km">公里</option>
<option value="miles">英里</option>
</select>

在 Dart 程式碼中,我們想要儲存單位,並在單位更新時更新距離:

1
2
3
4
5
6
7
8
String _unit = 'km';

String get unit => _unit;

set unit(String value) {
_unit = value;
_updateDistance();
}

並且別忘了在距離計算中更新先前硬編碼的公里:

1
2
3
4
5
6
7
8
/// 將公里轉換為英里的常數值。
const double milesPerKm = 0.621371;

// ... 與之前的程式碼相同
if (unit == 'miles') {
d *= milesPerKm;
}
distance = '${d.round()} $unit';

格式化坐標

如果我們想發佈標記的位置怎麼辦?最簡單的方法是在 Dart 中公開位置值,這樣我們就可以在模板中使用 {{a}}{{b}}

1
2
3
// 公開位置值。
LatLng get a => _aMarker?.position;
LatLng get b => _bMarker?.position;

當標記尚未初始化時,模板可以隱藏標籤,防止潛在的空值問題:

1
2
<div *ngIf="a != null">A: {{a}}</div>
<div *ngIf="b != null">B: {{b}}</div>

但是,{{a}} 將會轉換為調用 LatLng.toString(),它會給我們兩個非常長的 double 值,而幾個數字就足夠了。一種解決方案是在模板中使用管道:

1
2
3
<div *ngIf="a != null">A: {{a.lat | number : '1.4-4'}}, {{a.lng | number : '1.4-4'}}</div>
<div *ngIf="b != null">B: {{b.lat | number : '1.4-4'}}, {{b.lng | number : '1.4-4'}}</div>

模板語法指南建議將該邏輯放在控制器類別中,以便更好地進行測試:

1
2
3
4
5
6
7
8
9
10
11
/// 'A' 標記的格式化位置。
String get aPosition => _formatPosition(a);

/// 'B' 標記的格式化位置。
String get bPosition => _formatPosition(b);

String _formatPosition(LatLng pos) {
if (pos == null) return null;
return '${pos.lat.toStringAsFixed(4)}, '
'${pos.lng.toStringAsFixed(4)}';
}

這樣,模板就可以更簡單了:

1
2
<div *ngIf="a != null">A: {{aPosition}}</div>
<div *ngIf="b != null">B: {{bPosition}}</div>

清理模板

最後一步,將所有剩餘的程式碼部分從模板移動到控制器:

1
2
3
4
5
6
7
8
/// 是否應顯示 'A' 標記的位置
bool get showA => a != null;

/// 是否應顯示 'B' 標記的位置
bool get showB => b != null;

/// 是否應顯示 'distance' 標籤
bool get showDistance => distance != null;

這樣做,模板只會參考 getter:

1
2
3
4
5
6
<div *ngIf="showA">A: {{aPosition}}</div>
<div *ngIf="showB">B: {{bPosition}}</div>

<p *ngIf="showDistance">
距離:{{distance}}
</p>

結語

如您所見,使用 Google 地圖實作雙向整合很容易:原始碼乾淨且易讀。在 Dart 工具鏈的全力支援下,它很容易擴展,而無需擔心我們可能會在其他地方造成破壞。

讀者可以練習一下,使用 Dart 中的相同 API 將熱圖視覺化新增到地圖中。查看 google_maps 套件以了解所有可能性。


使用 AngularDart 的 Google 地圖 最初發佈在 Medium 的 dartlang 上,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

使用 Flutter 從零到一

2016 年夏末,我在丹麥奧胡斯 Google 辦公室的新工作是使用 FlutterDart 在 Android/iOS 應用程式中實作動畫圖表。除了是「Noogler」之外,Flutter、Dart 和動畫對我來說都是全新的。事實上,我以前從未做過行動應用程式。我的第一支智慧型手機才用了幾個月——因為擔心用我的舊 Nokia 接聽電話面試會失敗而驚慌失措地買的……

我確實有一些來自桌面 Java 的圖表經驗,但那不是動畫的。我覺得……很奇怪。一部分像恐龍,一部分像重生。

簡而言之: 透過為 Android/iOS 應用程式編寫 Dart 圖表動畫,發現 Flutter 的 Widget 和 tween 概念的優勢。

轉移到新的開發堆疊會讓您意識到自己的優先事項。在我的清單中,最重要的是以下三項:

  • 強大的概念 透過提供簡單、相關的方式來組織想法、邏輯或資料,從而有效地處理複雜性。
  • 清晰的程式碼 讓我們能夠清晰地表達這些概念,而不會被語言陷阱、過多的樣板或輔助細節所分心。
  • 快速迭代 是實驗和學習的關鍵——軟體開發團隊以學習為生:真正的需求是什麼,以及如何用程式碼表達的概念來最好地實現這些需求。

Flutter 是一個新的平台,可以使用 Dart 從單一程式碼庫開發 Android 和 iOS 應用程式。由於我們的需求是相當複雜的 UI,包括動畫圖表,因此只建構一次的想法似乎非常有吸引力。我的任務包括使用 Flutter 的 CLI 工具、一些預先建構的 Widget 和它的 2D 渲染引擎——除了編寫大量的純 Dart 程式碼來建模和製作圖表動畫之外。我將在下面分享我學習經驗中的一些概念重點,並為您自己評估 Flutter/Dart 堆疊提供一個起點。

開發過程中從 iOS 模擬器擷取的簡單動畫長條圖

這是 Flutter 及其「Widget」和「tween」概念的 兩部分 介紹的第一部分。我將透過使用它們來顯示和製作如上所示的圖表動畫來說明這些概念的優勢。完整的程式碼範例應該可以讓您了解使用 Dart 可以達到的程式碼清晰度。而且我將包含足夠的細節,以便您可以在自己的筆記型電腦(以及模擬器或設備)上跟進,並體驗 Flutter 開發週期的長度。

起點是全新 安裝 Flutter。執行

1
$ flutter doctor

以檢查設定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ flutter doctor
[✓] Flutter (on Mac OS, channel master)
• Flutter at /Users/mravn/flutter
• Framework revision 64bae978f1 (7 hours ago), 2017-02-18 21:00:27
• Engine revision ab09530927
• Tools Dart version 1.23.0-dev.0.0
[✓] Android toolchain - develop for Android devices
(Android SDK 24.0.2)
• Android SDK at /Users/mravn/Library/Android/sdk
• Platform android-25, build-tools 24.0.2
• Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
[✓] iOS toolchain - develop for iOS devices (Xcode 8.2.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Xcode 8.2.1, Build version 8C1002
• ios-deploy 1.9.1
[✓] IntelliJ IDEA Community Edition (version 2016.3.4)
• Dart plugin version 163.13137
• Flutter plugin version 0.1.10
[✓] Connected devices
• iPhone SE • 664A33B0-A060-4839-A933-7589EF46809B • ios •
iOS 10.2 (simulator)

如果有足夠的勾號,您就可以建立 Flutter 應用程式。讓我們將其稱為 charts

1
$ flutter create charts

這應該會產生一個同名目錄:

1
2
3
4
5
charts
android
ios
lib
main.dart

已產生大約五十個檔案,組成一個完整的範例應用程式,可以安裝在 Android 和 iOS 上。我們將在 main.dart 和同級檔案中完成所有程式碼編寫,無需修改任何其他檔案或目錄。

您應該驗證是否可以啟動範例應用程式。啟動模擬器或連接設備,然後在 charts 目錄中執行

1
$ flutter run

然後,您應該在模擬器或設備上看到一個簡單的計數應用程式。它使用了 Material Design Widget,這很好,但它是可選的。作為 Flutter 架構的最頂層,這些 Widget 是完全可以替換的。

首先,讓我們用下面的程式碼替換 main.dart 的內容,這是一個簡單的起點,用於玩圖表動畫。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import 'package:flutter/material.dart';
import 'dart:math';

void main() => runApp(new ChartsApp());

class ChartsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Charts',
home: new ChartPage(),
);
}
}

class ChartPage extends StatefulWidget {
@override
ChartPageState createState() => new ChartPageState();
}

class ChartPageState extends State<ChartPage> {
int dataSet = null;

void _refreshData() {
setState(() {
dataSet = new Random().nextInt(101);
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Text('Data set: $dataSet'),
),
floatingActionButton: new FloatingActionButton(
onPressed: _refreshData,
tooltip: 'Refresh',
child: new Icon(Icons.refresh),
),
);
}
}

儲存變更,然後重新啟動應用程式。您可以從終端機執行此操作,方法是按 R 鍵。此「完全重新啟動」操作會捨棄應用程式狀態,然後重建 UI。對於在程式碼變更後現有應用程式狀態仍然有效的情況,可以按 r 鍵進行「熱重載」,這只會重建 UI。IntelliJ IDEA 也有一個 Flutter 外掛,提供與 Dart 編輯器整合的相同功能:

IntelliJ IDEA 中使用 Flutter 外掛的螢幕截圖,顯示右上角的重新載入和重新啟動按鈕。如果應用程式是從 IDE 中啟動的,則這些按鈕會啟用。

重新啟動後,應用程式會顯示一個置中的文字標籤,顯示「Data set: null」,以及一個用於重新整理資料的浮動動作按鈕。是的,萬事起頭難。

要了解熱重載和完全重新啟動之間的區別,請嘗試以下操作:按下浮動動作按鈕幾次後,記下目前的資料集編號,然後在程式碼中將 Icons.refresh 替換為 Icons.add,儲存並執行熱重載。觀察按鈕的變化,但應用程式狀態會保留;我們仍然在隨機數字流中的同一個位置。現在撤消圖示變更,儲存並執行完全重新啟動。應用程式狀態已重設,我們回到了「Data set: null」。

我們的簡單應用程式展示了 Flutter Widget 概念的兩個核心面向:

  • 使用者介面是由不可變 Widget 樹定義的,該樹是透過建構函數調用(您可以在其中設定 Widget)和建構方法(Widget 實作可以決定其子樹的外觀)的組合建構的。我們應用程式的結果樹狀結構如下所示,其中每個 Widget 的主要角色都在括弧中。如您所見,雖然 Widget 概念相當廣泛,但每個具體的 Widget 類型通常都有一個非常專注的職責。
1
2
3
4
5
6
7
MaterialApp                    (導航)
ChartPage (狀態管理)
Scaffold (佈局)
Center (佈局)
Text (文字)
FloatingActionButton (使用者互動)
Icon (圖形)
  • 使用不可變 Widget 樹定義使用者介面後,變更該介面的唯一方法是重建樹。Flutter 會在下一幀到期時處理這件事。我們所要做的就是告訴 Flutter,子樹所依賴的某些狀態已變更。此類狀態相依子樹的根必須是 StatefulWidget。與任何正常的 Widget 一樣,StatefulWidget 不是可變的,但其子樹是由 State 物件建構的,State 物件是可變的。Flutter 會在樹重建過程中保留 State 物件,並在建構過程中將每個物件附加到新樹中各自的 Widget。然後,它們會決定如何建構該 Widget 的子樹。在我們的應用程式中,ChartPage 是一個 StatefulWidgetChartPageState 作為其狀態。每當使用者按下按鈕時,我們都會執行一些程式碼來變更 ChartPageState。我們已使用 setState 標記了變更,以便 Flutter 可以執行其內務處理並排程 Widget 樹以進行重建。當這種情況發生時,ChartPageState 將建構一個以新的 ChartPage 實例為根的稍微不同的子樹。

不可變 Widget 和狀態相依子樹是 Flutter 提供給我們的用於處理複雜 UI 中狀態管理複雜性的主要工具,這些 UI 會回應非同步事件,例如按鈕按下、計時器刻度或傳入資料。根據我的桌面經驗,我認為這種複雜性是非常真實的。評估 Flutter 方法的優勢是——也應該是——讀者的練習:在一些非平凡的事情上嘗試一下。

我們的圖表應用程式在 Widget 結構方面將保持簡單,但我們將做一些動畫的自訂圖形。第一步是用一個非常簡單的圖表替換每個資料集的文字表示。由於資料集目前只涉及 0..100 區間內的一個數字,因此圖表將是一個只有一個長條的長條圖,其高度由該數字決定。我們將使用初始值 50 以避免空高度:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import 'package:flutter/material.dart';
import 'dart:math';

// ...

class ChartPageState extends State<ChartPage> {
int dataSet = 50; // Initial value

void _refreshData() {
setState(() {
dataSet = new Random().nextInt(101);
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new CustomPaint(
size: new Size(200.0, 200.0), // Adjust size as needed
painter: new BarChartPainter(dataSet),
)),
floatingActionButton: new FloatingActionButton(
onPressed: _refreshData,
tooltip: 'Refresh',
child: new Icon(Icons.refresh),
),
);
}
}

class BarChartPainter extends CustomPainter {
final int dataSet;
BarChartPainter(this.dataSet);

@override
void paint(Canvas canvas, Size size) {
final paint = new Paint()
..color = Colors.blue[400]
..style = PaintingStyle.fill;
canvas.drawRect(
new Rect.fromPoints(
new Offset(0.0, size.height - dataSet.toDouble()),
new Offset(size.width, size.height),
),
paint,
);
}

@override
bool shouldRepaint(BarChartPainter old) => old.dataSet != dataSet;
}

CustomPaint 是一個將繪製委託給 CustomPainter 策略的 Widget。我們對該策略的實作會繪製單個長條。

下一步是加入動畫。每當資料集變更時,我們希望長條平滑地而不是突然地變更高度。Flutter 有一個 AnimationController 概念來協調動畫,透過註冊一個監聽器,我們會在動畫值(從零到一的雙精度值)變更時收到通知。每當這種情況發生時,我們可以像以前一樣調用 setState 並更新 ChartPageState

出於說明原因,我們第一次嘗試會很醜陋:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart'; // Import animation library
import 'dart:math';

// ...

class ChartPageState extends State<ChartPage> with SingleTickerProviderStateMixin {
int startDataSet = 50;
int currentDataSet = 50;
int endDataSet = 50;

AnimationController _controller;
Animation<double> _animation;

@override
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = new Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
setState(() {
currentDataSet = (startDataSet + (endDataSet - startDataSet) * _animation.value).toInt();
});
});
}

void _refreshData() {
setState(() {
startDataSet = currentDataSet;
endDataSet = new Random().nextInt(101);
_controller.forward(from: 0.0);
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new CustomPaint(
size: new Size(200.0, 200.0),
painter: new BarChartPainter(currentDataSet),
)),
floatingActionButton: new FloatingActionButton(
onPressed: _refreshData,
tooltip: 'Refresh',
child: new Icon(Icons.refresh),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}


// ... BarChartPainter remains the same

哎呦。複雜性已經開始顯現,而我們的資料集仍然只是一個數字!設定動畫控制所需的程式碼是一個小問題,因為當我們獲得更多圖表資料時,它不會產生分支。真正問題是變數 startHeightcurrentHeightendHeight,它們反映了對資料集和動畫值的變更,並且在三個不同的地方更新。

我們需要一個概念來處理這個爛攤子。

輸入 tweens。雖然遠非 Flutter 獨有,但它們是一個令人愉悅的簡單概念,用於組織動畫程式碼。它們的主要貢獻是用函數式方法取代了上面的命令式方法。tween 是一個_值_。它描述了在其他值的空間中兩個點之間的路徑,例如長條圖,因為動畫值從零到一。

Tweens 在這些其他值的類型中是通用的,並且可以用 Dart 表示為 Tween<T> 類型的物件:

1
2
3
4
5
6
7
8
class Tween<T> {
final T begin;
final T end;

Tween({ this.begin, this.end });

T lerp(double t) =&gt; T.lerp(begin, end, t);
}

術語 lerp 來自電腦圖形領域,是 線性插值(作為名詞)和 線性插值(作為動詞)的縮寫。參數 t 是動畫值,因此 tween 應該從 begin(當 t 為零時)到 end(當 t 為一時)進行插值。

Flutter SDK 的 Tween<T> 類別與上述非常相似,但它是一個支援變更 beginend 的具體類別。我不完全確定為什麼做出這個選擇,但在 SDK 的動畫支援領域中,我還沒有探索過,可能有很好的理由。在接下來的文章中,我將使用 Flutter Tween<T>,但假設它是不可變的。

我們可以使用單個 Tween<double> 來清理我們的長條高度程式碼:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'dart:math';

// ...

class ChartPageState extends State<ChartPage> with SingleTickerProviderStateMixin {

Tween<double> _tween;

AnimationController _controller;
Animation<double> _animation;

@override
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);

_tween = new Tween(begin: 50.0, end: 50.0);
_animation = _tween.animate(_controller);

}

void _refreshData() {
setState(() {
_tween = new Tween(begin: _tween.end, end: new Random().nextInt(101).toDouble());
_controller.forward(from: 0.0);
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new CustomPaint(
size: new Size(200.0, 200.0),
painter: new BarChartPainter(_animation.value), // Use _animation.value directly
)),
floatingActionButton: new FloatingActionButton(

onPressed: _refreshData,
tooltip: 'Refresh',
child: new Icon(Icons.refresh),
),
);
}

// ... dispose method remains the same


}


class BarChartPainter extends CustomPainter {
final double dataSet; // Now takes a double
BarChartPainter(this.dataSet);

@override
void paint(Canvas canvas, Size size) {
final paint = new Paint()
..color = Colors.blue[400]
..style = PaintingStyle.fill;
canvas.drawRect(
new Rect.fromPoints(
new Offset(0.0, size.height - dataSet.toDouble()), // Convert to double if needed
new Offset(size.width, size.height),
),
paint,
);
}

@override
bool shouldRepaint(BarChartPainter old) => old.dataSet != dataSet;
}

我們使用 Tween 將長條高度動畫端點打包成單個值。它與 AnimationControllerCustomPainter 整合得很好,避免了動畫過程中 Widget 樹重建,因為 Flutter 基礎結構現在標記了 CustomPaint 以便在每個動畫刻度重新繪製,而不是標記整個 ChartPage 子樹以進行重建、重新佈局和重新繪製。這些都是明確的改進。但 tween 概念還有更多內容;它提供了_結構_來組織我們的想法和程式碼,而我們還沒有真正認真对待這一點。tween 概念說,

透過在所有 T 的空間中繪製路徑來製作 T 的動畫,因為動畫值從零到一。使用 Tween 建模路徑。

在上面的程式碼中,T 是一個雙精度值,但我們不希望對雙精度值進行動畫處理,我們希望對長條圖進行動畫處理!好吧,現在是單個長條,但這個概念很強大,如果我們允許的話,它可以擴展。

(您可能會想知道為什麼我們不進一步爭論,堅持對資料集而不是它們作為長條圖的表示進行動畫處理。這是因為資料集(與作為圖形物件的長條圖相反)通常不在存在平滑路徑的空間中。長條圖的資料集通常涉及對應於離散資料類別的數值資料。但是,如果沒有作為長條圖的空間表示,則不同類別的兩個資料集之間沒有合理的平滑路徑概念。)

回到我們的程式碼,我們將需要一個 Bar 類型和一個 BarTween 來對其進行動畫處理。接下來,讓我們將與長條相關的類別提取到它們自己的 bar.dart 檔案中,與 main.dart 並列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'dart:ui' show lerpDouble;

class Bar {
final double height;
Bar({this.height});

static Bar lerp(Bar begin, Bar end, double t) {
return new Bar(height: lerpDouble(begin.height, end.height, t));
}
}

class BarTween extends Tween<Bar> {
BarTween({Bar begin, Bar end}) : super(begin: begin, end: end);

@override
Bar lerp(double t) => Bar.lerp(begin, end, t);
}

我在這裡遵循 Flutter SDK 約定,根據 Bar 類別上的靜態方法定義 BarTween.lerp。這對於像 BarColorRect 和許多其他簡單類型來說效果很好,但我們需要重新考慮更複雜圖表類型的方法。Dart SDK 中沒有 double.lerp,因此我們使用 dart:ui 套件中的函數 lerpDouble 來達到相同的效果。

我們的應用程式現在可以用長條表示,如下面的程式碼所示;我趁機取消了 dataSet 欄位。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'dart:math';
import 'bar.dart'; // Import bar classes


void main() => runApp(new ChartsApp());

// ... ChartsApp remains the same

class ChartPage extends StatefulWidget {
@override
ChartPageState createState() => new ChartPageState();
}

class ChartPageState extends State<ChartPage> with SingleTickerProviderStateMixin {

BarTween _barTween;
AnimationController _controller;
Animation<Bar> _animation;


@override
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);

_barTween = new BarTween(begin: new Bar(height:50.0), end: new Bar(height:50.0));
_animation = _barTween.animate(_controller);

}

void _refreshData() {
setState(() {

_barTween = new BarTween(
begin: _barTween.end, end: new Bar(height: new Random().nextInt(101).toDouble()));

_controller.forward(from: 0.0);


});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new CustomPaint(
size: new Size(200.0, 200.0),
painter: new BarChartPainter(_animation.value), // Pass the Bar object
)),
floatingActionButton: new FloatingActionButton(
onPressed: _refreshData,
tooltip: 'Refresh',
child: new Icon(Icons.refresh),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}



class BarChartPainter extends CustomPainter {
final Bar bar;

BarChartPainter(this.bar);

@override
void paint(Canvas canvas, Size size) {
final paint = new Paint()
..color = Colors.blue[400]
..style = PaintingStyle.fill;
canvas.drawRect(
new Rect.fromPoints(
new Offset(0.0, size.height - bar.height),
new Offset(size.width, size.height),
),
paint,
);
}

@override
bool shouldRepaint(BarChartPainter old) => old.bar != bar;
}


新版本更長,額外的程式碼應該有其價值。在 第二部分 中,我們將處理增加的圖表複雜性時,它會發揮作用。我們的需求是彩色的長條、多個長條、部分資料、堆疊長條、分組長條、堆疊和分組長條,……所有這些都是動畫的。敬請期待。

我們將在第二部分中製作的動畫之一的預覽。


使用 Flutter 從零到一 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

使用 Dart 進行 hack

Python 的互動模式很棒。Dart 目前沒有互動模式,但它非常適合快速製作原型,所以讓我們看看是否可以將一些東西拼湊在一起!

聲明:我的確在 Google 工作,但這篇文章是關於一個個人專案。我不在 Dart 團隊或相關團隊。這只是我的淺見和我的故事。一起來吧。

像 Python 或 Ruby 等語言的互動模式有助於讓初學者更容易上手。但這些並不是唯一使用 REPL 概念的語言。REPL 是一個讀取-評估-列印循環。當您在終端機中與它們互動時,像 BASH 或 zsh 等 shell 也會使用 REPL。這也是您在 Jupyter (IPython Notebooks)、MatlabMathematicaMaple 中使用筆記本時所獲得的。這種互動式計算方式在研究人員中非常流行。

這就是 Python 互動模式的樣子:

Good ol’ Python REPL

Dart 不支援像 Python 那樣在全域範圍內評估語句,因此沒有明顯的方法可以正確地執行此操作。在 Dart 中,語句必須在函式內,而 main 函式是程式的進入點。就像在 C++ 或 C# 中一樣。就個人而言,我更喜歡這種方式,因為它更容易理解程式運行時發生的事情。但是… 我仍然非常想要一個互動模式。它可以讓我玩轉想法,更快地嘗試事物。那麼讓我們看看是否可以建立一個 REPL 作為概念驗證!

如何在 Dart 中建立 REPL?

Dart 非常適合製作原型。所以讓我們這樣做,不要被語言設計問題所困擾 :)

現在沒有像 JavaScript 或 Python 中那樣的 eval 函式,而且我也不想自己寫一個完整的直譯器來實現它。它不會很快,而且我也沒有很多時間。但是,當您在 Intellij 中除錯 Dart 程式碼 時,您可以在逐步執行程式碼時評估表達式。評估表達式正是我們想要做的,不是嗎?我們可以使用這個功能嗎?

Dart 的除錯功能透過其 VM 服務 公開。它是 Dart VM 提供的 JSON-RPC 服務,您可以連接到它來除錯您的應用程式。Natalie Weizenbaum 發表了一篇關於 vm_service_client 套件的文章,該套件提供了一個非常好的 API 來與 VM 服務互動:http://news.dartlang.org/2016/05/unboxing-packages-vmserviceclient.html

現在我們可以做的是:我們的 REPL 可以連接到它自己的 VM 服務來評估它從終端機讀取的表達式!這聽起來很瘋狂…

… 但它有效!我寫了一個 快速 spike,它確實有效。Dart 支援 非同步程式設計,這非常方便,因為它可以防止程式在與自己的 VM 服務通訊時阻塞自己。

Spike 是測試驅動開發中的一個概念:它們是用於找出一些技術問題的快速且粗糙的實驗。

解決了可行性問題後,很容易編寫一個合適的概念驗證。為了評估超出 1 + 1 的表達式,我們需要支援變數。現在無法輕鬆建立變數,因為變數宣告在 Dart 中不是表達式。在 Python 中,您可以透過賦值來即時宣告變數。在 Dart 中,我們可以透過重載 noSuchMethod 來即時在字典中建立元素,從而模擬動態欄位。我稱這個類別為 Scope,當我們在它的實例中 評估表達式 時,可以像全域變數一樣存取欄位。

程式碼實際上更直接:

有了這個,我們已經可以做一些事情,例如 a = 3b = a * 3

Simple expressions: DONE

我們很快就會遇到一個限制,那就是我們只能存取在宣告 Scope 類別的檔案中匯入的符號。import '...'; 無法使用 VM 服務進行評估。因此,如果我們沒有明確匯入 dart:io,就沒有 dart:io,也沒有自訂函式庫 :(

哦,等等!Dart 可以使用帶有 Isolate.spawnUri 的 URI 產生新的 isolates(獨立的工作線程)。使用者可以在命令列上指定額外的匯入,REPL 可以產生程式碼以包含這些匯入,然後使用產生的程式碼產生新的 isolate,這些程式碼具有使用者可用的匯入。

它有效 \o/

Custom imports: DONE

支援更多 Dart

現在的另一個問題是我們只能評估表達式。像 if/else 塊或 while 迴圈這樣的控制語句不是表達式。對於語句,我們可以將它們包裝在一個閉包中並執行該閉包,這是一個函式調用表達式。因此,if (a == 1) print('a is 1!!'); 將變成

1
() { if (a == 1) print('a is 1!!'); }();

我們只需要弄清楚輸入是表達式還是語句。這很困難,因為我們必須為此編寫一個 Dart 解析器。但是 Dart 是用 Dart 編寫的,而且 analyzer 套件提供了一個可以免費解析任何 Dart 程式碼的解析器!

因此,這也解決了。

Statements and expressions: DONE

更多匯入

最後一點阻止我們匯入任何函式庫的是,預設情況下,新的 Isolate 只能看到在其 pubspec.yaml 中提到的套件。我們希望支援匯入任何函式庫。Isolate.spawnUri 有一個 packageConfig 參數,允許我們指定從套件名稱到套件路徑的映射。我們可以使用另一個命令列參數來定位另一個套件,並在我們的 Isolate 中使用其套件設定。喔!

我們很快就會遇到問題,我們的 Isolate 需要存取 analyzer 套件(和其他套件),而這些套件可能不會被您想要在 REPL 會話中使用的任何套件載入。套件 package_resolver 來救援!有了它,我們可以輕鬆地操作套件設定。

有了這些,我們就實作了一個完整的工作流程:

Import any library from another package: DONE

接下來是什麼?

這是一篇很長的文章… 整個概念驗證的程式碼量為 451 行,幾乎和這篇文章一樣長。

程式碼可以在 https://github.com/BlackHC/dart_repl/tree/master/lib 上找到。如果您安裝了 Dart,您可以輕鬆地試用它:

1
2
pub global activate dart_repl
pub global run dart_repl

我真的很喜歡在我的閒暇時間建立這個概念驗證。所有部分都在幾個小時內就位。Intellij 中對 Dart 的 IDE 支援非常出色,而且現在有很多文件和文章。例如,請查看 Natalie Weizenbaum 的 Unboxing Packages 系列:http://news.dartlang.org/2016/04/unboxing-packages-async-part-3.html 等。Dart 中的低階 hack 很有趣,而且有很多很棒的函式庫可以發揮創意。code_builder 看起來非常有前途,built_collection 提供了不可變的集合。David Morgan 也一直在發表關於 Dart 中 不可變集合 的文章。

對於 dart_repl,如果可以在運行時匯入額外的函式庫而無需重新啟動,那就太好了。Dart 團隊最近在 VM 中加入了對熱重載的支援。這主要為 Flutter 中的行動應用程式開發人員提供了更好的體驗。也許,這也可以用於臨時匯入以及在 REPL 中定義函式和類別。

總體而言,Dart 對於研究和研究人員來說可能非常有用,我絕對希望看到 Jupyter 支援 Dart。只需要實作其核心介面,就可以使這樣的 Dart REPL 與之相容… :) 那應該很容易,對吧?


Dart REPL 概念驗證 最初發佈在 Medium 的 dartlang 上,人們在那裡透過醒目顯示和回應這個故事來繼續討論。

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

基於 LLVM 的 Dart 編譯實驗

這篇文章講述了一個使用 LLVM 編譯器框架編譯 Dart 語言的實驗。乍看之下,這似乎沒有什麼意義,因為:

Dart 已經擁有一個出色的 虛擬機器,它使用 即時編譯 (JIT) 來獲得優異的效能。由於 Dart 是動態型別的(更準確地說,它是 可選型別的),JIT 編譯器是一個很自然的選擇——它可以使用運行時可用的型別來執行靜態編譯器無法進行的優化。

Dart-on-LLVM 看起來像是一項徒勞無功的工作的另一個原因是,儘管名稱如此,LLVM 並不是一個虛擬機器,而且直到最近它才適用於帶有垃圾回收的語言。所謂適用,我們指的是:

  • 移動、精確(無洩漏)的垃圾回收
  • 高度優化

這是因為,一旦優化器修改了您的程式碼,您就再也無法找到堆疊上的可垃圾回收指標。一種常見的策略是將所有指標移動到特殊的記憶體區域,但这會降低現代編譯器中的許多優化策略,現代編譯器依賴於區域變數的暫存器分配來發揮其魔力。您可以擁有良好的垃圾回收或完全的效能,但不能兩者兼得。

然而,LLVM 領域正在颳起新的風。最近,LLVM 以實驗性的 Statepoint 功能的形式增加了一些垃圾回收支援。這已被各種勇敢的團隊使用,包括 LLV8 實驗背後的人員和 Azul,他們正在將其用於 JVM 的新型頂級編譯器。

構建一個基於 LLVM 的真正虛擬機器似乎已從「不可能的任務」變成了僅僅是「困難的任務」。同時,強模式 使 Dart 更具靜態型別,並且減少了動態性。此外,我們 Google 正在為 iOS 構建 Flutter,而 iOS 禁止 JIT 編譯。這兩個發展都使 Dart 更符合 LLVM 專案的目標和權衡。

為什麼選擇 LLVM?

LLVM 是一個現代的、維護良好的開源編譯器框架,它為我們提供了許多優化和平台,而且是「免費的」。例如,有一個完整的 內聯 pass,可以將任何函數內聯到任何其他函數中,並包含關於何時這樣做的 啟發式方法

它看起來也是一個開放、友好的社群,歡迎各種貢獻。

實驗目標

  • 上下文是在提前編譯場景中的強模式 Dart
  • 評估使用 Statepoint 支援精確、移動垃圾回收的可行性
  • 評估效能

方法

我們(Erik Corry 和 Dmitry Olshansky)的實驗基於已停止的「Dartino」運行時。這是一個針對小型設備進行了優化的實驗性 Dart 運行時。與使用 DartVM 作為基礎相比,它對我們有一些優勢:

  • 已經有一個由 Martin Kustermann 構建的 Dartino 的實驗性 LLVM 後端。它沒有垃圾回收支援,因此在記憶體不足時會崩潰。
  • Dartino 利用了 Dart2JS 的許多機制,因此它不需要完整的解析器、前端等。我們用作輸入的 Dartino 位元組碼已經降低了许多困難的 Dart 功能。例如,閉包是物件,可選參數已變成了不同版本的函數。
  • 我們都已經熟悉 Dartino。
  • Dartino 附帶了一個相對完整的運行時,並且能夠運行大型應用程式,例如託管 Dart2JS。它沒有很多 Unix IO 支援,而且執行緒模型也不同,因此它不是一個直接替代品。

Dartino 中的垃圾回收

現有的 Dartino LLVM 實驗是在一段時間之前從 Dartino 分叉出來的,當時的垃圾回收非常簡單(半空間 Cheney 收集器,沒有分代,長時間暫停,2 倍記憶體佔用開銷)。我們從 Dartino 主分支中挑選了一些變更,以獲得更傳統的帶有寫入屏障的 2 代垃圾回收。沒有讀取屏障,收集是停止所有執行緒的,沒有併發垃圾回收(雖然 LLVM Statepoints 似乎確實支援這些功能,而且它們幾乎肯定被 Azul 在其閉源虛擬機器中使用)。

我們沒有從較新的 Dartino 版本中挑選壓縮舊分代的支援。

架構

上面的流程顯示了從 Dart 原始碼到機器碼的路徑。在實際的實作中,第一部分將被基於「kernel」格式的內容替換(預先解析的 Dart 原始碼前端)。

翻譯成 LLVM 和高階優化

llvm-codegen 連結到我們自己的 LLVM 副本,並執行高階優化。在此階段,LLVM 維持指標在垃圾回收過程中有效的假設,但指標使用非預設的「地址空間」標記,這禁止 LLVM 以在存在移動垃圾回收的情況下不正確的方式推理其位元模式。各種自訂的 LLVM 內建函數用於標記可能發生垃圾回收的點。

由於標記指標,LLVM 位元組碼非常醜陋,有很多轉換和加法。因此,本文檔包含「LLVM 偽程式碼」,而不是真正的 .ll 檔案。如果您習慣於實際的 .ll 檔案,這看起來就像「寶貝的第一個 .ll 咿呀學語」,抱歉!以下程式碼表示 mem2reg 之後的動態分派,該 pass 將區域變數從堆疊提升到 SSA 暫存器中:

在優化器運行後,上面的相當費力的查找已從迴圈中移除,只剩下調用指令。這是可能的,因為類別指標在 Dart 中是不可變的,並且我們已將各種中繼資料附加到載入指令(未顯示),包括 invariant.loadnever.faults(後者是我們 修補的 LLVM 版本 的新增功能)。

降低

一旦高階優化運行完成,我們將大多數內建函數降低為普通的 LLVM 指令。例如,寫入屏障簡化為一系列儲存(Dartino 使用卡片標記方案,這很大程度上歸功於 Urs博士論文 第 6.2.3 節)。降低後,每個區域變數指標都在每個可能的垃圾回收點(基本上是每個調用)被一個不透明的內建函數重寫。這會抑制許多優化(這就是為什麼我們必須在降低之前進行優化 pass),但有兩個目的:

  • 內建函數稍後將用於生成堆疊映射,詳細說明堆疊上可垃圾回收指標的位置。
  • SSA 值被分解為垃圾回收前和垃圾回收後的值,這使得垃圾回收對優化器可見,並防止無效的程式碼生成。

調用現在看起來更像這樣(分派已從迴圈中提升出來,因此 %code 包含程式碼指標——迴圈未顯示)

轉換相當笨拙,在轉換後的調用中建立了一個特殊標記,並將其用作 gc.resultgc.relocate 調用中的參數。可垃圾回收指標仍然被特殊標記(使用非零地址空間,在上面的偽 LLVM 中未顯示),這在下一階段抑制了一些優化。

程式碼生成

最後一步是程式碼生成,由 LLVM 程式 llc 執行。這一步可以使用完全未修補的 ToT LLVM 透過命令 llc -O3 完成。目前唯一支援實驗性垃圾回收內建函數的後端是 x64,但我們沒有看到任何將 ARM 支援加入和上游化的根本性障礙。動態分派調用站點現在看起來像:

這使用 x64 的標準(主要是基於暫存器)調用約定。在每次調用之前,一堆暫存器會溢出到堆疊中,如果需要,垃圾回收可以在堆疊中移動它們。不支援被調用者儲存的可垃圾回收值(V8 和 DartVM 也不支援)。

效能

Dartino 位元組碼在一個非常動態的型別環境中針對簡潔性和緊湊性進行了優化。在此分析中,我們嘗試展望一個使用強模式並且在編譯時知道型別的場景。在這種情況下,方法的分派和對物件上成員變數的存取將更簡單、更快。為了更接近這種情況,我們在生成 LLVM 程式碼時使用了一些全程式分析。

最重要的結果是,如果只有少數類別具有方法 foo(),那麼我們會檢查這些類別並直接調用 foo() 方法。與某些類似虛擬函數表的分派機制不同,這讓 LLVM 可以在有意義的地方內聯方法。這是一個巨大的勝利,尤其是對於 getter 和 setter,這是 Dart 的一個很棒的功能。

編譯器仍然必須處理許多動態語言問題,它大多數都能正確處理(請參閱下面的測試狀態部分)。特別是,整數可能會溢出並隨時變成真正的堆分配數字物件。再加上運算子的重載,這使得即使是簡單的 for 迴圈也相當複雜。更多的靜態分析可能會改善這一點。

與真正的 DartVM 的一個區別是,我們不會檢查堆疊溢出,也不會在迴圈返回邊緣檢查執行緒中斷。根據 V8 的經驗,我們估計修復這個問題可能會損失大約 10% 的效能。

我們與常規的 JIT DartVM 以及已為 Flutter 加入到 DartVM 中的新的提前編譯支援進行了比較。基準測試 來自 Dartino

運行一個像 Hello World 這樣短暫的程式主要顯示啟動所需的時間。基於 JIT 的系統花費時間編譯程式碼,而這裡的兩個非 LLVM 解決方案都在啟動時反序列化資料堆。

效能結論

我們的效能與 Flutter 現有的提前編譯技術相當(這是一個移動的目標——這些測量是在 2016 年 11 月下旬在一台強大的 64 位 Linux 工作站上進行的)。JIT 仍然遙遙領先。我們正在運行的 Dartino 分支的垃圾回收效能還沒有達到標準。

我們還測量了啟動時間。Dartino-LLVM 為類別、常數和分派表生成靜態資料。這些資料由高度優化的 ld.linux 運行時連結器載入,它們的載入速度比目前的 Dart AOT 資料堆快照更快,從而為啟動提供了非常好的效能。對於啟動測試,CPU 調控器設定為「效能」。

關於相容性的說明

在這項研究中,我們並沒有特別關注獲得 100% 的 Dart 相容性。證明「困難的事情」是可能的,例如垃圾回收和異常處理,就足夠了。在某些情況下,我們採用了一種捷徑,表明真正的解決方案是可能的,而無需浪費時間實際實作真正的解決方案。以下是一些我們妥協的地方:

  • 像 Dartino 一樣,我們沒有無限精度的整數。但是,我們確實會檢查所有整數運算是否溢出,並動態切換到裝箱數字表示形式(但是,裝箱表示形式只有 64 位,會換行)。
  • 在 no-such-method(本質上是一個失敗的型別檢查)上,我們沒有遵循完整的 Dart 語義,這包括調用 no-such-method 方法並檢查是否存在與缺少的方法同名的 getter,並返回一個帶有「call」方法的物件。但是,我們確實會在安全點(可以進行分配的點)拋出異常。
  • 我們不會在調用時檢查堆疊溢出,也不會在迴圈返回邊緣檢查中斷。LLVM 確實對此提供了實驗性支援。我們比較的解決方案確實支援這一點。V8 的經驗表明,修復這個問題可能會導致大約 10% 的效能下降。
  • 我們的前端編譯器是一個修改過的 Dart2JS。由於 Dartino 已停止,它沒有跟上語言的最新變化,因此有一些測試我們無法運行。
  • Dart 異常處理已完全實作,除了與 no-such-method 相關的異常。為此,我們使用了 LLVM 內建的異常處理支援,這看起來足以勝任這項任務,並且與 Dart 的異常模型非常吻合(這與 LLVM 設計的 C++ 並沒有太大的不同)。

總之,我們通過了 Dartino 可以通過的近 90% 的測試。在我們失敗的測試中,最大的原因是編譯器前端的問題和處理 no-such-method 事件的問題。

在大約 11.6% 的失敗測試中,以下是它們失敗原因的細分:

結論

實驗性的 LLVM 垃圾回收支援似乎在 x64 上完全正常工作。

原型的效能與我們更成熟的基於 DartVM 的提前編譯解決方案相當。

對於效能分析,我們沒有使用 Dart 強模式,預計這將產生發揮 LLVM 優勢的優化機會。但是,我們正在使用一些封閉世界假設,我們認為這是現實的。

我們能夠僅使用未修補的 LLVM ToT 版本將最後階段從 LLVM 位元組碼編譯為機器碼(在上面的流程圖中以藍色標記)。我們觀察到,在此階段執行的優化 (-O3) 並沒有導致任何錯誤編譯或垃圾回收問題。

未來

關於如何以及是否將這種方法用於 Dart 或 Flutter 尚未做出決定,但以下是一些關於可以探索的有趣方向的隨機想法。

  • 擁有一種自定義的語言,而不是帶有控制代碼的 C++,來編寫運行時例程。後端將是帶有 Statepoint 的 LLVM。(目前的版本中有一個小的 Forth 實驗,但需要更強大的東西才能編寫非常簡單的原生例程)。
  • 包裝 64 位整數會產生什麼影響?
  • 我們如何使用全程式知識來生成程式碼,同時仍然允許並行編譯大型專案?

參考

LLVM 垃圾回收支援 http://llvm.org/docs/Statepoints.html
Dartino-LLVM 儲存庫 https://github.com/dartino/sdk/tree/llvm
修改後的 LLVM 儲存庫 https://github.com/ErikCorryGoogle/llvm
Urs Hölzle 博士論文: http://hoelzle.org/publications/urs-thesis.pdf
LLV8: https://github.com/ispras/llv8


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