你是否曾經以為,Firebase Cloud Messaging (FCM) 就只是個用來發送「您的訂單已出貨」這類通知的工具?
今天我們要來玩點不一樣的!我們要挑戰不使用傳統的 WebSocket 或 Realtime Database,僅僅依靠 FCM 本身,來實作一個即時聊天室。
這篇教學的核心概念在於混合利用 FCM 的兩種訊息類型:「通知 (Notifications)」 與 「數據 (Data Payloads)」,來傳遞聊天內容,讓 App 在背景靜默接收並觸發 UI 更新,達成看似即時通訊的效果。
我們的架構非常簡單暴力:< Client A 發送 ➔ 呼叫 FCM API ➔ FCM Server 廣播 ➔ Client B 接收並刷新畫面 >

最後的成果就會像是⋯⋯

事前準備
- Firebase 帳號
- Flutter 開發環境
如果以上都準備好了,那接下來跟著一步步執行,最後就可以得到簡單的無伺服器聊天室囉😎
專案建置
在開始撰寫聊天邏輯前,我們需要先打好基礎!這個步驟會將你的 Flutter 專案與 Firebase 雲端服務連結,並安裝必要的工具包。請跟著以下步驟完成基礎建設:
- 建立 Firebase 專案:在 Firebase Console 點擊「建立新的 Firebase 專案」依照指示完成建立。
- 環境工具準備:為了讓 Flutter 與 Firebase 溝通,你需要確保電腦已安裝 Firebase CLI。詳細可參考官方文件Firebase CLI 參考資料,若已安裝可略過。
- 配置設定檔:有了 CLI 後,我們不需要手動去各個平台複製貼上。在 Flutter 專案的根目錄中執行以下指令,完成後系統會自動透過 Firebase 註冊每個平台專屬的應用程式,並將設定檔新增至 Flutter 專案中。
$ flutterfire configure --project="YOUR_PROJECT_ID"
- 安裝 Library:在 Flutter 專案目錄中執行下列指令,安裝本次專案所需的 Library
$ flutter pub add firebase_core
$ flutter pub add firebase_messaging
$ flutter pub add http
- 初始化 Firebase:建置的最後一個步驟,我們需要在 main 方法中使用設定檔匯出的 DefaultFirebaseOptions 物件初始化 Firebase
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
💡 跟著以上步驟建置完成後,你的專案就已經具備雲端通訊的能力了!下一步,我們要先做一個簡單的測試,利用 FCM 發送測試訊息,確認通知通道是否暢通。
測試通知
我們怎麼確認手機是不是真的可以接收到通知呢?正常接收到通知的情況要如下圖這樣,在手機的通知中心顯示出我們定義的測試通知。要達到這樣的效果需要先在 Flutter 端進行權限請求及 FCM Token 取得,接著透過 Firebase Console 的 Messaging 所提供「傳送測試訊息」功能來向 FCM Token 發送通知。話不多說,繼續跟著我們的步伐完成測試吧!

Flutter 端實作
- 通知權限請求:FCM 已經幫我們包好權限請求的方法了,直接呼叫即可
// 在你的 Flutter 程式碼初始化的位置 (例如 main.dart 或聊天室頁面的 initState)
FirebaseMessaging messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
- 取得 FCM Token:由於 FCM 測試通知需要指定 Token ,才能發送測試通知,所以我們要先印出 FCM Token 備用
String? token = await messaging.getToken();
// 取得 FCM Token 並列印出來,方便測試
debugPrint("FCM Token: $token");
Firebase 發送測試通知
Flutter 端完成實作並取得 Token 後,接著就讓我們移動到 Firebase Console 進行操作。





如果您的手機順利跳出了通知,恭喜您,這代表 Firebase 的基礎連線與憑證設定已經大功告成!
然而,手動發送通知僅僅是驗證環境。要將其轉化為自動化的聊天應用,我們需要將原本由「人工操作」的部分,轉交給「程式碼」來處理。這時候,我們面臨了三個核心的技術挑戰:
- 挑戰一:怎麼在 Flutter接收到通知訊息?我的 Flutter 程式碼要怎麼攔截到這則訊息並把它變成聊天氣泡?
- 挑戰二:怎麼指定接收的對象?聊天總要有對象,我該怎麼指定這則訊息要傳給誰?是傳給所有人還是特定的人?
- 挑戰三:怎麼發送自己的訊息?我總不能每次聊天都開 Firebase 後台吧?我要怎麼在手機上發送自己的訊息?
接下來的內容,我們將針對這三個挑戰提供對應的實作與關鍵程式碼。
首先,讓我們先從最基礎、也最重要的功能開始:如何讓 Flutter App 「知道」進來的訊息。
挑戰一:怎麼在 Flutter 接收到通知訊息呢?
還記得我們一開始說過的核心概念嗎?我們需要讓 App 在背景靜默接收通知訊息,並觸發 UI 更新。
在 Flutter 裡,Firebase Messaging 提供了幾個關鍵的監聽器 (Listeners)。為了讓聊天室能即時運作,我們最需要關注的是 onMessage,它專門負責處理「App 在前景 (Foreground)」時收到的訊息。
實作邏輯:
// 在你的 Flutter 程式碼初始化的位置 (例如 main.dart 或聊天室頁面的 initState)
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('收到訊息了!');
// 記得我們前面說的嗎?我們要看的是 'data' 欄位
if (message.data.isNotEmpty) {
String user = message.data['sender'];
String text = message.notification!.body;
print('發送者: $user');
print('內容: $text');
// TODO: 在這裡呼叫 setState() 把這則訊息加到你的聊天列表中
}
});
💡 小撇步: onMessage 只能監聽 App 在「前景」時的訊息。如果你希望 App 在背景時也能收到並處理(例如顯示一個自訂的通知),你會需要設定 onBackgroundMessage,但在這個聊天室教學中,我們先專注在前景的即時互動。
挑戰二:怎麼指定接收的對象呢?
訊息能收到了,但問題是⋯⋯ 這則訊息該傳給誰?是傳給你?傳給我?還是傳給所有人?
在 FCM 的世界裡,有兩種主要方式:
- Token (權杖):像是身分證字號,指定傳給「某一台特定的手機」。
- Topic (主題):像是廣播頻道,傳給「所有訂閱這個頻道的人」。
為了讓我們的聊天室架構最簡單(不需要資料庫來記每個人的 Token),我們採用 Topic (主題) 機制。想像我們開了一個名為 general_chat 的房間,只要進入 App 的人,就自動訂閱這個房間。訂閱之後,即可接收該主題的所有訊息。
實作邏輯: 在 App 啟動或進入聊天頁面時,直接幫使用者訂閱主題。
void setupChatRoom() async {
// 訂閱一個叫做 'general_chat' 的主題
// 只要有人對 'general_chat' 發送訊息,就會收到
await FirebaseMessaging.instance.subscribeToTopic('general_chat');
print('已加入聊天室頻道!');
}
挑戰三:怎麼發送自己的訊息呢?
最後一塊拼圖:我們如何把輸入框裡的文字發送出去?
通常這需要一個後端伺服器,但為了讓這個教學極致輕量化,我們將使用 HTTP 請求 直接從手機呼叫 FCM 的 API。
重要提醒
真實的上架產品中,將 Access Token 放在 App 原始碼是非常危險的。但作為學習 FCM 原理的專案,這是最快能看到成果的方式!
實作邏輯: 我們使用 http 套件,發送一個 POST 請求給 Google。
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> _sendMessageToTopic(String text) async {
const String projectId = '你的_PROJECT_ID_填在這裡'; // 從 firebase_options.dart 取得
// FCM 的發送網址
const String fcmUrl = 'https://fcm.googleapis.com/v1/projects/$projectId/messages:send';
// 在 Google 的 OAuth 2.0 Playground 中取得
final String accessToken = '你的_ACCESS_TOKEN_填在這裡';
try {
debugPrint("正在透過 FCM v1 API 發送推播至主題 $_globalTopic...");
final response = await http.post(
Uri.parse(fcmUrl),
headers: <String, String>{
'Content-Type': 'application/json',
'Authorization': 'Bearer $accessToken',
},
body: jsonEncode(<String, dynamic>{
'message': <String, dynamic>{
'topic': _globalTopic, // 我們可以把 general_chat 存成常數使用
'notification': <String, dynamic>{'title': '新訊息', 'body': text},
'data': <String, String>{'sender': _userAccount}, // 這裡要填上發送端帳號,才能在接收訊息時切分 UI 顯示
'android': {'priority': 'high'},
},
}),
);
debugPrint("FCM v1 回傳狀態碼: ${response.statusCode}");
if (response.statusCode == 200) {
debugPrint("推播發送成功!");
} else {
debugPrint("推播發送失敗。狀態碼: ${response.statusCode}");
debugPrint("回傳內容: ${response.body}");
if (response.statusCode == 401) {
debugPrint(
"原因提示:401 Unauthorized。v1 API 不再使用 Server Key,請檢查是否使用了 Access Token。",
);
} else if (response.statusCode == 403) {
debugPrint(
"原因提示:403 Forbidden。請確保已在 Google Cloud Console 中啟用 'Firebase Cloud Messaging API'。",
);
}
}
} catch (e) {
debugPrint("發送推播時發生例外狀況: $e");
}
}
我們把這三個疑惑解決後,聊天室的邏輯就通了✅
接下來,我們就把這三段程式碼,組裝進我們的 UI 介面,完成一個完整的聊天室!
| 聊天室我們簡單分為三個部分:
UI 實作的部分就交給各位自由發揮了~只要掌握這三點,就可以跟前面的邏輯搭配在一起了👍🏻👍🏻👍🏻 |
| 為了讓收件者知道訊息是誰傳的,我們在發送時必須夾帶發送者帳號。這裡我們採用最直觀的方式:設計一個簡單的輸入區,讓使用者自由設定自己的帳號身分(Sender ID)。 void _showAccountSettingsDialog() { |
| 當發送按鈕被按下時,程式會立即抓取輸入框文字,將其發送至聊天室 Topic,並同步將這則訊息推播到我們自己的聊天畫面中,完成即時發送的體驗。 void _handleSubmitted(String text) async { |
| 接著,我們稍微優化一下「挑戰一」的接收邏輯:加入『過濾自身訊息』的判斷,並將收到的訊息轉換為漂亮的聊天氣泡,讓整個聊天室的互動更加完整且直覺! FirebaseMessaging.onMessage.listen((RemoteMessage message) { |
恭喜大家!實作到這裡,你已經親手打造出一個輕量級的「無伺服器聊天室」了!🎉
實作到這裡,你已經親手打造出一個輕量級的即時通訊系統了!我們成功打破了「推播只是單向接收」的刻板印象,讓每一台手機都能同時化身為發送站與接收站。
這雖然是一個簡單的實驗專案,但它完整展示了 FCM 數據傳輸的強大潛力——不需要複雜的後端架構,就能在雲端實現裝置間的對等對話。看到兩台裝置透過你寫的程式碼「你來我往」的交換訊息,是不是非常有成就感呢?
這只是一個起點,非常期待看到大家在這個「雙向通訊」的基礎上,發展出更完整、更酷的功能(例如加入圖片傳輸、表情符號,或是整合 Firestore 記錄歷史訊息)並跟我們分享成果!
如果在實作過程中發現任何錯誤,或是有更好的優化建議,隨時歡迎在下方留言告訴我們,讓我們一起在技術路上教學相長!


