跳至內容

Flutter 搭配 FCM 打造極簡「無伺服器」聊天室

你是否曾經以為,Firebase Cloud Messaging (FCM) 就只是個用來發送「您的訂單已出貨」這類通知的工具?

今天我們要來玩點不一樣的!我們要挑戰不使用傳統的 WebSocket 或 Realtime Database,僅僅依靠 FCM 本身,來實作一個即時聊天室。

這篇教學的核心概念在於混合利用 FCM 的兩種訊息類型:「通知 (Notifications)」 與 「數據 (Data Payloads)」,來傳遞聊天內容,讓 App 在背景靜默接收並觸發 UI 更新,達成看似即時通訊的效果。

我們的架構非常簡單暴力:< Client A 發送 ➔ 呼叫 FCM API ➔ FCM Server 廣播 ➔ Client B 接收並刷新畫面 >

架構圖取自 Firebase 官方文件

最後的成果就會像是⋯⋯

事前準備

  1. Firebase 帳號
  2. Flutter 開發環境

如果以上都準備好了,那接下來跟著一步步執行,最後就可以得到簡單的無伺服器聊天室囉😎


專案建置

在開始撰寫聊天邏輯前,我們需要先打好基礎!這個步驟會將你的 Flutter 專案與 Firebase 雲端服務連結,並安裝必要的工具包。請跟著以下步驟完成基礎建設:

  1. 建立 Firebase 專案:在 Firebase Console 點擊「建立新的 Firebase 專案」依照指示完成建立。
  2. 環境工具準備:為了讓 Flutter 與 Firebase 溝通,你需要確保電腦已安裝 Firebase CLI。詳細可參考官方文件Firebase CLI 參考資料,若已安裝可略過。
  3. 配置設定檔:有了 CLI 後,我們不需要手動去各個平台複製貼上。在 Flutter 專案的根目錄中執行以下指令,完成後系統會自動透過 Firebase 註冊每個平台專屬的應用程式,並將設定檔新增至 Flutter 專案中。
    1. $ flutterfire configure --project="YOUR_PROJECT_ID"
  4. 安裝 Library:在 Flutter 專案目錄中執行下列指令,安裝本次專案所需的 Library
    1. $ flutter pub add firebase_core
    2. $ flutter pub add firebase_messaging
    3. $ flutter pub add http
  5. 初始化 Firebase:建置的最後一個步驟,我們需要在 main 方法中使用設定檔匯出的 DefaultFirebaseOptions 物件初始化 Firebase
    1. void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
      );
      runApp(const MyApp());
      }
💡 跟著以上步驟建置完成後,你的專案就已經具備雲端通訊的能力了!下一步,我們要先做一個簡單的測試,利用 FCM 發送測試訊息,確認通知通道是否暢通。


測試通知

我們怎麼確認手機是不是真的可以接收到通知呢?正常接收到通知的情況要如下圖這樣,在手機的通知中心顯示出我們定義的測試通知。要達到這樣的效果需要先在 Flutter 端進行權限請求及 FCM Token 取得,接著透過 Firebase Console 的 Messaging 所提供「傳送測試訊息」功能來向 FCM Token 發送通知。話不多說,繼續跟著我們的步伐完成測試吧!

Flutter 端實作

  1. 通知權限請求:FCM 已經幫我們包好權限請求的方法了,直接呼叫即可
    1. // 在你的 Flutter 程式碼初始化的位置 (例如 main.dart 或聊天室頁面的 initState)

      FirebaseMessaging messaging = FirebaseMessaging.instance;
      await messaging.requestPermission();
  2. 取得 FCM Token:由於 FCM 測試通知需要指定 Token ,才能發送測試通知,所以我們要先印出 FCM Token 備用
    1. 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 的世界裡,有兩種主要方式:

  1. Token (權杖):像是身分證字號,指定傳給「某一台特定的手機」。
  2. 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 介面,完成一個完整的聊天室!

聊天室我們簡單分為三個部分:

  1. 身分設定區
    一個按鈕點擊後彈出文字輸入視窗,將用來設定你自己的帳號(Sender ID)。

  2. 對話顯示區
    這是聊天室的核心,我們將訊息分為左右兩側來區分身分
    1. 左側(對方訊息):顯示發送者帳號、訊息內容以及接收時間。
    2. 右側(自己訊息):顯示自己的訊息內容與發送時間。

  3. 訊息發送區
    包含一個文字輸入元件與發送按鈕。當點擊按鈕時,會將訊息發送到當前聊天室中,並且標記發送者為自己。


UI 實作的部分就交給各位自由發揮了~只要掌握這三點,就可以跟前面的邏輯搭配在一起了👍🏻👍🏻👍🏻

為了讓收件者知道訊息是誰傳的,我們在發送時必須夾帶發送者帳號。這裡我們採用最直觀的方式:設計一個簡單的輸入區,讓使用者自由設定自己的帳號身分(Sender ID)。

void _showAccountSettingsDialog() {
final TextEditingController accountController = TextEditingController(
text: _userAccount,
);

showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('設定'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: accountController,
decoration: const InputDecoration(labelText: '您的帳號'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
final newAccount = accountController.text.trim();
if (newAccount.isNotEmpty) {
setState(() {
_userAccount = newAccount;
});
debugPrint("已更新帳號名稱為: $newAccount");
}
if (mounted) Navigator.pop(context);
},
child: const Text('儲存'),
),
],
);
},
);

當發送按鈕被按下時,程式會立即抓取輸入框文字,將其發送至聊天室 Topic,並同步將這則訊息推播到我們自己的聊天畫面中,完成即時發送的體驗。

void _handleSubmitted(String text) async {
if (text.isEmpty) return;
_textController.clear();

// 1. 發送訊息到群組主題(這是挑戰三的方法喔!)
await _sendMessageToTopic(text);

// 2. 將訊息顯示在自己的畫面
setState(() {
_messages.insert(
0,
ChatMessage(
text: text,
timestamp: DateTime.now(),
isMe: true,
sender: _userAccount,
),
);
});
}

接著,我們稍微優化一下「挑戰一」的接收邏輯:加入『過濾自身訊息』的判斷,並將收到的訊息轉換為漂亮的聊天氣泡,讓整個聊天室的互動更加完整且直覺!

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (message.notification != null) {
final sender = message.data['sender'];

// 如果發送者是自己,則略過
if (sender == _userAccount) {
debugPrint("收到自己發送的訊息,略過顯示");
return;
}

setState(() {
_messages.insert(
0,
ChatMessage(
text:
message.notification!.body ?? message.notification!.title ?? '',
timestamp: DateTime.now(),
isMe: false,
sender: sender,
),
);
});
}
});

恭喜大家!實作到這裡,你已經親手打造出一個輕量級的「無伺服器聊天室」了!🎉

實作到這裡,你已經親手打造出一個輕量級的即時通訊系統了!我們成功打破了「推播只是單向接收」的刻板印象,讓每一台手機都能同時化身為發送站與接收站。

這雖然是一個簡單的實驗專案,但它完整展示了 FCM 數據傳輸的強大潛力——不需要複雜的後端架構,就能在雲端實現裝置間的對等對話。看到兩台裝置透過你寫的程式碼「你來我往」的交換訊息,是不是非常有成就感呢?

這只是一個起點,非常期待看到大家在這個「雙向通訊」的基礎上,發展出更完整、更酷的功能(例如加入圖片傳輸、表情符號,或是整合 Firestore 記錄歷史訊息)並跟我們分享成果!

如果在實作過程中發現任何錯誤,或是有更好的優化建議,隨時歡迎在下方留言告訴我們,讓我們一起在技術路上教學相長!

網誌: IoT分享
Flutter 搭配 FCM 打造極簡「無伺服器」聊天室
吳逸鈞 2026年1月15日
分享這個貼文
標籤
登入 發表評論
Flutter 實作 Sign in with Apple 完整教學