今日的程式碼 => GITHUB
路由器教學 => 【第三天 - Flutter Route 規劃分享】
server send message => 教學文章
接續前一篇文章 =>  【第十七天 - Flutter Cloud Messaging(上)】
這個物件,我自己最常用的就是 notification 和 data 了。還記得 【第十七天 - Flutter Cloud Messaging(上)】,在 Cloud Message 有要你們傳一個 route 和 secondPage 嗎?
這種客製化的資料就會傳到 RemoteMessage.data 裡面。
FCM 的背景處理、初始化flutterLocalNotificationsPlugin 初始化terminal 點擊推播事件foreground 前景推播事件/// 背景 Handler for FCM
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // 確保初執行前有初始化
  await Firebase.initializeApp();
  print('Handling a background message ${message.messageId}');
}
/// flutterLocalNotificationsPlugin 初始設定
const AndroidNotificationChannel channel = AndroidNotificationChannel(
  'high_importance_channel', // id
  'High Importance Notifications', // title
  'This channel is used for important notifications.', // description
  importance: Importance.max,
);
/// flutterLocalNotificationsPlugin 初始宣告
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  /// 背景處理
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  /// 創建一個 Android 通知通道。
  /// 我們在`AndroidManifest.xml`文件中使用這個通道來覆蓋
  /// 默認 FCM 通道啟用抬頭通知。
  /// https://github.com/FirebaseExtended/flutterfire/blob/master/packages/firebase_messaging/firebase_messaging/example/lib/main.dart
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(channel);
  runApp(MyApp());
}
class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  _MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    /// 初始化 LocalNotification 設定
    LocalNotificationService.initialize(context);
    ///gives you the message on which user taps
    ///and it opened the app from terminated state
    /// App 被完全關掉後,時點選通知開啟App(Terminated)
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message != null) {
        print('從 App 被完全關閉狀態打開:' + message.data["route"]);
        final routeFromMessage = message.data["route"];
        Navigator.pushNamed(context, routeFromMessage);
      }
    });
    /// 監聽 terminal 推播事件。
    FCMManager.foregroundMessage();
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: RouteName.homePage,
      onGenerateRoute: MyRouter.generateRoute,
    );
  }
}
LocalNotificationService
FCMManager
class LocalNotificationService {
  static void initialize(BuildContext context) {
    /// 初始化 LocalNotification 的動作。
    /// iOS 這邊還需要加上其他設定。
    final InitializationSettings initializationSettings =
        InitializationSettings(
            android: AndroidInitializationSettings("@mipmap/ic_launcher"));
    flutterLocalNotificationsPlugin.initialize(
      initializationSettings,
    );
  }
}
class FCMManager {
  static void onMessageOpenedApp(BuildContext context) {
    ///When the app is in background but opened and user taps
    ///on the notification
    /// 從背景處中點擊推播當 App 縮小狀態時,開啟應用程式時,該流會發送 RemoteMessage。背景處理。
    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      print('從背景中打開' + message.toString());
      final routeFromMessage = message.data["route"];
      if(routeFromMessage!=Null && routeFromMessage==RouteName.secondPage) {
        Navigator.of(context).pushNamed(routeFromMessage);
      }
    });
  }
  static void foregroundMessage() {
    /// foreground work
    /// 前景處理
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('前景處理 FcM 觸發' + message.toString());
      RemoteNotification? notification = message.notification;
      AndroidNotification? android = message.notification?.android;
      // If `onMessage` is triggered with a notification, construct our own
      // local notification to show to users using the created channel.
      if (notification != null && android != null && !kIsWeb) {
        flutterLocalNotificationsPlugin.show(
            notification.hashCode,
            notification.title,
            notification.body,
            NotificationDetails(
              android: AndroidNotificationDetails(
                channel.id,
                channel.name,
                channel.description,
                icon: android.smallIcon,
              ),
            ));
      }
    });
  }
}
device_token 一樣的訂閱主題資料channel1 頻道。FireStore
unsubscribe 取消訂閱。FireStore。可以看到有訂閱 channel1 的,就會記錄在 collection == channels,document 會等於自己的 device token

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
  /// 要取得 device token,好讓 JS 檔案和 FCM TEST MESSAGE 可以傳送指定 token
  late String deviceToken;
  /// 訂閱的 Topic
  List subscribed = [];
  /// 有哪些頻道可供 topic 訂閱。
  List channels = [
    'channel1',
    'channel2',
    'channel3',
    'channel4',
    'channel5',
    'channel6',
    'channel7'
  ];
  @override
  void initState() {
    super.initState();
    /// 監聽背景推播
    FCMManager.onMessageOpenedApp(context);
    getToken();
    getTopics();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FCM DEMO'),
        centerTitle: true,
      ),
      body: ListView.builder(
        itemCount: channels.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(channels[index]),
          trailing: subscribed.contains(channels[index])
              ? ElevatedButton(
                  onPressed: () async => cancelSubscribed(index),
                  child: Text('unsubscribe'),
                )
              : ElevatedButton(
                  onPressed: () async => startSubscribed(index),
                  child: Text('subscribe')),
        ),
      ),
    );
  }
  /// 向 FCM 請求 device_token
  void getToken() async {
    var token = (await FirebaseMessaging.instance.getToken())!;
    setState(() {
      deviceToken = token;
    });
    print('device token:' + deviceToken);
  }
  /// 向 firebase 取得 collection == channels 並去找到 documnet == token 的那筆資料裡面的所有 key 值
  void getTopics() async {
    await FirebaseFirestore.instance
        .collection('channels')
        .get()
        .then((value) => value.docs.forEach((element) {
              if (deviceToken == element.id) {
                subscribed = element.data().keys.toList();
              }
            }));
    setState(() {
      subscribed = subscribed;
    });
  }
  /// 開始訂閱 topic 到 fireStore
  void startSubscribed(int index) async {
    await FirebaseMessaging.instance.subscribeToTopic(channels[index]);
    await FirebaseFirestore.instance
        .collection('channels')
        .doc(deviceToken)
        .set({channels[index]: 'subscribe'}, SetOptions(merge: true));
    setState(() {
      subscribed.add(channels[index]);
    });
  }
  /// 取消訂閱 topic from fireStore
  void cancelSubscribed(int index) async {
    await FirebaseMessaging.instance.unsubscribeFromTopic(channels[index]);
    await FirebaseFirestore.instance
        .collection('channels')
        .doc(deviceToken)
        .update({channels[index]: FieldValue.delete()});
    setState(() {
      subscribed.remove(channels[index]);
    });
  }
}
官方文件,這裡大多數是參考官網文件。裡面有很詳細的解說和程式碼。
會分成兩種
function 的 foldersendbroad.js、sendnotif.js。cd function
npm install firebase-admin --save
node sendbroad.js
node sendnotif.js
要記得改掉 serviceAccount 裡面的路徑。
// 初始化 token
var admin = require("firebase-admin");
var serviceAccount = require("serviceAccountKey.json 的路徑");
admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
});
var db = admin.firestore();
async function start() {
    var topics = [];
    const col = await db.collection('channels').get();
    col.forEach((doc) => {
        topics.push(doc.id);
    })
    console.log('topics:',topics)
    var message = {
        notification: {
            title: 'FCM DEMO',
            body: 'BroadCast ^_^'
        },
        // token: registrationToken
    };
    admin.messaging().sendToDevice(topics, message)
        .then((response) => {
            // Response is a message ID string.
            console.log('Successfully sent message:', response);
        })
        .catch((error) => {
            console.log('Error sending message:', error);
        });
}
start()
// Send a message to the device corresponding to the provided
// registration token.
這邊可以改 message 裡面的資料
admin.messaging().sendToTopic('channel1',message),這行裡面的 channel1 可以改成其他的 topic 比方說 channel2、channel3 等等...
//初始化 firebase admin 設定
var admin = require("firebase-admin");
var serviceAccount = require("/Users/wayne/AndroidStudioProjects/IT30/day_17/serviceAccountKey.json");
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});
// This registration device token comes from the client FCM SDKs.
var registrationToken = 'eXOC-WITTI6JrAX0sQ-P1P:APA91bF5OPjAi9t3JoVHYeYyVLvg06kRY_Qr2cM4aln3c6ejQtIofkDhNL75KhkwmnzKVAlRByOqEZa-9CjRbLwdGZQ4t4K1UPL_wnW_Y8hG9ltCum3VlLhm7_ncX9OTsuiUiQSdyxAz';
// 要傳的推播訊息。
var message = {
//  data:{
//     route:'secondPage'
//  },
  notification: {
    title: 'FCM DEMO',
    body: 'Only subscribers receive it~~'
  },
};
// Send a message to the device corresponding to the provided
// registration token.
// 開始發送推播資訊給 device token == registrationToken 的人,且那位使用者需要有訂閱 Topic == channel1。
admin.messaging().sendToTopic('channel1',message)
  .then((response) => {
    // Response is a message ID string.
    console.log('Successfully sent message:', response);
  })
  .catch((error) => {
    console.log('Error sending message:', error);
  });