Native local notifications for Flet apps (Android)
Project description
flet-android-notifications
Native Android notifications for Flet apps — a Flet extension bridging Python to the flutter_local_notifications plugin.
Flet has no built-in notifications, and Python-side approaches (plyer, Pyjnius) fail because Flet's Python process is sandboxed from Android APIs.
Left: action buttons, determinate progress, BigTextStyle, large icon thumbnail, BigPictureStyle. Right: indeterminate progress, scheduled secret, sub-text header, only-alert-once, custom vibration, and a foreground service with colorized=True (full red background observed on one Samsung OneUI device). All from the demo app in main.py.
Install
pip install flet-android-notifications
For a normal app, add the PyPI dependency and Android permissions to your pyproject.toml:
[project]
dependencies = ["flet>=0.82.0", "flet-android-notifications"]
[tool.flet.android.permission]
"android.permission.POST_NOTIFICATIONS" = true
"android.permission.SCHEDULE_EXACT_ALARM" = true # for scheduled/periodic
"android.permission.RECEIVE_BOOT_COMPLETED" = true # survive reboots
After flet build apk generates build/flutter, run the package patcher once per clean build directory:
flet-android-notifications-patch --project-root build/flutter
The patcher adds the flutter_local_notifications receivers/service plus Gradle desugaring/multidex required by scheduled notifications, action callbacks, and foreground services.
Do not copy the demo app's local-development settings into your app:
[tool.flet.app]
exclude = ["flet_android_notifications"]
[tool.flet.dev_packages]
flet-android-notifications = "flet_android_notifications"
Those are only for building this repo's demo against the local checkout. In a PyPI-installed app, tool.flet.dev_packages makes flet build apk pass flet-android-notifications @ flet_android_notifications to pip, which pip rejects as an invalid URL when no local flet_android_notifications directory exists.
Quick start
from datetime import datetime, timedelta
import flet as ft
from flet_android_notifications import FletAndroidNotifications
def main(page: ft.Page):
notifications = FletAndroidNotifications()
async def send(e):
await notifications.request_permissions()
await notifications.show_notification(
notification_id=1, title="Hello", body="It works!",
)
page.add(ft.Button(content="Send", on_click=send))
ft.run(main)
Instantiate FletAndroidNotifications once. Don't add it to page.overlay or page.controls — it's a service, not a visual control.
See examples/ for more: simple, action buttons, scheduled, styles, periodic, timeout, query, foreground service.
API overview
Core methods
| Method | Description |
|---|---|
show_notification(id, title, body, ...) |
show a notification immediately |
schedule_notification(id, title, body, scheduled_time, ...) |
fire at a future time via AlarmManager |
periodically_show(id, title, body, repeat_interval, ...) |
repeat every minute / hour / day / week |
periodically_show_with_duration(id, title, body, duration_seconds, ...) |
repeat at a custom interval |
start_foreground_service(id, title, body, ...) |
start a foreground service with persistent notification |
stop_foreground_service() |
stop the foreground service and remove its notification |
cancel(notification_id) |
cancel one notification |
cancel_all() |
cancel all notifications |
Query methods
| Method | Returns |
|---|---|
get_active_notifications() |
list[dict] — currently displayed (id, title, body, channel_id, payload) |
get_pending_notifications() |
list[dict] — scheduled/periodic (id, title, body, payload) |
Permission & status methods
| Method | Returns |
|---|---|
request_permissions() |
bool — request POST_NOTIFICATIONS (Android 13+) |
request_exact_alarm_permission() |
bool — request SCHEDULE_EXACT_ALARM (Android 14+) |
request_full_screen_intent_permission() |
bool — request USE_FULL_SCREEN_INTENT (Android 14+) |
are_notifications_enabled() |
bool — are notifications enabled for the app |
can_schedule_exact_notifications() |
bool — may the app schedule exact alarms |
has_notification_policy_access() |
bool — has do-not-disturb policy access (gates channel_bypass_dnd) |
request_notification_policy_access() |
opens the system DND-access screen; confirm afterwards with has_notification_policy_access() |
Use the status checks (are_notifications_enabled, can_schedule_exact_notifications,
has_notification_policy_access) to tell why a notification didn't appear instead of guessing.
Channel management methods
Channel sound/vibration/importance are immutable after creation — delete and recreate a channel to change them.
| Method | Description |
|---|---|
create_notification_channel(channel_id, channel_name, ...) |
create/configure a channel up front (sound, importance, vibration, bypass_dnd, group_id, …) |
delete_notification_channel(channel_id) |
delete a channel (so it can be recreated with new settings) |
get_notification_channels() |
list[dict] — id, name, description, importance, play_sound, enable_vibration, bypass_dnd, show_badge |
create_notification_channel_group(group_id, name, ...) |
create a channel group |
delete_notification_channel_group(group_id) |
delete a channel group and its channels |
Tap callback
import json
def on_tap(e):
data = json.loads(e.data) # {"payload": "...", "action_id": "..."}
notifications = FletAndroidNotifications(on_notification_tap=on_tap)
action_id is "" when the body is tapped (not an action button). Inline reply text is returned as data["input"].
Rich Android actions
from flet_android_notifications import NotificationAction, NotificationActionInput
await notifications.show_notification(
notification_id=10,
title="Message",
body="Reply from the notification shade.",
actions=[
NotificationAction(
"reply",
"Reply",
semantic_action="reply",
allow_generated_replies=True,
inputs=[NotificationActionInput(label="Type a reply")],
),
NotificationAction(
"archive",
"Archive",
semantic_action="archive",
shows_user_interface=False,
),
],
)
Existing dict actions still work. NotificationAction additionally supports title_color, icon, icon_type, contextual, allow_generated_replies, inputs, semantic_action, and invisible. Android requires contextual actions to include a valid icon. Visual rendering of some action details is OEM-dependent, so verify on target devices.
Notification parameters
show_notification, schedule_notification, periodically_show, and periodically_show_with_duration all share a common set of parameters. Only the required ones differ per method.
Required parameters
| Parameter | show |
schedule |
periodically_show |
periodically_show_with_duration |
|---|---|---|---|---|
notification_id |
int | int | int | int |
title |
str | str | str | str |
body |
str | str | str | str |
scheduled_time |
— | datetime | — | — |
repeat_interval |
— | — | str | — |
duration_seconds |
— | — | — | int|float |
repeat_interval is one of "every_minute", "hourly", "daily", "weekly".
Common optional parameters
These work on all four methods above (exceptions are noted in the Description column).
Basics:
| Parameter | Type | Default | Description |
|---|---|---|---|
payload |
str |
"" |
returned in tap callback |
actions |
list[NotificationAction|dict] |
None |
action buttons and optional inline reply inputs |
importance |
str |
"high" |
none, min, low, default, high, max |
timeout_after |
int|None |
None |
auto-dismiss after N milliseconds |
category |
str|None |
None |
notification type hint for DND filtering |
full_screen_intent |
bool |
False |
launch a full-screen / high-priority heads-up UI. show_notification and schedule_notification only. Needs USE_FULL_SCREEN_INTENT (Android 14+, see request_full_screen_intent_permission()) |
Channel:
| Parameter | Type | Default |
|---|---|---|
channel_id |
str |
"flet_notifications" |
channel_name |
str |
"Flet Notifications" |
channel_description |
str |
"Notifications from Flet app" |
channel_bypass_dnd |
bool |
False |
channel_bypass_dnd only takes effect when the app has do-not-disturb policy access — check with
has_notification_policy_access() and request it via request_notification_policy_access(). A
channel's sound/importance/vibration are fixed at creation; use the channel-management methods to
configure or replace a channel (see below).
Appearance:
| Parameter | Type | Default | Description |
|---|---|---|---|
icon |
str|None |
None |
drawable resource for small icon |
large_icon |
str|None |
None |
thumbnail on right side |
large_icon_type |
str |
"drawable_resource" |
or "file_path" |
color |
str|None |
None |
hex accent color, e.g. "#FF5722" |
colorized |
bool |
False |
color as background — only takes effect on start_foreground_service calls |
sub_text |
str|None |
None |
small text below content |
visibility |
str|None |
None |
"public", "private", or "secret" |
Behavior:
| Parameter | Type | Default | Description |
|---|---|---|---|
play_sound |
bool |
True |
play notification sound |
enable_vibration |
bool |
True |
vibrate |
sound |
str|None |
None |
raw resource name (e.g. "alert_tone") |
vibration_pattern |
list[int]|None |
None |
e.g. [0, 500, 200, 500] |
ongoing |
bool |
False |
can't be swiped away |
auto_cancel |
bool |
True |
dismiss on tap |
silent |
bool |
False |
suppress sound and vibration |
only_alert_once |
bool |
False |
alert on first show only |
Styles and progress:
| Parameter | Type | Default | Description |
|---|---|---|---|
style |
BigTextStyle|BigPictureStyle|InboxStyle|None |
None |
rich expandable style |
show_progress |
bool |
False |
show progress bar |
max_progress |
int |
0 |
max value |
progress |
int |
0 |
current value |
indeterminate |
bool |
False |
spinning progress bar |
Grouping:
| Parameter | Type | Default | Description |
|---|---|---|---|
group_key |
str|None |
None |
bundle notifications together |
set_as_group_summary |
bool |
False |
this is the group summary |
group_alert_behavior |
str |
"all" |
"all", "summary", "children" |
Scheduling parameters
| Parameter | Applies to | Type | Default | Description |
|---|---|---|---|---|
schedule_mode |
schedule_notification, periodically_show, periodically_show_with_duration |
str |
"inexact_allow_while_idle" |
see modes below |
match_date_time_components |
schedule_notification only |
str|None |
None |
"time" (daily), "day_of_week_and_time" (weekly), "day_of_month_and_time" (monthly), "date_and_time" (yearly) |
schedule_mode lets the periodic methods choose an exact mode too (previously they were hardcoded
to inexact_allow_while_idle). Exact modes require SCHEDULE_EXACT_ALARM — check first with
can_schedule_exact_notifications(), otherwise the OS rejects them with exact_alarms_not_permitted.
Schedule modes:
| Mode | Exact alarm permission? | Fires in Doze? |
|---|---|---|
"inexact" |
no | no |
"inexact_allow_while_idle" |
no | yes |
"exact" |
yes | no |
"exact_allow_while_idle" |
yes | yes |
"alarm_clock" |
yes | yes |
Foreground service
For persistent background tasks (music, GPS tracking, uploads) that require a visible notification:
await notifications.start_foreground_service(
notification_id=1, # must not be 0
title="Uploading",
body="3 files remaining...",
foreground_service_types=["special_use"],
ongoing=True,
)
# when done:
await notifications.stop_foreground_service()
Parameters specific to foreground service:
| Parameter | Type | Default | Description |
|---|---|---|---|
start_type |
str |
"start_sticky" |
start_sticky, start_not_sticky, start_sticky_compatibility, start_redeliver_intent |
foreground_service_types |
list[str]|None |
None |
e.g. ["special_use"], ["location"], ["media_playback"] |
All other notification parameters (channel, appearance, behavior, etc.) are the same as show_notification.
Important:
notification_idmust not be 0 (Android constraint)- The notification is not removed by
cancel()orcancel_all()— usestop_foreground_service() - Requires
FOREGROUND_SERVICEpermission plus a type-specific permission (e.g.FOREGROUND_SERVICE_SPECIAL_USE)
AndroidManifest.xml — add inside <application>:
The package patcher can add this entry automatically.
<service android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false"
android:foregroundServiceType="specialUse" />
Adjust foregroundServiceType to match your use case (e.g. location, mediaPlayback).
pyproject.toml permissions:
[tool.flet.android.permission]
"android.permission.FOREGROUND_SERVICE" = true
"android.permission.FOREGROUND_SERVICE_SPECIAL_USE" = true
Styles
from flet_android_notifications import BigTextStyle, BigPictureStyle, InboxStyle
# expandable long text
style=BigTextStyle("Full text here...", content_title="Expanded title")
# full-width image when expanded
style=BigPictureStyle(drawable_resource="splash")
# list of lines
style=InboxStyle(["Line 1", "Line 2", "Line 3"], summary_text="3 items")
Building the APK
# first build — generates Flutter template, may fail at Gradle
flet build apk -v
# patch AndroidManifest.xml receivers/services and Gradle desugaring:
flet-android-notifications-patch --project-root build/flutter
# rebuild
flet build apk -v
Needed because flutter_local_notifications v19+ uses Java 8 APIs, and action/scheduling/foreground-service support requires app-level manifest entries. Apply once per clean build directory.
AndroidManifest.xml entries
Register BroadcastReceivers inside <application> in build/flutter/android/app/src/main/AndroidManifest.xml. Required for schedule_notification and the periodically_show* methods to fire at all — not just for reboots: without them AlarmManager fires but has no listener and the notification is silently dropped.
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
<service android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false"
android:foregroundServiceType="specialUse" />
Installing on device
Always full-uninstall before installing. Flet caches the extracted Python environment:
adb uninstall com.yourapp.package
adb install build/apk/app-release.apk
On Windows, set PYTHONIOENCODING=utf-8 before building to avoid Unicode crashes.
Custom resources
- Small icons: vector drawable XML in
res/drawable/(24dp, white on transparent) - Sounds: audio files in
res/raw/, reference by name without extension:sound="alert_tone"
Add res/raw/keep.xml to prevent resource stripping:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@raw/*,@drawable/ic_*" />
A channel's sound is fixed at creation. Change it with a new channel_id, or
delete_notification_channel(id) then create_notification_channel(id, ..., sound=...) to reuse
the same id.
OEM rendering notes
coloron regular notifications: reaches the OS but may not render visibly on every skin. Verify per device before relying on it.colorizedon regular notifications: silently ignored everywhere — per Android contract it only applies to foreground-service / media-style notifications.colorizedonstart_foreground_service: observed working on one Samsung OneUI device (demo button 23).- Bottom line: if visible color matters, test both regular and foreground-service colorized notifications on your target devices.
- On-device audit: every demo button was verified on a Galaxy S25 (One UI, Android 16) — see
docs/button-tests/README.mdanddocs/samsung-claims-audit.mdfor per-feature evidence.
Limitations
- Android only. iOS support would need
DarwinNotificationDetailsin the Dart layer. - Desktop: the service instantiates without error but notifications won't appear.
How it works
Python app → FletAndroidNotifications (ft.Service)
→ _invoke_method() over Flet protocol
→ NotificationsService (FletService, Dart)
→ flutter_local_notifications plugin → Android NotificationManager
The extension ships as a Python package with a flutter/ directory containing the Dart code. flet build apk discovers it in site-packages and includes it as a Flutter path dependency.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file flet_android_notifications-0.8.1.tar.gz.
File metadata
- Download URL: flet_android_notifications-0.8.1.tar.gz
- Upload date:
- Size: 24.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2af64da65713c184c6408eec00f9fc85d91cdf68c0cb734fd27b8b9b0724c66f
|
|
| MD5 |
0ff05df7afd68d337bdfa633cd1cdf7d
|
|
| BLAKE2b-256 |
ac3a8bb707b625d216a975677d4ccb5e058a602d30ebbbae1460623ef4026c36
|
File details
Details for the file flet_android_notifications-0.8.1-py3-none-any.whl.
File metadata
- Download URL: flet_android_notifications-0.8.1-py3-none-any.whl
- Upload date:
- Size: 27.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bddecde4e99c374db68a36cd823f335cbe533922193ec2a82f94c7fb392e417
|
|
| MD5 |
31523b95a86d98c3d9a7388edd11a88c
|
|
| BLAKE2b-256 |
ad096d8a2936c3b927e29dd2f83cd8d5edbefed4928eb40c562673f88f866957
|