iT邦幫忙

2021 iThome 鐵人賽

DAY 18
0
Mobile Development

Flutter - 複製貼上到開發套件之旅系列 第 18

【第十八天 - Flutter Cloud Messaging(下)】

  • 分享至 

  • xImage
  •  

前言

今日的程式碼 => GITHUB

路由器教學 => 【第三天 - Flutter Route 規劃分享】
server send message => 教學文章
接續前一篇文章 => 【第十七天 - Flutter Cloud Messaging(上)】

Flutter Code

RemoteMessage

這個物件,我自己最常用的就是 notificationdata 了。還記得 【第十七天 - Flutter Cloud Messaging(上)】,在 Cloud Message 有要你們傳一個 route 和 secondPage 嗎?
這種客製化的資料就會傳到 RemoteMessage.data 裡面。
https://ithelp.ithome.com.tw/upload/images/20210910/20134548npyYHs2PUL.png

Main

  • 啟用 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

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,
              ),
            ));
      }
    });
  }
}

HomePage

  1. 監聽背景推播
  2. 先去取得當下 device_token
  3. 向 FireStore 取得和當下 device_token 一樣的訂閱主題資料
  4. 點擊 subscribe => 訂閱 channel1 頻道。
  5. 訂閱 channel1 頻道到 FireStore
  6. 點擊 unsubscribe 取消訂閱。
  7. 更新 FireStore

可以看到有訂閱 channel1 的,就會記錄在 collection == channels,document 會等於自己的 device token

https://ithelp.ithome.com.tw/upload/images/20210910/20134548qCVyMbiDTw.png

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]);
    });
  }
}

JS Code

官方文件,這裡大多數是參考官網文件。裡面有很詳細的解說和程式碼。
會分成兩種

  • broadcast 的推播
  • topic 的推播(有訂閱才會收到)

執行前

  1. 記得要將 【第十七天 - Flutter Cloud Messaging(上)】 的 serviceAccountKey.json 放到目錄裡
  2. 建立一個 function 的 folder
  3. 建立兩個檔案 sendbroad.jssendnotif.js
  4. 到 ternimal 下這個指令
cd function
npm install firebase-admin --save
node sendbroad.js
node sendnotif.js

sendbroad.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.

sendnotif.js

這邊可以改 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);
  });


上一篇
【第十七天 - Flutter Cloud Messaging(上)】
下一篇
【第十九天 - Flutter Firebase Dynamic Links】
系列文
Flutter - 複製貼上到開發套件之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言