instant_request_handler

Reusable Flutter plugin for real-time incoming request experiences. It lets an app register notification types, match incoming FCM/data payloads, show a custom widget immediately, fetch details after the UI is visible, and recover pending requests after background or killed-app launches.

The package is generic. It does not include app controllers, repositories, API clients, GetX, or ride-specific business logic.

Installation

dependencies:
  instant_request_handler: ^0.0.1
  firebase_core: ^4.2.1
  firebase_messaging: ^16.0.4

Run:

flutter pub get

Android Setup

Add the permissions your app needs to android/app/src/main/AndroidManifest.xml. The plugin manifest also declares them, but keeping them in the app manifest makes the requirements explicit.

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

Set minSdk to 24 or higher.

android {
  defaultConfig {
    minSdk = 24
  }
}

Use singleTop or a similar launch mode for your main activity so native notification intents are delivered to the existing Flutter activity when possible.

Firebase Setup

Configure Firebase normally for your app:

await Firebase.initializeApp(
  options: DefaultFirebaseOptions.currentPlatform,
);

FirebaseMessaging.onBackgroundMessage(instantRequestBackgroundHandler);
FirebaseMessaging.onMessage.listen(
  InstantRequestHandler.handleForegroundMessage,
);

Send FCM data payloads that include either action or type, plus the request id key you registered.

Registering Types

await InstantRequestHandler.initialize(
  notificationTypes: [
    InstantNotificationType<Map<String, dynamic>>(
      action: 'new_ride_request',
      type: 'new_ride_request',
      idKey: 'ride_request_id',
      timeoutSeconds: 30,
      presentation: InstantRequestPresentation.fullScreenOrSheet,
      onFetchData: (requestId, payload) async {
        return fetchDetailsFromBackend(requestId);
      },
      onAccept: (requestId, data) async {
        await acceptRequest(requestId);
      },
      onReject: (requestId, data, reason) async {
        await rejectRequest(requestId, reason);
      },
      builder: (context, state) {
        return MyIncomingRequestWidget(state: state);
      },
    ),
  ],
);

Attach the package navigator key to your app:

MaterialApp(
  navigatorKey: InstantRequestHandler.navigatorKey,
  home: const AppHome(),
);

You may pass your own GlobalKey<NavigatorState> to initialize.

Foreground Handling

FirebaseMessaging.onMessage.listen(
  InstantRequestHandler.handleForegroundMessage,
);

Foreground payloads are matched against registered types. The UI is shown with isLoading == true, the countdown starts immediately, and onFetchData runs after the widget is visible.

Background Handler

@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) {
  return instantRequestBackgroundHandler(message);
}

Register it before runApp:

FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);

The background handler asks Android native code to match the data payload, store the pending request, create a high-priority/full-screen notification, briefly wake the device, and open the host activity when Android allows it.

Killed App / Native Behavior

The Android side includes:

  • InstantFirebaseMessagingService
  • InstantRequestHandlerPlugin
  • InstantRequestLauncher
  • MethodChannel and EventChannel bridge
  • Android SharedPreferences pending request storage
  • High-importance notification channel
  • Full-screen intent notification
  • Wake lock
  • Intent recovery for app launches and onNewIntent

When Flutter starts, initialize configures native matchers and recovers pending requests from both native and Flutter storage.

Custom Widget Builder

The builder receives InstantRequestState<T>:

builder: (context, state) {
  if (state.isLoading) return const CircularProgressIndicator();
  return Column(
    children: [
      Text('${state.secondsRemaining}s'),
      FilledButton(
        onPressed: state.accept,
        child: const Text('Accept'),
      ),
      OutlinedButton(
        onPressed: state.reject,
        child: const Text('Reject'),
      ),
    ],
  );
}

state.retry() re-runs onFetchData. state.close() dismisses the UI and calls onReject with InstantRequestCloseReason.dismissed.

Permission Helpers

final status = await InstantRequestHandler.checkPermissions();
await InstantRequestHandler.requestNotificationPermission();
await InstantRequestHandler.openOverlaySettings();
await InstantRequestHandler.openBatteryOptimizationSettings();
await InstantRequestHandler.openFullScreenIntentSettings();

The package also includes InstantRequestPermissionOnboarding for a generic permission setup screen.

Local Simulation

await InstantRequestHandler.handlePayload({
  'action': 'new_ride_request',
  'type': 'new_ride_request',
  'ride_request_id': '123',
});

Testing With ADB

adb shell am start \
  -n your.package.name/.MainActivity \
  --es action new_ride_request \
  --es ride_request_id 12345

Use a fresh request id for repeated tests. Completed request ids are remembered for duplicate protection, so reusing the same id can intentionally do nothing.

For FCM tests, send a data-only payload containing the same keys.

Android Limitations

Android controls background activity launches, full-screen intent eligibility, notification permission, OEM battery rules, and lock-screen behavior. This plugin requests the strongest allowed path for incoming request alerts, but it cannot guarantee that every device will wake, unlock, or show full screen.

Full-screen intent access is user-controllable on newer Android versions. Battery optimization and overlay permissions also require user approval.

For v1, if a request is active and another request arrives, the new request is ignored. Duplicate request ids are ignored while active or after completion.

Example

See example/ for a Flutter app that registers a new_ride_request type, simulates payloads locally, shows a custom ride request sheet, and includes Android manifest setup.