# Push Notifications & More: Building a Chat App with Flutter & Firebase (推播通知及更多:使用Flutter及Firebase建立聊天應用程式) ## 269.Module Introduction (模組介紹) **1. 身分驗證** **2. 圖片上傳** **3. 推送通知** **4. 通過SDK連接後端** ## 270.App & Firebase Setup (應用程式及Firebase設定) **1. 建立Chat App** **2. 設定main.dart** ![5](https://hackmd.io/_uploads/B1VDJ7mpa.jpg) **3. 建立Firebase項目並啟用身分驗證中的電子郵件/密碼** ## 271.Adding an Authentication Screen (新增驗證畫面) **1. 新增screens資料夾** **2. 在screens中建立auth.dart檔案** **3. 新增一個assets/images資料夾,將chat.png放入** **4. 修改pubspec.yaml(注意縮排位置)** ![1](https://hackmd.io/_uploads/S1df0Mm6T.jpg) **5. 於auth.dart中放入圖片** ![2](https://hackmd.io/_uploads/SkhpDG76a.jpg) **6. 完成輸入框** ![3](https://hackmd.io/_uploads/By7w0G7Tp.jpg) **7. 在main.dart中導入auth.dart並修改home:** ![4](https://hackmd.io/_uploads/HkafJmQpp.jpg) ## 272.Adding Buttons & Modes to the Authentication Screen (將按鈕和模式新增至身份驗證畫面) **1. 在auth.dart中添加2個按鈕** ```dart= const SizedBox(height: 12.0), // 用於登入的按鈕 ElevatedButton( onPressed: () {}, child: const Text('Signup'), ), // 用於註冊的按鈕 TextButton( onPressed: () {}, child: const Text('Create an account'), ), ``` **2. 建立一個型別為boolean新變量,用來控制是否為登入模式** ![1](https://hackmd.io/_uploads/r1ELKZV6a.jpg) **3. 修改登入及註冊的按鈕** ```dart= // 用於登入的按鈕 ElevatedButton( onPressed: () {}, child: Text(_isLogin ? 'Login' : 'Signup'), ), // 用於註冊的按鈕 TextButton( onPressed: () { setState(() { // _isLogin = _isLogin ? false : true; // 可簡化如下 _isLogin = !_isLogin; }); }, child: Text(_isLogin ? 'Create an account' : 'I already have an account'), ), ``` **4. 修改登入按鈕樣式** ![2](https://hackmd.io/_uploads/B1drKbVaT.jpg) ## 273.Validating User Input (驗證使用者輸入) **1. 處理帳號輸入框的驗證功能** ![1](https://hackmd.io/_uploads/ByzatbNpT.jpg) **2. 處理密碼輸入框的驗證功能** ![2](https://hackmd.io/_uploads/H1GZs-46T.jpg) **3. 新建一個全局的表單鍵及建立一個觸發驗證功能的方法** ![1](https://hackmd.io/_uploads/BJF0XM4Tp.jpg) **4. 透過表單鍵訪問表單** ![1](https://hackmd.io/_uploads/rkc340La6.jpg) **5. 完善觸發驗證功能的方法** ``` dart= void _sumit() { // 建立一個變數isValue,如果_form.currentState不為null則isValue為true // 因為這個方法是透過按鈕觸發驗證功能,所以只要能通過驗證功能就一定不會為null // 因此_form.currentState後面會加上! final isValid = _form.currentState!.validate(); // 如果isValue為true則儲存_form當前狀態 if (isValid) { _form.currentState!.save(); } } ``` **6. 新建儲存電子郵件及密碼的變數** ![1](https://hackmd.io/_uploads/rJpDLf4Tp.jpg) **7. 在表單中儲存電子郵件及密碼** ![2](https://hackmd.io/_uploads/ryvG_zEpT.jpg) ## 274.Firebase CLI & SDK Setup 1/2 (Firebase CLI及SDK設定1/2) ### **安裝Firebase CLI** **windows直接下載安裝登入後在vscode中會無法正常使用flutterfire configure 需要安裝Node.js,即可在vscode中的終端機直接使用npm install -g firebase-tools** ## 275.Firebase CLI & SDK Setup 2/2 (Firebase CLI及SDK設定2/2) **1. 執行flutterfire configure** **2. 執行flutter pub add firebase_core** **3. 執行flutter pub add firebase_auth** **4. 再執行一次flutterfire configure(確保Firebase設置保持最新狀態)** **5. 於main.dart中導入firebase套件並修改main()** ![1](https://hackmd.io/_uploads/r1dCtr4TT.jpg) ## 276.Signing Users UP (註冊用戶) ### **修改註冊模式** **1. 於auth.dart中導入firebase_auth並新增一個全局變量,用於處理用戶身份驗證相關的操作** ![1](https://hackmd.io/_uploads/HJ6QDPEpT.jpg) **2. 修改void _sumin(){}** ```dart= void _sumit() async { // 建立一個變數isValue,如果_form.currentState不為null則isValue為true // 因為這個方法是透過按鈕觸發驗證功能,所以只要能通過驗證功能就一定不會為null // 因此_form.currentState後面會加上! final isValid = _form.currentState!.validate(); // 如果isValue為false則返回 if (!isValid) { return; } // 如果isValue為true則儲存_form當前狀態 _form.currentState!.save(); // 檢查是登入還是註冊 if (_isLogin) { // 登入邏輯 } else { try { // 使用firebase註冊新用戶 final userCredentials = await _firebase.createUserWithEmailAndPassword( email: _enteredEmail, password: _enteredPassword, ); // 處理firebase身分驗證異常 } on FirebaseAuthException catch (error) { // 如果郵件已經在使用中的情況 if (error.code == 'email-already-in-use') { //... } // 使用Snackbar顯示錯誤消息 ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(error.message ?? 'Authentication failed.'), ), ); } } } ``` ## 277.Logging Users In (登入用戶) **修改登入邏輯** ![1](https://hackmd.io/_uploads/rJ21UFFa6.jpg) ## 278.Showing Different Screens Based On The Authentication State (根據身份驗證狀態顯示不同的螢幕) **1. 新增chat.dart** ![1](https://hackmd.io/_uploads/B1sQUFFTT.jpg) **2. 修改main.dart中的home** ![1](https://hackmd.io/_uploads/HJHyqFFa6.jpg) ## 279.Adding a Splash Screen(Loading Screen) (新增啟動畫面(載入畫面)) **1. 新增splash.dart** ![1](https://hackmd.io/_uploads/rkJg2KKaT.jpg) **2. 在StreamBuilder中進行修改** ![1](https://hackmd.io/_uploads/S1XO3YYTT.jpg) ## 280.Adding User Logout (新增用戶登出) **在chat.dart中新增Signout功能** ![1](https://hackmd.io/_uploads/HkYahRKpa.jpg) ## 281.Image Upload: Setup & First Steps (圖片上傳:設定與第一步) **1. 至Firebase網站開啟Storage功能,並設置權限為登入者才能存取** ![1](https://hackmd.io/_uploads/B1LA2knTp.jpg) **2. 於終端機安裝firebase_storage套件** ![1](https://hackmd.io/_uploads/r1YuTy3ap.jpg) **3. 於終端機安裝image_picker套件** ![1](https://hackmd.io/_uploads/BkQZAk3p6.jpg) ## 282.Adding a User Image Picker Widget (新增使用者圖像選擇小工具) **1. 新增widgets資料夾** **2. 於資料夾中新增user_image_picker.dart** ![1](https://hackmd.io/_uploads/By4M-ghpT.jpg) ## 283.Using the ImagePicker Package (使用ImagePicker套件) ### 修改user_image_picker.dart **1. 新增選擇圖片的方法及儲存選擇圖片的方法** ![1](https://hackmd.io/_uploads/r1ZHqgn6a.jpg) **2. 修改CircleAvatar()及TextButton.icon()** ![1](https://hackmd.io/_uploads/HJ2AYg36T.jpg) ### 將user_image_picker.dart添加到auth.dart中 ![1](https://hackmd.io/_uploads/BktT9gn6a.jpg) ## 284.Managing The Selected Image In The Authentication Form (管理認證表單中選擇的圖像) **1. 於auth.dart中新增File型別的屬性** ![1](https://hackmd.io/_uploads/r1QkQQ36p.jpg) **2. 於user_image_picker.dart中新增一個函數並實例化屬性** ![1](https://hackmd.io/_uploads/ByECQQ3ap.jpg) **3. 修改選擇圖像的方法** ![1](https://hackmd.io/_uploads/Byx44QnT6.jpg) **4. 修改auth.dart中關於UserImagePicker的部分** ![1](https://hackmd.io/_uploads/BkqNB7hpT.jpg) **5. 修改_submit函數** ![1](https://hackmd.io/_uploads/HyRpBNn6p.jpg) ## 285.Uploading Images To Firebase (將圖片上傳至Firebase) **於_sumit中註冊新用戶的程式碼中新增上傳圖片到FirebaseStroage的方法** ![1](https://hackmd.io/_uploads/Sk3w5wqAa.jpg) ## 286.Showing a Loading Spinner Whilst Uploading (上傳時顯示載入微調器) **1. 建立變數(初始值為false)用以檢查是否上傳中** ![1](https://hackmd.io/_uploads/HJT3OOcCa.jpg) **2. 於_sumit中將該變數變更為true** ![1](https://hackmd.io/_uploads/ryV4F_c06.jpg) **3. 於按鈕前添加檢查該變數,若變數為true時顯示圓形進度指示器** ![1](https://hackmd.io/_uploads/ByCv7icC6.jpg) **4. 於發生錯誤時,將該變數變更為false,讓使用者可以正常繼續操作** ![1](https://hackmd.io/_uploads/H1UEjOqC6.jpg) ## 287.Adding a Remote Database: Firestore Setup (新增遠端資料庫:Firestore設定) **1. 使用Firestore Database** **2. 設置Firestore Database存取規則** ![1](https://hackmd.io/_uploads/BJduY5qCT.jpg) **3. 安裝cloud_firestore套件** ![1](https://hackmd.io/_uploads/rJJOcccRp.jpg) ## 288.Sending Data to Firebase (向Firestore發送數據) **1. 導入cloud_firestore.dart** ```dart= import 'package:cloud_firestore/cloud_firestore.dart' ``` **2. 將數據儲存至firestore中** ![1](https://hackmd.io/_uploads/SJp_C59RT.jpg) **這邊需要將虛擬機程式重新啟動** **若發生錯誤則需查看錯誤訊息,才能決定如何除錯,在app/build.gradle中添加multiDexEnabled true** ![1](https://hackmd.io/_uploads/B1h1Ms90p.jpg) ## 289.Storing a Username (儲存用戶名) **1. 於auth.dart中新增儲存使用者名稱的變數** ![1](https://hackmd.io/_uploads/BkWv_jq0a.jpg) **2. 於auth.dart中新增輸入使用者名稱的表單(於註冊模式下才顯示)** ![1](https://hackmd.io/_uploads/rJNhOoc0p.jpg) **3. 將使用者名稱上傳至firestore** ![1](https://hackmd.io/_uploads/S10JKicCT.jpg) ## 290.Adding ChatMessages & Input (新增聊天訊息和輸入小工具) **1. 在widgets資料夾中新增chat_messages.dart** ![1](https://hackmd.io/_uploads/Bk-iGT9Ap.jpg) **2. 在widgets資料夾中新增new_messages.dart** ```dart= import 'package:flutter/material.dart'; class NewMessages extends StatefulWidget { const NewMessages({super.key}); @override State<NewMessages> createState() => _NewMessagesState(); } class _NewMessagesState extends State<NewMessages> { // 用於控制文本輸入框的變數 var _messageController = TextEditingController(); @override void dispose() { _messageController.dispose(); super.dispose(); } // 取得文本輸入框內的訊息內容 void _submitMessage() { final enteredMessage = _messageController.text; // 如果enteredMessage為空則返回 if (enteredMessage.trim().isEmpty) { return; } // send to Firebase // 清除文本輸入框的內容 _messageController.clear(); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only( left: 15.0, right: 1.0, bottom: 14.0, ), child: Row( children: [ Expanded( child: TextField( controller: _messageController, // 設置首字母大寫 textCapitalization: TextCapitalization.sentences, // 啟用自動修正 autocorrect: true, // 啟用輸入建議 enableSuggestions: true, decoration: const InputDecoration(labelText: 'Send a message...'), ), ), IconButton( color: Theme.of(context).colorScheme.primary, icon: const Icon( Icons.send, ), onPressed: _submitMessage, ), ], ), ); } } ``` **3. 將ChatMessages及NewMessages放入chat.dart中** ![1](https://hackmd.io/_uploads/HJnt4p506.jpg) ## 291.A Note About Reading Data From Firestore (關於從Firestore讀取資料的注意事項) **如果在Firestore讀取資料時發生問題,請參考下列QA** **1. https://www.udemy.com/course/learn-flutter-dart-to-build-ios-android-apps/learn/lecture/37736704#questions/19981674** **2. https://www.udemy.com/course/learn-flutter-dart-to-build-ios-android-apps/learn/lecture/37736700#questions/19980322** ## 292.Sending & Reading Data To & From Firestore (向Firestore發送資料以及從Firestore讀取數據) **1. 修改new_message.dart中的_submitMessage函數** ![1](https://hackmd.io/_uploads/r1vTjtf1A.jpg) **2. 按下按鈕後關閉鍵盤** ![1](https://hackmd.io/_uploads/HyHLTYGyC.jpg) ## 293.Loading & Displaying Chat Message as a Stream (以Stream的形式載入和顯示聊天訊息) **修改chat_messages.dart** ```dart= import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; class ChatMessages extends StatelessWidget { const ChatMessages({super.key}); @override Widget build(BuildContext context) { return StreamBuilder( // 監聽FirebaseFirestore中chat集合的數據 // 按照createdAt進行升序排序 stream: FirebaseFirestore.instance .collection('chat') .orderBy( 'createdAt', descending: false, ) .snapshots(), // 依照數據狀態建構不同的Widget builder: (ctx, chatSnapShots) { // 如果連接狀態為等待,顯示進度指示器 if (chatSnapShots.connectionState == ConnectionState.waiting) { return const Center( child: CircleAvatar(), ); } // 如果沒有數據或chat集合中沒有消息,顯示No messages found. if (!chatSnapShots.hasData || chatSnapShots.data!.docs.isEmpty) { return const Center( child: Text('No messages found.'), ); } // 如果出現錯誤,顯示錯誤提示 if (chatSnapShots.hasError) { return const Center( child: Text('Somethong went wrong...'), ); } // 從快照中獲取加載的消息列表 final loadedMessages = chatSnapShots.data!.docs; // 建構一個ListView來顯示加載的消息 return ListView.builder( itemCount: loadedMessages.length, itemBuilder: (ctx, index) => Text( loadedMessages[index].data()['text'], ), ); }, ); } } ``` ## 294.Stying Chat Message Bubbles (設計聊天訊息氣泡的樣式) #### **修改chat_messages.dart** **1. 添加padding** ![1](https://hackmd.io/_uploads/Bkp4QjfkC.jpg) **2. 添加reverse將數據從底部開始顯示(因此要將先前的升序排序改為降序排序,如圖二)** ![1](https://hackmd.io/_uploads/BkHdNsMy0.jpg) ![1](https://hackmd.io/_uploads/r1gsEsG1C.jpg) 圖二 **3. 在widgets資料夾中新增message_bubble.dart檔案並將資源中的程式碼輸入** **4. 新增authenticatedUser變數用以以儲存從Firebase認證中獲取當前已認證用戶** ![1](https://hackmd.io/_uploads/ryfMG3fk0.jpg) **5. 修改ListView中的itemBuilder** ![1](https://hackmd.io/_uploads/r1_Dz3GJC.jpg) ## 295.Push Notifications - Setup & First Steps (推播通知-設定第一步) **1. 蘋果設定(安卓不須設定)** **2. 於終端機安裝firebase_messaging套件** ![1](https://hackmd.io/_uploads/Bk_5F3MJC.jpg) ## 296.Requesting Permissions & Getting an Address Token (請求權限並取得地址令牌) **1. 將chat.dart由StatelessWidget轉為StatefulWidget** **2. 新建函數用以建立FirebaseMessaging實例,並請求推送通知權限及獲取推送通知令牌** ![1](https://hackmd.io/_uploads/SJ8YLRGyC.jpg) ## 297.Testing Push Notifications (測試推播通知) ## 298.Working with Notification Topics (使用通知主題) **修改setupPushNotifications函數** ![1](https://hackmd.io/_uploads/H14VAAGJ0.jpg) ## 299.Sending Push Notifications Automatically via Cloud Functions (透過雲端功能自動發送推播通知) ## 300.Module Summary (模組摘要)