8.0 KiB
Guide: Flutter Integration for Push Notifications & Dynamic Routing
This document serves as the official technical integration guide for Flutter developers to handle Firebase Cloud Messages (FCM) sent by the backend. It outlines the payload structure, routing schema, and provides sample Dart implementation snippets using GoRouter.
1. Firebase Cloud Messaging (FCM) Payload Structure
Every push notification sent by the backend contains a standardized data payload. This payload provides the mobile app with necessary metadata to resolve target screens and dynamically navigate the user.
Example JSON Payload
{
"to": "FCM_DEVICE_TOKEN",
"notification": {
"title": "Доступ к курсу активирован",
"body": "Ваш доступ к курсу «Курс 1» успешно активирован."
},
"data": {
"type": "course_access_granted",
"action": "navigate",
"navigate_to": "/course_info?slug=course-1",
"course_id": "12",
"room_id": "8",
"session_id": "5"
}
}
Key Properties in data Payload
type(String): Indicates the unique notification trigger type (e.g.lesson_completed,teacher_reply_private).action(String): Defines the response action when a user clicks the notification. Currently defaults to"navigate".navigate_to(String): A GoRouter path ready for execution (contains query parameters, e.g./course_info?slug=slug-value).- Dynamic IDs (e.g.,
course_id,room_id,session_id): Additional metadata payload attributes that can be used if custom widgets need parameters directly.
2. Notification Types & Navigation Mapping
Below is the complete registry of all 18 system notification types, their dynamic parameters, and the target routing paths:
Notification Type (type) |
Action / Event | Target Route (navigate_to) |
Dynamic Parameters in data |
|---|---|---|---|
course_registered |
Enrollment confirmation | /course_info?slug={course_slug} |
course_id |
course_access_granted |
Admin activated course access | /course_info?slug={course_slug} |
course_id |
lesson_completed |
Student completed lesson | /lesson?slug={course_slug} |
course_id, lesson_id |
course_completed |
Student finished whole course | /course_info?slug={course_slug} |
course_id |
new_course_weekly |
Weekly newly registered courses | /course_info?slug={course_slug} |
course_id |
low_quiz_result |
Recommendation to retake quiz | /course_info/quiz?slug={course_slug} |
course_id, quiz_id |
live_class_rescheduled |
Rescheduled live class | /course_info?slug={course_slug} |
session_id, course_id |
live_class_cancelled |
Cancelled live session | /course_info?slug={course_slug} |
session_id, course_id |
live_recording_available |
Live class recording uploaded | /course_info?slug={course_slug} |
session_id, course_id |
live_class_reminder |
Reminder before live session | /course_info?slug={course_slug} |
session_id, course_id |
missed_live_sessions |
User missed 2 live sessions | /course_info?slug={course_slug} |
course_id |
teacher_reply |
Teacher reply in general chat | /chat/chat_details?room_id={room_id} |
room_id, message_id |
teacher_reply_private |
Teacher reply in private room | /chat/chat_details?room_id={room_id} |
room_id, message_id |
teacher_reply_course |
Teacher message in course chat | /chat/chat_details?room_id={room_id} |
room_id, message_id |
support_reply |
Support consultant message | /chat/chat_details?room_id={room_id} |
room_id, message_id |
payment_successful |
Course payment success | /profile/transactions |
course_id |
payment_failed |
Course payment failure | /profile/transactions |
course_id |
refund_completed |
Refund completed | /profile/transactions |
course_id |
student_inactivity |
Weekly inactivity warning | /home |
- |
3. Flutter Implementation Guide
To ensure navigation works in all app states (terminated, background, and foreground), follow the implementation patterns below.
A. Initializing and Handling Click Events
The app must check if it was opened from a terminated state (cold start) or was already running in the background.
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class NotificationService {
final GoRouter router;
NotificationService(this.router);
Future<void> initialize() async {
// 1. App was terminated and opened by clicking a notification
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
_handleNotificationClick(initialMessage);
}
// 2. App is in the background and opened by clicking a notification
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
_handleNotificationClick(message);
});
}
void _handleNotificationClick(RemoteMessage message) {
final data = message.data;
if (data.containsKey('action') && data['action'] == 'navigate') {
final String? route = data['navigate_to'];
if (route != null && route.isNotEmpty) {
// Execute dynamic GoRouter redirect
router.push(route);
}
}
}
}
B. Safe Route Integration with GoRouter
Make sure your GoRouter configurations matches paths to parse query parameters (such as slug and room_id) cleanly:
final GoRouter appRouter = GoRouter(
initialLocation: '/home',
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/course_info',
builder: (context, state) {
// Parse 'slug' query parameter (e.g. /course_info?slug=my-course-slug)
final slug = state.uri.queryParameters['slug'] ?? '';
return CourseDetailScreen(courseSlug: slug);
},
),
GoRoute(
path: '/lesson',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'] ?? '';
return LessonDetailScreen(courseSlug: slug);
},
),
GoRoute(
path: '/course_info/quiz',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'] ?? '';
return QuizScreen(courseSlug: slug);
},
),
GoRoute(
path: '/chat/chat_details',
builder: (context, state) {
final roomId = state.uri.queryParameters['room_id'] ?? '';
return ChatDetailsScreen(roomId: roomId);
},
),
GoRoute(
path: '/profile/transactions',
builder: (context, state) => const TransactionHistoryScreen(),
),
],
);
C. Handling Foreground Notifications (Optional)
If a user is inside the app and receives a notification, you may want to display an in-app banner instead of navigating immediately. Use the following stream:
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// Show in-app banner or toast notification.
// Do NOT navigate immediately to avoid breaking user workflow.
debugPrint("Foreground message received: ${message.notification?.title}");
});
4. Troubleshooting & Best Practices
- Cold Start Timing Delay:
When launching the app from a terminated state, GoRouter might not be fully initialized when
getInitialMessageresolves. If redirection fails during cold-starts, defer execution by a tick usingWidgetsBinding.instance.addPostFrameCallback:WidgetsBinding.instance.addPostFrameCallback((_) { router.push(route); }); - Dynamic Placeholders Fallback:
If the backend has not yet populated a template or data parameter, query parameters will contain empty placeholders. Implement default screen fallbacks on your pages when
slugorroom_idis empty. - Registering Device Tokens:
Ensure the device registers/updates the token dynamically on user login or token refresh via the
/api/account/update-fcm/endpoint.