【文章內容使用 Gemini 1.5 Pro 自動產生】
為什麼選擇 Cloud Firestore?
FlutterFire 技術堆疊,由 Flutter 和 Firebase(特別是 Cloud Firestore)組成,在您構建和發佈應用程式時,為您解鎖了前所未有的開發速度。在本文中,您將探索這兩種技術之間的強大整合,重點關注測試和使用乾淨的架構模式。但是,您將一步一步地構建自己的方法,而不是直接跳到最終實作,這樣每個步驟背後的理由就清晰明瞭。
您將構建什麼?
為了展示將 Cloud Firestore 作為您應用程式後端的乾淨方法,您將構建經典 Flutter 計數器應用程式的修改版本。唯一的區別是,每次點擊的時間戳都儲存在 Cloud Firestore 中,並且顯示的計數是從儲存的時間戳的數量中推導出來的。您將使用 Provider 和 ChangeNotifier 保持依賴項和狀態管理程式碼乾淨,並且您將更新生成的測試以保持程式碼的 正確性!
開始之前
本文假設您已 觀看並按照此教學中的步驟操作 將您的應用程式與 Firebase 整合。簡要說明一下:
- 建立一個新的 Flutter 專案,並命名為 firebasecounter。
- 在 Firebase 主控台 中建立一個 Firebase 應用程式。
- 將您的應用程式連結到 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 | class CounterManager { |
在程式碼就位後,將以下行新增到 firebasecounter/lib/main.dart
的頂部:
1 | import 'package:firebasecounter/counter_manager.dart'; |
然後,將 _MyHomePageState
的程式碼更改為以下內容:
1 | class _MyHomePageState extends State<MyHomePage> { |
儲存此程式碼更改後,您的應用程式可能會顯示崩潰並顯示一個紅色的錯誤螢幕。這是因為您引入了一個新的變數 manager
,它初始化的機會已經過了。當您更改狀態的 初始化方式 時,這在 Flutter 中很常見,並且可以使用熱重新啟動輕鬆解決。
熱重新啟動後,您應該回到開始的地方:計數為 0,並且可以根據需要點擊浮動動作按鈕。
現在是運行 Flutter 在任何新的專案中提供的單一測試的好時機。您可以在 test/widget_test.dart
中找到它的定義,並透過運行以下命令執行它:
1 | $ flutter test |
假設測試通過,您就可以繼續進行!
注意:如果您在此部分遇到問題,請將您的更改與教學儲存庫中的 [這個提交](https://github.com/craiglabenz/flutter-firestore-counter/commit/483dd3b3833bf710b04db4a3ba347b1d1ecbe5de) 進行比較。
持續時間戳
初始應用程式描述中提到了持續儲存每次點擊的時間戳。到目前為止,您尚未添加任何基礎架構來滿足第二個要求,因此請建立另一個名為 app_state.dart
的新檔案,並添加以下類別:
1 | /// 應用程式狀態的完整容器。 |
從現在開始,AppState
類別的工作是表示應該渲染的內容的狀態。該類別不包含任何可以修改自身的方法,只有一個單獨的 copyWith
方法,其他類別將使用它。
牢記測試,您可以開始更改 CounterManager
概念。從長遠來看,使用單一類別將行不通,因為應用程式最終會與 Cloud Firestore 進行互動。但是,您不想在每次運行測試時都建立真實的記錄。為此,您需要一個抽象介面來定義應用程式應該如何表現。
再次打開 counter_manager.dart
,並在檔案的頂部添加以下程式碼:
1 | import 'package:firebasecounter/app_state.dart'; |
下一步是更新 CounterManager
以顯式地從 ICounterManager
繼承。將其定義更新為以下內容:
1 | class CounterManager implements ICounterManager { |
在這個階段,我們的輔助程式碼看起來已經很好了,但是 main.dart
落後了。main.dart
中沒有 ICounterManager
的參考,而事實上,它是它應該知道的 唯一 的 Manager
類別。在 main.dart
中,更新並應用以下更改:
- 將遺漏的匯入新增到
main.dart
的頂部:
1 | import 'package:firebasecounter/app_state.dart'; |
- 將
_MyHomePageState
更新為以下內容:
1 | class _MyHomePageState extends State<MyHomePage> { |
此更改應該會移除 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 | dependencies: |
在將該行添加到您的 pubspec.yaml
檔案後,運行以下命令將 Provider 下載到您的機器上:
1 | $ flutter pub get |
在 main.dart
旁邊,建立一個名為 dependencies.dart
的新檔案,並將以下程式碼複製到其中:
1 | import 'package:firebasecounter/counter_manager.dart'; |
關於 DependenciesProvider
的一些注意事項:
- 它使用
MultiProvider
,儘管它的列表中只有一個條目。從技術上講,這可以摺疊為一個單獨的Provider
Widget,但是實際的應用程式可能會包含許多這樣的服務,因此最好從一開始就使用MultiProvider
。 - 它需要一個子 Widget,它遵循 Flutter 中 Widget 組合的慣例,允許我們將此輔助程式碼插入 Widget 樹的頂部,從而使
ICounterManager
實例可用於整個應用程式。
接下來,讓新的 DependenciesProvider
可用於整個應用程式。一種簡單的方法是使用它來包裝整個 MaterialApp
Widget。打開 main.dart
,並將 main
方法更新為如下所示:
1 | void main() { |
您還需要在 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 | // 在頂部添加此匯入 |
此外,在 main.dart
的頂部匯入 Provider
:
1 | import 'package:provider/provider.dart'; |
將 MyHomePage
包裹在 Consumer
Widget 中,允許您到達 Widget 樹中任意高的地方,以存取所需的資源,並將它們注入到需要它們的 Widget 中。在本教學中,這可能感覺沒有必要的工作,因為您只向上到達 MyApp()
一層,但是這在實際的生產應用程式中可能會透過數十個 Widget 延伸。
接下來,在同一個檔案中,對 MyHomePage
進行以下編輯:
注意:如果在儲存此更改後您看到紅色螢幕,請不要擔心。需要更多編輯才能完成重構!
1 | class MyHomePage extends StatefulWidget { |
這個簡單的建構函式更改允許程式碼接受在先前程式碼片段中傳入的變數。
最後,透過對 _MyHomePageState
進行以下編輯來完成重構:
1 | class _MyHomePageState extends State<MyHomePage> { |
注意:您可能需要執行熱重新啟動來修復您的應用程式。
如您所知,所有 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 | import 'package:firebasecounter/counter_manager.dart'; |
透過直接使用 MyHomePage
實例(以及一個包裝的 MaterialApp
來提供有效的 BuildContext
物件),您已經為自己建立了與 Cloud Firestore 的單元測試整合!
注意:如果您在此部分遇到問題,請將您的更改與教學儲存庫中的 [這個提交](https://github.com/craiglabenz/flutter-firestore-counter/commit/bb68c1d3bb3746eca5f2dea16bd799c98ff232f1) 進行比較。
實作 Cloud Firestore
到目前為止,您已經移動了很多程式碼,並引入了幾個輔助類別,但是您還沒有更改應用程式的任何工作方式。好消息是,一切都已就位,可以開始撰寫一些了解 Cloud Firestore 的程式碼。首先,打開 pubspec.yaml
,並添加以下兩行:
1 | dependencies: |
與往常一樣,當您對 pubspec.yaml
進行更改時(除非您的 IDE 替您完成此操作),請運行以下命令來下載和連結新的函式庫:
1 | $ flutter pub get |
注意:如果您尚未建立資料庫:請訪問 Firebase 主控台的專案,點擊 **Cloud Firestore** 標籤,然後點擊 **建立資料庫** 按鈕。
等待 Firebase
成功使用 Cloud Firestore 的第一步是初始化 Firebase,最關鍵的是 在任務成功之前不要嘗試使用任何 Firebase 資源。幸運的是,您可以使用一個 StatefulWidget 來包含該邏輯,而不是將該任務散佈在整個程式碼中。
在 firebasecounter/lib/firebase_waiter.dart
中建立一個新檔案,並添加以下程式碼:
1 | import 'package:firebase_core/firebase_core.dart'; |
此類別使用 Flutter 中的模式,利用特定 Widget 來完全處理應用程式中的特定依賴項或問題。要使用此 FirebaseWaiter
Widget,請返回 main.dart
,並對 MyApp
應用以下更改:
1 | // 在頂部添加此匯入 |
現在,應用程式可以等待 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 | class FirestoreCounterManager implements ICounterManager { |
注意:此類別幾乎是正確的,但它會建立一個稍後會探討的錯誤。如果您現在將此程式碼添加到您的應用程式中並運行它,您將會看到行為與您想要的不同。請繼續閱讀,以了解詳細的解釋!
這裡有很多事情正在發生,因此讓我們一步一步地進行。
首先,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 | import 'package:firebasecounter/counter_manager.dart'; |
診斷錯誤
如果您按原樣運行此程式碼,您 幾乎 會看到所需的行為。一切都看起來正確,除了螢幕始終落後於實際點擊次數一次。這是怎麼回事?
問題出現在初始計數器實作和當前基於流的實作之間的不相容性。浮動動作按鈕的 onPressed
處理器看起來像這樣:
1 | floatingActionButton: FloatingActionButton( |
該處理器調用 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 | // 將 `extends ChangeNotifier` 添加到您的宣告中 |
如果您尚未匯入 flutter/material.dart
,請打開 firebasecounter/lib/counter_manager.dart
,並將其添加到底部:
1 | import 'package:flutter/material.dart'; |
您現在已準備好更新 CounterManager
和 FirestoreCounterManager
的定義。對於 CounterManager
,請將其程式碼替換為以下實作:
1 | class CounterManager extends ChangeNotifier implements ICounterManager { |
對於 FirestoreCounterManager
,應用以下更改:
- 修改它的簽章以匹配以下內容:
1 | class FirestoreCounterManager extends ChangeNotifier |
- 將相同的
notifyListeners();
行添加到_watchCollection()
的末尾,如下所示:
1 | void _watchCollection() { |
您現在已經建立了讓 ICounterManager
類別告訴 Widget 何時重新渲染的必要更改的一半。管理員類別正在告訴 Widget 重新渲染,但是如果您現在運行您的應用程式,您將會看到 Widget 沒有在監聽。
要修復此問題,請打開 dependencies.dart
,並將 DependenciesProvider
的實作替換為以下內容:
1 | class DependenciesProvider extends StatelessWidget { |
作為最後的更改,請從 _MyHomePageState
中移除 setState()
,以跳過不必要的重新渲染。將其 FloatingActionButton
更新為如下所示:
1 | floatingActionButton: FloatingActionButton( |
就這樣!ChangeNotifierProvider
確保 Widget 是「監聽器」,因此當 ICounterManager
類別調用 notifyListeners()
時,Widget 會收到重新渲染的訊息。
在這個階段,您應該能夠熱重新啟動您的應用程式,並看到所有內容都正常工作!
注意:如果您在此部分遇到問題,請將您的更改與公開儲存庫中的 這個提交 進行比較。