Flutter Screen Launch by Notification - Deep Linking & Navigation Guide

Flutter Screen Launch by Notification - Deep Linking & Navigation Guide

3 min read
flutter notifications deep-linking navigation

Learn how to detect notification launches, handle deep links, and route directly to specific screens in Flutter apps. Skip splash screens when launched from notifications!

Introduction

One of the most common challenges in Flutter app development is detecting when your app was launched by tapping a notification. Unlike native Android and iOS apps, Flutter doesn’t provide built-in functionality to detect this scenario.

The screen_launch_by_notfication plugin solves this problem by allowing you to:

  • Detect when your app was launched from a notification
  • Retrieve notification payload data
  • Route directly to notification-specific screens
  • Skip unnecessary splash screens
  • Handle deep links automatically

The Problem

Default Flutter Behavior

By default, Flutter cannot detect whether the app was launched by:

  • ❌ Tapping a notification (when app is killed)
  • ❌ Tapping a notification (when app is in background)
  • ❌ Opening a deep link (when app is not running)

This means users always see the splash screen, even when they tap a notification that should take them directly to a specific screen.

Native App Behavior

Native Android and iOS apps can detect notification launches even when:

  • The app is completely terminated
  • The app is in the background
  • The app is not running at all

The Solution

The screen_launch_by_notfication plugin bridges this gap by:

  1. Native Code Capture: Native code captures the notification launch event
  2. Flag Storage: Native code saves a flag and payload in shared preferences
  3. Early Detection: Flutter reads the flag via MethodChannel before runApp()
  4. Smart Routing: Flutter decides the initial screen → splash / home / notification screen

Quickstart checklist (copy/paste)

  1. Add the plugin + flutter_local_notifications.
  2. Initialize before runApp().
  3. Route on first frame using the payload.
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final launch = await ScreenLaunchByNotfication.instance.initialize();

  runApp(MyApp(
    initialRoute: launch.isFromNotification ? '/notification' : '/splash',
    payload: launch.payload,
  ));
}

Installation

Add the package to your pubspec.yaml:

dependencies:
  screen_launch_by_notfication: ^2.3.0
  flutter_local_notifications: ^19.5.0  # Recommended for sending notifications
  get: ^4.6.6  # Required only if using GetMaterialApp

Then run:

flutter pub get

Platform Setup

Android Setup

Good News: No native code setup required! 🎉 The plugin handles everything automatically.

Just ensure you have the required dependencies in your android/app/build.gradle.kts:

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

To enable deep linking, add intent filters to your android/app/src/main/AndroidManifest.xml:

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTop">
    <!-- ... other intent filters ... -->
    
    <!-- Deep link intent filter -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="yourapp" />
    </intent-filter>
</activity>

Replace yourapp with your custom scheme (e.g., myapp, notificationapp).

iOS Setup

Good News: No native code setup required! 🎉 The plugin handles everything automatically.

For iOS, you only need to request notification permissions in your app (if you haven’t already):

import UserNotifications

// In your AppDelegate or wherever you request permissions
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
  if granted {
    DispatchQueue.main.async {
      UIApplication.shared.registerForRemoteNotifications()
    }
  }
}

To enable deep linking, add URL scheme to your ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourapp</string>
        </array>
    </dict>
</array>

Replace yourapp with your custom scheme (e.g., myapp, notificationapp).

Note: The plugin automatically handles all notification detection, payload storage, and deep link handling. No need to modify AppDelegate.swift or MainActivity.kt!

Usage

The easiest way to use this plugin is with the SwiftFlutterMaterial widget. Simply wrap your existing MaterialApp or GetMaterialApp:

With MaterialApp

import 'package:flutter/material.dart';
import 'package:screen_launch_by_notfication/screen_launch_by_notfication.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return SwiftFlutterMaterial(
      materialApp: MaterialApp(
        title: 'My App',
        theme: ThemeData(primarySwatch: Colors.blue),
        initialRoute: '/splash',
        routes: {
          '/splash': (_) => SplashScreen(),
          '/notification': (_) => NotificationScreen(),
          '/home': (_) => HomeScreen(),
        },
      ),
      onNotificationLaunch: ({required isFromNotification, required payload}) {
        if (isFromNotification) {
          return SwiftRouting(
            route: '/notification',
            payload: payload, // Pass full payload or null
          );
        }
        return null; // Use MaterialApp's initialRoute
      },
      onDeepLink: ({required url, required route, required queryParams}) {
        // Handle deep links (e.g., myapp://product/123)
        if (route == '/product') {
          return SwiftRouting(
            route: '/product',
            payload: {'productId': queryParams['id']},
          );
        }
        return null; // Skip navigation for unknown routes
      },
    );
  }
}

With GetMaterialApp

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:screen_launch_by_notfication/screen_launch_by_notfication.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return SwiftFlutterMaterial(
      materialApp: GetMaterialApp(
        title: 'My App',
        theme: ThemeData(primarySwatch: Colors.blue),
        initialRoute: '/splash',
        getPages: [
          GetPage(name: '/splash', page: () => SplashScreen()),
          GetPage(name: '/notification', page: () => NotificationScreen()),
          GetPage(name: '/home', page: () => HomeScreen()),
        ],
      ),
      onNotificationLaunch: ({required isFromNotification, required payload}) {
        if (isFromNotification) {
          return SwiftRouting(
            route: '/notification',
            payload: payload,
          );
        }
        return null;
      },
      onDeepLink: ({required url, required route, required queryParams}) {
        if (route == '/product') {
          return SwiftRouting(
            route: '/product',
            payload: {'productId': queryParams['id']},
          );
        }
        return null;
      },
    );
  }
}

Method 2: Manual Detection (Advanced)

If you need more control, you can manually detect notification launches:

import 'package:screen_launch_by_notfication/screen_launch_by_notfication.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Check if app was launched from notification
  final launchInfo = await ScreenLaunchByNotification.checkLaunchFromNotification();
  
  String initialRoute = '/splash';
  Map<String, dynamic>? initialPayload;
  
  if (launchInfo.isFromNotification) {
    initialRoute = '/notification';
    initialPayload = launchInfo.payload;
  }
  
  runApp(MyApp(
    initialRoute: initialRoute,
    initialPayload: initialPayload,
  ));
}

Handling Notification Payload

Basic Payload Access

The payload contains all notification data:

onNotificationLaunch: ({required isFromNotification, required payload}) {
  if (isFromNotification && payload != null) {
    final notificationId = payload['id'];
    final notificationType = payload['type'];
    final notificationData = payload['data'];
    
    // Route based on notification type
    if (notificationType == 'message') {
      return SwiftRouting(
        route: '/chat',
        payload: {'chatId': notificationData['chatId']},
      );
    } else if (notificationType == 'order') {
      return SwiftRouting(
        route: '/order',
        payload: {'orderId': notificationData['orderId']},
      );
    }
  }
  return null;
}

Complex Payload Structure

// Example notification payload structure
{
  "id": "12345",
  "type": "message",
  "title": "New Message",
  "body": "You have a new message",
  "data": {
    "chatId": "chat_123",
    "userId": "user_456",
    "timestamp": "2025-01-20T10:00:00Z"
  }
}

URL Structure

Deep links follow this structure:

yourapp://route/segment?param1=value1&param2=value2

Examples:

  • myapp://product/123
  • myapp://chat/456?userId=789
  • myapp://profile?tab=settings
onDeepLink: ({required url, required route, required queryParams}) {
  print('Deep link received: $url');
  print('Route: $route');
  print('Query params: $queryParams');
  
  // Handle different routes
  switch (route) {
    case '/product':
      return SwiftRouting(
        route: '/product',
        payload: {'productId': route.split('/').last},
      );
      
    case '/chat':
      return SwiftRouting(
        route: '/chat',
        payload: {
          'chatId': route.split('/').last,
          'userId': queryParams['userId'],
        },
      );
      
    case '/profile':
      return SwiftRouting(
        route: '/profile',
        payload: {'tab': queryParams['tab']},
      );
      
    default:
      return null; // Unknown route, use default
  }
}

Real-World Examples

E-Commerce App

SwiftFlutterMaterial(
  materialApp: MaterialApp(
    // ... app config
  ),
  onNotificationLaunch: ({required isFromNotification, required payload}) {
    if (isFromNotification && payload != null) {
      final type = payload['type'];
      
      switch (type) {
        case 'order_status':
          return SwiftRouting(
            route: '/orders',
            payload: {'orderId': payload['orderId']},
          );
        case 'promotion':
          return SwiftRouting(
            route: '/products',
            payload: {'category': payload['categoryId']},
          );
        default:
          return SwiftRouting(route: '/home');
      }
    }
    return null;
  },
)

Chat/Messaging App

onNotificationLaunch: ({required isFromNotification, required payload}) {
  if (isFromNotification && payload != null) {
    final chatId = payload['data']?['chatId'];
    if (chatId != null) {
      return SwiftRouting(
        route: '/chat',
        payload: {
          'chatId': chatId,
          'messageId': payload['data']?['messageId'],
        },
      );
    }
  }
  return null;
}

Social Media App

onNotificationLaunch: ({required isFromNotification, required payload}) {
  if (isFromNotification && payload != null) {
    final notificationType = payload['type'];
    
    if (notificationType == 'new_follower') {
      return SwiftRouting(
        route: '/profile',
        payload: {'userId': payload['data']?['userId']},
      );
    } else if (notificationType == 'like') {
      return SwiftRouting(
        route: '/post',
        payload: {'postId': payload['data']?['postId']},
      );
    }
  }
  return null;
}

Common Use Cases

1. Skip Splash Screen on Notification Launch

onNotificationLaunch: ({required isFromNotification, required payload}) {
  if (isFromNotification) {
    // Skip splash, go directly to notification screen
    return SwiftRouting(
      route: '/notification',
      payload: payload,
    );
  }
  // Normal launch, show splash
  return null;
}

2. Handle Different Notification Types

onNotificationLaunch: ({required isFromNotification, required payload}) {
  if (isFromNotification && payload != null) {
    final type = payload['type'];
    
    if (type == 'urgent') {
      return SwiftRouting(route: '/urgent-notification');
    } else if (type == 'reminder') {
      return SwiftRouting(route: '/reminders');
    } else {
      return SwiftRouting(route: '/notifications');
    }
  }
  return null;
}

3. Pass Complex Data

onNotificationLaunch: ({required isFromNotification, required payload}) {
  if (isFromNotification && payload != null) {
    return SwiftRouting(
      route: '/details',
      payload: {
        'id': payload['id'],
        'type': payload['type'],
        'data': payload['data'],
        'timestamp': DateTime.now().toIso8601String(),
      },
    );
  }
  return null;
}

Best Practices

1. Always Check Payload

onNotificationLaunch: ({required isFromNotification, required payload}) {
  if (isFromNotification && payload != null) {
    // Always validate payload structure
    if (payload.containsKey('type') && payload.containsKey('data')) {
      // Handle notification
    }
  }
  return null;
}

2. Provide Fallback Routes

onNotificationLaunch: ({required isFromNotification, required payload}) {
  if (isFromNotification) {
    try {
      // Try to route based on notification
      return SwiftRouting(
        route: '/notification',
        payload: payload,
      );
    } catch (e) {
      // Fallback to home on error
      return SwiftRouting(route: '/home');
    }
  }
  return null;
}
onDeepLink: ({required url, required route, required queryParams}) {
  // Validate deep link
  if (route.startsWith('/public')) {
    // Public routes are safe
    return SwiftRouting(route: route, payload: queryParams);
  } else if (route.startsWith('/private')) {
    // Check authentication first
    if (isAuthenticated) {
      return SwiftRouting(route: route, payload: queryParams);
    } else {
      return SwiftRouting(route: '/login');
    }
  }
  return null;
}

Troubleshooting

Notification Not Detected

Problem: App doesn’t detect notification launch.

Solutions:

  1. Ensure notification permissions are granted
  2. Check that flutter_local_notifications is properly configured
  3. Verify notification payload structure
  4. Check AndroidManifest.xml / Info.plist configuration

Problem: Deep links don’t open the app.

Solutions:

  1. Verify URL scheme is correctly configured
  2. Check intent filters in AndroidManifest.xml
  3. Verify CFBundleURLSchemes in Info.plist
  4. Test with adb shell am start -W -a android.intent.action.VIEW -d "yourapp://test"

Payload is Null

Problem: Payload is null when notification is tapped.

Solutions:

  1. Ensure notification has a data payload
  2. Check notification payload structure matches expected format
  3. Verify notification is sent with proper data field

Testing

Test Notification Launch (Android)

# Send test notification
adb shell am broadcast -a com.google.firebase.MESSAGING_EVENT \
  --es "notification" '{"title":"Test","body":"Test notification","data":{"type":"test"}}'
adb shell am start -W -a android.intent.action.VIEW \
  -d "yourapp://product/123"
xcrun simctl openurl booted "yourapp://product/123"

Performance Considerations

  1. Fast Detection: The plugin checks notification launch before runApp(), ensuring minimal delay
  2. Payload Size: Keep payload sizes reasonable (< 4KB recommended)
  3. Navigation: Direct routing skips unnecessary screens, improving perceived performance

Conclusion

The screen_launch_by_notfication plugin provides a seamless way to handle notification launches and deep links in Flutter apps. By detecting notification launches before the app initializes, you can provide a native-like experience that routes users directly to relevant screens.

Key benefits:

  • ✅ Detect notification launches in all app states
  • ✅ Zero native setup required
  • ✅ Automatic deep link handling
  • ✅ Works with MaterialApp and GetMaterialApp
  • ✅ Cross-platform support (iOS & Android)

Resources


Happy Building! 🚀