今日的程式碼 => 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);
});