0%

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

使用 Material Icons 和 Flutter 打造可存取的表達力

Material Design 和 Flutter 都能幫助開發人員打造出靈活、可存取且富於表現力的體驗,同時提供出色的效能和效率。現在,我們很高興為您提供更多方法來使用改進的 Material Icons 支援在 Flutter 中建立獨特的 UI。

Material Icons 是為常見動作和項目精心設計的圖示,包括從用於導航的簡單箭頭和指示器,到代表可存取性、錯誤回報,甚至洗手等概念的圖示。

Flutter 現在支援所有四種圖示樣式——填滿、描邊、銳利和圓形——幫助您讓您的應用程式或網站對使用者來說熟悉且易於存取,同時讓您以獨特的方式傳達您的品牌。而且,透過對樹狀搖動的支援,Flutter 會優化您的應用程式建構,只包含您正在使用的圖示,從而使載入時間和記憶體使用更加有效率。

以下是一些展示您在 Material 目錄中找到的選項範圍的圖示。您可以在 Material.io 上瀏覽完整的圖示集並下載位圖或向量版本以供在您喜愛的設計工具中使用,或在 Icons API 頁面上查看 Flutter 版本。

Sample of 4 icons: shopping_cart, chat, masks, wash shown in the 4 supported styles: filled, outlined, rounded, sharp.
Sample of 4 icons: shopping_cart, chat, masks, wash. Displayed in the 4 supported styles: filled, outlined, rounded, sharp.

入門

若要開始使用 Icon 類別,請確保在您專案的 pubspec.yaml 檔案中設定 uses-material-design: true。這會告訴 Flutter 將圖示資產包含在您的應用程式中。

Flutter 中的圖示透過圖示字體以向量形式提供,因此您可以無限調整大小和顏色,而無需擔心影像品質下降。在 GitHub 上查看用於生成上述圖示集的程式碼。

不要跳過語義標籤!

為了讓使用螢幕閱讀器等輔助科技的使用者能夠有效地導航您的應用程式或網站,提供有意義且上下文適當的語義標籤非常重要。

預設情況下,螢幕閱讀器會朗讀螢幕上顯示的任何文字。為了將圖示等視覺元素準確地轉換為基於文字的 UI,您需要仔細標記這些元素。

例如,當圖示與動作元素(如按鈕)結合使用時,您應該始終將語義標籤設定為描述使用者點擊該圖示時會發生什麼的字串。

Material.io 提供指南 來指示透過動作來表示 UI 元素。常見的錯誤是將語義標籤預設為圖示的名稱,而不是選擇該圖示時執行的動作。

an example of what to do: a Pencil icon with the label “edit” and what not to do: a Pencil icon with the label “Pencil”

若要進一步了解建立可存取的數位體驗的重要性,請參閱 flutter.dev 上的 可存取性

只發佈您使用的內容!

當您為發佈建構應用程式時,Flutter 編譯器會執行「樹狀搖動」,移除未使用的程式碼和資產(包括圖示),以優化應用程式的佔用空間,並幫助最大限度地減少下載和載入時間。可以使用您想要的任何圖示!在編譯期間,會生成一個自訂圖示字體,其中只包含專案中使用的圖示子集。這可以大幅減少二進制檔案的大小——所有人都能受益!

除了樹狀搖動之外,這裡還有一篇關於 縮減 Flutter 應用程式大小的最佳實務 的很棒社群文章。

向我們展示您的傑作!

全世界的開發人員都在使用 Material Icons 來確保在大型和小尺寸條件下都能保持可讀性和清晰度;這些圖示已針對在所有 Flutter 支援的平台和顯示解析度上美麗顯示而進行優化。

在 @MaterialDesign 和 @@FlutterDev 上關注我們,並展示您如何使用 Material Design 和 Flutter 打造出美麗、可存取的數位體驗!


使用 Material Icons 和 Flutter 打造可存取的表達力 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

本篇文章主要介紹在2020年 Goolge 改版後的 Play Store Console 如何上架 Android App

首先到 Play Store Console 右邊點擊新增應用程式

填寫以下應用程式詳細資訊

  • 應用程式名稱:會顯示在商店的App名稱
  • 預設語言:預設App使用的語言,作為商店一開始提供的預設語言
  • 應用程式類型:分類只有遊戲與應用程式,目的是要把遊戲區分出來,日後可更改
  • 是否收費:設定App是否下載時需要費用,勾選收費後,需要至 付費應用程式 修改設定

最後同意 開發人員計畫政策美國出口法律 後,即可完成初始設定

接著進入到 資訊主頁

第一次設定時會出現 初始設定 提示,可依序點擊設定,該步驟都是必須完成的步驟,否則無法完成審查上架

應用程式存取權

設定 App 是否開放給全部使用者

或是部分功能有使用限制,需設定:

  • 名稱
  • 使用者名稱/電話號碼
  • 密碼
  • 任何操作說明

透過此可限制使用者下載應用程式

廣告

設定App中是否有廣告,若有勾選廣告則會在Play商店上顯示 含廣告內容 的標籤

內容分級

根據國際年齡分級聯盟(英語:International Age Rating Coalition,縮寫IARC)設計的簡化各國分級的內容分級問卷,降低產品評比的過程,參考資料

填寫內容分級問卷,讓使用者了解App的分類,及是否會有不宜兒童的內容

首先填寫電子郵件,問卷完成會寄送一份結果至該信箱

選擇 App 的類別,以 文藻校務通 為例,選擇 參考資訊、新聞或教育內容

根據選擇的類型,需填寫相關內容是否有未成年暴力色情訊息

完成後會顯示基本的報表,點擊右下角提交完成問卷

完成後,若日後想在修改問卷內容,需至右上角 點擊 Start new questionaire 重新提交問卷

目標對象

填寫詳細的App發布目標對象

首先點選 目標年齡層,主要是確認目標對象是不是兒童,若對象未滿13歲則需要新增隱私權政策

勾選 是否會引起兒童興趣,若上者點擊13歲以下,可勾選 宣稱適合兒童

最後點擊 儲存 完成目標對象和內容

接著就完成一半了~~

應用程式類別及詳細資料

應用程式分類,區分應用程式或遊戲的類別,在Play 商店中,也會有類別排名

接著設定商店的聯絡詳細資訊,分別為

  • 電子郵件地址 (必填)
  • 電話號碼
  • 網站

以上訊息皆會在Play商店上顯示

並勾選是否要在Play商店外行銷,讓外部網站可搜尋到你的App

完成後就只剩下最後一個步驟

商店資訊

首先會根據一開始設定的主要語言,設定 App 在 Play 商店的資訊,可根據不同語言,設定不同的商店資訊,可點擊 管理其他語言版本的翻譯內容 管理其他語言的內容

應用程式詳細資料

首先可設定

  • 應用程式名稱:App名稱,作為可供搜尋的關鍵字,上限50字
  • 簡短說明:可在App頁面首要看到簡短說明,上限80字
  • 完整說明:在點擊關於這個應用程式後顯示的完整說明

可參照 Play 商店對應位置

接著設定 應用程式圖示 會顯示在 Play 商店的圖示,限定尺寸為 512*512 的解析度,上傳後都會以橢圓裁剪顯示

主要圖片顯示於商店資訊的最頂端,可用於宣傳應用程式,大小限制 1024*500 解析度的圖片

螢幕截圖主要分為

  • 手機
  • 七吋平板電腦
  • 十吋平板電腦

基本上,對應類型的裝置截圖都適用,也可自行製作符合規定的尺寸的圖片,皆為使用 JPEG 或 24 位元 PNG 圖片,長寬比建議16:9

加入影片也會顯示於商店上

商店資訊設定完成後,若未來要修改都可直接修改,但修改後都需要等商店部署時間,通常都會為半天左右時間

上傳App至商店

發佈 的目錄下,選擇App目前要發布的方式,有分成

  • 正式版:會發布給商店中所有設置的地區
  • 公開測試:任何使用者可至 Play 商店點擊測試計畫,即可使用此版本
  • 封閉測試:由開發人員建立電子郵件清單,或是可透過連結加入測試計畫 https://play.google.com/apps/testing/{app id}
  • 內部測試:由開發人員建立電子郵件清單,或是可透過內部邀請測試連結:https://play.google.com/apps/internaltest/{test group id} 加入
  • 搶先註冊:若還沒發佈正式版時,可利用此功能,在Play商店中顯示搶先體驗的字樣,並提供測試人員特殊獎勵

不管利用哪種測試方式,接下來上傳App的方式都會相同,例如選擇 正式版 發布,並點擊右上方的 建立新版本

接著第一次上架時需點擊 同意使用 Google Play 應用程式簽署,Google 會管理你簽署所使用的金鑰,並且該金鑰只能提供給該 App 使用

若今天金鑰遺失,可請帳戶擁有者聯絡支援小組重新上傳金鑰

將利用 Android Studio 等等的 Android 編譯工具,將原生Android 的 Apk 或是 App Bundle 上傳至此頁面

每次新上傳的 版本號碼(version code) 皆需大於先前上傳的

關於金鑰使用詳細 可參考

版本詳細資訊

版本名稱會根據上傳的 ApkApp Bundle 命名

版本資訊會根據商店可提供的語言,以 XML 格式撰寫,將這次更新內容寫至 語言碼(language code)

完成後點擊儲存,並點擊檢查版本

接著會發現沒有設定提供地區

返回至上一頁的最上方,選擇 國家與地區 編輯針對正式版的發布國家/地區

若沒勾選,Play商店就不會發佈至此國家/地區

接著回到剛剛編輯的版本資訊,點擊 開始發布(正式版)

最後會跳回正式版的頁面,並顯示審查中

自 2019 年開始,Play 商店在第一次審查時,最久大約會至七天,爾後提交大約都是一下子就完成審查,並都是半天會完全部署至商店(所有使用者都可以看到更新)

本篇文章主要介紹在2020年 Apple 改版後的 App Store Connect 如何上架 iOS App

新增 App

App Store Connect 點擊 我的App

點擊 App 旁邊邊的 + 選擇 新的 App

註冊Identifier

憑證、識別碼及描述檔 註冊App的 Identifier

選擇 App IDs

選擇 App

Discription 填寫可以識別的名稱

App ID Prefix 選擇 Explicit 並填上與 Xcode 中的 Bundle Indentifier 相同的 ID

App ID Prefix 的 Bundle ID 上架後就不可修改

最後點擊 Continue 然後 Register 完成註冊

或是

懶人方法

使用 XcodeTarget 中的 Runner 選擇 Signing & Capabilities

選擇想要上架的Apple開發者帳號

並點擊 + Capability 隨意新增一個 Capability

此動作 Xcode 會自動註冊 Indentifier 至 App Store Connect

完成後再將其取消

沒取消的話審查時會納入審查範圍 要特別注意

接著填寫App基本資訊

  • 平台:選擇你的App會在哪些平台提供,若沒勾選可事後新增
  • 名稱:在Apple Store的名稱,無法跟已上架App相同,命名上需要先搶先贏,除非對方把App刪除或修改名稱,也是使用者搜尋時能找到你的App的首要途徑
  • 主要語言:首先可在商店提供的語言,若有發佈不同國家語系,可事後新增
  • 套件識別碼:選擇剛才新增的 Indentifier,選擇後就不能修改,對應到原生的 Bundle Indentifier
  • SKU:App ID 為獨一無二,可任意命名,填寫後不能修改,但不會公開顯示

App 資訊

新增 App 後,可先來 一般資訊 -> App 資訊,填寫 App 基本資訊,這邊會分兩個區塊,一個是可本地化資訊及一般資訊

可本地化資訊

  • 名稱:必填,為一開始輸入的 App 名稱
  • 副標題:必填,商店中會位於 App 名稱下方,建議使用簡短語句說明 App
  • 隱私權政策:必填,聲明你的 App 隱私權政策的網址,不限制格式

一般資訊

  • Apple ID:由 Apple 產生,作為網頁上的編號,可至 https://apps.apple.com/tw/app/id`${你的 Apple ID}`,找到你的App
  • 內容版權:聲明你的 App 是否有第三方內容,像是以校務通來說,資料來源於學校,就可以視為第三方內容
  • 年齡分級:必填,根據填寫 年齡分級問卷 的結果
  • 許可協議:必填,預設使用 《Apple 標準終端使用者許可協議》(EULA),可點擊自訂許可協議
  • 類別:必填,在商店的分類,可選擇兩個,也作為與同類型App排名的分類

定價與供應狀況

  • 價格排程:選擇你的App定價方式,最低免費到最高 32900元,使用者付費購買後90天內都可以反悔
  • 預定:第一次上架前會顯示,自訂日期在App成功發布後,會通知預定的使用者
  • 供應狀況:需先填寫供應國家與地區,若選擇停止供應,會從商店下架,直到重新供應為止
  • Mac 上的 iOS App:如果你的iOS App有使用到 Mac Catalyst (Flutter 目前不支援),會將你的iOS App發佈至 Mac App Store,若有發佈macOS App,則優先發布 macOS App
  • App發布方式:根據你的開發者帳號,若個人開發者帳號只能選擇公開在Apple Store,若是企業開發者帳號,則可以發布到私人的商店發布

App版本資訊

進入 App 版本資訊

首先設定 App預覽與截圖 可參考以下規格 每個至少三張

12.9 吋 iPad Pro 第三代 及 第二代 尺寸是可以相容的 故截圖第三代 可直接上傳到第二代

建議使用模擬器截圖 上傳完可用滑鼠拖曳改變顯示順序

螢幕尺寸說明 平台 截圖尺寸
iPhone 6.5 吋 iPhone 11 Pro Max、iPhone 11、iPhone X S Max、iPhone XR 1242 x 2688 像素(直向) 2688 x 1242 像素(橫向)
iPhone 5.5 吋 iPhone 8 Plus、iPhone 7 Plus、iPhone 6s Plus 1242 x 2208 像素(直向) 2208 x 1242 像素(橫向)
12.9 吋 iPad Pro(第三代) iPad Pro 2018、iPad Pro 2020 2048 x 2732 像素(直向) 2732 x 2048 像素(橫向)
12.9 吋 iPad Pro(第二代) iPad Pro 2017 2048 x 2732 像素(直向) 2732 x 2048 像素(橫向)

更多詳細資訊 可參考

接著填寫

  • 行銷宣傳文字:作為 App 行銷宣傳標語,建議簡短的文字,也要符合App的性質
  • 關鍵字:在商店搜尋時,透過關鍵字加強找到App,並使用逗點隔開
  • 描述:詳細介紹App的功能與特色
  • 支援URL:對應商店開發者網頁
  • 行銷URL:待補充
  • 版本:可參考 語意化版本,不限定兩碼或三碼,不能與先前定義的相同
  • 版權:宣告App版權的註解
  • 年齡分級:根據填寫 年齡分級問卷 的結果(需點擊編輯填寫)

對應在商店的位置可參考

年齡分級問卷

針對App的內容填寫是否有兒童不宜的

App審查資訊

蘋果在審查時都是採用 人工審查,審查過程都須填寫完整的資料

  • 登入資訊:若你的App有設計登入功能,需提供測試帳號給審查人員
  • 聯絡人資訊:當審查有問題時的聯絡人資訊,電話號碼需要加入國際冠碼
  • 備註:資訊則提供其他資訊供審查人員了解你的App,像是之前就被問過這幾個問題,爾後我都在新的App加入這些回答
    • Who is the target audience?
    • How do users obtain an account?
    • Is this app meant for internal distribution in your own company, in the company of one target client, or in multiple target clients’ companies?
    • In which countries will this app primarily be distributed?
    • If this app is meant for internal distribution, will the app be accessible by both internal and external partners? Or will it be exclusive to in-house employees?
      • 附件:可附上App操作影片,若你的App無法提供帳號測試,或是有些功能需要提供範例影片操作(ex.何時會使用到讀取wifi連線資訊的功能),都需要附上影片

建制版本

接著選擇要送至審查的App版本,需先透過Xcode Archive 後上傳至App Store Connect,點擊藍色按鈕

選擇對應想提交的版本,通常上傳完會需要一段時間處理,當完成處理都會email通知

屆時才會出現在這邊

接著選擇這次提交的出口合規資訊,點擊是否有使用加密功能,若點擊是的話,需要另外填寫出口法律資訊

廣告識別碼

最後是填寫是否有使用到 廣告識別碼,像是 Firebase Analytics 或是 Ad Mod 等第三方的廣告或分析工具,就需要聲明有使用到,若審查後發現錯誤,則需要重新上傳新的App版本

最後回到最上方點擊完成,並點擊審查,結著就會進入正在等待審查

通常審查時間已台灣都是晚上開始到半夜,對應到美國的白天時間,第一次審查都會比較久,大約2~3天,要耐心等待

但有時候會審查失敗,也會寄送 email 通知,可至連結查詢問題,並回覆審查人員

iOS Resolve Center : https://appstoreconnect.apple.com/apps/{apple id}/appstore/platform/ios/resolutioncenter?m=

macOS Resolve Center : https://appstoreconnect.apple.com/apps/{apple id}/appstore/platform/osx/resolutioncenter?m=

若審查成功,則會通知 Ready for Sale,完成這次版本審查

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

宣布 Flutter 架構的空安全支援

Dart 在 Flutter 中扮演著特殊的角色,為開發人員功能(如熱重載)提供動力,並透過 Dart 的靈活編譯器技術,為行動、桌面和網頁實現跨平台應用程式。我們致力於使 Dart 語言成為 Flutter 應用程式開發人員最具生產力的語言;例如,我們加入了 UI-as-code 語言結構,以優化 Dart 語法,用於編寫 Flutter Widget 樹。

在 6 月份,我們提供了 Dart 空安全的首個技術預覽。今天是另一個重要的里程碑,我們已經 期待已久:我們宣布 健全的空安全 的第二個技術預覽,包括對 Flutter 架構的支援。

空安全是一個主要的生產力功能,可以幫助您避免空引用錯誤,這種類型的錯誤通常很難發現。作為額外的優點,此功能還可以實現一系列效能改進。我們非常期待您的回饋。

為什麼要使用空安全?

Dart 是一種類型安全的語言。這表示當您獲得某種類型的變數時,編譯器可以保證它屬於該類型。但是,類型安全本身不能保證變數不是空引用。

空引用錯誤非常普遍。在 GitHub 上搜尋會發現由於 Dart 程式碼中出現意外的空引用而導致的數千個問題,以及數千個嘗試解決這些問題的提交。試著看看您是否能在下面的 Flutter 應用程式中發現可空性問題,想像 Config 和 WeatherService 是應用程式使用的後端服務:

如果 getAppName() 返回空引用,這個應用程式肯定會失敗;在這種情況下,我們將傳遞空引用給 AppBar 標題中使用的 Text Widget。

但是,還有更多微妙的情況需要考慮:getTemperatures() 也可能返回空引用。在這種情況下,for 迴圈會失敗。或者 getTemperatures() 可以像預期一樣返回一個列表,但該列表可能包含空值,在這種情況下,我們將呼叫 round() 到空引用上,應用程式將會失敗。

空安全功能透過 驗證您的程式碼 來解決這些問題,就像您在輸入時一樣:

Screenshot of the preceding code with null errors.
利用空安全,Dart 會在您的程式碼中找到潛在的空引用錯誤。

利用空安全,您可以更有信心地推論您的程式碼。部署的應用程式中不再有惱人的運行時空引用解除引用錯誤。相反,您在編寫程式碼時會出現靜態錯誤。

空安全原則

Dart 空安全支援基於以下三個核心設計原則:

  1. 預設不可空引用。除非您明確告訴 Dart 變數可以為空引用,否則它將被視為不可空引用。我們選擇這一點作為預設值,因為我們發現不可空引用是 API 中最常見的選擇。
  2. 可遞增採用。有很多 Dart 程式碼。您可以選擇在您想要的時候遞增地逐漸遷移到空安全,然後逐部分地進行遷移。可以在同一個專案中使用空安全的程式碼和非空安全的程式碼。我們還將提供工具來幫助您進行遷移。
  3. 完全健全。Dart 的空安全是 健全 的。這表示我們可以信任類型系統:如果它確定某個東西不是空引用,那麼它 永遠 不會是空引用。這可以實現編譯器最佳化. 當您將整個專案和相依項目遷移到空安全後,您將完全享受到健全性帶來的優勢 - 不僅是更少的錯誤,而且還包括更小的二進位檔案和更快的執行速度。

讓我們更詳細地回顧一下這些設計原則。

1. 預設不可空引用

核心語法非常簡單。以下是一些以不同方式宣告的不可空引用變數。請記住,不可空引用是預設值,所以這些宣告看起來與今天一樣,但它們的含義發生了變化。

Dart 會確保您永遠不會將空引用指派給上述任何變數。如果您嘗試執行 widget = null,幾千行程式碼之後,您將得到一個靜態分析錯誤和紅色的波浪線,您的程式將拒絕編譯。

可空引用變數

如果您想要您的變數可以為空引用,您可以使用 ?,比如這樣:

您可以在函數參數和返回值中使用 ? 語法,也是如此:

但是,再次強調,夢想是您很少需要使用 ?。您的絕大多數類型將是不可空引用。

使用空安全提高生產力

空安全不僅僅是關於安全。我們還希望您在使用此功能時能夠提高生產力,這表示此功能必須易於使用。例如,請看以下程式碼,它使用 if 來檢查空引用值:

請注意,Dart 工具如何檢測到當我們通過該 if 語句時,loudness 變數不可能為空引用。因此,Dart 讓我們可以呼叫 clamp() 方法,而無需跳躍障礙。這種便利性是由一種稱為流程分析的功能實現的:Dart 分析器會像執行程式一樣遍歷您的程式碼,自動找出有關程式碼的更多資訊。

以下是一個示例,它顯示了一個情況,即 Dart 可以確定變數為不可空引用,因為我們始終將不可空引用值指派給它:

如果您移除上述任何指派(例如,透過刪除 statusText = 'Update failed'; 這行程式碼),Dart 無法保證 statusText 為不可空引用:您將得到一個靜態錯誤,您的程式碼將無法編譯。您可以 在 DartPad 中嘗試一下

2. 可遞增採用

由於空安全對我們的類型系統來說是一個如此基礎性的改變,如果我們堅持強制採用,這將是極其破壞性的。我們希望讓您自行決定何時是適當的時間,因此空安全是一個選擇性功能:您將能夠使用最新的 Dart 和 Flutter 版本,而無需在您準備好之前強制您啟用空安全。您甚至可以從尚未啟用空安全的應用程式或套件中相依於已經啟用空安全的套件。

一旦您選擇採用,我們強烈建議您按照順序遷移程式碼,相依關係圖的葉節點優先遷移。例如,如果 C 相依於 B,而 B 相依於 A,則先將 A 遷移到空安全,然後是 B,最後是 C。此順序適用於 A、B 和 C 是函式庫、套件還是應用程式。

為什麼順序很重要?儘管您可以在相依項目遷移之前進行一些程式碼遷移,但如果相依項目在遷移期間更改了它們的 API,您可能會冒著需要進行第二次遷移的風險。當我們到達 beta 版本時,我們將提供工具來幫助您找出哪些相依項目已經遷移。如果您是套件作者,為了避免 API 斷裂的風險,請在發佈空安全的版本之前,等待所有相依項目都完成遷移。

當您的相依項目準備就緒時,您可以使用我們的遷移工具。該工具透過分析您所有現有的程式碼來工作。遷移工具是互動式的,因此您可以審查工具推斷出的可空性屬性。如果您不同意工具的任何結論,您可以添加可空性提示來更改推斷。添加幾個遷移提示會對遷移品質產生巨大的影响。

Screenshot of the migration tool
遷移工具有助於您以互動方式將程式碼遷移到空安全。

3. 完全健全

一旦您完全完成遷移,Dart 的空安全就是 健全 的。這表示 Dart 100% 確定在上面的示例中,返回值、列表和元素不可能為空引用。當 Dart 分析您的程式碼並確定變數為不可空引用時,該變數 始終 為不可空引用:如果您在偵錯工具中檢查正在執行的程式碼,您將看到不可空引用性在運行時被保留。相反,其他一些實作是非健全的,在許多情況下仍然需要執行運行時空引用檢查。Dart 與 Swift 共享 健全的空安全,但其他程式語言卻很少。

Dart 空安全的健全性還有另一個令人欣慰的含義:這表示您的程式可以更小更快。由於 Dart 非常確定不可空引用變數永遠不會為空引用,因此 Dart 可以進行最佳化。例如,Dart 的提前編譯 (AOT) 編譯器可以生成更小更快 Native 程式碼,因為它不需要在知道變數不是空引用時添加空引用檢查。

請注意,要獲得健全的空安全,您需要將整個專案和所有相依項目遷移到空安全。如果您的應用程式或相依項目的一部分尚未遷移,您將獲得部分空安全,它保留了大部分檢查,但沒有完全最佳化,也無法保證應用程式完全安全。

空安全路線圖

空安全何時才能準備好投入生產使用?以下是目前的時間表:

  1. Flutter 進行 技術預覽 2 嘗試:這就是今天。由於我們已成功將核心 Flutter 架構遷移到空安全,因此您可以嘗試使用空安全來學習新的語言功能併嘗試 一個小型 Flutter 樣本。如果您是套件作者,您也可以嘗試遷移,如果您有一個小型相依項目集已經被我們遷移了。您需要傳遞一個 實驗標誌,不應該在生產環境中使用它,也不應該發佈任何遷移的套件。
  2. 使用 beta 版本進行早期套件遷移:今年晚些時候,我們將完成效能調整,並擁有足夠的測試覆蓋率,讓我們有信心相信此功能按預期工作,並且向後相容性穩定。在那時,我們將發佈該功能的 beta 版本,您將不再需要傳遞實驗標誌。我們希望看到套件擁有者開始將他們的套件遷移到空安全,這將為我們提供最後一輪驗證,證明該功能已準備好發佈穩定版本。
  3. 使用 stable 版本投入生產:接下來,我們將解決 beta 版本中收到的回饋,修復任何剩餘的問題,然後發佈到 stable channel。很難為此設定一個具體的時間表,但我們認為是明年年初。一旦該功能穩定,我們希望看到空安全被廣泛採用,會有空安全的應用程式發佈到商店,以及許多空安全的套件在 stable channel 上發佈到 pub.dev。

立即嘗試

您今天就可以開始嘗試使用空安全!若要快速上手,請查看 附帶空安全的 DartPad 特殊版本

如果您想在 VS Code、Android Studio 或終端機中嘗試使用空安全,請查看 Flutter 空安全樣本應用程式。這個應用程式包含執行說明和一個小型氣象應用程式的兩個版本:一個不使用空安全,其中包含一些零散的空引用錯誤,另一個使用空安全來確保這些問題得到處理。如果您更願意嘗試使用一個新的 Flutter 應用程式,您可以運行 flutter create,然後按照 實驗說明 來啟用空安全。請注意,您將需要一個 dev channel 的 Flutter SDK(版本 1.24.0–3.0.pre 或更高版本),因為目前的穩定版本和 beta 版本的 Flutter 不支援空安全。

若要進一步了解功能設計,請閱讀我們最新的 了解空安全 文件。如果您更喜歡觀看簡短的影片,請查看幾個月前 Flutter Day 活動 中的 空安全影片

我們很高興將健全的空安全帶到 Dart。健全的空安全是 Dart 的一個特色功能,可以幫助您編寫更不易出錯的程式碼,並獲得更好的效能。我們希望您會 嘗試 在技術預覽中使用此功能,並透過我們的議題追蹤器 提供回饋。祝您編碼愉快!


Dart 健全的空安全:技術預覽 2 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

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

Flutter 網頁、Sliver 和平台特定問題:來自 2020 年第三季度的使用者調查結果

撰寫者:Flutter UXR 團隊(JaYoung LeeYouyang HouJack KimTao Dong

2020 年 8 月,Flutter 團隊發布了其第 10 個季度使用者調查。在 10 天的時間裡,全球有 7,668 名使用者回覆了調查。每位使用者平均花費 7.4 分鐘完成調查,相當於 39.4 天的開發者時間。我們非常感謝大家花時間提供意見回饋,我們也希望與大家分享這些結果。

如同往常,我們詢問了您對 Flutter 各個部分的滿意度。本季度,我們也專注於收集有關其他主題的意見回饋,例如 Flutter 網頁、Sliver(用於實現豐富捲動效果的 Widget)和除錯平台特定問題。我們將在本文中更詳細地探討每個主題。如果您有興趣,請繼續閱讀!

摘要

  • 94% 的受訪者對 Flutter 的整體滿意度很高(PSAT),58% 的受訪者非常滿意(VSAT)。雖然 PSAT 保持穩定,但 VSAT 不斷增加。
  • 在企業工作的使用者比例從 26% 增加到 31%。高級使用者的比例也在增加。
  • 在過去 3 個月使用 Flutter 網頁的使用者中,59% 的使用者對其效能感到滿意。71% 的使用者對 Flutter 建立在網頁上感覺自然的 UI 的能力感到滿意。
  • 想要使用 Sliver 實作豐富捲動效果的使用者中,有 79% 的使用者嘗試過使用它們。最大的問題(36%)是難以找到符合他們需求的 Widget。
  • 71% 的使用者有除錯平台特定問題的經驗。常見的問題包括工具(32%)、視覺差異(28%)和相依性管理問題(28%)。

滿意度和使用者群的變化

本季度,對 Flutter 非常滿意 的使用者比例創下新高,達到 58%。總體而言,Flutter 使用者對產品仍然感到滿意(94%),即使社群以指數級增長。以下圖表顯示了 Flutter 滿意度水平隨時間的推移。

58% 的使用者在回答「總體而言,您對 Flutter 的滿意度如何?」的問題時選擇了「非常滿意」。

我們的使用者群有一些顯著的變化。首先,在企業工作的使用者比例顯著增加,從第一季和第二季的 26% 增加到 31%,而在此期間,在創業公司工作的使用者比例穩定在 35% 左右。

雖然大多數 Flutter 使用者都在創業公司工作,但企業開發者比例從 26% 顯著增加到 31%。

另一個顯著的變化是對 Flutter 經驗水平的感知。如以下圖表所示,新手使用者的比例隨時間推移而下降,而高級使用者的比例隨時間推移而增加。這意味著我們社群中有更多有經驗的使用者可以幫助分享他們的知識給新手使用者。如果您有興趣獲得或分享知識,您可以與其他 Flutter 開發者在線交流。請訪問 flutter.dev 上的 社群標籤

高級使用者的比例持續增加。

Flutter 網頁

除了在 stable channel 支援行動裝置外,Flutter 也在 beta 版中支援網頁開發。在此調查中,我們詢問了早期採用者在效能、網頁體驗、工作流程和文件方面遇到的特定問題,以便我們可以優先處理重要問題。

受訪者積極使用 Flutter 建立網頁應用程式。約 33% 的受訪者表示他們已評估 Flutter 網頁以供潛在生產使用(15%)、建構了 demo(11%)或發布了生產應用程式(7%),如以下圖表第三行所示。

33% 的使用者(4,449 人中的 1,468 人)表示他們已評估 Flutter 網頁以供生產(15%)、建構了 demo(11%)或發布了生產應用程式(7%)。

更具體地說,在過去 3 個月擁有行動裝置和網頁開發經驗的使用者中,有 29.5% 的使用者更認真地使用 Flutter 網頁(供潛在生產使用)。這個百分比因開發者先前對平台的經驗而異。看起來,具有網頁開發背景的受訪者正在嘗試 Flutter 作為一種替代網頁框架(22% 的受訪者嘗試使用 Flutter 網頁以供潛在生產使用),而具有行動裝置開發背景的受訪者正在積極嘗試使用 Flutter 網頁作為通往網頁開發的途徑(16% 的受訪者嘗試使用 Flutter 網頁)。

在過去 3 個月嘗試使用 Flutter 網頁以供生產的使用者比例因他們先前對行動裝置和網頁平台的經驗而異。

Flutter 網頁團隊也收集了有關建立網頁體驗的各個問題的意見回饋。首先,我們了解到受訪者認為瀏覽器導航和路由歷史(55%)、複製/貼上選取文字(34%)、捲軸物理學(33%)和選取文字(32%)在建立網頁體驗中至關重要。受訪者也要求提供有關路由和調整行動版佈局以適應網頁的更好文件。

使用者將「瀏覽器導航和路由歷史」選為使用 Flutter 建立網頁體驗中最關鍵的功能。

Flutter 團隊正在根據我們收到的意見回饋積極進行改進。為了解決圍繞導航和路由的頂級使用者問題,團隊最近發布了 Navigator 2。我們還加入了對 複製/貼上可選文字 的支援,並計劃改進 可選文字 功能(特別是針對富文字)。我們將根據社群回報的問題,繼續努力改進捲軸物理學和效能。

最後,團隊收到了有關工作流程、效能和第三方 API 的意見回饋。在基本工作流程中,受訪者認為除錯最困難。頁面載入速度和捲軸是受訪者遇到的最常見的效能問題。受訪者希望 Flutter 網頁更好地支援本地儲存(例如 SQLite)、Firebase 儲存和 Google 地圖。當團隊在 Flutter 網頁方面取得進展時,這些領域將會成形。

Sliver

Sliver Widget(以「Sliver」開頭的 Widget,例如 SliverAppBar 和 SliverList)用於建立豐富的捲軸效果。雖然可以使用 ListView、GridView、PageView 或 AnimatedList 等 Widget 實現許多捲軸效果,但 Sliver Widget 有助於自訂捲軸檢視,並實現更美觀的 UI。

您可以使用 Sliver Widget 實現像這樣的豐富捲軸效果。

Flutter 團隊聽到了有關使用者對 Sliver 體驗的不同故事。有些人表示他們沒有在應用程式中使用 Sliver,而有些人表示他們需要更多 Sliver 來實現各種效果。因此,團隊決定在本季度的調查中加入一些問題,以更好地了解 Flutter 使用者如何使用 Sliver。

我們首先了解到,更多使用者使用簡單的捲軸效果(49%)設計他們的 UI,而不是使用豐富的捲軸效果(39%)。(調查中呈現了簡單捲軸效果和豐富捲軸效果的範例。)對於那些需要豐富捲軸效果的人來說,78% 的人表示他們需要 Sliver 來實現他們想要的效果。

更多使用者使用簡單的捲軸效果(49%)設計他們的 UI,而不是使用豐富的捲軸效果(39%)。

在那些需要 Sliver 的使用者中,20% 的使用者表示他們沒有嘗試過使用 Sliver。對我們來說更有趣的是,35% 的使用者表示他們嘗試過使用 Sliver 但遇到了問題。當我們詢問他們遇到的最大問題時,如以下圖表所示,發現問題最大(36%),其次是學習問題(30%),然後是可用性問題(19%)。

發現問題是 Sliver 使用者遇到的最大問題,其次是學習問題和可用性問題。

因為我們不希望您因這些問題而降低 UI 品質,所以我們計劃更新 flutter.dev,以便更容易找到 Sliver Widget 和有關 Sliver 的相關資訊,並且更容易學習。如果您正在尋找 Flutter 框架中沒有的新 Sliver,請考慮使用社群套件,例如 sliver_toolssticky_headers。Flutter 社群也歡迎您為此領域做出貢獻。

同時,如果您是 Sliver 的新手並且想了解更多,請參閱以下資源:

除錯平台特定問題

先前的調查 中,我們發現 除錯平台特定問題跨平台測試應用程式 是 Flutter 開發者最困難的任務。雖然我們看到了一些與平台特定問題相關的錯誤,但我們並不知道這些問題的相對優先順序,以及它們是否被捆綁在主題中或分散開來。為了找到除錯和測試難以執行的原因,我們加入了一些問題以詢問使用者遇到了什麼特定問題。

首先,我們詢問使用者除錯了哪些平台特定問題。我們發現最 常見 的平台特定問題是工具問題(32%)、不同平台的視覺差異(28%)、相依性管理問題(28%)、不同平台的行為差異(27%)、不同平台的 Plugin 行為差異(26%)和缺少原生功能(25%)。

最常見的平台特定問題是工具問題、視覺差異和相依性管理問題。

受訪者也評估了他們除錯的每個問題的重要性

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

為什麼選擇 Cloud Firestore?

FlutterFire 技術堆疊,由 Flutter 和 Firebase(特別是 Cloud Firestore)組成,在您構建和發佈應用程式時,為您解鎖了前所未有的開發速度。在本文中,您將探索這兩種技術之間的強大整合,重點關注測試和使用乾淨的架構模式。但是,您將一步一步地構建自己的方法,而不是直接跳到最終實作,這樣每個步驟背後的理由就清晰明瞭。

您將構建什麼?

為了展示將 Cloud Firestore 作為您應用程式後端的乾淨方法,您將構建經典 Flutter 計數器應用程式的修改版本。唯一的區別是,每次點擊的時間戳都儲存在 Cloud Firestore 中,並且顯示的計數是從儲存的時間戳的數量中推導出來的。您將使用 Provider 和 ChangeNotifier 保持依賴項和狀態管理程式碼乾淨,並且您將更新生成的測試以保持程式碼的 正確性

開始之前

本文假設您已 觀看並按照此教學中的步驟操作 將您的應用程式與 Firebase 整合。簡要說明一下:

  1. 建立一個新的 Flutter 專案,並命名為 firebasecounter。
  2. Firebase 主控台 中建立一個 Firebase 應用程式。
  3. 將您的應用程式連結到 iOS 和/或 Android,具體取決於您的開發環境和目標受眾。
注意:如果您將您的應用程式設定為在 Android 客戶端上運行,請務必 [建立一個 `debug.keystore` 檔案](https://gist.github.com/henriquemenezes/70feb8fff20a19a65346e48786bedb8f),然後再生成您的 SHA1 憑證。

在您在 Firebase 中生成 iOS 或 Android 應用程式之後,您就可以繼續進行。影片的其餘部分包含您在實際專案中可能需要的精彩內容,但對於本教學來說不是必需的。

如果您遇到問題

如果本教學中的任何步驟對您不起作用,請諮詢 這個公開的儲存庫,它將更改分解為不同的提交。在整個教學中,您將在適當的位置找到指向每個提交的連結。請隨時使用它來驗證您是否已按照預期的方式進行!

建立一個簡單的狀態管理員

要開始將您的應用程式與 Cloud Firestore 整合的過程,您必須首先重構生成的程式碼,以便初始 StatefulWidget 與單獨的類別進行通訊,而不是與自身的屬性進行通訊。這讓您最終可以指示該單獨的類別使用 Cloud Firestore。

在您的專案的自動生成的 main.dart 檔案旁邊,建立一個名為 counter_manager.dart 的新檔案,並將以下程式碼複製到其中:

1
2
3
4
5
6
7
8
9
10
11
12
class CounterManager {
/// 建立一個私有整數來儲存計數。將其設為私有,
/// 因此 Widget 無法直接修改它,而是必須
/// 使用官方方法。
int _count = 0;

/// 公開可存取的狀態參考。
int get count => _count;

/// 公開可存取的狀態修改器。
void increment() => _count++;
}

在程式碼就位後,將以下行新增到 firebasecounter/lib/main.dart 的頂部:

1
import 'package:firebasecounter/counter_manager.dart';

然後,將 _MyHomePageState 的程式碼更改為以下內容:

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
class _MyHomePageState extends State<MyHomePage> {
final manager = CounterManager();

void _incrementCounter() {
setState(() => manager.increment());
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text(
'${manager.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

儲存此程式碼更改後,您的應用程式可能會顯示崩潰並顯示一個紅色的錯誤螢幕。這是因為您引入了一個新的變數 manager,它初始化的機會已經過了。當您更改狀態的 初始化方式 時,這在 Flutter 中很常見,並且可以使用熱重新啟動輕鬆解決。

熱重新啟動後,您應該回到開始的地方:計數為 0,並且可以根據需要點擊浮動動作按鈕。

現在是運行 Flutter 在任何新的專案中提供的單一測試的好時機。您可以在 test/widget_test.dart 中找到它的定義,並透過運行以下命令執行它:

1
$ flutter test

假設測試通過,您就可以繼續進行!

注意:如果您在此部分遇到問題,請將您的更改與教學儲存庫中的 [這個提交](https://github.com/craiglabenz/flutter-firestore-counter/commit/483dd3b3833bf710b04db4a3ba347b1d1ecbe5de) 進行比較。

持續時間戳

初始應用程式描述中提到了持續儲存每次點擊的時間戳。到目前為止,您尚未添加任何基礎架構來滿足第二個要求,因此請建立另一個名為 app_state.dart 的新檔案,並添加以下類別:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// 應用程式狀態的完整容器。
/// 此類別的實例應該能夠告知任何時候
/// 渲染的內容。
class AppState {
/// 完整的點擊歷史記錄。用於非常重要的審計目的。
/// 點擊的計數變為此列表的 `length` 屬性。
final List<DateTime> clicks;

/// 預設生成式建構函式。對常數友好,以實現最佳
/// 效能。
const AppState([List<DateTime> clicks])
: clicks = clicks ?? const <DateTime>[];

/// 方便的輔助函式。
int get count => clicks.length;

/// 複製方法,它返回一個新的 AppState 實例,而不是
/// 修改現有的副本。
AppState copyWith(DateTime latestClick) => AppState([
latestClick,
...clicks,
]);
}

從現在開始,AppState 類別的工作是表示應該渲染的內容的狀態。該類別不包含任何可以修改自身的方法,只有一個單獨的 copyWith 方法,其他類別將使用它。

牢記測試,您可以開始更改 CounterManager 概念。從長遠來看,使用單一類別將行不通,因為應用程式最終會與 Cloud Firestore 進行互動。但是,您不想在每次運行測試時都建立真實的記錄。為此,您需要一個抽象介面來定義應用程式應該如何表現。

再次打開 counter_manager.dart,並在檔案的頂部添加以下程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'package:firebasecounter/app_state.dart';

/// 定義操作
/// 應用程式狀態所需的函式的介面。
///
/// 定義為抽象類別,以便測試可以在不
/// 與 Firebase 通訊的版本上運行。
abstract class ICounterManager {
/// 任何 `CounterManager` 都必須具有狀態的實例
/// 物件。
AppState state;

/// 處理必須儲存新的點擊的事件。不需要
/// 任何參數,因為它只會導致時間戳
/// 持續存在。
void increment();
}

下一步是更新 CounterManager 以顯式地從 ICounterManager 繼承。將其定義更新為以下內容:

1
2
3
4
5
class CounterManager implements ICounterManager {
AppState state = AppState();

void increment() => state = state.copyWith(DateTime.now());
}

在這個階段,我們的輔助程式碼看起來已經很好了,但是 main.dart 落後了。main.dart 中沒有 ICounterManager 的參考,而事實上,它是它應該知道的 唯一Manager 類別。在 main.dart 中,更新並應用以下更改:

  1. 將遺漏的匯入新增到 main.dart 的頂部:
1
import 'package:firebasecounter/app_state.dart';
  1. _MyHomePageState 更新為以下內容:
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
class _MyHomePageState extends State<MyHomePage> {
final ICounterManager manager;
_MyHomePageState({@required this.manager});

void _incrementCounter() => setState(() => manager.increment());

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text(
// 參考 `widget.manager` 而不是
// `manager` 直接
'${manager.state.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
// 參考 `widget.manager` 而不是 `manager` 直接
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

此更改應該會移除 IDE 中 _MyHomePageState 的任何紅色波浪線,但是現在 MyHomePage 會抱怨,因為它的 createState() 方法沒有向 _MyHomePageState 提供所有必需的參數。您可以讓 MyHomePage 要求此變數,並將物件傳遞到它的基於狀態的類別,但是這可能會導致很長的 Widget 鏈,這些 Widget 會要求並傳遞它們實際上不在乎的物件,僅僅因為某些後代 Widget 要求它,而某些祖先 Widget 提供它。顯然,這需要一個更好的策略。

輸入: Provider

使用 Provider 存取應用程式狀態

Provider 是一个函式庫,它简化了 Flutter 的 InheritedWidget 模式的使用。Provider 允許頂級 Widget 在您的 Widget 樹中被所有後代 Widget 直接存取。這可能感覺像一個全域變數,但替代方案是將您的資料模型透過每個中間 Widget 傳遞下去,其中許多 Widget 本身並不感興趣。這種「變數 桶式傳遞」反模式會模糊您的應用程式中的關注點分離,並且會使重構佈局變得不必要地繁瑣。InheritedWidget 和 Provider 透過讓 Widget 樹中的任何位置的 Widget 都可以直接獲取所需的資料模型來避免這些問題。

要將 Provider 添加到您的應用程式,請打開 pubspec.yaml,並在 dependencies 區段中添加它:

1
2
3
4
5
dependencies:
flutter:
sdk: flutter
# Add this
provider: ^4.3.2+2

在將該行添加到您的 pubspec.yaml 檔案後,運行以下命令將 Provider 下載到您的機器上:

1
$ flutter pub get

main.dart 旁邊,建立一個名為 dependencies.dart 的新檔案,並將以下程式碼複製到其中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:firebasecounter/counter_manager.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class DependenciesProvider extends StatelessWidget {
final Widget child;
DependenciesProvider({@required this.child});

@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<ICounterManager>(create: (context) => CounterManager()),
],
child: child,
);
}
}

關於 DependenciesProvider 的一些注意事項:

  1. 它使用 MultiProvider,儘管它的列表中只有一個條目。從技術上講,這可以摺疊為一個單獨的 Provider Widget,但是實際的應用程式可能會包含許多這樣的服務,因此最好從一開始就使用 MultiProvider
  2. 它需要一個子 Widget,它遵循 Flutter 中 Widget 組合的慣例,允許我們將此輔助程式碼插入 Widget 樹的頂部,從而使 ICounterManager 實例可用於整個應用程式。

接下來,讓新的 DependenciesProvider 可用於整個應用程式。一種簡單的方法是使用它來包裝整個 MaterialApp Widget。打開 main.dart,並將 main 方法更新為如下所示:

1
2
3
4
5
void main() {
runApp(
DependenciesProvider(child: MyApp()),
);
}

您還需要在 main.dart 中匯入 dependencies.dart

1
import 'package:firebasecounter/dependencies.dart';

使用 Consumer Widget

您已經看到了 MultiProvider Widget 的作用(實際上它只是一種宣告一系列單獨的 Provider Widget 的更便捷方式)。下一步是透過使用 Consumer Widget 來存取 ICounterManager 物件。

依賴注入

如果您使用過 Cloud Firestore 撰寫過 Flutter 應用程式,那麼您可能會發現 Firestore 會讓單元測試更難撰寫。畢竟,當 Firestore 整合直接連接到您的 Widget 樹中時,您如何避免在資料庫中生成真實的記錄?

如果您有過這種體驗,那麼您就會發現將依賴項直接烘焙到 UI 程式碼中的局限性,在 Flutter 的情況下,UI 程式碼是 Widget。這就是依賴注入的強大之處:如果您的 Widget 接受輔助類別,這些輔助類別促進它們與依賴項(如 Firebase、設備的檔案系統,甚至網路請求)的互動,那麼您可以在測試期間提供模擬或偽造物件,而不是真實的類別。這讓您可以測試 Widget 是否按預期工作,而無需等待緩慢的網路請求、填滿檔案系統或產生 Firebase 計費費用。

要實現這一點,您需要重構應用程式,以便有一個清晰的點,讓測試可以注入模擬真實 Cloud Firestore 行為的偽造物件。幸運的是,Consumer Widget 非常適合此工作。

打開 main.dart,並將您的 MyApp 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
// 在頂部添加此匯入
import 'package:firebasecounter/firebase_waiter.dart';

// 將 `MyApp` 替換為此
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: FirebaseWaiter(
builder: (context) => Consumer<ICounterManager>(
builder: (context, manager, _child) => MyHomePage(
manager: manager,
title: 'Flutter Demo Home Page',
),
),
// 這是放置您的啟動頁面的好地方!
waitingChild: Scaffold(
body: const Center(child: CircularProgressIndicator()),
),
),
);
}
}

此外,在 main.dart 的頂部匯入 Provider

1
import 'package:provider/provider.dart';

MyHomePage 包裹在 Consumer Widget 中,允許您到達 Widget 樹中任意高的地方,以存取所需的資源,並將它們注入到需要它們的 Widget 中。在本教學中,這可能感覺沒有必要的工作,因為您只向上到達 MyApp() 一層,但是這在實際的生產應用程式中可能會透過數十個 Widget 延伸。

接下來,在同一個檔案中,對 MyHomePage 進行以下編輯:

注意:如果在儲存此更改後您看到紅色螢幕,請不要擔心。需要更多編輯才能完成重構!
1
2
3
4
5
6
7
8
9
class MyHomePage extends StatefulWidget {
final ICounterManager manager;
MyHomePage({@required this.manager, Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

這個簡單的建構函式更改允許程式碼接受在先前程式碼片段中傳入的變數。

最後,透過對 _MyHomePageState 進行以下編輯來完成重構:

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
class _MyHomePageState extends State<MyHomePage> {

// 不再期望接收 `ICounterManager` 物件

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text(
// 參考 `widget.manager` 而不是
// `manager` 直接
'${widget.manager.state.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
// 參考 `widget.manager` 而不是 `manager` 直接
onPressed: () => setState(() => widget.manager.increment()),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
注意:您可能需要執行熱重新啟動來修復您的應用程式。

如您所知,所有 State 物件都包含對它們在 Widget 屬性中包含的 StatefulWidget 包裹器的參考。因此,_MyHomePageState 物件可以透過將其程式碼從 manager 更改為 widget.manager 來存取這個新的 manager 屬性。

就這樣!您已經將依賴項注入到需要它們的 Widget 中,而不是硬編碼生產實作。

測試應用程式

如果您現在運行 flutter test,您將會看到測試套件不再通過。當您檢查 widget_test.dart 時,原因可能很清楚:測試函式實例化了 MyApp(),但沒有像您在真實程式碼中所做的那樣用 DependenciesProvider 包裹它,因此 MyApp 中添加的 Consumer Widget 無法在其祖先 Widget 中找到滿足的 Provider

這就是依賴注入開始發揮作用的地方。您不必在測試中模仿生產程式碼(透過將 MyApp 包裹在 DependenciesProvider 中),而是更改測試以初始化 MyHomePage。將 widget_test.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
import 'package:firebasecounter/counter_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:firebasecounter/main.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(
MaterialApp(
home: MyHomePage(
manager: CounterManager(),
title: 'Test Widget',
),
),
);

// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

透過直接使用 MyHomePage 實例(以及一個包裝的 MaterialApp 來提供有效的 BuildContext 物件),您已經為自己建立了與 Cloud Firestore 的單元測試整合!

注意:如果您在此部分遇到問題,請將您的更改與教學儲存庫中的 [這個提交](https://github.com/craiglabenz/flutter-firestore-counter/commit/bb68c1d3bb3746eca5f2dea16bd799c98ff232f1) 進行比較。

實作 Cloud Firestore

到目前為止,您已經移動了很多程式碼,並引入了幾個輔助類別,但是您還沒有更改應用程式的任何工作方式。好消息是,一切都已就位,可以開始撰寫一些了解 Cloud Firestore 的程式碼。首先,打開 pubspec.yaml,並添加以下兩行:

1
2
3
4
5
6
7
8
dependencies:
# Add this
cloud_firestore: ^0.14.1
# Add this
firebase_core: ^0.5.0
flutter:
sdk: flutter
provider: ^4.3.2+2

與往常一樣,當您對 pubspec.yaml 進行更改時(除非您的 IDE 替您完成此操作),請運行以下命令來下載和連結新的函式庫:

1
$ flutter pub get
注意:如果您尚未建立資料庫:請訪問 Firebase 主控台的專案,點擊 **Cloud Firestore** 標籤,然後點擊 **建立資料庫** 按鈕。

等待 Firebase

成功使用 Cloud Firestore 的第一步是初始化 Firebase,最關鍵的是 在任務成功之前不要嘗試使用任何 Firebase 資源。幸運的是,您可以使用一個 StatefulWidget 來包含該邏輯,而不是將該任務散佈在整個程式碼中。

firebasecounter/lib/firebase_waiter.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
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

class FirebaseWaiter extends StatefulWidget {
final Widget Function(BuildContext) builder;
final Widget waitingChild;
const FirebaseWaiter({
@required this.builder,
this.waitingChild,
Key key,
}) : super(key: key);

@override
_FirebaseWaiterState createState() => _FirebaseWaiterState();
}

class _FirebaseWaiterState extends State<FirebaseWaiter> {
Future<FirebaseApp> firebaseReady;

@override
void initState() {
super.initState();
firebaseReady = Firebase.initializeApp();
}

@override
Widget build(BuildContext context) => FutureBuilder<FirebaseApp>(
future: firebaseReady,
builder: (context, snapshot) => //<
snapshot.connectionState == ConnectionState.done
? widget.builder(context)
: widget.waitingChild,
);
}

此類別使用 Flutter 中的模式,利用特定 Widget 來完全處理應用程式中的特定依賴項或問題。要使用此 FirebaseWaiter Widget,請返回 main.dart,並對 MyApp 應用以下更改:

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
// 在頂部添加此匯入
import 'package:firebasecounter/firebase_waiter.dart';

// 將 `MyApp` 替換為此
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: FirebaseWaiter(
builder: (context) => Consumer<ICounterManager>(
builder: (context, manager, _child) => MyHomePage(
manager: manager,
title: 'Flutter Demo Home Page',
),
),
// 這是放置您的啟動頁面的好地方!
waitingChild: Scaffold(
body: const Center(child: CircularProgressIndicator()),
),
),
);
}
}

現在,應用程式可以等待 Firebase 初始化,但可以在測試期間透過簡單地不使用 FirebaseWaiter 來跳過此過程。

注意:上述更改可能會導致 Flutter 抱怨缺少 Firebase Plugin。如果出現這種情況,請完全關閉應用程式並重新開始除錯,這將允許 Flutter 安裝所有特定於平台的依賴項。

從 Cloud Firestore 獲取資料

首先,透過將以下行添加到 counter_manager.dart 的頂部來匯入 Cloud Firestore:

1
import 'package:cloud_firestore/cloud_firestore.dart';

接下來,也在 counter_manager.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
class FirestoreCounterManager implements ICounterManager {
AppState state;
final FirebaseFirestore _firestore;

FirestoreCounterManager()
: _firestore = FirebaseFirestore.instance,
state = const AppState() {
_watchCollection();
}

void _watchCollection() {
// Part 1
_firestore
.collection('clicks')
.snapshots()
// Part 2
.listen((QuerySnapshot snapshot) {
// Part 3
if (snapshot.docs.isEmpty) return;
// Part 4
final _clicks = snapshot.docs
.map<DateTime>((doc) {
final timestamp = doc.data()['timestamp'];
return (timestamp != null)
? (timestamp as Timestamp).toDate()
: null;
})
// Part 5
.where((val) => val != null)
// Part 6
.toList();
// Part 7
state = AppState(_clicks);
});
}

@override
void increment() {
_firestore.collection('clicks').add({
'timestamp': FieldValue.serverTimestamp(),
});
}
}
注意:此類別幾乎是正確的,但它會建立一個稍後會探討的錯誤。如果您現在將此程式碼添加到您的應用程式中並運行它,您將會看到行為與您想要的不同。請繼續閱讀,以了解詳細的解釋!

這裡有很多事情正在發生,因此讓我們一步一步地進行。

首先,FirestoreCounterManager 實作了 ICounterManager 介面,因此它是生產 Widget 中可用的候選者。(最終,它將由 DependenciesProvider 提供!)FirestoreCounterManager 也維護了 FirebaseFirestore 的實例,它與生產資料庫的實時連接。FirestoreCounterManager 在初始化期間也調用 _watchCollection() 來建立與您關心的特定資料的連接,這就是事情變得有趣的地方。

_watchCollection() 方法做了很多事情,值得單獨研究。

在第一部分中,_watchCollection() 調用 _firestore.collection('clicks').snapshots(). 這會返回一個流,每當集合中的資料發生更改時,就會更新。

在第二部分中,_watchCollection() 立即使用 .listen() 為該流註冊一個監聽器。傳遞給 listen() 的回調在資料發生任何更改時接收一個新的 QuerySnapshot 物件。此更新物件稱為快照,因為它反映了資料庫在某一時刻的正確狀態,但是,在任何時候都可能會被新的快照替換。

在第三部分中,回調會在集合為空時短路。

在第四部分中,回調會遍歷快照的文件,並返回一個包含混合的 null 和 DateTime 值的列表。

在第五部分中,回調會捨棄任何 null 值。這些是因將要修復的錯誤而產生的,但是這種防禦性程式設計在處理來自 Cloud Firestore 的資料時總是一個好主意。

在第六部分中,回調會解決 map() 返回的是迭代器而不是列表的事實。對迭代器調用 .toList() 會強制它處理整個集合,這正是您想要的。

最後,在第七部分中,回調會更新狀態物件。

要使用這個新的類別,請打開 dependencies.dart,並將其內容替換為以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'package:firebasecounter/counter_manager.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class DependenciesProvider extends StatelessWidget {
final Widget child;
DependenciesProvider({@required this.child});

@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// `Provider` 已被 `ChangeNotifierProvider` 替換
ChangeNotifierProvider<ICounterManager>(
create: (context) => FirestoreCounterManager(),
),
],
child: child,
);
}
}

診斷錯誤

如果您按原樣運行此程式碼,您 幾乎 會看到所需的行為。一切都看起來正確,除了螢幕始終落後於實際點擊次數一次。這是怎麼回事?

問題出現在初始計數器實作和當前基於流的實作之間的不相容性。浮動動作按鈕的 onPressed 處理器看起來像這樣:

1
2
3
4
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => widget.manager.increment()),
...
)

該處理器調用 increment() 並立即調用 setState(),這會告訴 Flutter 重新渲染。

當同步更新儲存在設備記憶體中的狀態時,這非常有效。但是,新的基於流的實作會啟動一系列異步步驟。這意味著,按原樣,程式碼會立即調用 setState(),然後僅在一個未知的未來時間點,管理員物件才會更新其狀態屬性。簡而言之,onPressed 處理器中的 setState() 調用過早了!更糟糕的是,因為所有這些活動都發生在回調內,深深地位於 FirestoreCounterManager 中,沒有任何 Widget 知道它,因此沒有任何 Future 可以讓 Widget 等待來解決問題。

這就像管理員物件需要能夠告訴 Widget 何時重新繪製一樣。 🤔

輸入: ChangeNotifier

注意:如果您在此部分遇到問題,請將您的更改與公開儲存庫中的 [這個提交](https://github.com/craiglabenz/flutter-firestore-counter/commit/3bf17b9bfac6c907b8650e1c668fa19b1160a51d) 進行比較。這些更改包括添加 Firebase 後產生的 Xcode 和 build.gradle 的更改,但是您可能可以專注於 Dart 檔案中的更改。

使用 ChangeNotifier 重新渲染 Widget 樹

ChangeNotifier 是一個類別,它完全像它的名字所暗示的那樣做:當發生需要重新渲染的更改時,它會通知 Widget。

此過程的第一步是更新 ICounterManager 介面以擴展 ChangeNotifier。要執行此操作,請打開 firebasecounter/lib/counter_manager.dart,並對 ICounterManager 宣告進行以下更改:

1
2
3
4
// 將 `extends ChangeNotifier` 添加到您的宣告中
abstract class ICounterManager extends ChangeNotifier {
// 類別中的所有內容都相同。
}

如果您尚未匯入 flutter/material.dart,請打開 firebasecounter/lib/counter_manager.dart,並將其添加到底部:

1
import 'package:flutter/material.dart';

您現在已準備好更新 CounterManagerFirestoreCounterManager 的定義。對於 CounterManager,請將其程式碼替換為以下實作:

1
2
3
4
5
6
7
8
9
10
11
12
class CounterManager extends ChangeNotifier implements ICounterManager {
AppState state = AppState();

/// 使用最近一次點擊的時間戳複製狀態物件,
/// 並告訴流更新。
void increment() {
state = state.copyWith(DateTime.now());
// 添加這行是 `ChangeNotifier` 告訴 Widget
// 重新渲染自身的方式。
notifyListeners();
}
}

對於 FirestoreCounterManager,應用以下更改:

  1. 修改它的簽章以匹配以下內容:
1
2
3
4
class FirestoreCounterManager extends ChangeNotifier
implements ICounterManager {
...
}
  1. 將相同的 notifyListeners(); 行添加到 _watchCollection() 的末尾,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void _watchCollection() {
_firestore
.collection('clicks')
.snapshots()
.listen((QuerySnapshot snapshot) {

// 為了清晰起見,省略了 `_clicks` 的生成,但不要
// 更改該程式碼。

state = AppState(_clicks);

// 唯一必要的更改是添加這行!
notifyListeners();
});
}

您現在已經建立了讓 ICounterManager 類別告訴 Widget 何時重新渲染的必要更改的一半。管理員類別正在告訴 Widget 重新渲染,但是如果您現在運行您的應用程式,您將會看到 Widget 沒有在監聽。

要修復此問題,請打開 dependencies.dart,並將 DependenciesProvider 的實作替換為以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DependenciesProvider extends StatelessWidget {
final Widget child;
DependenciesProvider({@required this.child});

@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// `Provider` 已被 `ChangeNotifierProvider` 替換
ChangeNotifierProvider<ICounterManager>(
create: (context) => FirestoreCounterManager(),
),
],
child: child,
);
}
}

作為最後的更改,請從 _MyHomePageState 中移除 setState(),以跳過不必要的重新渲染。將其 FloatingActionButton 更新為如下所示:

1
2
3
4
5
6
floatingActionButton: FloatingActionButton(
// 移除 `setState()`!
onPressed: widget.manager.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
),

就這樣!ChangeNotifierProvider 確保 Widget 是「監聽器」,因此當 ICounterManager 類別調用 notifyListeners() 時,Widget 會收到重新渲染的訊息。

在這個階段,您應該能夠熱重新啟動您的應用程式,並看到所有內容都正常工作!

注意:如果您在此部分遇到問題,請將您的更改與公開儲存庫中的 這個提交 進行比較。

修復測試

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

網頁上的效能測試

本文於 2020 年 11 月 25 日更新,以使用 `web_benchmarks` 套件。

概觀

在開發過程中,我們經常希望在瀏覽器中測試應用程式的效能。效能測試很有用,因為它可以揭露可能導致應用程式變慢的潛在錯誤。

本文介紹了一種在 Chrome 中測試應用程式效能的方法。此方法類似於我們測試 新的 Flutter Gallery 效能的方式。

範例應用程式

我們使用一個簡單的應用程式,其中包含一個 appbar、一個浮動動作按鈕和一個無限項目列表。列表還顯示按鈕被按下次數。

應用程式有一個包含一些資訊的第二頁。

您可以在這裡複製應用程式:

測試什麼?

我們希望在以下使用情境下測試應用程式在 Chrome 中的效能:

  1. 使用者在無限列表中捲軸。
  2. 使用者在兩個頁面之間切換。
  3. 使用者點擊浮動動作按鈕。

設定框架

pubspec.yaml 中加入以下內容:

此相依性會導入 web_benchmarks,一個實現 Chrome 中效能測試的最小套件。

此套件改編自 macrobenchmarksdevicelab,這兩個套件由 Flutter 用於在 Flutter Gallery 上進行網頁效能測試。目前,這兩個套件專門用於 flutter/flutter 中的網頁效能測試,因此導入更通用的套件 web_benchmarks 更容易。

執行 flutter pub get 以導入此套件。

撰寫第一個測試

lib 目錄下加入 benchmarks 目錄,並在其中加入一個名為 runner.dart 的新 dart 檔案:

檔案的內容如下:

此測試在做什麼?

  • 當此應用程式執行時,會建立一個 ScrollRecorder 物件,它會透過自動執行手勢來驅動應用程式。在本例中,在應用程式啟動後不久,它會開始向下捲軸無限列表。
  • ScrollRecorder 類別擴展了 AppRecorder 類別,AppRecorder 類別擴展了 WidgetRecorder 類別,WidgetRecorder 類別還會在驅動應用程式時記錄效能資料。
  • runBenchmarks 是在 package:web_benchmarks/client.dart 中定義的函式,它允許使用者選擇要執行的基準測試,並在瀏覽器中顯示結果。
  • automate 方法使用 flutter_test 套件,該套件提供了在應用程式中執行手勢或尋找特定 Widget 的方法。

執行第一個測試

在專案的根目錄中,執行 flutter run -d chrome -t lib/benchmarks/runner.dart。這會指示 Flutter 使用 runner.dart 作為入口點,而不是 main.dart

到目前為止,我們只有一個基準測試,因此點擊「捲軸」以啟動它。

測試開始,列表會自動向下捲軸。

測試在幾秒鐘內結束,顯示以下螢幕:

此圖表顯示應用程式繪製每個(記錄的)畫面所需的時間。橫軸表示時間流逝;縱軸表示每個畫面所需的時間。

圖表的前 2/3 部分具有灰色背景;這些畫面被認為是「預熱畫面」,並且從統計資料中省略。預熱畫面通常會讓 JIT 編譯器有時間編譯程式碼並建立各種快取,以便測量畫面能產生反映應用程式「最終」效能的數字,而不是它最初幾秒鐘的效能。預熱階段不應始終被忽略 - 它可以提供應用程式最初幾秒鐘效能的寶貴資訊,這仍然會影響對應用程式品質的感知。

紅色畫面是「異常值」 - 它們是繪製時間比其他畫面顯著長得多的畫面。有些異常值幾乎不可察覺。例如,動畫開始或結束時的卡頓,直到一定程度才會被看見。但是,動畫 中間 的卡頓畫面非常明顯。

異常值很好地表明了應用程式卡頓的程度。透過改進您的應用程式,您可以降低異常值的數值或減少異常值的數量,這表示您的應用程式已變得更流暢。

從 Chrome 的 DevTools 收集資料

此基準測試完全在 Chrome 中執行。將以下檔案新增為 test/run_benchmarks.dart

然後,執行 dart test/run_benchmarks.dart

大約一分鐘後,您應該看到以下結果:

準確的基準測試數值可能會因機器而異。

此測試在做什麼?

  • 執行 test/run_benchmarks.dart 會為網頁構建應用程式。然後,它會啟動一個 Chrome 實例並在其中執行應用程式。
  • test/run_benchmarks.dart 會連接到 Chrome 的 DevTools 連接埠,並監聽和收集相關的效能資料。

結果的含義是什麼?

  • 渲染畫面時,會走訪 層級樹 兩次。
  • 「預先處理」是第一次走訪。它不會渲染任何內容,但會計算稍後用於渲染的值。例如:轉換矩陣、轉換的逆矩陣和剪裁。
  • 「應用程式畫面」是實際渲染 UI 的第二次走訪。
  • 「繪製畫面」是框架渲染畫面所需的時間。它包含「預先處理」和「應用程式畫面」,但它還包含花費在構建和佈局 Widget 上的時間。
  • 「總 UI 畫面」包含「繪製畫面」中的所有內容,但它還包含瀏覽器執行的某些隱藏工作,例如層級樹更新、樣式重新計算和瀏覽器端佈局(不要與 Flutter 自己進行的佈局混淆)。
  • 收集資料集(持續時間列表)時,演算法會移除異常值。
  • 首先,計算資料的平均值和標準差,並且任何比(平均值 + 1 個標準差)高的資料點都被視為異常值。
  • 非異常值(乾淨資料)的平均值和標準差用於計算資料集的平均值和雜訊,然後報告這些值。
  • 所有異常值的平均值以及「異常值平均值」和「非異常值平均值」的比率也會被報告。
  • 對於每個資料集,「異常值比率」和「雜訊」都是應用程式效能中存在多少雜訊的良好指標。如果結果過於雜亂,則可能表示效能不一致(例如,GC 暫停時的卡頓畫面)。透過降低雜訊,您可以使應用程式執行更順暢。

加入更多測試

編輯 lib/benchmarks/runner.dart 以加入兩個更多測試。

首先,修改 main 函式:

最後,加入兩個更多擴展 AppRecorder 的類別:

這些測試在做什麼?

  • 我們加入了兩個剩餘的基準測試:一個用於在頁面之間切換,另一個用於點擊浮動動作按鈕。
  • animationStops 反覆檢查動畫是否正在進行,並在所有動畫停止時停止。這會確保,例如,成功轉換到「關於」頁面。
  • 在「頁面」和「點擊」基準測試中,_completed 布林值追蹤自動手勢是否已完成。
  • 在「頁面」和「點擊」基準測試中,覆寫 shouldContinue 方法會導致 AppRecorder 所有手勢完成 停止記錄畫面。

如何執行這些測試?

若要在 Chrome 中執行這些測試(並查看動畫),請執行:

  • flutter run -d chrome -t lib/benchmarks/runner.dart --profile

若要執行這些測試並收集 DevTools 資料,請執行:

  • dart test/run_benchmarks.dart

接下來要做什麼?

在您有方法收集效能資料後,您可以隨意使用它:

  • 您可以為 CI 設定一個作業,只要有人提交 PR 就執行這些基準測試,以避免引入效能負擔大的變更。
  • 您也可以設定一個儀表板來追蹤效能基準測試的趨勢。這就是我們對 Flutter Gallery 所做的事情(請參閱 Flutter Dashboard)。


網頁上的效能測試 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

宣佈 Dart 2.10

一個新的、統一的 dart 工具,適用於所有核心任務。此外,還更新了空安全時間表和遷移原則。

作者: Michael ThomsenKevin Moore

今天,我們宣佈推出 Dart 的新版本 2.10(_二點一零_)。此版本具有一個新的、統一的 Dart 開發工具:一個滿足所有開發人員需求的單一工具,例如建立專案、分析和格式化程式碼、執行測試以及編譯應用程式。我們還更新了空安全路線圖和時間表,並討論了將現有程式碼遷移到空安全的原則。

一個新的、統一的 Dart 開發工具

Dart 構成了 Flutter SDK 的基礎:它不僅提供了支援 Flutter 應用程式的語言和執行時環境,還支援許多核心開發人員任務,例如格式化、分析和測試程式碼。然而,雖然 Flutter 有一個單一的、通用的開發人員工具(flutter 命令),但 Dart 歷來有許多較小的開發人員工具(例如,dartfmt 和 dartanalyzer)。Dart 2.10 有一個新的、統一的 dart 開發工具,它與 flutter 工具非常相似。這個新工具支援所有常見任務,例如建立新專案和套件;分析和格式化程式碼;以及執行、編譯和測試專案。要使用該工具,只需執行 dart 命令:

透過執行 dart help 了解 2.10 dart 工具。

Flutter 在 Flutter SDK 中包含了這個新的 Dart 工具。從今天的 Flutter 1.22 SDK 開始,/bin 目錄(您可能在 PATH 中有此目錄)包含 flutter 和 dart 命令。如果您同時進行 Flutter 和通用 Dart 開發,則可以從單個 Flutter SDK 中獲得兩種開發人員體驗,而無需安裝任何其他東西。

注意:如果您想要下載並安裝第二個 Dart SDK(可能是因為您需要不同的版本),請確保您想要預設使用的 dart 工具的 SDK 位於 PATH 環境變數的開頭。

在即將發佈的穩定版本中,我們計劃在此 dart 工具中加入更多功能,並逐步棄用較小的工具(dartdoc、dartfmt、dartanalyzer 等)。明年,我們預計將發佈僅包含單個 dart 工具的 Dart SDK。我們建議您現在執行 Dart 命令時切換到新工具,無論是在終端機中手動執行還是透過持續整合(_CI_)指令碼執行,如果缺少任何功能或功能未按預期工作,請向我們提供回饋

展望空安全

自從幾個月前我們推出第一個技術預覽版以來,我們對空安全產生了濃厚的興趣。我們都期待空安全作為一種避免難以發現的空錯誤的工具,此外,我們還期望看到聲音空安全帶來的效能提升。如果您渴望了解更多資訊,我們推薦您閱讀我們新的理解空安全頁面。如果您更喜歡觀看短片,請查看幾個月前 Flutter Day 活動 中的空安全影片

空安全何時可以使用?以下是目前的時間表:

  1. Flutter 使用 技術預覽版 2 進行實驗:我們已成功遷移 Flutter 的大部分內容。很快——可能在下個月內——我們預計將遷移完整的 Flutter 架構,從而準備好與 Flutter 一起啟用實驗性使用。您將能夠在 Flutter 範例中嘗試空安全,並對您的 Flutter 應用程式和套件進行試驗性遷移。您將需要傳遞一個實驗標誌,不應在生產環境中使用它,也不應發佈任何已遷移的套件。
  2. 使用 測試版 進行早期套件遷移:今年晚些時候,我們將完成效能調整,並擁有足夠的測試覆蓋率,讓我們確信該功能能夠按預期工作,並且向後相容性是可靠的。屆時,我們將發佈該功能的測試版,您將不需要傳遞實驗標誌。我們希望看到套件所有者開始將他們的套件遷移到空安全,並進行最後一輪驗證,以確保該功能已準備好發佈穩定版。
  3. 使用 穩定版 進行生產環境使用:根據測試版發佈的回饋,我們將修復任何剩餘問題,然後發佈到穩定版。很難說明具體的時間表,但我們認為是在明年年初。一旦該功能穩定下來,我們希望看到空安全得到廣泛採用,將空安全應用程式發佈到應用程式商店,並在 pub.dev 上發佈許多穩定版本的空安全套件。

遷移到空安全的原則

我們想分享我們指導空安全遷移的原則。

準備好後再採用

空安全是 Dart 類型系統的根本性變革。它改變了變數宣告的基礎,因為我們決定預設將變數設為不可為空:

1
2
無空安全                      有空安全
String s; // 一個 String 或 null。 String s; // 一個 String,不可為 null。

如果我們堅持強制採用,如此根本性的變革將會極具破壞性。我們希望讓 決定何時是合適的時機,因此空安全是一個可選功能:您將能夠使用最新的 Dart 和 Flutter 版本,而無需在準備好之前強制啟用空安全。您甚至可以依賴尚未啟用空安全的應用程式或套件中已啟用空安全的套件。

按順序逐步採用

我們強烈建議按順序遷移程式碼,首先遷移依賴圖的葉子。例如,如果 C 依賴於 B,而 B 依賴於 A,則首先將 A 遷移到空安全,然後是 B,然後是 C。此順序適用於 A、B 和 C 是程式庫、套件還是應用程式。

為什麼順序很重要?儘管您可以在依賴項遷移之前在遷移程式碼方面取得一些進展,但如果您的依賴項在其遷移期間更改其 API,則您可能會冒險進行第二次遷移。我們將提供工具來幫助您找出哪些依賴項已遷移。如果您是套件作者,為了避免破壞 API 的風險,請等到所有依賴項都已遷移後再發佈空安全版本。

使用自動化工具來降低遷移成本

當您的依賴項準備就緒並且您選擇進行遷移時,您可以使用我們的遷移工具。該工具透過分析所有現有程式碼來工作,尋找哪些宣告可以不可為空(保持不變),哪些必須可為空(需要在宣告上加上 ? 空值標記)。

遷移工具是互動式的,因此您可以檢查該工具推斷出的空值屬性。如果您不同意該工具的任何結論,您可以加入空值提示來更改推論。例如,即使需要一些重構,如果您想要使 API 不可為空,您可以告知該工具並重新執行遷移分析。加入一些遷移提示可以對遷移品質產生巨大影響。

充分利用才能獲得全部收益

一旦 所有 程式碼——以及它所依賴的套件——都已遷移,您的程式碼就可以在 聲音 空安全的情況下執行。在此之前,您的程式碼將繼續像今天一樣執行和編譯,但完全聲音的空安全可以啟用完整的執行時驗證和編譯器優化。使用聲音空安全執行測試可幫助您避免執行時空斷言的問題,使用聲音空安全編譯應用程式可確保您獲得目前和未來的優化,例如更小的編譯輸出和更快的執行速度。

後續步驟

新的 dart 開發人員工具現已在 Dart 2.10 SDK 和 Flutter 1.22 SDK 中提供。如果您已經擁有 Flutter SDK,只需執行 flutter upgrade 即可獲得帶有 dart 命令的 Dart SDK;這將為您提供嵌入 Dart 2.10 的 Flutter 1.22 SDK。我們建議您立即切換到新的 dart 工具,如果缺少任何功能或功能未按預期工作,請向我們提供回饋

我們很快就會有更多關於空安全的新聞——很可能在下個月內,我們預計 Flutter 團隊的朋友們將會有一個啟用空安全的 Flutter 架構,準備用於實驗。請密切關注 Flutter 部落格 以獲取更新。同時,您可以使用 DartPad with Null Safety 嘗試使用空安全的 Dart 程式碼,並透過閱讀我們的空值安全文件來了解更多關於功能設計的資訊。


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

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

宣布 Flutter 1.22

我們很高兴地宣布 Flutter 的最新版本,它对 iOS 14 和 Android 11 提供了广泛的支持。Flutter 1.22 在以前版本的基础上构建,让开发者能够从单一代码库构建快速、美观的跨平台用户体验。我们的季度稳定版本包含最新的功能、性能改进和错误修复,适合广泛的生产使用。

由于这是新移动操作系统版本的发布季节,因此此版本专注于确保 Android 11 和 iOS 14 与 Flutter 完美配合。对这两个操作系统的更新包括大量幕后工作,以符合最新的 SDK,并确保一切通过我们的全面测试套件。对于 iOS 14,此版本包含对新 Xcode 12、新图标的支持,以及对新 iOS 14 App Clips 功能的预览支持。对于 Android 11,更新支持新的显示切口类型,以及在弹出软键盘时的更流畅动画。

此版本在我们的 1.20 版本发布两个月后发布,因此比大多数版本都要短。即使在这段时间内,我们也关闭了 3,024 个问题,并合并了来自 197 位贡献者的 1,944 个 PR。在这 197 位贡献者中,有 114 位(58%)来自更广泛的社区,他们贡献了 271 个 PR。最大的单一贡献者是 a14n,他再次成为我们的顶级贡献者榜单,贡献了 20 个 PR,其中大多数是作为 Flutter 中支持空安全的工作的一部分完成的(更多相关内容即将推出)。

除了对新移动操作系统版本的支持外,我们还有很多新闻要分享,包括对 Android 最受欢迎功能之一的预览:状态恢复、一个新的 Material 按钮“宇宙”、与热重载配合使用的新国际化和本地化支持、一个新的 Navigator、Platform Views 的稳定版本(Google Maps 和 WebView Plugin 的基础)以及可以在代码中添加的开关,以改善具有高频率显示屏的设备上的滚动效果。我们还提供了一个新的工具来剖析应用大小,并确保您构建的 Plugin 仅支持您想要支持的平台。

针对 iOS 14

每当宣布新版本的移动操作系统时,我们都会对其进行彻底测试,寻找影响 Flutter 及其工具的不兼容性或更改。

在 iOS 14 的情况下,我们对 Flutter 做了一些更改,以确保它按开发者想要的方式工作:

  • Xcode 12 要求 iOS 9.0 或更高版本,因此我们的默认模板将默认值从 8.0 提高到 9.0。
  • iOS 14 特定的崩溃和字体渲染问题已在 Flutter 1.22 中修复。
  • 截至 Flutter 1.20.4,已修复部署到实体设备的问题。
  • 一项新政策,当应用访问其剪贴板时会显示使用通知,导致 Flutter 应用出现虚假通知,这个问题已在 Flutter 1.20.4 中修复。
  • 限制禁止在 iOS 14 设备上运行调试应用,除非作为调试过程的一部分。
  • 针对本地调试的 Flutter 应用的网络安全新政策会导致 iOS 14 显示一次性确认对话框(仅在开发期间,而不是针对已发布的 Flutter 应用)。

底线:如果您要针对 iOS 14 使用 Flutter 应用,我们强烈建议您使用 Flutter 1.22 重新构建它并将其部署到 App Store,以确保您的 iOS 14 用户获得最佳体验。

有关使用 Flutter 针对 iOS 14 的更多详细信息,包括一些 Add-to-App、深层链接和通知注意事项,请参阅 flutter.dev 上的 iOS 14 文档

希望所有这些对工具和 SDK 支持的工作可以让您专注于您关心的编码——利用新的 iOS 14 功能。

其中一项功能是对 iOS 的新 SF Symbols 字体的更新支持,这启发了我们花了一些时间对 cupertino_icon 包 进行刷新。一旦您将 cupertino_icons 依赖项更新到新的 1.0 主要版本,现有的 CupertinoIcons 使用情况将自动映射到新样式。如果您将 cupertino_icons 1.0 与 Flutter 1.22 结合使用,您还可以通过 CupertinoIcons API 访问约 900 个新图标。

您可以在 cupertino_icons 预览页面上查看完整的图标列表,以及 flutter.dev 上的迁移详细信息页面

您可以尝试在 iOS 14 上使用 Flutter 的另一项功能是 App Clips,这是一项新的 iOS 14 功能,支持快速、无需安装的应用执行,可以执行 10MB 以下的应用的轻量级版本。在 Flutter 1.22 版本中,我们对使用 Flutter 构建的 App Clip 目标进行了预览。

由 Flutter 提供支持的 App Clip 体验

有关使用 Flutter 构建 App Clips 的更多详细信息,请查看 flutter.dev 上的文档。您还可以参考这个 简单的示例项目

Android 11

此版本的 Flutter 也与本月推出的 Android 11 版本同步。Flutter 框架和引擎已更新,以支持最新版本的 Android 中引入的两个新功能。

首先,Flutter 现在支持公开 Android 切口、切出部分和瀑布显示边缘的安全内边距。

通过使用 MediaQuerySafeArea API,您可以确保将活跃的 UI 和交互式元素放置在设备显示屏的无障碍区域。此外,您还应避免在容易发生意外触碰的瀑布边缘区域放置手势检测器。

其次,动画与 Android 11 同步,因为它显示了软件键盘。

查看 FAB 的位置动画

问题 #19279 是一个长期存在的问题,即系统键盘显示/隐藏动画与 Flutter 的内边距不同步。这个问题已在 Android 11 中修复。

关于 Android 嵌入 API 的一个说明。在去年的 Flutter 1.12 版本中,我们为 Android 推出了新的 Flutter 引擎和 Flutter Plugin API 集。我们创建了这些 v2 API 以更好地支持我们在 Android 上的 Add-to-App 用户。一年后,超过 80% 的 Android Plugin 使用了新的 Android API。从 1.22 版本开始,我们将弃用旧的 v1 API。

如果您仍在使用 Android v1 API,以下内容与您相关:

  • 新创建的 Plugin 将不再针对 v1 API。
  • Flutter 工具的 -no-enable-android-embedding-v2 配置标志已删除,现在是默认行为。
  • 仍在使用 v1 API 的旧应用将在构建过程中显示一个弃用警告,该警告指向 支持新的 Android Plugin API 的文档

同时,如果您仍然具有基于 v1 Android API 的 Flutter 应用,它将继续工作。但是,您可能会开始遇到仅针对 v2 API 且无法被 v1 Android API 使用的新 Plugin。有关更多详细信息,请参阅 重大更改文档

扩展按钮“宇宙”

一个新的 Material Design 按钮“宇宙”

现有的 Flutter 按钮看起来不错,但可能 难以使用,特别是在您需要自定义主题时。此外,Material 规范已扩展到包含具有新样式的新按钮。

为了让 Flutter 与 Material 指南保持同步,我们很高兴地宣布 Flutter 1.22 中推出了全新的按钮“宇宙”。

与其尝试就地演变现有的按钮类及其主题,此 PR 引入了新的替换按钮 Widget 和主题。除了让我们摆脱演化现有类所必需的后向兼容迷宫外,新名称还使 Flutter 与 Material Design 规范 同步,该规范使用新名称来表示按钮组件。

新主题遵循 Flutter 最近为新的 Material Widget 采用的“规范化”模式。如果您想尝试一个演示,这里有一个很棒的 DartPad 演示。这不是重大更改,因为 FlatButton、OutlineButton、RaisedButton、ButtonBar、ButtonBarTheme 和 ButtonTheme 的语义不会改变。您可以根据自己的喜好混合和匹配旧按钮和新按钮。

新的国际化和本地化支持

从 Flutter 的诞生之日起,Flutter 就提供了应用程序国际化 (i18n) 和本地化 (l10n) 所需的核心功能。但是,在此版本中,我们将对最佳实践的看法融入到我们的工具中,甚至启用热重载支持,以便在您添加新的 l10n 信息时更新您的应用。

如果您希望了解有关 Flutter 对 l10n 支持的更多详细信息,包括本地化消息、带有参数的消息、日期、数字和货币,请 阅读 Flutter 国际化用户指南

此外,如果您对 i18n 和 l10n 感兴趣,那么您可能也对不适合纯 ASCII 字符串的字符(例如 Unicode 和表情符号)感兴趣。最近,Dart 团队发布了 characters 包,它帮助开发者处理 Unicode(扩展)字形群集。此包帮助解决了诸如如何将类似于“A 🇬🇧 text in English”的字符串正确缩短到前 15 个字符的问题。使用 String 类,该缩写将是“A 🇬🇧 text in”,这只有 12 个用户感知字符。另一方面,使用 characters 包会产生正确的缩写“A 🇬🇧 text in Eng”。

通过 此 PR,Flutter 使用 characters 包来正确处理这些复杂的字符。例如,当使用带有 maxLength 限制的 TextField 时,像👨‍👩‍👦这样的字符现在被正确地视为单个字符。此外,通过 此 PR,characters 包在 Flutter 所在的项目中自动可用,无需手动添加它。希望这将使处理来自所有语言环境的所有类型的字符串变得更加容易。有关 characters 包的更多详细信息,请查看这篇文章:如何正确进行 Dart 字符串操作

Google Maps 和 WebView Plugin 现已做好生产准备

在 Flutter 团队中,我们通常谨慎地将某些东西标记为“生产就绪”,直到我们自己对其进行彻底测试。在 google_maps_flutterwebview_flutter Plugin 的情况下,限制因素一直是底层的 Platform Views 实现,它允许来自 Android 和 iOS 的原生 UI 组件托管在 Flutter 应用中。在此版本的 Flutter 中,我们很高兴地宣布,我们已充分增强了框架管道,足以宣布这两个 Plugin 为生产就绪。

webview_flutter Plugin 托管 flutter.dev

在 Flutter 1.22 中,我们添加了一个替代的 Platform Views 实现,它修复了 Android 视图的所有已知键盘和辅助功能问题。此外,它与 Android API 级别 19 及更高版本配合使用(以前需要级别 20)。我们还在 iOS 上进行了线程改进,这些改进使平台视图更有效、更健壮(并且不再需要将 io.flutter.embedded_views_preview 标志添加到您的 iOS Info.plist 中)。

webview_flutter Plugin 支持新的 Android Platform Views 模式,但目前需要 手动启用。一旦它在更广泛的社区中得到更多使用,我们将在未来的版本中默认启用它。

Google Maps 和 WebView Plugin 已经从 Platform Views 中的改进中受益。如果您想使用 Platform Views 在 iOS 或 Android 上托管您自己的原生 UI 组件,您可以在 使用 Platform Views 在 Flutter 应用中托管原生 Android 和 iOS 视图 上了解如何操作。

如果您以前在 Flutter 应用中使用过 导航,您可能已经注意到核心数据结构(用户正在浏览的页面堆栈)对您隐藏了。相反,要管理它,您会调用 Navigator.pop() 或 Navigator.push()。例如,假设您想在主页上显示一个 Widget 列表,并允许用户点击其中一个 Widget 以进入专门针对该颜色的详细信息页。

这两个屏幕可以这样实现:

使用最简单的 Navigator 1.0 样式可以让您以看起来非常简单的方式在这两个屏幕之间导航:

调用 Navigator.push() 是将另一个页面推到第一个页面之上的所需操作,从而创建一个包含两个页面的堆栈。但是,与在 ColorListScreen 的 build 方法中创建的 Container 列表不同,该堆栈对您隐藏了。由于它被隐藏了,因此很难管理其他场景,例如处理由原生嵌入提供的初始路由的深层链接,例如来自网页的 URL 或来自 Android 的 intent。管理相同页面不同排列之间的嵌套路由也极其困难。

Navigator 2.0 通过使页面堆栈可见来解决这些问题以及更多问题。以下是在 ColorListScreen 和 ColorScreen 之间导航的更新示例:

应用明确地创建一个 Navigator,并为它提供一个页面列表,该列表表示完整的堆栈。我们创建了一个空 _selectedColor 来指示尚未选择任何颜色,因此我们不会最初显示 ColorScreen。当用户选择颜色时,我们会像往常一样调用 setState() 以指示 Flutter 您希望再次调用 build() 方法,该方法现在将在顶部创建一个包含 ColorScreen 的堆栈。

例如,您可以在 OnPopPage 回调中更新您的状态,如果用户弹出了,那么他们就“取消选择”了当前颜色,我们不再希望显示该页面。

如果 Navigator 2.0 看起来与 Flutter 的其他部分一样,那就是我们的意图——它是声明性的,与命令式的 Navigator 1.0 不同。我们的想法是统一导航和 Flutter 其他部分之间的模型,同时修复大量问题并添加功能。事实上,这个小例子只是 Navigator 2.0 中一部分内容的表面描述。有关详细信息,我强烈推荐这篇文章:Flutter 中的声明式导航和路由

此外,您现有的 Navigator 1.0 使用方式将继续与今天一样工作,并且不会很快被移除。如果您喜欢该模型,您当然可以继续使用它。但是,如果您尝试使用 Navigator 2.0,我们认为您会喜欢它。

预览:Android 的状态恢复

此版本中可供您体验的新功能是对 Android 上的状态恢复 的支持。这是我们 最受欢迎的功能之一,获得了 217 个点赞!

对于那些不熟悉状态恢复需求的人来说,移动操作系统可能会终止后台应用以回收资源以供前台应用使用。发生这种情况时,操作系统会通知要终止的应用尽快保存任何 UI 状态,以便在用户返回到该应用时恢复它。如果实现正确,这将为用户提供无缝的体验,同时更好地利用设备资源。到目前为止,Flutter 不支持状态恢复,在没有框架支持的情况下正确地做到这一点非常困难。这就是我们很高兴能够为 Android 提供此功能的基础实现的原因。

以下是如何 恢复默认 Flutter Counter 应用的状态的简单示例

简而言之,每个 Widget 都获得一个存储桶,该存储桶使用唯一的 ID 与 RestorationMixin 关联。通过使用 RestorableProperty 类型(此处使用的是 RestorableInt)来存储 UI 特定的数据,并通过将该数据与状态恢复功能关联,数据将在 Android 终止应用之前自动存储,并在应用恢复时恢复。就这样。存储在 Restoration* 类型中的任何数据,例如 RestorableInt、RestorableString 和 RestorableTextEditingController(我们有很多这样的类型),都将被恢复。如果我们没有涵盖您希望恢复的所有类型,您可以通过扩展 RestorableProperty<T> 创建您自己的类型。

为了对状态恢复进行自动化测试,我们已向 WidgetTester 添加了一个新的 restartAndRestore API。为了手动测试,最简单的方法是在 Android 设备上启动启用了状态恢复的 Flutter 应用,在 Android 的开发者设置中启用“不保留活动”,运行 Flutter 应用,将其置于后台,然后返回到它。此时,Android 将终止并恢复您的应用,因此您可以查看一切是否按预期工作。

虽然我们很高兴将此状态恢复的预览版本交给您,但仍有更多工作要做。例如,状态恢复不仅适用于 Android,iOS 应用也可以从中受益。此外,我们正在忙于更新我们自己的 Widget,以在恢复期间保持其状态。我们已经在 Scrollable 类(例如 ListView 和 SingleChildScrollView)(记住用户的滚动位置)和 TextField(恢复用户输入的文本)中提供了支持,并计划将其扩展到其他 Widget。

但是,我们尚未添加的关键恢复支持,也是使它成为预览版本的原因是导航(1.0 或 2.0)。这意味着用户在应用中的位置将不会被恢复。该功能将很快在测试版中推出,并在 Flutter 的下一个稳定版本中推出。

预览:为无与伦比的输入和显示频率提供流畅的滚动效果

Flutter 团队与我们内部的 Google 合作伙伴合作,在输入频率和显示频率不相同的情况下大大改善了滚动的性能。例如,Pixel 4 的输入频率为 120hz,而显示频率为 90hz。这种不匹配会导致滚动时出现性能损失。使用新的 resamplingEnabled 标志,您可以利用我们在 Flutter 中完成的性能工作来解决此问题:

根据所涉及的频率差异,通过启用此标志,您可能会看到滚动时的卡顿减少高达 97%。当我们确定这是最佳体验时,我们计划在未来的版本中默认启用此标志。

一个新的统一 Dart 开发者工具

与往常一样,更新 Flutter 不仅仅意味着引擎和框架,还包括工具。Flutter 1.22 包括新的 Dart 版本(2.10),并且有一个新的 dart CLI 工具,您可能会发现它也很有用。

Dart 历史上拥有许多较小的开发者工具(例如用于格式化的 dartfmt 和用于代码分析的 dartanalyzer)。在 Dart 2.10 中新增了一个统一的 dart 开发者工具,它与 flutter 工具非常相似。

Screenshot of the output of `dart help`.

从今天的 Flutter 1.22 SDK 开始,您会发现 <flutter-sdk>/bin 文件夹(您可能将其放在 PATH 中)包含 flutter 和 dart 命令。有关更多详细信息,请参阅 Dart 2.10 部落格文章

应用大小分析工具

作为 Flutter 1.22 的一部分发布的工具包含一个新的输出大小分析实用程序。此工具帮助诊断 Flutter 您的应用的大小分解随着时间的推移是否发生了变化。

您可以使用此工具通过传递一个--analyze-size 标志到以下任何命令来收集分析所需的数据:

  • flutter build apk
  • flutter build appbundle
  • flutter build ios
  • flutter build linux
  • flutter build macos
  • flutter build windows

在构建 Flutter 输出工件时使用此标志将打印工件大小和组成的摘要。这包括原生代码、资产,甚至编译的 Dart 代码的包级别分解。

Flutter Gallery 发布的 APK 的示例分解

此摘要对于快速识别应用程序包大小使用情况中的热点很有用。此外,收集的数据还可用作 Dart DevTools 中的 JSON 文件,它允许您进一步探索应用的内容,查明大小问题,并通过遵循 flutter.dev 上的说明 查看两个不同 JSON 文件之间的更改。加载 JSON 文件后,您将拥有一个界面,该界面将为您提供应用大小的树状图视图。

Dart DevTools 中的示例 APK 分解

有关使用应用大小工具可以执行的操作的更多详细信息,请阅读 flutter.dev 上的使用应用大小工具文档

预览:DevTools 中更新的网络页面

此版本中的另一个 DevTools 预览功能是能够在 网络 选项卡中查看 HTTP 和 HTTPS 响应正文。

若要启用此功能,请确保您通过 flutter channel devflutter channel upgrade 位于 Flutter dev 频道。

此外,对于有大量网络流量的应用,我们提供搜索和筛选功能。

有关 网络 选项卡文档,请参阅 flutter.dev 上的使用网络视图

IntelliJ 中托管的 DevTools Inspector 选项卡

一段时间以来,我们一直在维护我们 Flutter 某些工具的两个副本,例如 IntelliJ 中的 Inspector 窗格和 Dart DevTools 中的 Inspector 选项卡。这不仅会降低我们的速度,因为我们必须维护两个代码库,而且某些功能(例如布局资源管理器)尚未加入 IntelliJ Plugin。因此,为了解决这两个问题,我们启用了直接在 IntelliJ 中托管来自 Dart DevTools 的 Inspector 选项卡的功能。

请注意布局资源管理器的添加,您可以在代码旁边使用它。若要打开此选项,请转到 首选项 > 语言和框架 > Flutter > 启用嵌入式 DevTools Inspector

Visual Studio Code 中改进的输出链接

所有 Flutter 开发者都会遇到的一个常规操作是从终端或堆栈跟踪中的错误输出开始。在 Visual Studio Code 的 Flutter 扩展的最新版本中,这些链接现在将被正确地解析,以启用从输出中的链接直接进行跳转。

这看起来似乎是一个小事情,但对这一功能的初步反馈已经非常积极。

与往常一样,这里列出的工具更改太多,但我建议您查看以下公告以获取详细信息:

客户案例:EasyA

EasyA 是一款订阅应用,旨在让学龄学生通过即时消息与优秀的导师联系,并使用 Flutter 编写。最近它被 Apple 评为 Apple 的“今日应用”

“当学校今年早些时候开始上网时,我们知道我们需要快速推出我们的辅导应用来帮助学生。使用 Flutter 进行开发的超快速度意味着我们能够为 iOS 和 Android 实现获奖设计,并发布到网页——恰逢封锁期间!通常情况下,这是不可能实现的。但由于 Flutter 允许我们同时定位所有三个平台,因此我们能够高效地共享代码,并充分利用我们的小型开发者团队。”
— 菲利普·郭,[EasyA](https://easya.io/) 的联合创始人

重大更改

与往常一样,我们一直努力将重大更改的数量降至最低。以下是 Flutter 1.22 版本的列表。

总结

Flutter 1.22 稳定版本可能是在上一个版本之后快速发布的,但它包含了如此多的优秀内容,以至于这篇文章无法全部提及。我们希望此版本能帮助您为 iOS 和 Android 构建出色的应用,我们迫不及待地想看看您将会实现什么!感谢您的支持——我们为你们构建 Flutter。


宣布 Flutter 1.22 最初发布在 Flutter 上的 Medium,人们在那里通过突出显示和回应这个故事,继续进行讨论。

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

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

學習 Flutter 的全新導航和路由系統

本文說明 Flutter 的全新 Navigator 和 Router API 如何運作。如果您關注 Flutter 的公開 設計文件,您可能已經看到這些新功能被稱為 Navigator 2.0 和 Router。我們將探討這些 API 如何讓您更精細地控制應用程式中的螢幕,以及如何使用它們來解析路由。

這些新的 API 並不是重大變更,它們只是添加了一個新的 聲明式 API。在 Navigator 2.0 之前,很難推送或彈出多個頁面,或移除當前頁面下方的一個頁面。但是,如果您對當前 Navigator 的運作方式感到滿意,您可以繼續以相同的方式(命令式)使用它。

Router 提供了從底層平台處理路由和顯示適當頁面的能力。在本文中,Router 被配置為解析瀏覽器 URL 以顯示適當的頁面。

本文將幫助您選擇最適合您的應用程式的 Navigator 模式,並說明如何使用 Navigator 2.0 來解析瀏覽器 URL 並完全控制活動頁面堆疊。本文中的練習展示了如何構建一個應用程式,該應用程式可以處理來自平台的進站路由,並管理應用程式的頁面。以下 GIF 顯示了示例應用程式的運作方式:

如果您使用 Flutter,您可能正在使用 Navigator,並且熟悉以下概念:

  • Navigator - 一個管理 Route 物件堆疊的 Widget。
  • Route - 由 Navigator 管理的一個物件,表示一個螢幕,通常由 MaterialPageRoute 等類別實作。

在 Navigator 2.0 之前,Route 被推送和彈出到 Navigator 的堆疊上,使用 命名路由匿名路由。接下來的幾節將簡要回顧這兩種方法。

匿名路由

大多數行動應用程式會在彼此之上顯示螢幕,就像一個堆疊一樣。在 Flutter 中,使用 Navigator 就可以輕鬆實現這一點。

MaterialApp 和 CupertinoApp 在幕後都已使用 Navigator。您可以使用 Navigator.of() 存取 Navigator,或使用 Navigator.push() 顯示一個新的螢幕,並使用 Navigator.pop() 返回上一個螢幕:

當呼叫 push() 時,DetailScreen Widget 會被放置在 HomeScreen Widget 的頂部,如下所示:

上一個螢幕 (HomeScreen) 仍然是 Widget 樹的一部分,因此與其關聯的任何 State 物件都會在 DetailScreen 可見時保留下來。

命名路由

Flutter 也支援命名路由,這些路由在 MaterialApp 或 CupertinoApp 的 routes 參數中定義:

這些路由必須預先定義。雖然您可以 將參數傳遞給命名路由,但您無法從路由本身解析參數。例如,如果應用程式在網頁上運行,您無法從像 /details/:id 這樣的路由中解析 ID。

使用 onGenerateRoute 的進階命名路由

處理命名路由的更靈活方法是使用 onGenerateRoute。此 API 使您能夠處理所有路徑:

以下是完整的示例:

這裡,settings 是 RouteSettings 的一個實例。name 和 arguments 字段是呼叫 Navigator.pushNamed 時提供的數值,或者設定為 initialRoute 的數值。

Navigator 2.0 API 為框架添加了新的類別,以便使應用程式的螢幕成為應用程式狀態的函數,並提供從底層平台(例如 Web URL)解析路由的能力。以下是新功能的概述:

  • Page - 一個不可變的物件,用於設定導航器的歷史堆疊。
  • Router - 配置 Navigator 要顯示的頁面列表。通常,這個頁面列表會根據底層平台或應用程式狀態的變化而變化。
  • RouteInformationParser,它會從 RouteInformationProvider 中獲取 RouteInformation,並將其解析為一個使用者定義的資料類型。
  • RouterDelegate - 定義 Router 如何了解應用程式狀態變化以及如何對它們做出響應的應用程式特定行為。它的工作是監聽 RouteInformationParser 和應用程式狀態,並使用當前的 Page 列表構建 Navigator。
  • BackButtonDispatcher - 向 Router 報告後退按鈕按下。

下圖顯示了 RouterDelegate 如何與 Router、RouteInformationParser 和應用程式的狀態交互:

以下是這些部分如何交互的示例:

  1. 當平台發出一個新的路由(例如,「books/2」)時,RouteInformationParser 會將其轉換為一個抽象資料類型 T,您可以在您的應用程式中定義這個資料類型(例如,一個名為 BooksRoutePath 的類別)。
  2. RouterDelegate 的 setNewRoutePath 方法會使用這個資料類型被呼叫,並且必須更新應用程式狀態以反映變化(例如,透過設定 selectedBookId)並呼叫 notifyListeners。
  3. 當呼叫 notifyListeners 時,它會告訴 Router 重新建立 RouterDelegate(使用其 build() 方法)。
  4. RouterDelegate.build() 返回一個新的 Navigator,其頁面現在反映了應用程式狀態的變化(例如,selectedBookId)。

本節將引導您完成使用 Navigator 2.0 API 的練習。我們將最終獲得一個應用程式,該應用程式可以與 URL 列保持同步,並處理來自應用程式和瀏覽器的後退按鈕按下,如以下 GIF 所示:

若要繼續操作,請 切換到 master channel使用 Web 支援建立一個新的 Flutter 專案,並將 lib/main.dart 的內容替換為以下內容:

頁面

Navigator 在其建構函數中有一個新的 pages 參數。如果 Page 物件列表發生變化,Navigator 會更新路由堆疊以匹配。若要了解這如何運作,我們將構建一個顯示書籍列表的應用程式。

在 _BooksAppState 中,保留兩個狀態資訊:書籍列表和選中的書籍:

然後在 _BooksAppState 中,返回一個帶有 Page 物件列表的 Navigator:

由於這個應用程式有兩個螢幕,一個是書籍列表,另一個是顯示詳細資訊的螢幕,因此如果選中了書籍,請添加第二個(詳細資訊)頁面(使用 集合中的 if):

請注意,頁面的鍵由書籍物件的數值定義。這告訴 Navigator,當書籍物件不同時,這個 MaterialPage 物件與另一個物件不同。如果沒有唯一的鍵,框架無法確定何時顯示不同 Page 之間的轉場動畫。

注意:如果您願意,您也可以擴展 Page 以自訂行為。例如,此頁面添加了一個自訂轉場動畫:

最後,在沒有提供 onPopPage 回撥的情況下提供 pages 參數是一個錯誤。此函數在呼叫 Navigator.pop() 時被呼叫。它應該用於更新狀態(決定頁面列表的狀態),並且必須呼叫路由上的 didPop 來確定彈出是否成功:

在更新應用程式狀態之前檢查 didPop 是否失敗非常重要。

使用 setState 會通知框架呼叫 build() 方法,該方法在 _selectedBook 為 null 時返回一個帶有單個頁面的列表。

以下是完整的示例:

就目前而言,這個應用程式只允許我們以聲明式的方式定義頁面堆疊。我們無法處理平台的後退按鈕,並且瀏覽器的 URL 在我們導航時不會改變。

Router

到目前為止,應用程式可以顯示不同的頁面,但它無法處理來自底層平台的路由,例如,如果使用者在瀏覽器中更新 URL。

本節將說明如何實作 RouteInformationParser、RouterDelegate 並更新應用程式狀態。設定好後,應用程式將與瀏覽器的 URL 保持同步。

資料類型

RouteInformationParser 會將路由資訊解析為一個使用者定義的資料類型,因此我們將首先定義它:

在這個應用程式中,應用程式中的所有路由都可以使用單個類別來表示。相反,您可能選擇使用實作超類別的不同類別,或以其他方式管理路由資訊。

RouterDelegate

接下來,添加一個擴展 RouterDelegate 的類別:

在 RouterDelegate 上定義的泛型類型是 BookRoutePath,它包含決定要顯示哪些頁面所需的所有狀態。

我們需要將一些邏輯從 _BooksAppState 移動到 BookRouterDelegate,並建立一個 GlobalKey。在這個示例中,應用程式狀態直接儲存在 RouterDelegate 上,但也可以分開到另一個類別中。

為了在 URL 中顯示正確的路徑,我們需要根據應用程式的當前狀態返回一個 BookRoutePath:

接下來,RouterDelegate 中的 build 方法需要返回一個 Navigator:

onPopPage 回撥現在使用 notifyListeners 而不是 setState,因為這個類別現在是 ChangeNotifier,而不是 Widget。當 RouterDelegate 通知其監聽器時,Router Widget 也會被通知 RouterDelegate 的 currentConfiguration 已更改,並且需要再次呼叫其 build 方法以構建一個新的 Navigator。

_handleBookTapped 方法也需要使用 notifyListeners 而不是 setState:

當一個新的路由被推送到應用程式時,Router 會呼叫 setNewRoutePath,這讓我們的應用程式有機會根據路由的變化來更新應用程式狀態:

RouteInformationParser

RouteInformationParser 提供了一個鉤子來解析進站路由 (RouteInformation) 並將其轉換為使用者定義的類型 (BookRoutePath)。使用 Uri 類別來處理解析:

此實作特定於此應用程式,而不是通用的路由解析解決方案。稍後將詳細說明。

若要使用這些新的類別,我們使用新的 MaterialApp.router 建構函數,並傳遞我們自定義的實作:

以下是完整的示例:

在 Chrome 中運行此示例現在會顯示路由的導航方式,並在手動編輯 URL 時導航到正確的頁面。

TransitionDelegate

您可以提供 TransitionDelegate 的自定義實作,它會自訂當頁面列表發生變化時路由在螢幕上出現(或從螢幕中移除)的方式。如果您需要自訂此功能,請繼續閱讀,但如果您對預設行為感到滿意,您可以跳過本節。

為 Navigator 提供一個自訂 TransitionDelegate,它定義了所需的行為:

例如,以下實作會禁用所有轉場動畫:

此自定義實作會覆蓋 resolve(),它負責將各種路由標記為推送、彈出、添加、完成或移除:

  • markForPush - 使用動畫轉場顯示路由
  • markForAdd - 使用 動畫轉場顯示路由
  • markForPop - 使用動畫轉場移除路由,並使用結果完成它。在這個上下文中,「完成」意味著結果物件會被傳遞給 AppRouterDelegate 上的 onPopPage 回撥。
  • markForComplete - 使用無轉場移除路由,並使用結果完成它
  • markForRemove - 使用無動畫轉場移除路由,並且不完成。

此類別只會影響 聲明式 API,這就是為什麼 後退 按鈕仍然顯示轉場動畫的原因。

此示例如何運作: 此示例會查看新的路由和即將離開螢幕的路由。它會遍歷 newPageRouteHistory 中的所有物件,並使用 markForAdd 將它們標記為無轉場動畫地添加。接下來,它會遍歷 locationToExitingPageRoute 映射的數值。如果它找到一個被標記為 isWaitingForExitingDecision 的路由,那麼它會呼叫 markForRemove 來指示該路由應該使用無轉場和無完成的方式移除。

以下是完整的示例(Gist)

嵌套路由

這個更大的演示展示了如何在另一個 Router 中添加 Router。許多應用程式需要為 BottomAppBar 中的目標設定路由,以及為其上方的視圖堆疊設定路由,這 需要兩個 Navigator。若要做到這一點,應用程式會使用一個應用程式狀態物件來儲存應用程式特定的導航狀態(選中的菜單索引和選中的書籍物件)。此示例還展示了如何配置哪個 Router 處理後退按鈕。

嵌套路由示例(Gist)

接下來要做什麼

本文探討了如何為特定應用程式使用這些 API,但也可以用於構建一個更高階的 API 套件。我們希望您能加入我們,探索建立在這些功能之上的更高階 API 能夠為使用者做些什麼。


學習 Flutter 的全新導航和路由系統 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。