【文章翻譯】Announcing Dart null safety beta

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

宣佈 Dart 空安全 Beta 版

此文章交叉發佈於此處和 Flutter 部落格

今天我們宣佈 Dart 和 Flutter 的 Beta 版已提供完善的空安全功能。空安全是我們最新的主要生產力功能,旨在幫助您避免空值錯誤,這是一類通常難以發現的錯誤。如果您正在尋找我們為何對空安全感到興奮的快速摘要,請查看這部新影片:

隨著空安全進入 Beta 版,是時候開始社群遷移 pub.dev 上可用的數千個套件了。我們已經遷移了 Dart 核心程式庫、Flutter 架構,以及超過 40 個 Dart 和 Flutter 套件。有了這些,我們希望看到社群透過遷移他們的套件來擁抱空安全。

Timeline of Dart sound null safety support, from Technical Preview 1 to Stable in early 2021

隨著 Beta 版的推出,我們也開始在發佈空安全功能的穩定版本之前的最後階段。我們希望您能使用此功能,並讓我們知道它的哪些部分可以改進,UI 訊息是否可以更容易理解,或者文件是否可以更清晰。我們**非常期待收到您的回饋**。

選擇加入空安全

在我們討論空安全遷移之前,需要重複的是(如我們的空安全原則中所述),您可以控制何時開始採用空安全。只有當應用程式和套件的最低 Dart SDK 限制 至少是 Dart 2.12 預發佈版時,它們才會以空安全模式執行:

1
2
environment:
sdk: ">=2.12.0-0 <3.0.0"

要體驗這一點,請嘗試建立一個小的空安全 hello 應用程式(例如,使用 dart create),其中包含如下所示的程式碼。然後,您可以嘗試在更改 SDK 限制並執行 dart pub get 之前和之後執行應用程式,並體驗程式行為的變化。(確保使用在 dart --version 中報告 2.12 的 SDK。)

1
2
3
4
5
6
7
8
9
// bin/hello.dart:
...
void main() {
var hello = 'Hello Dart developers';
if (someCondition) {
hello = null;
}
print(hello);
}
1
2
# 更改 SDK 限制之前:
$ dart run
1
null
1
2
# 更改 SDK 限制之後(並執行 dart pub get):
$ dart run
1
2
3
4
bin/hello.dart:6:13: Error: Null can't be assigned to a variable of type 'String' because 'String' is not nullable.

hello = null;
^

遷移至空安全

要將套件(或簡單的應用程式)遷移至空安全,請按照以下五個步驟操作,這些步驟在 dart.dev 的遷移指南中已完整記錄。

步驟 1:檢查您的依賴項是否已準備就緒

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

Illustration of dependency order vs. migration order

為什麼順序很重要?儘管您可以在依賴項遷移之前在遷移程式碼方面取得一些進展,但如果您的依賴項在其遷移期間更改其 API,則您可能會冒險進行第二次遷移。如果您的某些依賴項不是空安全的,請考慮使用 pub.dev 上列出的每個套件的聯絡方式與套件發佈者聯繫。

驗證依賴項是否已準備就緒

要驗證您的應用程式或套件是否已準備好開始遷移,您可以使用空安全模式下的 dart pub outdated。以下範例顯示,如果此應用程式將其依賴項升級到 可解析 欄中列出的 pathprocesspedantic 的預發佈版本,則它已準備好遷移。

Screenshot of `dart pub outdated` output

如果在新的小版本中提供空安全支援,您將在 可升級 欄中看到它們。通常,在新的大版本中會提供空安全支援;在這種情況下,您將在過時輸出的 可解析 下看到列出的版本。要升級到這些版本,請編輯您的 pubspec.yaml 檔案以允許這些主要版本。例如,您可以將 process: ^3.0.13 更改為 process: ^4.0.0-nullsafety

您還可以使用套件頁面上的新 空安全 標籤(例如 collection 1.15)和新的進階搜尋 空安全搜尋選項 在 pub.dev 上找到具有空安全支援的套件。

Screenshot of pub.dev search

步驟 2:使用遷移工具進行遷移

如果您的依賴項已準備就緒,則可以使用遷移工具 dart migrate 繼續遷移您的應用程式或套件。

遷移工具是互動式的,因此您可以查看工具推斷出的可空性屬性。如果您不同意工具的任何結論,您可以加入可空性提示以更改推斷。加入一些遷移提示可以對遷移品質產生巨大影響。

Screenshot of the migration tool UI

我們已經讓少數 Dart 套件作者使用空安全的早期預覽版本測試遷移,他們的回饋令人鼓舞。遷移指南中包含有關如何使用遷移工具的更多提示。

步驟 3:靜態分析您遷移的程式碼

在您的 IDE 或命令列中使用 pub get 更新您的套件。然後使用您的 IDE 或命令列對您的 Dart 程式碼執行靜態分析:

1
2
$ dart pub get
$ dart analyze

或者在您的 Flutter 程式碼上:

1
2
$ flutter pub get
$ flutter analyze

步驟 4:確保測試通過

執行您的測試並確保它們通過。您可能需要更新預期空值的測試,以防您更改了套件程式碼以不再允許空值。

步驟 5:發佈您的空安全套件

當遷移完成且測試通過後,您可以將您的套件作為預發佈版本發佈。以下是最佳實務的簡要摘要:

  • 將您的版本號遞增到下一個主要版本(例如,從 2.3.x 到 3.0.0)。此最佳實務可確保您的套件的使用者在準備好自己使用空安全之前不會升級到它,並且讓您可以自由地重構 API 以最佳地利用空安全。
  • 在 pub.dev 上將您的套件作為預發佈版本進行版本控制和發佈。(例如,使用 3.0.0-nullsafety.0,而不是 3.0.0。)

有關遷移和版本控制的完整詳細資訊,請參閱遷移指南

完善空安全的好處

我們之前關於 Dart 和 Flutter 中空安全的技術預覽的部落格文章透過許多範例討論了空安全的好處。現在,隨著空安全即將完成,我們看到了此好處的一些真實範例。

更安全的程式碼

就在最近,我們在 Flutter master channel 中發現了一個錯誤,其中各種 flutter tool 命令會在某些機器配置上因空值錯誤而崩潰:在空值上呼叫了方法 '>='。根本原因是最近的一個拉取請求,用於加入對偵測 Android Studio 4.1 的支援。該 PR 加入了如下程式碼:

1
2
3
4
5
6
final int major = version?.major;
final int minor = version?.minor;
if (globals.platform.isMacOS) {
/// Android Studio 4.1 版之後的 Plugin 路徑已更改。
if (major >= 4 && minor >= 1) {
...

您能發現錯誤嗎?因為 version 可能為空,majorminor 也可能為空。這個錯誤在這裡單獨看來似乎很容易發現,但在實務中,這樣的程式碼總是會溜走,即使是像 Flutter 儲存庫中使用的嚴格程式碼審查流程也是如此。使用空安全,靜態分析立即捕獲此問題

Screenshot of analysis output in an IDE

那是一個相當簡單的錯誤。在我們在 Google 內部程式碼中早期使用空安全的過程中,我們已經看到更多複雜的錯誤被捕獲,然後透過空安全解決。以下是一些範例:

  • 一個內部團隊發現,他們經常在空安全知道永遠不會為空的程式碼中檢查空值。此問題最常見於使用 protobuf 的程式碼中,其中可選欄位在未設定時返回預設值,而不是空值。這會導致程式碼透過混淆預設值和空值來錯誤地檢查預設條件。
  • Google Pay 團隊在他們的 Flutter 程式碼中發現了錯誤,在嘗試在 Widget 上下文之外存取 Flutter State 物件時,它們會失敗。在空安全之前,這些會返回空值並掩蓋錯誤;使用空安全,完善的分析確定這些屬性永遠不會為空,並拋出分析錯誤。
  • Flutter 團隊發現了一個錯誤,如果將空值傳遞給 Window.render() 中的 scene 參數,Flutter 引擎可能會崩潰。在空安全遷移期間,他們加入了一個提示以將 Scene 標記為不可為空,然後能夠輕鬆地防止如果傳遞空值會觸發的潛在應用程式崩潰。

在編譯期間利用完善的空安全

Dart 空安全的完善性還有另一個令人歡迎的含義:這意味著 Dart 編譯器可以利用可空性資訊。這可能會使您的程式更小、更快。我們還沒有很多真實世界的應用程式完全遷移到完善的空安全(因為我們現在才剛剛開始這些應用程式所依賴的套件的生態系統遷移以實現完善性),但我們從核心架構中看到了非常令人鼓舞的結果。

我們最近對 hello_world 範例進行了測試重新編譯,以測量空安全對應用程式大小的影響。這是一個最小化的範例,只顯示「hello world」。在比較編譯程式碼的總大小時,未壓縮的(安裝在設備上的)程式碼大小縮小了 3.5%,除了使用完善的空安全重新編譯之外沒有做任何事情。儘管這個應用程式只有 10 行程式碼,但這也是可能的,因為所有包含的程式庫的程式碼大小都縮小了;例如 Flutter 架構本身(package:flutter)縮小了 3.9%。

至於程式碼速度,必須強制執行完善的型別系統可能會增加開銷。但是,進行更少的空值檢查也可能會使程式碼更快。我們對基準測試的初步分析顯示,效能與以前的版本相當,並且新的額外型別資訊為我們未來進行新型別的效能改進創造了潛力。我們計畫在未來的部落格文章中詳細介紹我們的效能工作。

在某些情況下,我們已經看到空安全會帶來效能提升,通常是在遷移至空安全時發現了程式碼邏輯中的缺陷。例如,我們在 Flutter 網頁文字佈局快取中發現了一個問題。此快取使用的鍵是可為空的,然後是一些在為空時使用 TextAlign.start 的邏輯。此邏輯導致快取中的缺陷,即使元素仍然具有預設值,它們看起來也像已更改。因此,經常發生快取未命中。加入一個不可為空的 textAlign getter 有助於修復快取缺陷,從而在文字被快取的情況下使文字渲染效能提高 14 倍

立即開始!

包含空安全的 Dart 和 Flutter 的 Beta 版本今天已經準備好了。如果您使用 Flutter 進行開發,您可以使用 flutter channel beta 切換到 Beta,然後使用 flutter upgrade。否則,您可以從 Dart SDK 存檔 中獲取獨立的 Dart SDK。

如果您開發套件,我們鼓勵您閱讀我們的遷移指南並規劃您的遷移。如果您有任何問題或建議,請告知我們

如果您是應用程式開發人員,您可能希望延遲遷移,直到該功能進入我們的穩定頻道。我們計畫快速處理 Beta 版的回饋,修復任何剩餘的問題。很難說明空安全何時會在穩定版本中發佈的具體時間表,但我們認為是明年年初。

感謝您的支援和回饋,我們正在努力使 Dart 成為更強大的語言,使 Flutter 成為更強大的架構!


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

【文章翻譯】Deprecation Lifetime in Flutter

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

Flutter 中的棄用生命週期

新的一年即將到來,Flutter 團隊正在為 2021 年的全新開始做好準備。

為此,我們將從 Flutter 架構中移除一些已棄用的 API。

在 Flutter 架構中移除棄用並非新鮮事。我過去也曾經 自己處理過快速搜尋 結果顯示,過去移除棄用沒有定義的時間軸或排程,因為我們的 棄用策略 並沒有規定何時移除它們。由於我們希望為使用者提供一致且可靠的體驗,因此我們決定為棄用建立標準生命週期。

本文將討論我們目前針對重大變更和棄用的策略,以及這些策略如何付諸實行,以及如何發展以為使用者提供更好的體驗。

Flutter 中的重大變更

Photo by CHUTTERSNAP on Unsplash

在 Flutter 中棄用程式碼的流程始於我們的 重大變更策略。棄用被視為重大變更,這意味著它們必須遵循 公開設計文件 指南,該指南要求一段時間用於討論。(此提案的設計文件為 實作棄用生命週期。) 除了提供設計文件外,我們還會在 Flutter 貢獻者 Discord 和受歡迎的社群,例如 flutter-announceflutter-dev 上發佈訊息,所有這些都在進行任何變更之前。這是我作為工程師最喜歡的流程之一:我能夠聽到您的意見。我們很棒的社群在這個流程中非常活躍,這也讓我們變得更好。在多次場合中,我看到一個功能的設計在透過這些管道發佈後有了很大的改進,這全都是因為社群的回饋。

重大變更的作者會遷移受重大變更影響的客戶程式碼。在我們重大變更策略的最後一次更新中,我們 邀請 您將測試提交到我們的 客戶測試儲存庫。這些測試是我們標誌著變更會造成中斷的訊號,我們承諾會在進行變更之前與測試作者合作遷移他們的程式碼。

我們還為我們的重大變更提供 遷移指南。這些指南旨在幫助使用者在更新到最新版本的 Flutter 後遷移他們的程式碼。如果您看過我們的 發行公告 之一,我們也會在特定版本中指出中斷,並引導使用者到有用的資源,幫助他們更新應用程式。

過去的調查 中,我們詢問使用者他們對重大變更的偏好。我們了解到使用者認為重大變更使程式碼更乾淨,對他們很有價值,這就是為什麼我們通常更喜歡重大變更而不是棄用的原因。根據研究:

總體而言,78% 的人表示他們更喜歡導致最乾淨的 API 的重大變更;這也使 API 更易於學習和使用。

我們的重大變更策略基於此研究。與所有規則一樣,總是會有例外。我最近 棄用了一些非常受歡迎的方法,改用新的 Widget。如果我們知道特定變更會造成重大中斷,影響許多使用者,我們就會棄用。棄用允許遷移緩衝期,同時仍然提供重大變更策略所產生所有工具。

日漸老舊的棄用

Photo by Dilyara Garifullina on Unsplash

遷移緩衝期從棄用在 穩定頻道 上發佈時開始。緩衝期結束的時間軸為 1 個日曆年或 4 個穩定版本,取較長者。為了制定出我們認為舒適的時間軸,我們查看了我們的成長率和採用率,並評估了一些已公開發佈的 Flutter 應用程式和套件。

我們希望使用者能夠預期這個排程,而不是擔心下一個版本是否會因為棄用消失而帶來意外的升級障礙。我們還希望確保我們提前通知這些變更,並且我們承諾在開始移除棄用之前,會宣布棄用即將結束生命週期。本著這種精神,我想要與您分享 第一批即將結束生命週期的棄用,現在 Flutter 1.22 已推出。

由於這是我們第一次實施這項策略,因此有很多棄用,其中有些甚至早於 Flutter 1.0!其中一些棄用也早於我們的重大變更策略。我們將會為所有這些棄用努力建立遷移工具和指南。預計在我們下一個穩定版本推出之前,這些棄用將從 Flutter 架構中移除,並會在移除時透過我們通常的管道發布公告。

在我們開始實施這些變更的同時,我們希望聽到您的意見!我們始終歡迎您將測試提交到我們的客戶測試儲存庫,我們會在進行中斷之前與您合作遷移您的程式碼。若要進一步了解測試儲存庫以及如何提交測試,請查看 Flutter 測試 README。在進行這些變更時,我們將透過 Flutter 貢獻者 Discordflutter-announceflutter-dev 頻道通知您並徵求您的回饋。因此,如果您尚未加入其中一個社群,請務必加入!我們期待收到您的訊息,並與您合作。


Flutter 中的棄用生命週期 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

【文章翻譯】New ad formats for Flutter

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

為 Flutter 行動廣告 Plugin 開放嵌入式橫幅和原生廣告的 Beta 程式

有許多方法可以讓行動應用程式開發人員獲得工作的報酬。透過廣告變現應用程式是許多行動開發人員(包括使用 Flutter 的開發人員)的熱門選擇之一。

原生廣告格式支援,特別是 Flutter 的前五名熱門功能請求之一。Flutter 團隊先前提供的 Google 廣告 Plugin(也稱為 firebase_admob Plugin)支援覆蓋橫幅、插頁式和獎勵影片,但無法渲染嵌入式橫幅或原生廣告等格式。

嵌入式橫幅和原生廣告

顧名思義,嵌入式橫幅廣告讓您將廣告單元整合為 Flutter 視圖的一部分,而不是將橫幅覆蓋在 UI 上。同樣地,原生廣告允許您設計一種感覺像是您應用程式內容自然一部分的廣告體驗。一些開發人員告訴我們,缺少嵌入式橫幅和原生廣告會阻礙他們建立美麗自然的廣告體驗的能力,而另一些開發人員則表示,沒有這些功能可能會阻止他們將 Flutter 採用到他們的應用程式中。

為了響應您的請求,我們一直在開發對這些格式的支援。一年前,我們開始與 Google Ads 密切合作,為 Flutter 實作健全且值得信賴的嵌入式廣告解決方案。我們還完全重構了 Plugin 程式碼,以提高效能和穩定性。今天,在完成實作和一系列 Alpha 測試後,我們很高兴地宣布,此更新的 Plugin 的 beta 版本將提供僅限邀請的存取權。

藉由這個 beta 測試,我們在現有 Plugin 上進行了建置,並為 AdMob 和 Ad Manager 新增了對新格式的支援。與所有 beta 版本一樣,仍然存在一些需要解決的問題,而且功能支援可能會在未經通知的情況下發生變化。如果被選為參與 beta 測試,我們強烈建議您在將應用程式發佈給使用者之前,在您的應用程式中徹底測試整合。

立即申請

若要申請參與我們的 beta 測試,請填寫 我們的申請表。我們特別尋找已有應用程式上架且願意與我們團隊合作,在他們的生產應用程式中使用我們的 beta 版本的客戶。如果您被選為參與 beta 測試,您將收到一封包含更多說明的電子郵件。

根據此 beta 測試的結果,我們預計在 2021 年初向更廣泛的受眾開放使用。

我們希望嵌入式橫幅和原生廣告能幫助您為您的 Flutter 應用程式解鎖更多收入增長。

祝您程式設計和廣告愉快!


Flutter 的新廣告格式 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

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

【文章翻譯】Accessible expression with Material Icons and Flutter

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

Android app 上架流程

本篇文章主要介紹在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 商店在第一次審查時,最久大約會至七天,爾後提交大約都是一下子就完成審查,並都是半天會完全部署至商店(所有使用者都可以看到更新)

iOS app 上架流程

本篇文章主要介紹在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,完成這次版本審查

【文章翻譯】Dart sound null safety: technical preview 2

【文章內容使用 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/

【文章翻譯】Flutter on the web, slivers, and platform-specific issues: user survey results from Q3 2020

【文章內容使用 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%)。

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

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

【文章翻譯】Testable Flutter and Cloud Firestore

【文章內容使用 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 會收到重新渲染的訊息。

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

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

修復測試

【文章翻譯】Performance testing on the web

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