Skip to main content

Notifications

Show, schedule, and cancel system notifications in your Flet app using the flet-notifications extension. It wraps Flutter's flutter_local_notifications package.

The notifications are local: your app posts them itself, either at once or scheduled ahead of time, in which case the operating system delivers them even while the app isn't running. Push notifications sent from a server, through Firebase Cloud Messaging or the Apple Push Notification service, are not supported by this extension.

Platform Support​

PlatformWindowsmacOSLinuxiOSAndroidWeb
Supported✅✅✅✅✅✅

Usage​

Add flet-notifications to your project dependencies:

uv add flet-notifications

Requirements​

The following sections describe the configuration required for each platform.

Android​

The extension adds the receivers that deliver action buttons and scheduled notifications, and restore schedules after a reboot or an app update, to your app's manifest, with the POST_NOTIFICATIONS, VIBRATE and RECEIVE_BOOT_COMPLETED permissions. flet build also enables the core library desugaring the notifications plugin requires, through the extension's build settings.

Some features need permissions only your app can decide to request: exact schedules (see AndroidScheduleMode), full-screen notifications (see AndroidOptions.full_screen_intent), and foreground services (see start_foreground_service()).

See also:

Windows​

Building the app with flet build windows needs Visual Studio's C++ ATL for latest build tools component, for the architecture you build for. The Visual Studio Installer selects the x86 & x64 one with the Desktop development with C++ workload, but not in Build Tools installations, and the ARM64 one, needed on Windows on Arm, is never selected. Without it, the build fails with Cannot open include file: 'atlbase.h'. Add it in the Visual Studio Installer, under Modify > Individual components.

Web​

The browser only accepts Notification.requestPermission() from a user gesture. Call request_permissions() from an on_click handler before calling show(). Serve the app over HTTPS or a trusted localhost origin. If permission was denied, the user must change the site's browser permissions before requesting again.

are_notifications_enabled() checks browser permission without prompting. It returns False when permission is denied, undecided, or notifications are unsupported.

Use WebOptions to customize browser notifications. The browser and operating system determine which presentation options are honored. NotificationAction buttons are forwarded on Web, but browser support and the number of visible buttons vary. Inline action inputs are not supported; provide essential actions within the app as well.

Examples​

Basic notifications​

play_arrowTry Online
import flet as ft
import flet_notifications as ftn


def main(page: ft.Page):
def handle_tap(e: ftn.NotificationEvent):
page.show_dialog(ft.SnackBar(f"Opened notification {e.payload}"))

notifications = ftn.Notifications(on_tap=handle_tap)

async def notify(e: ft.Event[ft.Button]):
if not await notifications.request_permissions():
page.show_dialog(ft.SnackBar("Notifications are turned off."))
return
await notifications.show(
id=1,
title="Notification Title",
body="This is the notification body...",
payload="notif-1",
)

page.add(
ft.SafeArea(content=ft.Button("Notify", on_click=notify)),
)


if __name__ == "__main__":
ft.run(main)

Permissions​

Calls open_settings() when the user has turned notifications off, since the system won't ask again. The full-screen and Do Not Disturb buttons appear only on Android, and need the USE_FULL_SCREEN_INTENT and ACCESS_NOTIFICATION_POLICY permissions declared in pyproject.toml.

play_arrowTry Online
import flet as ft
import flet_notifications as ftn


async def main(page: ft.Page):
notifications = ftn.Notifications()
android = not page.web and page.platform in {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
}
has_settings = not page.web and page.platform in {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
ft.PagePlatform.IOS,
ft.PagePlatform.MACOS,
}

async def refresh():
enabled = await notifications.are_notifications_enabled()
status.value = "Notifications are on." if enabled else "Notifications are off."
page.update()

async def request(e: ft.Event[ft.Button]):
await notifications.request_permissions()
await refresh()

async def open_settings(e: ft.Event[ft.Button]):
await notifications.open_settings()

async def request_full_screen(e: ft.Event[ft.Button]):
granted = await notifications.request_full_screen_intent_permission()
page.show_dialog(ft.SnackBar(f"Full-screen notifications: {granted}"))

async def request_policy_access(e: ft.Event[ft.Button]):
if not await notifications.has_notification_policy_access():
await notifications.request_notification_policy_access()

async def handle_lifecycle(e: ft.AppLifecycleStateChangeEvent):
# The user may have changed the setting while the app was in the background.
if e.state == ft.AppLifecycleState.RESUME:
await refresh()

page.on_app_lifecycle_state_change = handle_lifecycle

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
status := ft.Text(),
ft.Button("Ask for permission", on_click=request),
ft.Button(
"Open notification settings",
on_click=open_settings,
disabled=not has_settings,
tooltip=None if has_settings else "Android, iOS and macOS only",
),
ft.Button(
"Allow full-screen notifications",
on_click=request_full_screen,
disabled=not android,
tooltip=None if android else "Android only",
),
ft.Button(
"Allow Do Not Disturb access",
on_click=request_policy_access,
disabled=not android,
tooltip=None if android else "Android only",
),
]
)
)
)
await refresh()


if __name__ == "__main__":
ft.run(main)

Action buttons and replies​

Button presses arrive in on_action, with the typed reply in input. The Archive button uses foreground=False, so it is handled without opening the app. Inline replies need Android, iOS, macOS or Windows, and Delete asks to unlock the device on iOS.

play_arrowTry Online
import flet as ft
import flet_notifications as ftn


def main(page: ft.Page):
def handle_tap(e: ftn.NotificationEvent):
log.controls.append(ft.Text(f"Opened {e.payload}"))
log.update()

def handle_action(e: ftn.NotificationActionEvent):
message = f"{e.action_id} on {e.payload}"
if e.input is not None:
message += f": {e.input!r}"
log.controls.append(ft.Text(message))
log.update()

notifications = ftn.Notifications(on_tap=handle_tap, on_action=handle_action)

async def show_message(e: ft.Event[ft.Button]):
if not await notifications.request_permissions():
page.show_dialog(ft.SnackBar("Notifications are turned off."))
return
await notifications.show(
id=1,
title="Ada",
body="Lunch at noon?",
payload="chat:ada",
actions=[
ftn.NotificationAction(
id="reply",
title="Reply",
input=ftn.NotificationActionInput(label="Message"),
),
# Handled without opening the app.
ftn.NotificationAction(id="archive", title="Archive", foreground=False),
ftn.NotificationAction(
id="delete",
title="Delete",
destructive=True,
require_authentication=True,
),
],
)

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Button("Show message", on_click=show_message),
log := ft.Column(),
]
)
)
)


if __name__ == "__main__":
ft.run(main)

Scheduling​

A naive datetime is device time; attach a ZoneInfo to when to schedule in a specific time zone. Windows schedules one-off notifications only, so the recurring buttons are hidden there. Android delivers these inexactly; see the exact alarms example for on-the-dot timing.

play_arrowTry Online
from datetime import datetime, time, timedelta

import flet as ft
import flet_notifications as ftn

SUPPORTED = {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
ft.PagePlatform.IOS,
ft.PagePlatform.MACOS,
ft.PagePlatform.WINDOWS,
}


async def main(page: ft.Page):
if page.web or page.platform not in SUPPORTED:
page.add(
ft.SafeArea(
content=ft.Text(
"Scheduling needs Android, iOS, macOS or Windows. "
"Run this example on one of them."
)
)
)
return

notifications = ftn.Notifications()
windows = page.platform == ft.PagePlatform.WINDOWS
one_off_only = "Windows schedules one-off notifications only" if windows else None

async def refresh():
scheduled = await notifications.get_pending()
# Windows reports pending notifications without their title.
rows = [ft.Text(f"#{n.id} {n.title or ''}") for n in scheduled]
pending.controls = rows or [ft.Text("Nothing scheduled.")]
page.update()

async def in_ten_seconds(e: ft.Event[ft.Button]):
await notifications.request_permissions()
await notifications.schedule(
1, when=timedelta(seconds=10), title="Stretch", body="Time to stand up."
)
await refresh()

async def every_morning(e: ft.Event[ft.Button]):
await notifications.request_permissions()
# A naive datetime is device time, so this follows the user across zones.
next_nine = datetime.combine(datetime.now().date(), time(9))
if next_nine < datetime.now():
next_nine += timedelta(days=1)
await notifications.schedule(
2,
when=next_nine,
title="Good morning",
body="Here is your daily summary.",
match_components=ftn.DateTimeComponents.TIME,
)
await refresh()

async def every_hour(e: ft.Event[ft.Button]):
await notifications.request_permissions()
await notifications.periodically_show(
3,
repeat_interval=ftn.RepeatInterval.HOURLY,
title="Hourly check-in",
body="How is it going?",
)
await refresh()

async def cancel_all(e: ft.Event[ft.Button]):
await notifications.cancel_all_pending()
await refresh()

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Button("In 10 seconds", on_click=in_ten_seconds),
ft.Button(
"Every day at 9:00",
on_click=every_morning,
disabled=windows,
tooltip=one_off_only,
),
ft.Button(
"Every hour",
on_click=every_hour,
disabled=windows,
tooltip=one_off_only,
),
ft.Button(
"Cancel all",
on_click=cancel_all,
disabled=windows,
tooltip="Windows cancels scheduled notifications one by one"
if windows
else None,
),
ft.Text("Pending", weight=ft.FontWeight.BOLD),
pending := ft.Column(),
]
)
)
)
await refresh()


if __name__ == "__main__":
ft.run(main)

Exact alarms​

Exact [schedule modes][flet_notifications.AndroidScheduleMode] need the SCHEDULE_EXACT_ALARM permission, which Android 14+ denies by default. request_exact_alarm_permission() opens the system page where the user allows it, and the example checks again when the app comes back to the foreground. Android only.

play_arrowTry Online
from datetime import timedelta

import flet as ft
import flet_notifications as ftn


async def main(page: ft.Page):
if page.web or page.platform not in {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
}:
page.add(
ft.SafeArea(
content=ft.Text(
"Exact alarms are an Android permission. Other platforms "
"deliver scheduled notifications on time without it."
)
)
)
return

notifications = ftn.Notifications()

async def refresh():
allowed = await notifications.can_schedule_exact_alarms()
status.value = (
"Exact alarms are allowed." if allowed else "Exact alarms are not allowed."
)
allow_button.disabled = allowed
allow_button.tooltip = "Already allowed" if allowed else None
page.update()

async def allow(e: ft.Event[ft.Button]):
# Opens the system's "Alarms & reminders" page for this app.
await notifications.request_exact_alarm_permission()

async def schedule(e: ft.Event[ft.Button]):
if not await notifications.can_schedule_exact_alarms():
page.show_dialog(ft.SnackBar("Allow exact alarms first."))
return
await notifications.request_permissions()
await notifications.schedule(
1,
when=timedelta(minutes=1),
title="One minute is up",
body="Delivered on the dot, even in Doze.",
schedule_mode=ftn.AndroidScheduleMode.EXACT_ALLOW_WHILE_IDLE,
)
page.show_dialog(ft.SnackBar("Scheduled for one minute from now."))

async def handle_lifecycle(e: ft.AppLifecycleStateChangeEvent):
if e.state == ft.AppLifecycleState.RESUME:
await refresh()

page.on_app_lifecycle_state_change = handle_lifecycle

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
status := ft.Text(),
allow_button := ft.Button("Allow exact alarms", on_click=allow),
ft.Button("Remind me in exactly one minute", on_click=schedule),
]
)
)
)
await refresh()


if __name__ == "__main__":
ft.run(main)

Progress​

On Android, showing the same ID again updates the notification. On Windows, update_windows_progress() updates the bar in place, and returns False once the user has dismissed the notification. Android and Windows only.

play_arrowTry Online
import asyncio

import flet as ft
import flet_notifications as ftn

STEPS = 10


async def main(page: ft.Page):
android = not page.web and page.platform in {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
}
windows = not page.web and page.platform == ft.PagePlatform.WINDOWS
if not (android or windows):
page.add(
ft.SafeArea(
content=ft.Text(
"Progress bars in notifications need Android or Windows. "
"Run this example on one of them."
)
)
)
return

notifications = ftn.Notifications()

async def show_android_progress(step: int):
# Showing the same ID again updates the notification in place.
await notifications.show(
1,
title="Downloading report.pdf",
options=ftn.NotificationOptions(
android=ftn.AndroidOptions(
progress=ftn.AndroidProgress(value=step, max=STEPS),
only_alert_once=True,
ongoing=True,
)
),
)

async def download(e: ft.Event[ft.Button]):
await notifications.request_permissions()
if windows:
await notifications.show(
1,
title="Downloading report.pdf",
options=ftn.NotificationOptions(
windows=ftn.WindowsOptions(
# Updates change only the fields the notification was
# shown with, so it starts with a label too.
progress=ftn.WindowsProgress(
status="Downloading", max=STEPS, label=f"0/{STEPS}"
)
)
),
)
for step in range(1, STEPS + 1):
await asyncio.sleep(0.5)
if android:
await show_android_progress(step)
elif not await notifications.update_windows_progress(
1,
ftn.WindowsProgress(
status="Downloading", value=step, max=STEPS, label=f"{step}/{STEPS}"
),
):
page.show_dialog(ft.SnackBar("Download notification closed."))
return
await notifications.show(1, title="report.pdf", body="Download complete")

page.add(
ft.SafeArea(content=ft.Button("Download", on_click=download)),
)


if __name__ == "__main__":
ft.run(main)

Shown notifications​

Groups the messages under a summary on Android, where dismissing the summary dismisses the whole group, and in one thread on iOS and macOS. get_active() lists only this app's notifications that are still shown. On Windows, listing and removing them needs an app packaged as MSIX.

play_arrowTry Online
import flet as ft
import flet_notifications as ftn

SUPPORTED = {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
ft.PagePlatform.IOS,
ft.PagePlatform.MACOS,
ft.PagePlatform.WINDOWS,
}
MESSAGES = {2: "Ada: Lunch?", 3: "Grace: Slides attached", 4: "Alan: Ping"}


async def main(page: ft.Page):
if not page.web and page.platform not in SUPPORTED:
page.add(
ft.SafeArea(
content=ft.Text(
"Listing shown notifications isn't available on this platform."
)
)
)
return

notifications = ftn.Notifications()

def in_inbox(summary: bool = False) -> ftn.NotificationOptions:
# Android groups by key under a summary; Apple platforms by thread.
return ftn.NotificationOptions(
android=ftn.AndroidOptions(
group=ftn.AndroidGroup(key="inbox", summary=summary)
),
ios=ftn.DarwinOptions(thread_identifier="inbox"),
macos=ftn.DarwinOptions(thread_identifier="inbox"),
)

async def refresh():
shown = await notifications.get_active()
listed.controls = [
ft.Row(
controls=[
ft.Text(f"#{n.id} {n.title or ''}"),
ft.TextButton(
"Dismiss",
data=n,
on_click=dismiss,
),
]
)
for n in shown
] or [ft.Text("Nothing shown.")]
page.update()

async def post(e: ft.Event[ft.Button]):
await notifications.request_permissions()
for id, text in MESSAGES.items():
await notifications.show(id, title=text, options=in_inbox())
await notifications.show(
1, title="3 new messages", options=in_inbox(summary=True)
)
await refresh()

async def dismiss(e: ft.Event[ft.TextButton]):
shown: ftn.ActiveNotification = e.control.data
await notifications.cancel(shown.id, tag=shown.tag)
await refresh()

async def dismiss_all(e: ft.Event[ft.Button]):
await notifications.cancel_all()
await refresh()

async def handle_lifecycle(e: ft.AppLifecycleStateChangeEvent):
# Notifications may have been dismissed in the meantime.
if e.state == ft.AppLifecycleState.RESUME:
await refresh()

page.on_app_lifecycle_state_change = handle_lifecycle

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Text(
"Windows lists and removes notifications only for apps "
"packaged as MSIX.",
visible=not page.web
and page.platform == ft.PagePlatform.WINDOWS,
),
ft.Button("Post messages", on_click=post),
ft.Button("Dismiss all", on_click=dismiss_all),
ft.Text("Shown", weight=ft.FontWeight.BOLD),
listed := ft.Column(),
]
)
)
)
await refresh()


if __name__ == "__main__":
ft.run(main)

Android styles​

The big picture is landscape.png from the example's assets folder, sent as bytes. Android only.

play_arrowTry Online
from datetime import datetime, timedelta
from pathlib import Path

import flet as ft
import flet_notifications as ftn

PICTURE = Path(__file__).parent / "assets" / "landscape.png"


async def main(page: ft.Page):
if page.web or page.platform not in {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
}:
page.add(
ft.SafeArea(
content=ft.Text(
"Notification styles are an Android feature. "
"Run this example on Android."
)
)
)
return

notifications = ftn.Notifications()

async def show(id: int, title: str, body: str, style: ftn.AndroidStyle):
await notifications.request_permissions()
await notifications.show(
id,
title=title,
body=body,
options=ftn.NotificationOptions(android=ftn.AndroidOptions(style=style)),
)

async def big_text(e: ft.Event[ft.Button]):
await show(
1,
"Release notes",
"Version 2.0 is out",
ftn.AndroidBigTextStyle(
text="Version 2.0 brings offline mode, a redesigned editor, "
"faster sync and dozens of fixes. Expand to read the highlights."
),
)

async def big_picture(e: ft.Event[ft.Button]):
await show(
2,
"Photo shared",
"Ada shared a photo",
ftn.AndroidBigPictureStyle(
image=ftn.AndroidBytesBitmap(data=PICTURE.read_bytes()),
summary_text="Hills at sunrise",
),
)

async def inbox(e: ft.Event[ft.Button]):
await show(
3,
"3 new emails",
"From Ada, Grace and Alan",
ftn.AndroidInboxStyle(
lines=["Ada: Lunch tomorrow?", "Grace: Slides attached", "Alan: Ping"],
summary_text="work@example.com",
),
)

async def messaging(e: ft.Event[ft.Button]):
me = ftn.AndroidPerson(name="You")
ada = ftn.AndroidPerson(name="Ada")
now = datetime.now().astimezone()
await show(
4,
"Ada",
"See you there",
ftn.AndroidMessagingStyle(
person=me,
conversation_title="Lunch",
messages=[
ftn.AndroidMessage(
text="Noon at the usual place?",
timestamp=now - timedelta(minutes=2),
person=ada,
),
ftn.AndroidMessage(text="Sure!", timestamp=now, person=me),
],
),
)

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Button("Big text", on_click=big_text),
ft.Button("Big picture", on_click=big_picture),
ft.Button("Inbox", on_click=inbox),
ft.Button("Messaging", on_click=messaging),
]
)
)
)


if __name__ == "__main__":
ft.run(main)

Android channels​

A channel's importance, sound and vibration are fixed once it is created: after that, only the user can change them in the system settings. Android only.

play_arrowTry Online
import flet as ft
import flet_notifications as ftn

GROUP = "chat"
CHANNELS = [
ftn.NotificationChannel(
id="messages",
name="Messages",
description="New direct messages",
group_id=GROUP,
importance=ftn.AndroidImportance.HIGH,
),
ftn.NotificationChannel(
id="digest",
name="Daily digest",
description="A quiet summary once a day",
group_id=GROUP,
importance=ftn.AndroidImportance.LOW,
play_sound=False,
enable_vibration=False,
),
]


async def main(page: ft.Page):
if page.web or page.platform not in {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
}:
page.add(
ft.SafeArea(
content=ft.Text(
"Notification channels exist only on Android. "
"Run this example on Android."
)
)
)
return

notifications = ftn.Notifications()

async def refresh():
channels = await notifications.get_channels()
listed.controls = [
ft.Text(f"{c.name} ({c.importance.name.lower()})") for c in channels
] or [ft.Text("No channels yet.")]
# Android drops notifications posted to a channel that doesn't exist.
missing = "messages" not in {c.id for c in channels}
post_button.disabled = missing
post_button.tooltip = "Create the channels first" if missing else None
page.update()

async def create(e: ft.Event[ft.Button]):
await notifications.create_channel_group(GROUP, name="Chat")
for channel in CHANNELS:
await notifications.create_channel(channel)
await refresh()

async def post(e: ft.Event[ft.Button]):
await notifications.request_permissions()
await notifications.show(
1,
title="Ada",
body="Are you free?",
# The channel decides how the notification alerts.
options=ftn.NotificationOptions(
android=ftn.AndroidOptions(channel="messages")
),
)

async def delete(e: ft.Event[ft.Button]):
# Deleting a group deletes its channels too.
await notifications.delete_channel_group(GROUP)
await refresh()

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Button("Create channels", on_click=create),
post_button := ft.Button("Post a message", on_click=post),
ft.Button("Delete channels", on_click=delete),
ft.Text("Channels", weight=ft.FontWeight.BOLD),
listed := ft.Column(),
]
)
)
)
await refresh()


if __name__ == "__main__":
ft.run(main)

Foreground service​

Declares the plugin's service with the dataSync type, and the matching permissions, in pyproject.toml. See AndroidForegroundServiceType for the entries of other service types. Android only.

play_arrowTry Online
import flet as ft
import flet_notifications as ftn


def main(page: ft.Page):
if page.web or page.platform not in {
ft.PagePlatform.ANDROID,
ft.PagePlatform.ANDROID_TV,
}:
page.add(
ft.SafeArea(
content=ft.Text(
"Foreground services exist only on Android. "
"Run this example on Android."
)
)
)
return

notifications = ftn.Notifications()

async def start(e: ft.Event[ft.Button]):
await notifications.request_permissions()
await notifications.start_foreground_service(
1,
title="Syncing photos",
body="Keeps running while you use other apps",
service_types=[ftn.AndroidForegroundServiceType.DATA_SYNC],
)

async def stop(e: ft.Event[ft.Button]):
await notifications.stop_foreground_service()

page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Button("Start sync", on_click=start),
ft.Button("Stop sync", on_click=stop),
]
)
)
)


if __name__ == "__main__":
ft.run(main)

Description​

Provides native local notifications for Android, iOS, macOS, Windows, Linux, and Web.

Create one service per page and keep its reference for sending notifications and handling responses. It registers with the page automatically and does not need to be added to the page's visual controls. The native backend initializes when the service reaches the client, and method calls wait for it to finish. Changing android_default_icon initializes it again with the new value; a failed initialization is retried on the next method call. Windows keeps the identity the app's notifications first registered with until the app exits, so changes to windows_app_name, windows_app_user_model_id, and windows_guid apply from its next launch.

show posts from wherever the app's code runs, such as when a download finishes or a message arrives, so that code must be running. Desktop and web apps run while their window or tab is open. Android may stop an app's process some time after it leaves the screen; keep long work going with start_foreground_service. iOS suspends an app moments after it leaves the screen, so schedule the notifications iOS users should get later.

Note

Feature support varies: rich styles, progress, and grouping are Android-specific (see AndroidOptions); calendar and interval recurrence are available on Android, iOS, and macOS; Windows supports one-shot scheduling; Linux and Web do not provide scheduling through this extension. Platform permissions and native configuration still apply. See the notification service's requirements in the user guide.

Notification response handlers receive NotificationEvent. A tap that launches a closed app is delivered once the service initializes. When a background action runs without the app UI, a native Dart handler persists the event for replay when the service next initializes; it does not execute Python in the background.

Example
import flet as ft
import flet_notifications as ftn


def main(page: ft.Page):
def handle_tap(e: ftn.NotificationEvent):
page.add(ft.Text(f"Opened {e.payload}"))

notifications = ftn.Notifications(on_tap=handle_tap)

async def notify(e: ft.Event[ft.Button]):
if await notifications.request_permissions():
await notifications.show(
id=1,
title="Download complete",
body="Your file is ready.",
payload="download:42",
)

page.add(ft.SafeArea(content=ft.Button("Notify", on_click=notify)))


ft.run(main)

Inherits: Service

Properties

Events

  • on_action - Called when the user selects a notification action button.
  • on_tap - Called when the user selects the notification body.

Methods

Properties​

android_default_iconclass-attributeinstance-attribute​

android_default_icon: str = 'flet_notifications_icon'

Default small Android notification icon.

Supply a bundled drawable resource name, such as "ic_stat_notify", without an extension. See AndroidOptions.icon for the icon's requirements and how to bundle it.

The default is a silhouette of the Flet logo that this extension provides. Android draws the small icon from its transparency alone, so use a silhouette of your own to show your app's brand instead.

windows_app_nameclass-attributeinstance-attribute​

windows_app_name: str | None = None

Name Windows shows for the app's notifications, and in its notification settings.

None uses the app's product name, from its executable's version information.

windows_app_user_model_idclass-attributeinstance-attribute​

windows_app_user_model_id: str | None = None

Windows AppUserModelID identifying the source of the app's notifications.

Windows keeps notification settings and history per AppUserModelID, so apps must not share one. None derives one from the app's executable, such as "Flet.myapp.3f2a9c1e", that is stable across launches and differs between apps. Set one when Windows should treat the app as the one with that ID, such as the ID of its Start menu shortcut or installer.

Note

Windows shows the app's icon, from flet build, on its notifications, and keeps the icon it first showed for an AppUserModelID: after changing the app's icon, set a new ID to see the new one.

windows_guidclass-attributeinstance-attribute​

windows_guid: str | None = None

GUID of the component Windows activates when the user selects a notification.

None derives one from windows_app_user_model_id, so it differs between apps, and registers the app built with flet build as the one Windows starts to deliver a selection made after the app exited. With a custom GUID, the app's installer registers that component instead.

Events​

on_actionclass-attributeinstance-attribute​

on_action: (
    EventHandler[NotificationActionEvent[Notifications]]
    | None
) = None

Called when the user selects a notification action button.

Its handler receives a NotificationActionEvent; use its action_id to identify the button and input for reply text.

Note
  • When an action launches the app, this handler is called for it once the service initializes, as with on_tap.
  • On Windows, the event's inherited data mapping contains reply values keyed by NotificationAction.id. For an action with the ID "reply", read e.data["reply"]. When exactly one value is returned, NotificationActionEvent.input also contains that text.
  • Background Android/iOS actions are queued and replayed on the next service initialization; Python is not run in the background isolate.
  • On Linux, an action reaches the app only while it is running; selecting one after the app has closed doesn't launch it.
  • On Windows, an app built with flet build is launched to receive an action selected after it closed. An app run with flet run or packaged with flet pack receives an action only while it is running.

on_tapclass-attributeinstance-attribute​

on_tap: (
    EventHandler[NotificationEvent[Notifications]] | None
) = None

Called when the user selects the notification body.

The event includes the notification ID and application payload. Full-screen Android notification launches also use this handler.

Note
  • When selecting a notification launches the app, this handler is called for it once the service initializes, so set it when creating the service. It is called once per launch; use it, for example, to route the app to the notification's content.
  • On Linux, a selection reaches the app only while it is running; selecting a notification after the app has closed doesn't launch it.
  • On Windows, an app built with flet build is launched to receive a selection made after it closed. An app run with flet run or packaged with flet pack receives a selection only while it is running.

Methods​

are_notifications_enabledasync​

are_notifications_enabled() -> bool

Check application-level notification authorization where supported.

Note

On Android, queries whether the app's notifications are enabled. On iOS/macOS, includes provisional authorization, which permits quiet delivery. On Web, checks the current browser permission without displaying a prompt; denied, undecided, or unsupported notification access returns False.

This check does not guarantee a particular channel, sound, or alert can interrupt the user.

Returns:

  • bool - Native authorization status on Android/iOS/macOS, or browser authorization on Web. Returns True unconditionally on Windows and Linux, without checking system notification settings.

Raises:

can_schedule_exact_alarmsasync​

can_schedule_exact_alarms() -> bool

Check Android's authorization to schedule exact alarms.

Note

Android restricts exact alarms from Android 12 onward. This is separate from notification-display permission and channel settings.

Returns:

  • bool - Whether the Android backend reports exact alarms are allowed. Returns True on non-Android platforms, where exact scheduling is not restricted.

Raises:

cancelasync​

cancel(id: int, tag: str | None=None) -> None

Remove a displayed notification and cancel its scheduled request.

Note

On Android, a tag applies only to notifications posted immediately; scheduled alarms are identified by ID. On Windows, removal requires an MSIX package identity.

Parameters:

  • id (int) - Identifier of the notification to remove.
  • tag (str | None, default: None) - Android tag supplied in AndroidOptions.tag. Matches both tag and ID when provided; ignored on other platforms.

Raises:

cancel_allasync​

cancel_all() -> None

Remove all of the app's displayed and scheduled notifications.

Note

To remove only undelivered requests, use cancel_all_pending. Windows removal requires an MSIX package identity. Does not remove Android channels or channel groups.

Raises:

cancel_all_pendingasync​

cancel_all_pending() -> None

Cancel all scheduled notifications without removing displayed ones.

Note

Supported on Android, iOS, and macOS. Does nothing on the web, which has no scheduled notifications. Use cancel to remove a specific pending request.

Raises:

create_channelasync​

create_channel(channel: NotificationChannel) -> None

Create an Android notification channel or update its mutable details.

Note

Applies on Android 8.0+. Existing channel sound, vibration, lights, and importance cannot be reset through this call. See NotificationChannel for creation-time settings. Does nothing on other platforms, which have no channels.

Parameters:

Raises:

create_channel_groupasync​

create_channel_group(
    group_id: str, name: str, description: str | None = None
) -> None

Create a group organizing Android channels in system notification settings.

Note

Supported only on Android 8.0+. This is distinct from grouping displayed notifications with AndroidOptions.group. Create the group before creating channels that refer to its ID. Does nothing on other platforms, which have no channels.

Parameters:

  • group_id (str) - Stable identifier for the channel group.
  • name (str) - User-visible group name.
  • description (str | None, default: None) - Optional explanation of the group, supported on Android 9.0+.

Raises:

delete_channelasync​

delete_channel(channel_id: str) -> None

Delete an Android notification channel on Android 8.0+.

Note

This does not cancel pending schedules that still refer to the channel; cancel those separately if they should no longer deliver notifications. Does nothing on other platforms, which have no channels.

Parameters:

  • channel_id (str) - Identifier of the channel to delete.

Raises:

delete_channel_groupasync​

delete_channel_group(group_id: str) -> None

Delete an Android notification-channel group and all its channels.

Note

Supported only on Android 8.0+. This also removes channel configuration for the group's channels. Cancel unwanted pending notifications separately. Does nothing on other platforms, which have no channels.

Parameters:

  • group_id (str) - Identifier of the group to delete.

Raises:

get_activeasync​

get_active() -> list[ActiveNotification]

Query delivered notifications that have not been dismissed or removed.

Note

Supported on Android 6.0+, iOS 10+, macOS 10.14+, the web, and Windows with an MSIX package identity. Without that identity, Windows returns an empty list.

Returns:

  • list[ActiveNotification] - Snapshots of active notifications, with platform-dependent fields. Unavailable values are None.

Raises:

get_channelsasync​

get_channels() -> list[NotificationChannel]

Query the app's Android notification channels.

Returns:

  • list[NotificationChannel] - Channel snapshots on supported Android versions, or an empty list on non-Android platforms. Returned values reflect the native channel settings, including changes made by the user.

Raises:

get_pendingasync​

get_pending() -> list[PendingNotification]

Query notifications scheduled for future delivery.

Note

Supported on Android, iOS, macOS, and Windows, where the query depends on package identity. Returns an empty list on the web, which has no scheduled notifications.

Returns:

  • list[PendingNotification] - Snapshots of pending requests. These do not include every platform detail and cannot be edited to change the native schedule.

Raises:

has_notification_policy_accessasync​

has_notification_policy_access() -> bool

Check whether Android has granted notification-policy access.

This access is needed before creating a channel that requests Do Not Disturb bypass; it is distinct from notification-display permission.

Returns:

  • bool - Whether access is granted on Android, or False on other platforms.

Raises:

open_settingsasync​

open_settings() -> None

Open the operating system's notification settings for this app.

Note

On iOS, the plugin tries the app's notification settings and falls back to the app's general settings when necessary. The bridge discards the native success flag, so a successful return does not confirm that a settings screen opened.

Raises:

periodically_showasync​

periodically_show(
    id: int,
    repeat_interval: RepeatInterval | DurationValue,
    title: str | None = None,
    body: str | None = None,
    payload: str | None = None,
    actions: list[NotificationAction] | None = None,
    schedule_mode: AndroidScheduleMode = AndroidScheduleMode.INEXACT_ALLOW_WHILE_IDLE,
    options: NotificationOptions | None = None,
) -> None

Schedule a notification repeating after a fixed elapsed interval.

For a specific local time such as 09:00 every day, use schedule with DateTimeComponents instead.

Note
  • The first delivery is one interval after this call, not immediately.
  • Supported on Android, iOS, and macOS. Actual timing is subject to the system's power management and alarm policies.
  • On Android, schedules are restored after a reboot or an app update, through the RECEIVE_BOOT_COMPLETED permission flet-notifications adds by default.

Parameters:

  • id (int) - Notification identifier. Reuse it to update the same notification; on Android, a tag also participates in identifying immediately shown notifications. Native platforms can impose identifier range limits.
  • repeat_interval (RepeatInterval | DurationValue) - Standard repeat interval or a custom duration of at least one minute, such as ft.Duration(minutes=15). Integer values are interpreted as milliseconds.
  • title (str | None, default: None) - Notification title. None omits the title.
  • body (str | None, default: None) - Notification body. None omits the body.
  • payload (str | None, default: None) - Application-defined string returned with tap and action events. The service does not interpret this value. None attaches no payload.
  • actions (list[NotificationAction] | None, default: None) - Action buttons; see NotificationAction for platform support and background-response behavior.
  • schedule_mode (AndroidScheduleMode, default: AndroidScheduleMode.INEXACT_ALLOW_WHILE_IDLE) - Android alarm precision and idle-mode behavior. The default needs no special access, but Android may deliver the notification some minutes late. Exact modes deliver on time and require exact-alarm access. Ignored on other platforms.
  • options (NotificationOptions | None, default: None) - Additional per-platform details.

Raises:

request_exact_alarm_permissionasync​

request_exact_alarm_permission() -> bool

Request Android exact-alarm access through system settings when needed.

Opens the system settings screen where the user can allow the app's alarms; it is not a dialog. Returns once the user leaves that screen. On Android 14+, the access is off until the user turns it on there. Exact-alarm access is separate from request_permissions.

Note

Requires SCHEDULE_EXACT_ALARM in the app's permissions; without it, the settings screen cannot grant access and this returns False. See AndroidScheduleMode for what exact scheduling involves.

Returns:

  • bool - Whether access is granted after the request. Returns True on non-Android platforms, where exact scheduling is not restricted. A grant does not create a scheduled notification.

Raises:

request_full_screen_intent_permissionasync​

request_full_screen_intent_permission() -> bool

Request access to Android full-screen notification intents.

Note

On Android 14+, access can depend on the app's permitted use and user settings. Requires the USE_FULL_SCREEN_INTENT manifest permission. After obtaining access, enable AndroidOptions.full_screen_intent on the notification. A grant does not guarantee full-screen presentation; Android may show a heads-up notification instead.

Returns:

  • bool - Whether the Android backend reports access is granted; False on non-Android platforms.

Raises:

request_notification_policy_accessasync​

request_notification_policy_access() -> bool

Open Android's notification-policy access settings when required.

Note

Requires ACCESS_NOTIFICATION_POLICY in the Android manifest. Obtain access before creating channels with NotificationChannel.bypass_dnd enabled.

Returns:

  • bool - Whether Android reports access after the request, or False on other
  • bool - platforms. The user can decline the request.

Raises:

request_permissionsasync​

request_permissions(
    alert: bool = True,
    sound: bool = True,
    badge: bool = True,
    critical: bool = False,
    provisional: bool = False,
) -> bool

Request notification authorization where the platform supports a prompt.

The service never prompts on its own; call this when the app is ready to explain why it needs notifications.

Note
  • On Android 13+, requests POST_NOTIFICATIONS; older Android versions do not show that runtime prompt. On iOS/macOS, requests the kinds of authorization selected by the arguments. A previous denial may require the user to change system settings rather than displaying another prompt.
  • On Web, requests browser notification permission. Call from a user interaction, such as an Button.on_click handler, over HTTPS or a trusted localhost origin. An existing denial returns False without prompting again; the user must change the site's browser permissions. Dismissing the prompt without granting access also returns False. Unsupported browsers return False.
  • On Windows and Linux, returns True without requesting or checking permission.

Parameters:

  • alert (bool, default: True) - Whether to request permission to display alerts. iOS/macOS only.
  • sound (bool, default: True) - Whether to request permission to play sounds. Whether a sound plays also depends on DarwinOptions.present_sound. Has effect on iOS/macOS only.
  • badge (bool, default: True) - Whether to request permission to badge the app icon; set the value with DarwinOptions.badge_number. Has effect on iOS/macOS only.
  • critical (bool, default: False) - Whether to request permission for critical alerts, which sound through mute and Do Not Disturb. Requires Apple's critical-alert entitlement in the app; see DarwinInterruptionLevel.CRITICAL. Has effect on iOS/macOS only.
  • provisional (bool, default: False) - Whether to request provisional authorization instead of prompting: notifications are delivered quietly to Notification Center, where the user can keep or turn them off. No prompt is shown. Has effect on iOS/macOS only.

Returns:

  • bool - Whether the native permission request succeeded, or True on platforms where no permission request is performed. It does not guarantee delivery through per-channel restrictions, Focus, or Do Not Disturb.

Raises:

scheduleasync​

schedule(
    id: int,
    when: datetime | timedelta,
    title: str | None = None,
    body: str | None = None,
    payload: str | None = None,
    actions: list[NotificationAction] | None = None,
    match_components: DateTimeComponents | None = None,
    options: NotificationOptions | None = None,
    schedule_mode: AndroidScheduleMode = AndroidScheduleMode.INEXACT_ALLOW_WHILE_IDLE,
) -> None

Schedule a notification for a date, a delay, or a recurring calendar match.

To repeat after a fixed elapsed interval rather than at a particular local time, use periodically_show.

Note
  • Supported on Android, iOS, macOS, and Windows. Windows delivers once, ignoring match_components.
  • On Android, schedules are restored after a reboot or an app update, through the RECEIVE_BOOT_COMPLETED permission flet-notifications adds by default.
  • iOS keeps at most 64 pending notifications per app. Check get_pending when scheduling many.

Parameters:

  • id (int) - Notification identifier. Reuse it to update the same notification; on Android, a tag also participates in identifying immediately shown notifications. Native platforms can impose identifier range limits.
  • when (datetime | timedelta) - Delivery time as a datetime, or a delay as a timedelta. Naive datetimes are wall-clock times in the device timezone; aware datetimes preserve their exact instant. For calendar recurrence in a particular timezone, use a ZoneInfo time zone, such as datetime(2027, 1, 1, 9, tzinfo=ZoneInfo("Europe/Paris")); other values recur in the device timezone, which falls back to UTC if lookup fails. Native scheduling has at best second precision, and delay fractions are truncated to whole seconds.
  • title (str | None, default: None) - Notification title. None omits the title.
  • body (str | None, default: None) - Notification body. None omits the body.
  • payload (str | None, default: None) - Application-defined string returned with tap and action events. The service does not interpret this value. None attaches no payload.
  • actions (list[NotificationAction] | None, default: None) - Action buttons; see NotificationAction for platform support and background-response behavior.
  • match_components (DateTimeComponents | None, default: None) - Calendar components used for recurrence on Android, iOS, and macOS. None schedules once. The first delivery is the next matching occurrence, which may precede the full date in when. Windows ignores this argument and schedules once.
  • options (NotificationOptions | None, default: None) - Additional per-platform details; see NotificationOptions.
  • schedule_mode (AndroidScheduleMode, default: AndroidScheduleMode.INEXACT_ALLOW_WHILE_IDLE) - Android alarm precision and idle-mode behavior; see AndroidScheduleMode. The default needs no special access, but Android may deliver the notification some minutes late. Exact modes deliver on time and require exact-alarm access. Ignored on other platforms.

Raises:

Example
from datetime import datetime
from zoneinfo import ZoneInfo

import flet_notifications as ftn

await notifications.schedule(
id=2,
when=datetime(2027, 1, 1, 9, 0, tzinfo=ZoneInfo("Europe/Paris")),
title="Morning reminder",
match_components=ftn.DateTimeComponents.TIME,
)

showasync​

show(
    id: int,
    title: str | None = None,
    body: str | None = None,
    payload: str | None = None,
    actions: list[NotificationAction] | None = None,
    options: NotificationOptions | None = None,
) -> None

Display a local notification immediately.

Note

Reusing an ID normally updates the existing notification, which is useful for AndroidOptions.progress updates. Android's AndroidOptions.tag also participates in identifying immediately displayed notifications. Visible content, action support, and alerting depend on the current platform and the user's notification settings. On Web, obtain browser permission with request_permissions before showing a notification.

Parameters:

  • id (int) - Notification identifier. Reuse it to update the same notification; on Android, a tag also participates in identifying immediately shown notifications. Native platforms can impose identifier range limits.
  • title (str | None, default: None) - Notification title. None omits the title.
  • body (str | None, default: None) - Notification body. None omits the body.
  • payload (str | None, default: None) - Application-defined string returned with tap and action events. The service does not interpret this value. None attaches no payload.
  • actions (list[NotificationAction] | None, default: None) - Action buttons; see NotificationAction for platform support and background-response behavior.
  • options (NotificationOptions | None, default: None) - Additional per-platform details; see NotificationOptions.

Raises:

start_foreground_serviceasync​

start_foreground_service(
    id: int,
    title: str | None = None,
    body: str | None = None,
    payload: str | None = None,
    actions: list[NotificationAction] | None = None,
    options: NotificationOptions | None = None,
    start_type: AndroidServiceStartType | None = None,
    service_types: list[AndroidForegroundServiceType]
    | None = None,
) -> None

Start an Android foreground service with a visible notification.

Note

Requires a service declaration, appropriate foreground-service permissions, and types matching the actual work. See AndroidForegroundServiceType for the shared setup and each type's pyproject.toml entries and runtime requirements.

This starts the plugin's native foreground service; it does not create a Python worker or guarantee that Android will keep the process alive indefinitely. Android's start restrictions and service time limits still apply. Use stop_foreground_service when the work is finished.

Parameters:

  • id (int) - Nonzero identifier for the foreground-service notification.
  • title (str | None, default: None) - Notification title. None omits the title.
  • body (str | None, default: None) - Notification body. None omits the body.
  • payload (str | None, default: None) - Application-defined string returned with tap and action events. The service does not interpret this value. None attaches no payload.
  • actions (list[NotificationAction] | None, default: None) - Action buttons; see NotificationAction for platform support and background-response behavior.
  • options (NotificationOptions | None, default: None) - Additional per-platform details; see NotificationOptions.
  • start_type (AndroidServiceStartType | None, default: None) - Native service restart policy; see AndroidServiceStartType. This does not control restarting Python tasks.
  • service_types (list[AndroidForegroundServiceType] | None, default: None) - Kinds of work performed by the service; see AndroidForegroundServiceType. Must match the manifest declarations and permissions. None or an empty list uses the manifest's service types.

Raises:

stop_foreground_serviceasync​

stop_foreground_service() -> None

Stop the Android foreground service started by this extension.

Note

Removes the service's foreground notification. It does not terminate Python tasks you started; stop the associated application work yourself. Does nothing on other platforms, where no foreground service can run.

Raises:

update_windows_progressasync​

update_windows_progress(
    id: int, progress: WindowsProgress
) -> bool

Update the progress bar of a shown Windows notification in place.

Unlike showing the notification again, the update doesn't pop the toast up again. The notification must already have been shown with WindowsOptions.progress; to advance it, change the values of the same progress object and pass it here.

Note

Windows only updates the bar itself and its label:

  • value, max and indeterminate apply.
  • status and title keep the values the notification was shown with; changes to them are ignored.
  • label can only be updated if the notification was shown with one, and setting it to None keeps the last label shown.

Parameters:

  • id (int) - Identifier of the notification to update.
  • progress (WindowsProgress) - The progress to show.

Returns:

  • bool - Whether the notification was updated; False when it is no longer shown, such as after the user dismissed it.

Raises:

Example
import flet_notifications as ftn

progress = ftn.WindowsProgress(status="Downloading...", label="0/10")
await notifications.show(
id=7,
title="report.pdf",
options=ftn.NotificationOptions(
windows=ftn.WindowsOptions(progress=progress)
),
)
for step in range(1, 11):
progress.value = step * 10
progress.label = f"{step}/10"
await notifications.update_windows_progress(7, progress)