Building a Notification System with Socket.IO in Flutter

Adarsh Chauhan
4 min readOct 22, 2024

--

Building a Notification System with Socket.IO in Flutter

In modern mobile applications, real-time communication and push notifications are critical to keeping users engaged. One popular approach to achieving this is using Socket.IO for real-time data and FlutterLocalNotificationsPlugin for handling notifications within the Flutter ecosystem.

In this blog, I’ll guide you through implementing a robust, reusable, and maintainable notification system using Socket.IO and FlutterLocalNotificationsPlugin in Flutter. We will break down the code into two parts: the SocketAndNotifications class, which handles socket communication and the NotificationManager class, responsible for displaying notifications.

Project Setup

Before diving into the code, you need to add the following dependencies to your pubspec.yaml:

dependencies:
flutter:
sdk: flutter
socket_io_client: ^1.0.2
flutter_local_notifications: ^12.0.1

1. SocketAndNotifications Class

This class is responsible for managing the socket connection and triggering the notification system when data is received. It follows the Singleton pattern to ensure only one instance of the socket connection is active.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:socket_io_client/socket_io_client.dart' as socket_io_client;
import 'notification_manager.dart';
import 'navigation_service.dart'; // Add NavigationService for routing

/// A class to handle Socket.IO connections and notifications.
class SocketAndNotifications {
// Singleton instance to ensure only one instance is active at a time
static final SocketAndNotifications _instance = SocketAndNotifications._internal();

// Factory constructor returns the singleton instance
factory SocketAndNotifications() => _instance;

// Private constructor for internal use
SocketAndNotifications._internal();

// The Socket.IO client instance (nullable)
socket_io_client.Socket? _socket;

// Stream controller to manage socket responses
final StreamController<String> _socketResponseController =
StreamController<String>.broadcast();

// Flags to track the socket connection status
bool _isConnecting = false;
bool _isConnected = false;

// Exposing the socket response stream to allow other classes to listen
Stream<String> get socketResponse => _socketResponseController.stream;

/// Method to connect to the Socket.IO server and listen for events
Future<void> connectAndListen(String token, String domainUrl) async {
if (_isConnecting) {
debugPrint('SOCKET IO ==> Connection attempt already in progress.');
return;
}

if (_isConnected) {
debugPrint('SOCKET IO ==> Socket is already connected.');
return;
}

_isConnecting = true;

debugPrint('SOCKET IO ==> Attempting to connect with Token: $token');

// Create and configure the socket with authentication token
_socket = socket_io_client.io(
domainUrl,
socket_io_client.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.setAuth({'token': token})
.enableReconnection()
.build(),
);

// Handle successful socket connection
_socket?.onConnect((_) {
_isConnected = true;
_isConnecting = false;
debugPrint('SOCKET IO ==> Socket connected');
});

// Handle socket disconnection
_socket?.onDisconnect((_) {
_isConnected = false;
_isConnecting = false;
debugPrint('SOCKET IO ==> Socket disconnected');
});

// Listening to 'notification' events from the server
_socket?.on('notification', (data) {
debugPrint('SOCKET IO ==> Notification received: $data');
_socketResponseController.add(data.toString());
NotificationManager().showNotification(data['title'], data['message']);
});

// Handle account upgrades, such as triggering logout or any required action
_socket?.on('account_upgraded', (data) {
debugPrint('SOCKET IO ==> Account upgraded event received: $data');
// Perform necessary actions, e.g., logging out the user
handleAccountUpgrade();
});

// Connect the socket manually
_socket?.connect();
}

/// Method to clear the socket state and disconnect
void clearSocket() {
if (_socket != null) {
_socket?.disconnect();
_socket?.destroy();
_socket = null;
_isConnected = false;
_isConnecting = false;
debugPrint('SOCKET IO ==> Socket cleared and destroyed');
}
}

/// Dispose method to release resources
void dispose() {
debugPrint('SOCKET IO ==> Disposing resources');
clearSocket();
_socketResponseController.close();
}

/// Handle actions upon account upgrade, such as logging out
void handleAccountUpgrade() {
// Trigger app-specific logic for account upgrade
// Example: log out the user or refresh access token
debugPrint('SOCKET IO ==> Handling account upgrade event');
}
}

Key Features of SocketAndNotifications

  • Singleton Pattern: Ensures a single instance of the class exists across the app.
  • Connection Control: Includes flags to prevent multiple simultaneous connection attempts.
  • Socket Management: Connects and listens for specific socket events such as notifications and account upgrades.
  • StreamController: Broadcasts socket events, allowing multiple listeners within the app.

2. NotificationManager Class

The NotificationManager class uses FlutterLocalNotificationsPlugin to display local notifications when triggered by the SocketAndNotifications class.

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';
import 'navigation_service.dart'; // Add NavigationService for routing

/// A class to handle local notifications using FlutterLocalNotificationsPlugin.
class NotificationManager {
// Singleton instance
static final NotificationManager _instance = NotificationManager._internal();

// Factory constructor to return the singleton instance
factory NotificationManager() => _instance;

// Private constructor to initialize notification settings
NotificationManager._internal() {
_initializeNotifications();
}

// Instance of FlutterLocalNotificationsPlugin
late FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin;

/// Method to initialize notification settings for both Android and iOS
void _initializeNotifications() {
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

const initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
final initializationSettingsIOS = DarwinInitializationSettings(
onDidReceiveLocalNotification: _onDidReceiveLocalNotification,
);

final initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);

// Initialize the plugin with settings
_flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: _onSelectNotification,
);
}

/// Method to display a local notification with the given [title] and [body].
Future<void> showNotification(String title, String body) async {
const androidPlatformChannelSpecifics = AndroidNotificationDetails(
'channel_id',
'channel_name',
importance: Importance.max,
priority: Priority.high,
);

const iosPlatformChannelSpecifics = DarwinNotificationDetails();

const platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iosPlatformChannelSpecifics,
);

// Show the notification
await _flutterLocalNotificationsPlugin.show(
0, // Notification ID
title, // Notification title
body, // Notification body
platformChannelSpecifics,
payload: 'Notification Payload', // Custom payload for navigation
);
}

/// Callback for when a notification is tapped.
Future<void> _onSelectNotification(NotificationResponse notificationResponse) async {
debugPrint('Notification tapped with payload: ${notificationResponse.payload}');

// Navigate to a specific screen on notification tap
await Navigator.of(NavigationService.navigatorKey.currentState!.context)
.push(MaterialPageRoute(
builder: (context) => Dashboard(
index: 1,
isRedirect: true,
)));
}

/// iOS-specific handler for receiving notifications while in the foreground.
Future<void> _onDidReceiveLocalNotification(int id, String? title, String? body, String? payload) async {
debugPrint('iOS notification received: title=$title, body=$body, payload=$payload');
// Handle foreground notification for iOS
}
}

Key Features of NotificationManager

  • Cross-Platform Support: Handles notifications for both Android and iOS.
  • Custom Payload: Allows payloads to be passed and handled for deep linking or navigation when notifications are tapped.
  • Singleton Pattern: Like the socket class, it follows the singleton pattern for consistent usage across the app.

Conclusion

In this post, we’ve implemented a notification system in Flutter that leverages Socket.IO for real-time data and FlutterLocalNotificationsPlugin for displaying local notifications. This solution is scalable, reusable, and follows best practices like the singleton pattern and separating concerns between socket management and notifications.

By integrating this system, your app can handle real-time notifications and ensure users are always up-to-date with the latest events or updates.

Thankyou

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Adarsh Chauhan
Adarsh Chauhan

Written by Adarsh Chauhan

🚀 Flutter Developer | UI/UX Design Enthusiast | Sharing Art of Flutter

Responses (1)

Write a response