flutter_native_overlay 0.0.1
flutter_native_overlay: ^0.0.1 copied to clipboard
A Flutter plugin for displaying truly native system-level overlays on Android and iOS, with support for drag-to-dismiss, progress bars, avatars, and iOS Live Activities.
flutter_native_overlay #
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
- Screenshots
- Platform Support
- Installation
- Platform Setup
- Quick Start
- API Reference
- Styling Reference
- Event Handling
- Usage Examples
- License
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
Durationand 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 |
|---|---|---|
![]() |
![]() |
![]() |
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.plistwithNSSupportsLiveActivities = 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.



