flutter_native_overlay

pub package License Platform

A Flutter plugin for displaying truly native system-level overlays on Android and iOS. Unlike standard Flutter overlays (which live inside your app's window), these are drawn at the OS level — they persist over other apps, the home screen, and even the lock screen.


Table of Contents


Features

  • 📱 Cross-platform: Same API on Android and iOS — differences are platform-guarded and clearly documented.
  • 🎨 Rich styling: Customize colors, shapes (rounded, square, circle), sizes, and padding.
  • 🖼️ Avatar support: Show a circular contact photo or custom image alongside your text.
  • 📊 Progress bars: Linear auto-dismiss timers or manual circular progress rings.
  • 🖱️ Draggable: The user can freely drag the overlay anywhere on screen.
  • 💨 Drag-to-dismiss: Drag the overlay to a configurable zone to close it with a satisfying animation.
  • Auto-dismiss: Set a Duration and the overlay closes itself automatically.
  • 🧩 Custom widgets: Render any Flutter widget as a native overlay via showCustom().
  • 🍎 Live Activities (iOS only): Start, update, and end Dynamic Island / Lock Screen Live Activities.
  • 📐 Native padding: Query exact OS inset values for edge-to-edge layout precision.

Screenshots 📸

Android Overlay Android Linear Progress Android Circular Progress
Android Overlay Android Progress Android Circular Progress

Platform Support

Feature Android iOS
Native overlay window
Draggable overlays
Auto-dismiss timer
Linear progress bar
Circular progress ring
Avatar image
Custom Flutter widget
Overlay event stream
Edge-to-edge padding query
Overlay permission check
Live Activity

Installation

Add to your pubspec.yaml:

dependencies:
  flutter_native_overlay: ^0.0.1

Then run:

flutter pub get

Platform Setup

Android Setup

Android requires the SYSTEM_ALERT_WINDOW permission to draw over other apps.

1. Add the permission to android/app/src/main/AndroidManifest.xml:

<manifest ...>
  <!-- Required for native overlay -->
  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

  <application ...>
    ...
  </application>
</manifest>

2. Request the permission at runtime before calling showOverlay():

import 'dart:io';

final overlay = FlutterNativeOverlay();

if (Platform.isAndroid) {
  final hasPermission = await overlay.checkOverlayPermission();
  if (!hasPermission) {
    // Opens the Android Settings page for the user to grant permission
    await overlay.requestOverlayPermission();
  }
}

Note: After the user returns from the Settings page, call checkOverlayPermission() again to verify they granted it.


iOS Setup

Minimum deployment target: iOS 13.0 or higher.

No special permissions are needed for basic native overlays.

For Live Activities (Dynamic Island / Lock Screen), additional requirements apply:

  • iOS 16.2+ required on the user's device.
  • An ActivityKit Widget Extension target must be added in Xcode.
  • Configure your app's Info.plist with NSSupportsLiveActivities = YES.

See Apple's ActivityKit documentation for the full Widget Extension setup guide.


Quick Start

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_native_overlay/flutter_native_overlay.dart';

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final _overlay = FlutterNativeOverlay();

  @override
  void initState() {
    super.initState();
    // Listen to overlay interaction events
    _overlay.overlayEvents.listen((event) {
      debugPrint('Overlay event: ${event['event']}');
    });
  }

  Future<void> _show() async {
    // Android: ensure permission is granted first
    if (Platform.isAndroid) {
      final ok = await _overlay.checkOverlayPermission();
      if (!ok) {
        await _overlay.requestOverlayPermission();
        return;
      }
    }

    await _overlay.showOverlay(
      title: 'Incoming Call',
      content: 'John Doe',
      isDraggable: true,
      style: NativeOverlayStyle(
        container: NativeContainerStyle(
          backgroundColor: Colors.deepPurple,
          borderRadius: 20,
        ),
        button: NativeButtonStyle(
          showButton: true,
          text: 'Decline',
          backgroundColor: Colors.redAccent,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(onPressed: _show, child: const Text('Show Overlay'));
  }
}

API Reference

showOverlay() — Both Platforms

Displays a native overlay with text content and optional avatar.

Future<bool> showOverlay({
  required String title,
  String? content,
  Uint8List? avatarBytes,
  NativeOverlayStyle style = const NativeOverlayStyle(),
  bool isDraggable = true,
  Duration? duration,
})
Parameter Type Required Description
title String Main heading shown in the overlay.
content String? Secondary text shown below the title.
avatarBytes Uint8List? Raw PNG/JPEG bytes for a leading avatar image.
style NativeOverlayStyle Full visual customization (colors, shapes, fonts, etc.).
isDraggable bool Whether the overlay can be dragged. Default: true.
duration Duration? If set, the overlay auto-dismisses after this time.

Returns: true if the overlay was shown successfully.

Example:

await overlay.showOverlay(
  title: 'Upload Complete',
  content: 'your_file.zip has been uploaded.',
  duration: const Duration(seconds: 4),
  style: NativeOverlayStyle(
    container: NativeContainerStyle(
      backgroundColor: Colors.green.shade800,
      borderRadius: 14,
    ),
    progress: NativeProgressStyle(color: Colors.greenAccent),
  ),
);

showCustom() — Both Platforms

Renders any Flutter widget off-screen and displays it as a native overlay.

Future<bool> showCustom({
  required BuildContext context,
  required Widget widget,
  NativeOverlayStyle style = const NativeOverlayStyle(),
  bool isDraggable = true,
  double pixelRatio = 3.0,
  Duration? duration,
})
Parameter Type Required Description
context BuildContext Must have an Overlay ancestor (e.g. inside MaterialApp).
widget Widget The Flutter widget to capture and display natively.
style NativeOverlayStyle Container shape and position settings.
isDraggable bool Whether the overlay can be dragged. Default: true.
pixelRatio double Image capture resolution scale. Higher = sharper. Default: 3.0.
duration Duration? Auto-dismiss after this duration.

Example:

await overlay.showCustom(
  context: context,
  widget: Container(
    width: 80,
    height: 80,
    decoration: const BoxDecoration(
      shape: BoxShape.circle,
      gradient: LinearGradient(
        colors: [Colors.deepPurple, Colors.cyan],
      ),
    ),
    child: const Icon(Icons.phone, color: Colors.white, size: 36),
  ),
  style: const NativeOverlayStyle(
    container: NativeContainerStyle(
      shape: OverlayShape.circle,
      width: 80,
      height: 80,
    ),
  ),
);

hideOverlay() — Both Platforms

Hides and removes the currently active native overlay.

Future<bool> hideOverlay()

Returns: true if an overlay was active and successfully removed.

await overlay.hideOverlay();

overlayEvents — Both Platforms

A stream of interaction events from the native overlay.

Stream<Map<String, dynamic>> get overlayEvents

Each event map contains an 'event' key with one of the following values:

Value Trigger
'overlay_clicked' The user tapped the overlay body.
'button_clicked' The user tapped the action button.
'drag_dismissed' The overlay was dragged into the dismiss zone.

Example:

final sub = overlay.overlayEvents.listen((event) {
  switch (event['event']) {
    case 'overlay_clicked':
      Navigator.pushNamed(context, '/order-tracking');
      break;
    case 'button_clicked':
      overlay.hideOverlay();
      break;
    case 'drag_dismissed':
      debugPrint('Overlay dismissed by drag.');
      break;
  }
});

// Remember to cancel the subscription when done
sub.cancel();

getNativePadding() — Both Platforms

Returns the system inset values (in logical pixels) for precise edge-to-edge layout positioning.

Future<Map<String, double>> getNativePadding()

Returns a map with keys: 'top', 'bottom', 'left', 'right'.

Useful for positioning overlays below the status bar, above the nav bar, or accounting for the Dynamic Island on iPhone.

final padding = await overlay.getNativePadding();
print('Status bar height: ${padding['top']}');
print('Nav bar height: ${padding['bottom']}');

checkOverlayPermission() — Android Only

Checks if the app has the SYSTEM_ALERT_WINDOW (draw over other apps) permission.

Future<bool> checkOverlayPermission()

Returns: true if permission is granted. On iOS, always returns true.

if (Platform.isAndroid) {
  final hasPermission = await overlay.checkOverlayPermission();
}

requestOverlayPermission() — Android Only

Opens the Android system settings page so the user can grant the overlay permission.

Future<bool> requestOverlayPermission()

Returns: true after navigating to the settings page. On iOS, this is a no-op.

After this call returns, the user is in Settings. Call checkOverlayPermission() after they return to your app.

if (Platform.isAndroid) {
  await overlay.requestOverlayPermission();
  // User navigates back from Settings...
  final granted = await overlay.checkOverlayPermission();
}

startLiveActivity() — iOS Only

Starts an iOS Live Activity visible in the Dynamic Island and on the Lock Screen.

Future<String?> startLiveActivity({
  required String title,
  required String content,
  double? progress,
})
Parameter Type Description
title String Primary heading in the Dynamic Island and Lock Screen.
content String Supporting status or detail text.
progress double? Optional 0.0–1.0 value to show a progress bar in the activity.

Requirements: iOS 16.2+, ActivityKit Widget Extension configured in Xcode.

Returns: The activityId (String) on success, or null if the activity failed to start. Save this ID to update or end the activity later.

if (Platform.isIOS) {
  final activityId = await overlay.startLiveActivity(
    title: 'Order #1042',
    content: 'Arriving in 8 minutes',
    progress: 0.2,
  );
}

updateLiveActivity() — iOS Only

Pushes a real-time update to an active Live Activity.

Future<bool> updateLiveActivity({
  required String activityId,
  String? title,
  String? content,
  double? progress,
})
Parameter Type Description
activityId String The ID returned by startLiveActivity().
title String? Optional new heading. Keeps current if omitted.
content String? Optional new status text. Keeps current if omitted.
progress double? Optional new 0.0–1.0 progress value.

Returns: true on success.

await overlay.updateLiveActivity(
  activityId: activityId,
  content: 'Arriving in 2 minutes',
  progress: 0.9,
);

endLiveActivity() — iOS Only

Ends and dismisses an active Live Activity.

Future<bool> endLiveActivity({String? activityId})

Parameters:

  • activityId: Optional ID of a specific activity to end. If omitted, all active Live Activities started by the app are ended.

Returns: true on success.

// End a specific activity
await overlay.endLiveActivity(activityId: activityId);

// Or end all activities
await overlay.endLiveActivity();

Styling Reference

All visual customization goes through NativeOverlayStyle, which bundles five sub-style classes.

NativeOverlayStyle

The root style object passed to showOverlay() and showCustom().

const NativeOverlayStyle({
  NativeContainerStyle container,  // shape, background, position
  NativeContentStyle content,      // text colors and sizes
  NativeButtonStyle button,        // action button appearance
  NativeAvatarStyle avatar,        // avatar image size and shape
  NativeProgressStyle progress,    // countdown/progress bar
})

NativeContainerStyle

Controls the overlay's outer container.

Property Type Default Description
backgroundColor Color Color(0xFF121212) Background fill color.
shape OverlayShape OverlayShape.rounded rounded, square, or circle.
borderRadius double 16.0 Corner radius (only for rounded shape).
position OverlayPosition OverlayPosition.top top, bottom, center, topLeft, topRight, bottomLeft, bottomRight.
xOffset int 0 Horizontal pixel offset from the position.
yOffset int 100 Vertical pixel offset from the position.
width double? null (wrap content) Fixed width in logical pixels.
height double? null (wrap content) Fixed height in logical pixels.
padding NativeEdgeInsets NativeEdgeInsets.all(16) Internal padding.

OverlayShape values:

OverlayShape.rounded  // default, uses borderRadius
OverlayShape.square   // sharp corners
OverlayShape.circle   // perfect circle, ignores borderRadius

OverlayPosition values:

OverlayPosition.top         // centered at top
OverlayPosition.bottom      // centered at bottom
OverlayPosition.center      // screen center
OverlayPosition.topLeft
OverlayPosition.topRight
OverlayPosition.bottomLeft
OverlayPosition.bottomRight

NativeContentStyle

Controls the text inside the overlay.

Property Type Default Description
titleColor Color Colors.white Color of the title text.
titleFontSize double 18.0 Font size of the title.
textColor Color Colors.white70 Color of the content text.
contentFontSize double 14.0 Font size of the content text.

NativeButtonStyle

Controls the optional action button shown inside the overlay.

Property Type Default Description
showButton bool false Whether to show the button.
text String 'Close' Button label.
backgroundColor Color Color(0xFF424242) Button background color.
textColor Color Colors.white Button label color.
fontSize double 14.0 Button label font size.

NativeAvatarStyle

Controls the leading avatar image (shown when avatarBytes is provided).

Property Type Default Description
size double 48.0 Width and height of the avatar in logical pixels.
borderRadius double 24.0 Corner radius. Default gives a circle (size / 2).

NativeProgressStyle

Controls the auto-dismiss countdown progress bar (shown when duration is set).

Property Type Default Description
showProgressBar bool true Whether to show the progress bar.
color Color Colors.green Progress bar fill color.
height double 4.0 Height of the progress bar in logical pixels.

Event Handling

All user interactions with the overlay are forwarded to the overlayEvents stream as a Map<String, dynamic>:

final overlay = FlutterNativeOverlay();

final subscription = overlay.overlayEvents.listen((Map<String, dynamic> event) {
  final eventType = event['event'] as String?;

  if (eventType == 'overlay_clicked') {
    // Navigate to a detail screen, etc.
  } else if (eventType == 'button_clicked') {
    overlay.hideOverlay();
  } else if (eventType == 'drag_dismissed') {
    // The user swiped the overlay to dismiss it
  }
});

// Cancel when no longer needed (e.g. in dispose())
subscription.cancel();

Usage Examples

Simple notification badge

await overlay.showOverlay(
  title: '3 New Messages',
  style: NativeOverlayStyle(
    container: NativeContainerStyle(
      backgroundColor: Colors.indigo,
      borderRadius: 12,
      position: OverlayPosition.topRight,
    ),
  ),
);

Contact call overlay with avatar

final ByteData data = await rootBundle.load('assets/contact.png');
final Uint8List bytes = data.buffer.asUint8List();

await overlay.showOverlay(
  title: 'Sarah Connor',
  content: 'Incoming call...',
  avatarBytes: bytes,
  isDraggable: true,
  style: const NativeOverlayStyle(
    container: NativeContainerStyle(
      backgroundColor: Color(0xFF1A1A2E),
      borderRadius: 24,
    ),
    avatar: NativeAvatarStyle(size: 56, borderRadius: 28),
    button: NativeButtonStyle(
      showButton: true,
      text: 'Decline',
      backgroundColor: Colors.redAccent,
    ),
  ),
);

Auto-dismissing toast with countdown bar

await overlay.showOverlay(
  title: 'File saved',
  content: 'report_q1.pdf',
  duration: const Duration(seconds: 3),
  style: NativeOverlayStyle(
    container: const NativeContainerStyle(
      backgroundColor: Color(0xFF1B5E20),
      borderRadius: 10,
    ),
    progress: const NativeProgressStyle(
      color: Colors.lightGreenAccent,
      height: 3,
    ),
  ),
);

Circular widget overlay

await overlay.showCustom(
  context: context,
  widget: Container(
    width: 72,
    height: 72,
    decoration: const BoxDecoration(
      shape: BoxShape.circle,
      gradient: LinearGradient(
        colors: [Colors.deepOrange, Colors.amber],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
    ),
    child: const Icon(Icons.local_fire_department, color: Colors.white, size: 34),
  ),
  style: const NativeOverlayStyle(
    container: NativeContainerStyle(
      shape: OverlayShape.circle,
      width: 72,
      height: 72,
    ),
  ),
);

iOS Live Activity for delivery tracking

import 'dart:io';

if (Platform.isIOS) {
  // Start when order is dispatched
  final activityId = await overlay.startLiveActivity(
    title: 'Order #7831',
    content: 'Arriving in 15 minutes',
    progress: 0.1,
  );

  // Update as ETA changes
  if (activityId != null) {
    await overlay.updateLiveActivity(
      activityId: activityId,
      content: 'Arriving in 3 minutes',
      progress: 0.8,
    );

    // End when delivered
    await overlay.endLiveActivity(activityId: activityId);
  }
}

License

Copyright 2026 Basharat Mehdi

Licensed under the Apache License, Version 2.0. See the LICENSE file for details.