# 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**

**3. 建立Firebase項目並啟用身分驗證中的電子郵件/密碼**
## 271.Adding an Authentication Screen (新增驗證畫面)
**1. 新增screens資料夾**
**2. 在screens中建立auth.dart檔案**
**3. 新增一個assets/images資料夾,將chat.png放入**
**4. 修改pubspec.yaml(注意縮排位置)**

**5. 於auth.dart中放入圖片**

**6. 完成輸入框**

**7. 在main.dart中導入auth.dart並修改home:**

## 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新變量,用來控制是否為登入模式**

**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. 修改登入按鈕樣式**

## 273.Validating User Input (驗證使用者輸入)
**1. 處理帳號輸入框的驗證功能**

**2. 處理密碼輸入框的驗證功能**

**3. 新建一個全局的表單鍵及建立一個觸發驗證功能的方法**

**4. 透過表單鍵訪問表單**

**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. 新建儲存電子郵件及密碼的變數**

**7. 在表單中儲存電子郵件及密碼**

## 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()**

## 276.Signing Users UP (註冊用戶)
### **修改註冊模式**
**1. 於auth.dart中導入firebase_auth並新增一個全局變量,用於處理用戶身份驗證相關的操作**

**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 (登入用戶)
**修改登入邏輯**

## 278.Showing Different Screens Based On The Authentication State (根據身份驗證狀態顯示不同的螢幕)
**1. 新增chat.dart**

**2. 修改main.dart中的home**

## 279.Adding a Splash Screen(Loading Screen) (新增啟動畫面(載入畫面))
**1. 新增splash.dart**

**2. 在StreamBuilder中進行修改**

## 280.Adding User Logout (新增用戶登出)
**在chat.dart中新增Signout功能**

## 281.Image Upload: Setup & First Steps (圖片上傳:設定與第一步)
**1. 至Firebase網站開啟Storage功能,並設置權限為登入者才能存取**

**2. 於終端機安裝firebase_storage套件**

**3. 於終端機安裝image_picker套件**

## 282.Adding a User Image Picker Widget (新增使用者圖像選擇小工具)
**1. 新增widgets資料夾**
**2. 於資料夾中新增user_image_picker.dart**

## 283.Using the ImagePicker Package (使用ImagePicker套件)
### 修改user_image_picker.dart
**1. 新增選擇圖片的方法及儲存選擇圖片的方法**

**2. 修改CircleAvatar()及TextButton.icon()**

### 將user_image_picker.dart添加到auth.dart中

## 284.Managing The Selected Image In The Authentication Form (管理認證表單中選擇的圖像)
**1. 於auth.dart中新增File型別的屬性**

**2. 於user_image_picker.dart中新增一個函數並實例化屬性**

**3. 修改選擇圖像的方法**

**4. 修改auth.dart中關於UserImagePicker的部分**

**5. 修改_submit函數**

## 285.Uploading Images To Firebase (將圖片上傳至Firebase)
**於_sumit中註冊新用戶的程式碼中新增上傳圖片到FirebaseStroage的方法**

## 286.Showing a Loading Spinner Whilst Uploading (上傳時顯示載入微調器)
**1. 建立變數(初始值為false)用以檢查是否上傳中**

**2. 於_sumit中將該變數變更為true**

**3. 於按鈕前添加檢查該變數,若變數為true時顯示圓形進度指示器**

**4. 於發生錯誤時,將該變數變更為false,讓使用者可以正常繼續操作**

## 287.Adding a Remote Database: Firestore Setup (新增遠端資料庫:Firestore設定)
**1. 使用Firestore Database**
**2. 設置Firestore Database存取規則**

**3. 安裝cloud_firestore套件**

## 288.Sending Data to Firebase (向Firestore發送數據)
**1. 導入cloud_firestore.dart**
```dart=
import 'package:cloud_firestore/cloud_firestore.dart'
```
**2. 將數據儲存至firestore中**

**這邊需要將虛擬機程式重新啟動**
**若發生錯誤則需查看錯誤訊息,才能決定如何除錯,在app/build.gradle中添加multiDexEnabled true**

## 289.Storing a Username (儲存用戶名)
**1. 於auth.dart中新增儲存使用者名稱的變數**

**2. 於auth.dart中新增輸入使用者名稱的表單(於註冊模式下才顯示)**

**3. 將使用者名稱上傳至firestore**

## 290.Adding ChatMessages & Input (新增聊天訊息和輸入小工具)
**1. 在widgets資料夾中新增chat_messages.dart**

**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中**

## 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函數**

**2. 按下按鈕後關閉鍵盤**

## 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**

**2. 添加reverse將數據從底部開始顯示(因此要將先前的升序排序改為降序排序,如圖二)**


圖二
**3. 在widgets資料夾中新增message_bubble.dart檔案並將資源中的程式碼輸入**
**4. 新增authenticatedUser變數用以以儲存從Firebase認證中獲取當前已認證用戶**

**5. 修改ListView中的itemBuilder**

## 295.Push Notifications - Setup & First Steps (推播通知-設定第一步)
**1. 蘋果設定(安卓不須設定)**
**2. 於終端機安裝firebase_messaging套件**

## 296.Requesting Permissions & Getting an Address Token (請求權限並取得地址令牌)
**1. 將chat.dart由StatelessWidget轉為StatefulWidget**
**2. 新建函數用以建立FirebaseMessaging實例,並請求推送通知權限及獲取推送通知令牌**

## 297.Testing Push Notifications (測試推播通知)
## 298.Working with Notification Topics (使用通知主題)
**修改setupPushNotifications函數**

## 299.Sending Push Notifications Automatically via Cloud Functions (透過雲端功能自動發送推播通知)
## 300.Module Summary (模組摘要)