Flutter: Chat notifications using FCM in a socket.io powered chat app
A while ago, I wrote an article on how to create a chat app in flutter using a socket.io service. This application connects users to a websocket service and enables real time conversation. While the users can chat when they are online, the application disconnects them from the websocket when they come out of the application. This limitation brings us to the next major requirement in a chat app: receiving chat notifications when the app is running in the background.
Taking a cue from my previous article, here I will add the functionality of receiving chat notifications using FCM (Firebase Cloud Messaging).
Note: In this article, I am covering the changes for ANDROID devices only.
Prerequisites
This article is in continuation to my previous article: Flutter: A chat app in flutter using a Socket.IO service. Kindly follow the steps there before moving forward.
Step#1: Create a new Firebase App
Follow the steps here to create a new Firebase app. Do not forget to download the generated google-services.json and add Firebase SDKs to your android project.
Step#2: Add required dependencies
In the flutter chat_app, add the new dependencies required for the feature in the project’s pubspec.yaml.
firebase_core: ^1.7.0 # required for initializing firebase app
firebase_messaging: ^10.0.8 # required for sending/receiving messages through FCM
flutter_local_notifications: ^6.0.0 # required to show chat notifications
Download the new dependencies added above.
$ flutter pub get
Step#3: Enable your app to receive notifications
In order to create local notifications, you need to add the required intent filter in your android/app/src/main/AndroidManifest.xml.
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
Step#4: Initialize Firebase Messaging and Local Notification
Create a new dart file firebase/messaging.dart in lib and initialize FCM and Local Notification in it.
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
final FirebaseMessaging _fcm = FirebaseMessaging.instance;
FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin;
initializeMessaging() async {
await Firebase.initializeApp();
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings("@mipmap/ic_launcher");
final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
await _flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectionNotification);
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
print("onMessage: $message");
await handleMessage(message);
});
}
Future<dynamic> selectionNotification(String payload) async {
print('payload: $payload');
}
handleMessage(RemoteMessage message) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails('CHAT', "CHAT", 'CHAT', importance: Importance.max, priority: Priority.high, showWhen: true);
const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
await _flutterLocalNotificationsPlugin.show(0, "New Messages", message.data['sender'] + ": " + message.data['message'], platformChannelSpecifics,
payload: 'CHAT');
}
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await initializeMessaging();
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails('CHAT', "CHAT", 'CHAT', importance: Importance.max, priority: Priority.high, showWhen: true);
const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
await _flutterLocalNotificationsPlugin.show(0, "New Messages", message.data['sender'] + ": " + message.data['message'], platformChannelSpecifics,
payload: 'CHAT');
}Future<String> getFCMToken() async {
return await _fcm.getToken();
}
Firebase.initializeApp() initializes the Firebase app using the google-services.json you downloaded in your android project.
FirebaseMessaging.onMessage.listen() listens to FCM messages and calls handleMessage() which displays the notification in your device. This handles notification when the app is in the foreground.
firebaseMessagingBackgroundHandler() handles FCM messages when the app is in the background.
getFCMToken() returns the FCM registration token. This token will be used by the websocket service to send FCM messages to offline (not connected to websocket) devices.
Note: Do not create any class in firebase/messaging.dart as firebaseMessagingBackgroundHandler() is expected to an anonymous function.
And finally, initialize FCM from main.dart.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
initializeMessaging(); FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
...
runApp(MyApp());
}
Step#5: Send FCM token to websocket service on connect
In this step, we will let the websocket service know about the FCM token of a user. In chat.dart, while connecting to websocket, add queries to send userName & FCM token.
String registrationToken = await getFCMToken();
socket = IO.io('<websocket_url>', <String, dynamic>{
'transports': ['websocket'],
'autoConnect': false,
'query': {
'userName': widget.user,
'registrationToken': registrationToken
}
});
socket.connect();
Flutter changes are complete. We will now head to the websocket service discussed in the article: NestJs: Chat Server in NestJs backed by MongoDB.
Step#6: Maintain All and Connected users on the server
In src/chat/chat.service.ts, maintain a concurrent list of all users and connected users.
private allUsers = [];
private connectedUsers = []; userConnected(userName: string, registrationToken: string) {
let user = { userName: userName, registrationToken: registrationToken };
const filteredUsers = this.allUsers.filter(u => u.userName === userName);
if (filteredUsers.length == 0) {
this.allUsers.push(user);
} else {
user = filteredUsers[0];
user.registrationToken = registrationToken;
}
this.connectedUsers.push(userName);
console.log("All Users", this.allUsers);
console.log("Connected Users", this.connectedUsers);
} userDisconnected(userName: string) {
let userIndex = this.connectedUsers.indexOf(userName);
this.connectedUsers.splice(userIndex, 1);
console.log("All Users", this.allUsers);
console.log("Connected Users", this.connectedUsers);
}
userConnected() adds a user (with FCM token) in allUsers and connectedUsers list.
userDisconnected() removes a user from connectedUsers list.
Step#7: Call the corresponding methods from the websocket handlers
In src/chat/chat.gateway.ts, extract the query params userName & registrationToken sent by flutter app (step#5 above) and call the corresponding methods added above.
handleConnection(socket: any) {
const query = socket.handshake.query;
console.log('Connect', query);
this.chatService.userConnected(query.userName, query.registrationToken);
process.nextTick(async () => {
socket.emit('allChats', await this.chatService.getChats());
});
} handleDisconnect(socket: any) {
const query = socket.handshake.query;
console.log('Disconnect', socket.handshake.query);
this.chatService.userDisconnected(query.userName);
}
Step#8: Setup Firebase Admin
Add firebase-admin dependency in your project’s package.json.
"dependencies": {
...
"firebase-admin": "^10.0.0",
...
},
Install the new dependencies:
$ npm install
In tsconfig.json, add the below compiler options to allow resolution of json modules:
"resolveJsonModule": true,
"allowJs": true,
Follow the steps here to setup a firebase service account in the same firebase project. Download the serviceAccountKey.json generated and place in src/auth. In the same path src/auth, create another file firebaseAdmin.ts to initialize Firebase Admin and export the intialized app.
import * as firebase from 'firebase-admin';
import * as serviceAccount from './serviceAccountKey.json';const firebase_params = {
type: serviceAccount.type,
projectId: serviceAccount.project_id,
privateKeyId: serviceAccount.private_key_id,
privateKey: serviceAccount.private_key,
clientEmail: serviceAccount.client_email,
clientId: serviceAccount.client_id,
authUri: serviceAccount.auth_uri,
tokenUri: serviceAccount.token_uri,
authProviderX509CertUrl: serviceAccount.auth_provider_x509_cert_url,
clientC509CertUrl: serviceAccount.client_x509_cert_url
}const defaultApp = firebase.initializeApp({
credential: firebase.credential.cert(firebase_params),
databaseURL: "<firebase_db_url>"
});export {
defaultApp
}
Step#9: Send FCM messages to disconnected users
In src/chat/chat.service.ts, create a method to send FCM messages to disconnected users.
async sendMessagesToOfflineUsers(chat: any) {
var messagePayload = {
data: {
type: "CHAT",
title: 'chat',
message: chat.message,
sender: chat.sender,
recipient: chat.recipient,
time: chat.time
},
tokens: []
};
const userTokens = this.allUsers.filter(user => !this.connectedUsers.includes(user.userName)).map(user => { return user.registrationToken });
if (userTokens.length == 0) {
return;
}
messagePayload.tokens = userTokens;
try {
await defaultApp.messaging().sendMulticast(messagePayload);
} catch (ex) {
console.log(JSON.stringify(ex));
}
}
And finally, call this method when a new chat arrives in src/chat/chat.gateway.ts.
@Bind(MessageBody(), ConnectedSocket())
@SubscribeMessage('chat')
async handleNewMessage(chat: Chat, sender: any) {
console.log('New Chat', chat);
await this.chatService.saveChat(chat);
sender.emit('newChat', chat);
sender.broadcast.emit('newChat', chat);
await this.chatService.sendMessagesToOfflineUsers(chat);
}
Step#10: Let’s verify
I have started the chat-service in my local and using ngrok to tunnel from a public URL to this service.
I am connecting to the chat-service using a socket.io client tool to simulate the messaging using dummy query params:
{“query”: {“userName”: “Khushboo”, “registrationToken”: “”}}
I am running the flutter app in android emulator and accessing the chat with another user.
When both the users are connected, the users can converse through the websocket.
When the android user leaves the chat, it can receive the chat notifications through FCM.