notification_voip_plugin
A Flutter plugin for notifications and VoIP calls across Android, iOS, Web, macOS, Windows, and Linux.
What It Does
This plugin gives you a single Dart API to handle:
- System notifications with 8 visual templates (big text, big picture, progress bar, action buttons, and more)
- In-app banner overlays that slide in from the top
- VoIP calls with native call UI (CallKit on iOS, ConnectionService on Android) or a custom Flutter call screen
- Push tokens (FCM, APNs, VoIP via PushKit)
- Badge count management
- Live Activities on iOS (Dynamic Island / Lock Screen) — experimental
Everything is accessed through NotificationVoipPlugin.* — no instances, no singletons, no boilerplate.
Platform Support
| Feature | Android | iOS | macOS | Windows | Linux | Web |
|---|---|---|---|---|---|---|
| System Notifications | ✅ | ✅ | ✅ | stub | stub | ✅ |
| In-App Banners | ✅ | ✅ | — | — | — | — |
| Notification Templates | ✅ | ✅ | — | — | — | — |
| VoIP Calls (Native UI) | ✅ | ✅ | — | — | — | — |
| Custom Call Screen | ✅ | ✅ | — | — | — | — |
| Badge Count | ✅ | ✅ | ✅ | — | — | — |
| FCM Token | ✅ | — | — | — | — | — |
| APNs Token | — | ✅ | — | — | — | — |
| VoIP Token | — | ✅ | — | — | — | — |
| Live Activities ⚠️ | — | ✅ (16.2+) | — | — | — | — |
| Phone Account Check | ✅ | — | — | — | — | — |
Quick Start
1. Install
dependencies:
notification_voip_plugin: ^2.0.0
flutter pub get
2. Initialize
import 'package:notification_voip_plugin/notification_voip_plugin.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await NotificationVoipPlugin.init(
const NvpConfig(
appName: 'My App',
channelId: 'my_channel',
channelName: 'My Channel',
),
);
runApp(const MyApp());
}
NvpConfig is optional — init() works with sensible defaults.
3. Request Permission
final granted = await NotificationVoipPlugin.requestPermission();
4. Show a Notification
await NotificationVoipPlugin.showNotification(
const NvpNotification(title: 'Hello', body: 'World'),
);
That's it. You're up and running.
Setup
Android
Minimum SDK — set minSdk 24 in android/app/build.gradle:
android {
defaultConfig {
minSdk = 24
}
}
Firebase (for FCM tokens) — add Google Services plugin to android/build.gradle:
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
Apply it in android/app/build.gradle:
apply plugin: 'com.google.gms.google-services'
Place your google-services.json in android/app/.
Permissions — the plugin's manifest already declares all required permissions (merged automatically):
POST_NOTIFICATIONS, MANAGE_OWN_CALLS, FOREGROUND_SERVICE,
RECORD_AUDIO, BLUETOOTH, BLUETOOTH_ADMIN, READ_PHONE_STATE,
READ_PHONE_NUMBERS, ANSWER_PHONE_CALLS
No additional manifest entries needed.
Phone Account (VoIP) — on Android, the user must enable the VoIP phone account in system settings before incoming calls work:
final enabled = await NotificationVoipPlugin.isPhoneAccountEnabled();
if (!enabled) {
await NotificationVoipPlugin.openPhoneAccountSettings();
}
iOS
Deployment Target — iOS 14.0+ in your ios/Podfile:
platform :ios, '14.0'
Xcode Capabilities — enable on your Runner target:
- Push Notifications
- Background Modes → check "Voice over IP" and "Remote notifications"
Info.plist — add:
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
<string>remote-notification</string>
</array>
APNs Token Forwarding — in AppDelegate.swift:
import notification_voip_plugin
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
NotificationVoipPlugin.shared?.setAPNsToken(deviceToken)
}
Usage
Notifications
Simple notification:
await NotificationVoipPlugin.showNotification(
const NvpNotification(title: 'Hello', body: 'World'),
);
With a template:
// Big text (expandable)
await NotificationVoipPlugin.showNotification(
const NvpNotification(title: 'Article', body: 'Preview...'),
template: const NvpNotificationTemplate(
type: NvpNotificationTemplateType.bigText,
expandedText: 'Full article text that expands when pulled down.',
),
);
// Big picture
await NotificationVoipPlugin.showNotification(
const NvpNotification(title: 'Photo', body: 'New photo shared'),
template: const NvpNotificationTemplate(
type: NvpNotificationTemplateType.bigPicture,
imageUrl: 'https://example.com/photo.jpg',
),
);
// Progress bar
await NotificationVoipPlugin.showNotification(
const NvpNotification(title: 'Download', body: 'Downloading...', tag: 'dl-1'),
template: const NvpNotificationTemplate(
type: NvpNotificationTemplateType.progress,
progressValue: 65,
progressMax: 100,
),
);
Interactive (action buttons + text input):
await NotificationVoipPlugin.showNotification(
const NvpNotification(title: 'New Message', body: 'Hey!'),
template: const NvpNotificationTemplate(
type: NvpNotificationTemplateType.interactive,
actions: [
NvpNotificationAction(id: 'reply', title: 'Reply', isTextInput: true, textInputPlaceholder: 'Type a reply...'),
NvpNotificationAction(id: 'mark_read', title: 'Mark Read'),
],
),
);
Grouped notifications:
await NotificationVoipPlugin.showNotification(
const NvpNotification(title: 'Chat', body: 'New message', groupKey: 'chat-group'),
);
In-app banner overlay:
await NotificationVoipPlugin.showInAppNotification(
const NvpNotification(title: 'Alert', body: 'This is a banner overlay'),
);
Available template types: normal, richText, bigText, bigPicture, bigBanner, progress, interactive, custom
Badge Count
await NotificationVoipPlugin.setBadgeCount(5);
final count = await NotificationVoipPlugin.getBadgeCount();
await NotificationVoipPlugin.setBadgeCount(0); // clear
Push Tokens
final pushToken = await NotificationVoipPlugin.getPushToken(); // FCM on Android, APNs on iOS
final fcmToken = await NotificationVoipPlugin.getFCMToken(); // Android only
final apnsToken = await NotificationVoipPlugin.getAPNsToken(); // iOS only
final voipToken = await NotificationVoipPlugin.getVoIPToken(); // iOS only
VoIP Calls
Incoming call (native UI):
try {
await NotificationVoipPlugin.showIncomingCall(
const NvpCallConfig(
callId: 'session-123',
callerName: 'John Doe',
isVideo: true,
),
);
} on PlatformException catch (e) {
if (e.code == 'PHONE_ACCOUNT_NOT_ENABLED') {
await NotificationVoipPlugin.openPhoneAccountSettings();
}
}
Incoming call (custom Flutter screen):
await NotificationVoipPlugin.showIncomingCall(
const NvpCallConfig(
callId: 'session-123',
callerName: 'John Doe',
isVideo: false,
useNativeScreen: false,
),
);
Outgoing call:
await NotificationVoipPlugin.showOutgoingCall(
const NvpCallConfig(
callId: 'session-456',
callerName: 'Jane Doe',
isOutgoing: true,
),
);
Call controls:
await NotificationVoipPlugin.toggleMute('session-123');
await NotificationVoipPlugin.toggleSpeaker('session-123');
await NotificationVoipPlugin.toggleCamera('session-123');
await NotificationVoipPlugin.endCall('session-123');
await NotificationVoipPlugin.endAllCalls(); // cleanup on logout
Deferred connection (e.g., after WebRTC setup):
await NotificationVoipPlugin.setCallConnected('session-123');
Custom Call Screen Widget
The plugin includes a ready-to-use NvpCallScreen widget:
Navigator.push(context, MaterialPageRoute(
builder: (_) => NvpCallScreen(
event: NvpCallEvent(action: NvpCallAction.incoming, callId: 'session-123', callerName: 'John'),
controller: NvpCallScreenController(callId: 'session-123'),
),
));
NvpCallScreenController methods:
| Method | Returns | Description |
|---|---|---|
accept() |
Future<void> |
Accept incoming call |
reject() |
Future<void> |
Reject incoming call |
end() |
Future<void> |
End current call |
toggleMute() |
Future<bool> |
Toggle mute, returns new state |
toggleSpeaker() |
Future<bool> |
Toggle speaker, returns new state |
toggleCamera() |
Future<bool> |
Toggle camera, returns new state |
State getters: isMuted, isSpeakerOn, isCameraOn
If a platform call fails, the controller automatically rolls back the local state.
Event Streams
// Notification tapped (system or in-app banner)
NotificationVoipPlugin.onNotificationTap.listen((NvpNotification n) {
print('Tapped: ${n.title}');
});
// VoIP call events (accept, decline, ended, incoming, timeoutEnded)
NotificationVoipPlugin.onCallEvent.listen((NvpCallEvent e) {
print('Call: ${e.action} (${e.callId})');
});
// Real-time call state changes (mute, speaker, status)
NotificationVoipPlugin.onCallStateChanged.listen((NvpCallState s) {
print('State: ${s.status.name} muted=${s.isMuted}');
});
// Push token refreshed
NotificationVoipPlugin.onTokenRefresh.listen((String token) {
print('New token: $token');
});
// Push received in foreground (iOS) — inspect and optionally suppress
NotificationVoipPlugin.onPushReceived.listen((Map<String, dynamic> payload) {
if (payload['conversationId'] == activeConversationId) {
NotificationVoipPlugin.suppressNextForegroundNotification();
}
});
Callbacks via NvpConfig
As an alternative to streams, register callbacks directly in NvpConfig:
await NotificationVoipPlugin.init(
NvpConfig(
appName: 'My App',
onBannerTap: (notification) => print('Banner: ${notification.title}'),
onSystemNotificationTap: (notification) => print('Tapped: ${notification.title}'),
onCallAnswered: (event) => print('Answered: ${event.callId}'),
onCallDeclined: (event) => print('Declined: ${event.callId}'),
onCallEnded: (event) => print('Ended: ${event.callId}'),
onCallIncoming: (event) => print('Incoming: ${event.callId}'),
onCallTimeoutEnded: (event) => print('Timeout: ${event.callId}'),
onCallStateChanged: (state) => print('State: ${state.status.name}'),
onTokenRefresh: (token) => print('Token: $token'),
onPushReceived: (payload) => print('Push: $payload'),
),
);
Live Activities (iOS 16.1+) — ⚠️ Experimental
This feature is experimental. The API may change in future releases.
Setup:
- In Xcode: File → New → Target → Widget Extension
- Add
NSSupportsLiveActivities = YEStoRunner/Info.plist - Copy the example widget from
ios/WidgetExtensionExample/NvpLiveActivityWidget.swift - Set Widget Extension deployment target to iOS 16.1+
Usage:
// Check support
final enabled = await NotificationVoipPlugin.areLiveActivitiesEnabled();
// Start
await NotificationVoipPlugin.startLiveActivity(
const NvpLiveActivity(tag: 'download-1', title: 'Downloading...', progress: 0.0),
);
// Update
await NotificationVoipPlugin.updateLiveActivity(
const NvpLiveActivity(tag: 'download-1', title: 'Downloading...', body: '25/50 MB', progress: 0.5),
);
// End
await NotificationVoipPlugin.endLiveActivity('download-1');
await NotificationVoipPlugin.endAllLiveActivities();
Cleanup
await NotificationVoipPlugin.dispose();
This cancels all stream subscriptions, clears cached streams, and releases native resources. The plugin can be re-initialized after dispose.
API Reference
Initialization & Lifecycle
| Method | Description |
|---|---|
init([NvpConfig? config]) |
Initialize the plugin. Call once at app start. |
dispose() |
Clean up all resources and cancel subscriptions. |
config |
Current plugin configuration (getter). |
Permissions
| Method | Description |
|---|---|
requestPermission() → Future<bool> |
Request notification permission. Returns true if granted. |
isPermissionGranted() → Future<bool> |
Check if notifications are enabled. |
openSettings() |
Open OS notification settings for this app. |
Tokens
| Method | Description |
|---|---|
getPushToken() → Future<String?> |
Platform push token (FCM on Android, APNs on iOS). |
getFCMToken() → Future<String?> |
Firebase Cloud Messaging token (Android). |
getAPNsToken() → Future<String?> |
APNs device token (iOS). |
getVoIPToken() → Future<String?> |
VoIP push token via PushKit (iOS). |
Notifications
| Method | Description |
|---|---|
showNotification(NvpNotification, {NvpNotificationTemplate?}) → Future<bool> |
Show a system notification. |
showInAppNotification(NvpNotification, {NvpNotificationTemplate?}) → Future<bool> |
Show an in-app banner overlay. |
cancelNotification(String tag) |
Cancel a specific notification by tag. |
clearAll() |
Clear all notifications and dismiss banners. |
suppressNextForegroundNotification() |
Suppress the next foreground notification (iOS). |
Badge
| Method | Description |
|---|---|
setBadgeCount(int count) |
Set app badge count. Pass 0 to clear. |
getBadgeCount() → Future<int> |
Get current badge count. |
VoIP / Calls
| Method | Description |
|---|---|
showIncomingCall(NvpCallConfig) |
Show incoming call UI. Throws PHONE_ACCOUNT_NOT_ENABLED on Android if needed. |
showOutgoingCall(NvpCallConfig) |
Show outgoing call UI. |
endCall(String callId) |
End a call by session ID. |
endAllCalls() |
End all active calls. |
setCallConnected(String callId) |
Mark a call as connected (for deferred connection flows). |
getActiveCallIds() → Future<List<String>> |
Get list of active call IDs. |
toggleMute(String callId) |
Toggle mute. |
toggleSpeaker(String callId) |
Toggle speaker. |
toggleCamera(String callId) |
Toggle camera. |
Android-Only
| Method | Description |
|---|---|
isPhoneAccountEnabled() → Future<bool> |
Check if VoIP phone account is enabled. |
openPhoneAccountSettings() |
Open phone account settings. |
Live Activities (iOS) — ⚠️ Experimental
| Method | Description |
|---|---|
areLiveActivitiesEnabled() → Future<bool> |
Check if Live Activities are supported. |
startLiveActivity(NvpLiveActivity) → Future<Map?> |
Start a Live Activity. |
updateLiveActivity(NvpLiveActivity) → Future<bool> |
Update by tag. |
endLiveActivity(String tag) → Future<bool> |
End by tag. |
endAllLiveActivities() → Future<bool> |
End all. |
Event Streams
| Stream | Type | Description |
|---|---|---|
onNotificationTap |
Stream<NvpNotification> |
User tapped a notification. |
onCallEvent |
Stream<NvpCallEvent> |
Call lifecycle events (accept, decline, ended, incoming, timeoutEnded). |
onCallStateChanged |
Stream<NvpCallState> |
Real-time call state changes. |
onTokenRefresh |
Stream<String> |
Push token refreshed. |
onPushReceived |
Stream<Map<String, dynamic>> |
Foreground push received (iOS). |
Models
NvpConfig
Plugin configuration passed to init().
| Field | Type | Default | Description |
|---|---|---|---|
appName |
String? |
null |
App name for CallKit / ConnectionService display. |
bannerDuration |
Duration |
5 seconds |
How long in-app banners stay visible. |
payloadKeys |
NvpPayloadKeys |
defaults | Key mapping for push payload extraction. |
channelId |
String |
'default_channel' |
Android notification channel ID. |
channelName |
String |
'Default Channel' |
Android notification channel name. |
defaultTemplate |
NvpNotificationTemplate |
normal |
Default notification template. |
callScreenConfig |
NvpCallScreenConfig |
defaults | Call screen customization. |
defaultGroupKey |
String? |
null |
Default group key for notifications. |
Plus 13 optional callbacks: onBannerTap, onBannerDismiss, onSystemNotificationTap, onCallAnswered, onCallDeclined, onCallEnded, onCallIncoming, onCallTimeoutEnded, onCallStateChanged, onTokenRefresh, onPushReceived, onChatPayload, onCallPayload.
NvpNotification
| Field | Type | Description |
|---|---|---|
title |
String |
Notification title (required). |
body |
String |
Notification body (required). |
imageUrl |
String? |
Image / avatar URL. |
data |
Map<String, dynamic> |
Custom data payload. |
senderId |
String? |
Sender identifier. |
senderName |
String? |
Sender display name. |
senderAvatar |
String? |
Sender avatar URL. |
receiverId |
String? |
Receiver identifier. |
receiverName |
String? |
Receiver display name. |
receiverType |
String? |
Receiver type (user, group, etc.). |
conversationId |
String? |
Conversation / thread identifier. |
tag |
String? |
Unique tag for in-place updates and cancellation. |
unreadMessageCount |
int? |
Badge number on the notification. |
channelId |
String? |
Android channel ID override. |
channelName |
String? |
Android channel name override. |
groupKey |
String? |
Group key for notification bundling. |
sound |
String? |
Custom sound (iOS: bundle file name, Android: raw resource name). |
Includes fromMap(), toMap(), and copyWith().
NvpNotificationTemplate
| Field | Type | Default | Description |
|---|---|---|---|
type |
NvpNotificationTemplateType |
— | Template type (required). |
expandedText |
String? |
null |
Expanded text for bigText / richText. |
imageUrl |
String? |
null |
Image URL for bigPicture / bigBanner. |
summaryText |
String? |
null |
Summary text for bigBanner. |
progressValue |
int? |
null |
Current progress value. |
progressMax |
int |
100 |
Maximum progress value. |
progressIndeterminate |
bool |
false |
Show indeterminate progress bar. |
actions |
List<NvpNotificationAction>? |
null |
Action buttons for interactive. |
customLayoutName |
String? |
null |
Android custom XML layout name. |
customLayoutData |
Map? |
null |
Data for custom layout. |
iosCategoryIdentifier |
String? |
null |
iOS notification category identifier. |
Template types: normal, richText, bigText, bigPicture, bigBanner, progress, interactive, custom
NvpNotificationAction
| Field | Type | Default | Description |
|---|---|---|---|
id |
String |
— | Unique action identifier. |
title |
String |
— | Button label. |
foreground |
bool |
true |
Bring app to foreground on tap. |
isTextInput |
bool |
false |
Show text input field. |
textInputPlaceholder |
String? |
null |
Placeholder for text input. |
NvpCallConfig
| Field | Type | Default | Description |
|---|---|---|---|
callId |
String |
— | Unique call session ID (required). |
callerName |
String |
— | Caller display name (required). |
callerAvatar |
String? |
null |
Caller avatar URL. |
isVideo |
bool |
false |
Video call. |
isOutgoing |
bool |
false |
Outgoing call. |
useNativeScreen |
bool |
true |
Use native UI vs custom Flutter screen. |
duration |
int? |
null |
Auto-dismiss timeout in seconds. |
ringtone |
String? |
null |
Custom ringtone (iOS: bundle file, Android: raw resource). |
extra |
Map<String, dynamic> |
{} |
Extra data. |
Includes fromMap(), toMap(), and copyWith().
NvpCallEvent
| Field | Type | Description |
|---|---|---|
action |
NvpCallAction? |
accept, decline, ended, incoming, timeoutEnded |
callId |
String |
Call session ID. |
callerName |
String? |
Caller display name. |
callerAvatar |
String? |
Caller avatar URL. |
isVideo |
bool |
Video call. |
callType |
String? |
Call type identifier. |
conversationId |
String? |
Associated conversation ID. |
payload |
Map<String, dynamic> |
Extra event data. |
NvpCallState
| Field | Type | Description |
|---|---|---|
callId |
String |
Call session ID. |
status |
NvpCallStatus |
ringing, connected, onHold, ended |
isMuted |
bool |
Microphone muted. |
isSpeakerOn |
bool |
Speaker on. |
isCameraOn |
bool |
Camera on. |
connectedAt |
DateTime? |
When the call was connected. |
extra |
Map<String, dynamic> |
Extra state data. |
NvpCallScreenConfig
| Field | Type | Default | Description |
|---|---|---|---|
useNativeCallScreen |
bool |
true |
Native UI vs custom Flutter screen. |
callScreenBuilder |
Widget Function(NvpCallEvent, NvpCallScreenController)? |
null |
Custom call screen builder. |
backgroundColor |
Color? |
null |
Call screen background color. |
avatarPlaceholder |
Widget? |
null |
Placeholder for caller avatar. |
onCallInitiated |
Function? |
null |
Called when a call is initiated. |
onMuteToggled |
Function? |
null |
Called when mute is toggled. |
onSpeakerToggled |
Function? |
null |
Called when speaker is toggled. |
onCameraToggled |
Function? |
null |
Called when camera is toggled. |
NvpPayloadKeys
Configurable key mapping so the plugin works with any backend push payload format.
| Field | Default | Description |
|---|---|---|
detailsPath |
'data.notificationDetails' |
Dot-path to notification details in payload. |
titleKey |
'title' |
Key for notification title. |
bodyKey |
'body' |
Key for notification body. |
typeKey |
'type' |
Key for notification type. |
senderNameKey |
'senderName' |
Key for sender name. |
senderAvatarKey |
'senderAvatar' |
Key for sender avatar URL. |
senderIdKey |
'sender' |
Key for sender ID. |
callActionKey |
'callAction' |
Key for call action. |
sessionIdKey |
'sessionId' |
Key for call session ID. |
callTypeKey |
'callType' |
Key for call type. |
badgeCountKey |
'unreadMessageCount' |
Key for badge count. |
conversationIdKey |
'conversationId' |
Key for conversation ID. |
tagKey |
'tag' |
Key for notification tag. |
receiverAvatarKey |
'receiverAvatar' |
Key for receiver avatar URL. |
NvpLiveActivity — ⚠️ Experimental
| Field | Type | Default | Description |
|---|---|---|---|
tag |
String |
— | Unique identifier (required). |
title |
String |
'' |
Title for Dynamic Island / Lock Screen. |
body |
String |
'' |
Body text. |
progress |
double |
0.0 |
Progress (0.0 – 1.0). |
icon |
String? |
null |
SF Symbol name. |
data |
Map<String, String> |
{} |
Extra data for Widget Extension. |
Callback Types
typedef NvpBannerTapCallback = void Function(NvpNotification notification);
typedef NvpBannerDismissCallback = void Function(NvpNotification notification);
typedef NvpSystemNotificationTapCallback = void Function(NvpNotification notification);
typedef NvpCallAnsweredCallback = void Function(NvpCallEvent event);
typedef NvpCallDeclinedCallback = void Function(NvpCallEvent event);
typedef NvpCallEndedCallback = void Function(NvpCallEvent event);
typedef NvpCallIncomingCallback = void Function(NvpCallEvent event);
typedef NvpCallTimeoutEndedCallback = void Function(NvpCallEvent event);
typedef NvpCallStateChangedCallback = void Function(NvpCallState state);
typedef NvpTokenRefreshCallback = void Function(String token);
typedef NvpPushReceivedCallback = void Function(Map<String, dynamic> payload);
typedef NvpChatPayloadCallback = void Function(NvpNotification notification);
typedef NvpCallPayloadCallback = void Function(NvpCallEvent event);
Example App
A full example app is included in the example/ directory:
cd example
flutter run
Contributing
Contributions are welcome. Please open an issue first to discuss what you'd like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -am 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
License
MIT — see LICENSE for details.