<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mxolisi Masuku</title>
    <description>The latest articles on DEV Community by Mxolisi Masuku (@mxomasuku).</description>
    <link>https://dev.to/mxomasuku</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F940292%2F334ae70a-2833-4e4e-9343-df013c39bbb1.jpeg</url>
      <title>DEV Community: Mxolisi Masuku</title>
      <link>https://dev.to/mxomasuku</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mxomasuku"/>
    <language>en</language>
    <item>
      <title>Build Reliable Local Notifications in Flutter (Step-by-Step)</title>
      <dc:creator>Mxolisi Masuku</dc:creator>
      <pubDate>Thu, 16 Apr 2026 08:43:51 +0000</pubDate>
      <link>https://dev.to/mxomasuku/build-reliable-local-notifications-in-flutter-step-by-step-3l1f</link>
      <guid>https://dev.to/mxomasuku/build-reliable-local-notifications-in-flutter-step-by-step-3l1f</guid>
      <description>&lt;p&gt;&lt;em&gt;Companion post to &lt;a href="https://www.mxomasuku.com/blog/how-i-built-the-long-game-notification-system-a-journey-into-notifications-and-behavioral-engineering" rel="noopener noreferrer"&gt;How I Built the Long Game Notification System&lt;/a&gt;. This is the walkthrough. If you want the thinking behind the decisions, read that first. If you just want the recipe, you're in the right place.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What You're Building
&lt;/h2&gt;

&lt;p&gt;A notification system where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notifications fire at the &lt;strong&gt;exact time&lt;/strong&gt; you set, every day&lt;/li&gt;
&lt;li&gt;They work when the phone is &lt;strong&gt;locked&lt;/strong&gt;, the app is &lt;strong&gt;killed&lt;/strong&gt;, or the device is in &lt;strong&gt;Doze mode&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Notification content is &lt;strong&gt;personalized&lt;/strong&gt; with fresh data on every app open&lt;/li&gt;
&lt;li&gt;The entire system runs &lt;strong&gt;locally&lt;/strong&gt; — no push server, no Cloud Functions, zero delivery cost&lt;/li&gt;
&lt;li&gt;Pomodoro-style one-shot alarms fire on time even when the app is suspended&lt;/li&gt;
&lt;li&gt;Notifications &lt;strong&gt;survive Samsung, Xiaomi, and Huawei battery killers&lt;/strong&gt; without FCM&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What You're NOT Using
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;workmanager&lt;/code&gt; — the OS throttles background tasks. Your 15-minute poll becomes hours.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Dart Timer.periodic&lt;/code&gt; for delivery — dies the moment the phone locks.&lt;/li&gt;
&lt;li&gt;Firebase Cloud Messaging — overkill for local, user-specific scheduling.&lt;/li&gt;
&lt;li&gt;Background Fetch (iOS) — Apple gives you 0–2 executions per day if you're lucky.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Dependencies
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pubspec.yaml&lt;/span&gt;
&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flutter_local_notifications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^18.0.1&lt;/span&gt;
  &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.10.0&lt;/span&gt;
  &lt;span class="na"&gt;flutter_timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^3.0.1&lt;/span&gt;
  &lt;span class="na"&gt;permission_handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^11.3.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Four packages. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The notification plugin handles scheduling. &lt;/li&gt;
&lt;li&gt;The timezone packages ensure your 8:00 AM means 8:00 AM in Johannesburg, not UTC. &lt;/li&gt;
&lt;li&gt;The permission handler lets you request notification access on Android 13+ &lt;strong&gt;and&lt;/strong&gt; battery optimization exemption on Samsung/OEM devices.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 2: Android Permissions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- android/app/src/main/AndroidManifest.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.POST_NOTIFICATIONS"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.RECEIVE_BOOT_COMPLETED"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.SCHEDULE_EXACT_ALARM"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.USE_EXACT_ALARM"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.WAKE_LOCK"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each one does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required on Android 13+ to show any notification at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RECEIVE_BOOT_COMPLETED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alarms survive device reboots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SCHEDULE_EXACT_ALARM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fires at the exact second, even in Doze mode (API ≤ 33)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USE_EXACT_ALARM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Guaranteed exact alarms on API 33+ — no user prompt needed, unlike &lt;code&gt;SCHEDULE_EXACT_ALARM&lt;/code&gt; on API 34+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REQUEST_IGNORE_BATTERY_OPTIMIZATIONS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lets you show the system "Allow unrestricted battery?" dialog — critical for Samsung, Xiaomi, Huawei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WAKE_LOCK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keeps the CPU alive long enough to process the alarm and fire the notification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; On Android 14+ (API 34), &lt;code&gt;SCHEDULE_EXACT_ALARM&lt;/code&gt; isn't granted by default. &lt;code&gt;USE_EXACT_ALARM&lt;/code&gt; is a stronger alternative that's always granted for apps that declare it — but it may trigger Google Play review. Having both ensures maximum compatibility. Your code still needs the inexact fallback we'll cover in Step 7.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Initialize on App Start
&lt;/h2&gt;

&lt;p&gt;Before you can schedule anything, you need to initialize the timezone database and the notification plugin. This runs once in &lt;code&gt;main()&lt;/code&gt;, before &lt;code&gt;runApp()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.dart&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;WidgetsFlutterBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// ... your other init code (Firebase, etc.)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// notification_service.dart&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_local_notifications/flutter_local_notifications.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:timezone/timezone.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:timezone/data/latest_all.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tzdata&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_timezone/flutter_timezone.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:permission_handler/permission_handler.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;FlutterLocalNotificationsPlugin&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;FlutterLocalNotificationsPlugin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Initialize the timezone database and detect the device's zone.&lt;/span&gt;
    &lt;span class="c1"&gt;//    Without this, all scheduled times are wrong.&lt;/span&gt;
    &lt;span class="n"&gt;tzdata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeTimeZones&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;tzName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterTimezone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLocalTimezone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLocalLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tzName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Fallback: stays UTC if detection fails&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Initialize the plugin.&lt;/span&gt;
    &lt;span class="c1"&gt;//    We don't request permissions here — that happens later,&lt;/span&gt;
    &lt;span class="c1"&gt;//    at a moment that makes sense in your UX flow.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;androidSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AndroidInitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s"&gt;'@mipmap/ic_launcher'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;iosSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DarwinInitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;requestAlertPermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;requestBadgePermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;requestSoundPermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;androidSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;iosSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;onDidReceiveNotificationResponse:&lt;/span&gt; &lt;span class="n"&gt;_onNotificationTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;onDidReceiveBackgroundNotificationResponse:&lt;/span&gt; &lt;span class="n"&gt;_onBackgroundTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle taps when the app is alive&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onNotificationTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Navigate to the relevant screen, stop a timer, etc.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle taps when the app was killed — must be top-level or static&lt;/span&gt;
  &lt;span class="nd"&gt;@pragma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'vm:entry-point'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onBackgroundTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// App relaunches — handle in your normal init flow&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why timezone matters:&lt;/strong&gt; If you schedule for "8:00 AM" without initializing timezones, the plugin may interpret that as UTC. In Johannesburg (UTC+2), that's 10:00 AM. In New York (UTC-5), that's 3:00 AM. Always initialize before any &lt;code&gt;zonedSchedule&lt;/code&gt; call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Schedule a Daily Repeating Notification
&lt;/h2&gt;

&lt;p&gt;This is the core of the entire system. Three lines do the heavy lifting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scheduleDailyNotification&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                           &lt;span class="c1"&gt;// Fixed ID per notification type&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_nextInstanceOfTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;            &lt;span class="c1"&gt;// Next occurrence of this time&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;AndroidNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;'your_channel_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;'Your Channel Name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;channelDescription:&lt;/span&gt; &lt;span class="s"&gt;'What this channel is for'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;importance:&lt;/span&gt; &lt;span class="n"&gt;Importance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;priority:&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;DarwinNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;uiLocalNotificationDateInterpretation:&lt;/span&gt;
        &lt;span class="n"&gt;UILocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;absoluteTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;androidScheduleMode:&lt;/span&gt; &lt;span class="n"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;matchDateTimeComponents:&lt;/span&gt; &lt;span class="n"&gt;DateTimeComponents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// ← THIS IS THE KEY&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three things that make it work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exactAllowWhileIdle&lt;/code&gt;&lt;/strong&gt; — fires even in Doze mode. The OS wakes up just enough to deliver your notification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;matchDateTimeComponents: DateTimeComponents.time&lt;/code&gt;&lt;/strong&gt; — tells the OS: "repeat this every day at this hour:minute." You schedule it once. It fires every day. No background task. No polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_nextInstanceOfTime&lt;/code&gt;&lt;/strong&gt; — computes the next future occurrence of the target time.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt; &lt;span class="nf"&gt;_nextInstanceOfTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scheduled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// zonedSchedule requires a future datetime&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;scheduled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;days:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this helper exists:&lt;/strong&gt; &lt;code&gt;zonedSchedule&lt;/code&gt; requires the initial fire time to be in the future. If it's 9:00 AM and you schedule for 8:00 AM, the helper pushes it to 8:00 AM tomorrow. The &lt;code&gt;matchDateTimeComponents&lt;/code&gt; flag handles every day after that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: CRITICAL: The Cancel Trap
&lt;/h2&gt;

&lt;p&gt;==(This bit is annoying if not taken care of)==&lt;/p&gt;

&lt;p&gt;This is the bug that will silently break your notifications and you won't notice for days. I shipped it. It cost me a full day of missed notifications before I caught it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The broken pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;//  DO NOT DO THIS&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reschedule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                     &lt;span class="c1"&gt;// Destroys the repeating alarm&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;         &lt;span class="c1"&gt;// Creates a new one&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks correct. It's not. Here's what happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User opens the app at &lt;strong&gt;9:00 AM&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cancel(1)&lt;/code&gt; — destroys the existing repeating 8:00 AM alarm&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_nextInstanceOfTime(8, 0)&lt;/code&gt; — 8:00 AM today already passed, returns &lt;strong&gt;tomorrow 8:00 AM&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zonedSchedule(1, ...)&lt;/code&gt; — schedules a new alarm starting tomorrow&lt;/li&gt;
&lt;li&gt;Tomorrow, user opens the app at 9:00 AM again → &lt;strong&gt;same thing&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The notification is perpetually pushed to "tomorrow" and &lt;strong&gt;never fires&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The correct pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Only cancel when the user disables the notification&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// User turned it off — remove the alarm&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Calling zonedSchedule with the same ID REPLACES the existing alarm&lt;/span&gt;
  &lt;span class="c1"&gt;// without resetting the repeat cycle. No cancel needed.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; &lt;code&gt;zonedSchedule&lt;/code&gt; with the same ID overwrites the previous alarm — it updates the title, body, and schedule time without destroying the repeat. You only need &lt;code&gt;cancel&lt;/code&gt; when you want the notification to stop entirely.&lt;/p&gt;

&lt;p&gt;This means you can safely call &lt;code&gt;rescheduleAll()&lt;/code&gt; on every app open to refresh notification content (e.g., "You have 3 projects today" becomes "You have 4 projects today") without breaking delivery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Refresh Content on App Open
&lt;/h2&gt;

&lt;p&gt;Notification bodies are frozen at schedule time. If the user adds a project at 11 PM, you want tomorrow's morning notification to include it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.dart — after init, before runApp&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FirebaseAuth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentUser&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rescheduleAllNotifications&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;catchError&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rescheduleAllNotifications()&lt;/code&gt; fetches fresh data and calls &lt;code&gt;zonedSchedule&lt;/code&gt; for each enabled notification, overwriting the stale body. No &lt;code&gt;cancel&lt;/code&gt; — just overwrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two things to guard against:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auth check&lt;/strong&gt; — if your notification content depends on user-specific data (Firestore queries), and no user is signed in, the query will hang. This blocks &lt;code&gt;main()&lt;/code&gt; and your app never starts. Gate it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fire-and-forget&lt;/strong&gt; — use &lt;code&gt;.catchError((_) {})&lt;/code&gt;. If the reschedule fails (offline, Firestore timeout), the previously scheduled alarm still fires with yesterday's content. That's better than crashing on startup.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 7: Handle Android 14+ Exact Alarm Restriction
&lt;/h2&gt;

&lt;p&gt;Android 14 changed the rules. &lt;code&gt;SCHEDULE_EXACT_ALARM&lt;/code&gt; is no longer auto-granted. If your app calls &lt;code&gt;exactAllowWhileIdle&lt;/code&gt; without the permission, it throws.&lt;/p&gt;

&lt;p&gt;The graceful fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scheduleDailyNotification&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;scheduledTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_nextInstanceOfTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* your NotificationDetails */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Try exact first — best experience&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduledTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;uiLocalNotificationDateInterpretation:&lt;/span&gt;
          &lt;span class="n"&gt;UILocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;absoluteTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;androidScheduleMode:&lt;/span&gt; &lt;span class="n"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;matchDateTimeComponents:&lt;/span&gt; &lt;span class="n"&gt;DateTimeComponents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Exact alarm not permitted — fall back to inexact&lt;/span&gt;
    &lt;span class="c1"&gt;// Delivery may drift by ~10 minutes, but the notification still fires&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduledTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;uiLocalNotificationDateInterpretation:&lt;/span&gt;
          &lt;span class="n"&gt;UILocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;absoluteTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;androidScheduleMode:&lt;/span&gt; &lt;span class="n"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inexactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;matchDateTimeComponents:&lt;/span&gt; &lt;span class="n"&gt;DateTimeComponents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most notifications, a 10-minute window is fine. "Your day is 63% gone" at 2:10 PM instead of 2:00 PM doesn't break the experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apply this same pattern to one-shot alarms too.&lt;/strong&gt; Your pomodoro alarm scheduler should have the same try/catch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;schedulePhaseAlarm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;durationMinutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;NotificationDetails&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;fireAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;minutes:&lt;/span&gt; &lt;span class="n"&gt;durationMinutes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;alarmId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fireAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;uiLocalNotificationDateInterpretation:&lt;/span&gt;
          &lt;span class="n"&gt;UILocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;absoluteTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;androidScheduleMode:&lt;/span&gt; &lt;span class="n"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fall back to inexact — still fires, may drift ~5-10 min&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;alarmId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fireAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;uiLocalNotificationDateInterpretation:&lt;/span&gt;
            &lt;span class="n"&gt;UILocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;absoluteTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;androidScheduleMode:&lt;/span&gt; &lt;span class="n"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inexactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'[Notifications] Failed to schedule alarm: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 8: One-Shot Alarms (Pomodoro, Timers)
&lt;/h2&gt;

&lt;p&gt;Daily repeating alarms cover most cases. But sometimes you need a notification that fires once at a specific future time — like when a 25-minute pomodoro focus session ends.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scheduleOneShot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;minutesFromNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;NotificationDetails&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;fireAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;minutes:&lt;/span&gt; &lt;span class="n"&gt;minutesFromNow&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fireAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;uiLocalNotificationDateInterpretation:&lt;/span&gt;
        &lt;span class="n"&gt;UILocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;absoluteTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;androidScheduleMode:&lt;/span&gt; &lt;span class="n"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// NO matchDateTimeComponents — this fires once, doesn't repeat&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why you need this for pomodoro:&lt;/strong&gt; A Dart &lt;code&gt;Timer.periodic&lt;/code&gt; runs in your app's process. When the user locks their phone, the OS suspends the app within ~30 seconds. Your timer stops ticking. The phase ends, and — silence. The bell only rings when they unlock the phone and the app resumes.&lt;/p&gt;

&lt;p&gt;An OS alarm doesn't care about your app's lifecycle. It fires regardless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making one-shot alarms alarm-grade
&lt;/h3&gt;

&lt;p&gt;A standard &lt;code&gt;Importance.high&lt;/code&gt; notification won't cut it on Samsung, Xiaomi, or Huawei devices. These OEMs aggressively kill background processes and suppress notifications they deem non-essential. You need to make your notification look like an alarm clock to the OS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:typed_data'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;NotificationDetails&lt;/span&gt; &lt;span class="nf"&gt;_alarmGradeDetails&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isWorkPhase&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;soundFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isWorkPhase&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;'bell_focus'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'bell_break'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;channelId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isWorkPhase&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;'focus_bell'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'break_bell'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;channelName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isWorkPhase&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;'Focus Bell'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'Break Bell'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;AndroidNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;channelDescription:&lt;/span&gt; &lt;span class="s"&gt;'Pomodoro phase transition'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;importance:&lt;/span&gt; &lt;span class="n"&gt;Importance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                              &lt;span class="c1"&gt;// Maximum priority&lt;/span&gt;
      &lt;span class="nl"&gt;priority:&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;sound:&lt;/span&gt; &lt;span class="n"&gt;RawResourceAndroidNotificationSound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;soundFile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;playSound:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;fullScreenIntent:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                  &lt;span class="c1"&gt;// Wakes the screen&lt;/span&gt;
      &lt;span class="nl"&gt;category:&lt;/span&gt; &lt;span class="n"&gt;AndroidNotificationCategory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// Treated like an alarm&lt;/span&gt;
      &lt;span class="nl"&gt;visibility:&lt;/span&gt; &lt;span class="n"&gt;NotificationVisibility&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// Shows on lock screen&lt;/span&gt;
      &lt;span class="nl"&gt;enableVibration:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;vibrationPattern:&lt;/span&gt; &lt;span class="n"&gt;Int64List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;DarwinNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;presentAlert:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;presentSound:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;sound:&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$soundFile&lt;/span&gt;&lt;span class="s"&gt;.wav'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;interruptionLevel:&lt;/span&gt; &lt;span class="n"&gt;InterruptionLevel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeSensitive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Breaks through Focus&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each flag does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;importance: Importance.max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Heads-up notification — appears at the top of the screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fullScreenIntent: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wakes the screen and shows the notification even when locked. This is what alarm clock apps use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category: AndroidNotificationCategory.alarm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tells the OS this is time-critical. Samsung's battery manager respects this category.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;visibility: NotificationVisibility.public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Content visible on the lock screen without unlocking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vibrationPattern&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom vibration so the user physically feels it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interruptionLevel: InterruptionLevel.timeSensitive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;iOS 15+: breaks through Focus mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;This is the difference between a notification that works on your desk and one that works in your pocket.&lt;/strong&gt; Standard &lt;code&gt;Importance.high&lt;/code&gt; gets silently suppressed by Samsung's battery manager. &lt;code&gt;Importance.max&lt;/code&gt; + &lt;code&gt;fullScreenIntent&lt;/code&gt; + &lt;code&gt;category: alarm&lt;/code&gt; does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  The dual-delivery pattern
&lt;/h3&gt;

&lt;p&gt;When the app is in the foreground, the Dart timer catches the transition first (instant feedback). When the phone is locked, the OS alarm delivers it. To avoid the user hearing two bells:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your timer tick (foreground path)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onPomodoroPhaseEnd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alarmId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// Cancel the OS alarm (Dart beat it)&lt;/span&gt;
  &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;displayId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;       &lt;span class="c1"&gt;// Show the notification immediately&lt;/span&gt;
  &lt;span class="n"&gt;_scheduleNextPhaseAlarm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// Schedule for the next phase&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use separate IDs for the scheduled alarm and the instant notification if you want, or the same ID if you want one to replace the other. Either way, the user sees exactly one notification.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 9: Weekly Repeating Notifications
&lt;/h2&gt;

&lt;p&gt;For notifications that fire on specific weekdays — like a project reminder every Monday, Wednesday, and Friday at 6:00 PM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scheduleWeekly&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 1 = Monday, 7 = Sunday&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_nextInstanceOfWeekdayTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="cm"&gt;/* notificationDetails */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;uiLocalNotificationDateInterpretation:&lt;/span&gt;
        &lt;span class="n"&gt;UILocalNotificationDateInterpretation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;absoluteTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;androidScheduleMode:&lt;/span&gt; &lt;span class="n"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inexactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;matchDateTimeComponents:&lt;/span&gt; &lt;span class="n"&gt;DateTimeComponents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dayOfWeekAndTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ← Weekly&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt; &lt;span class="nf"&gt;_nextInstanceOfWeekdayTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scheduled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Advance to the target weekday&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;weekday&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;scheduled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;days:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// If it's already passed this week, push to next week&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;scheduled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;days:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Each weekday needs its own notification ID. If you want reminders on Monday, Wednesday, and Friday, that's three separate &lt;code&gt;zonedSchedule&lt;/code&gt; calls with three different IDs. When removing the reminder, cancel all of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generating unique IDs per project + weekday&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;notifId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;projectId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashCode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 10: Surviving Samsung Battery Optimization
&lt;/h2&gt;

&lt;p&gt;This is the step most Flutter notification tutorials skip, and it's why your notifications work perfectly during development but fail silently in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;Samsung, Xiaomi, Huawei, OnePlus, and most Chinese OEMs add an aggressive battery optimization layer &lt;strong&gt;on top of&lt;/strong&gt; Android's standard Doze mode. Even if your alarm is correctly scheduled with &lt;code&gt;exactAllowWhileIdle&lt;/code&gt;, the OEM's battery manager can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kill your app process after ~5 minutes of screen-off time&lt;/li&gt;
&lt;li&gt;Block &lt;code&gt;AlarmManager&lt;/code&gt; exact alarms from waking the app&lt;/li&gt;
&lt;li&gt;Silently suppress notifications from "sleeping" apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why WhatsApp, Duolingo, and Telegram work but your app doesn't — Google Play Services (which delivers FCM push notifications) is whitelisted at the system level. Your app is not. You need the user to manually exempt you.&lt;/p&gt;

&lt;h3&gt;
  
  
  The solution
&lt;/h3&gt;

&lt;p&gt;Android provides a system dialog that asks the user to whitelist your app from battery optimization. You can trigger it using the &lt;code&gt;permission_handler&lt;/code&gt; package (which you already have for notification permissions):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:io'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/foundation.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:permission_handler/permission_handler.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:shared_preferences/shared_preferences.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BatteryOptimizationService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;_keyAsked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'battery_optimization_asked'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;/// Check if the app is already exempted from battery optimizations.&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;isExempted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAndroid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Permission&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ignoreBatteryOptimizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isGranted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Request battery optimization exemption.&lt;/span&gt;
  &lt;span class="c1"&gt;/// Shows the system "Allow unrestricted battery?" dialog.&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requestExemption&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAndroid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Permission&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ignoreBatteryOptimizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isGranted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Check and prompt once — call on first pomodoro start.&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ensureExemptedForPomodoro&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAndroid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;isExempted&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;prefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SharedPreferences&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_keyAsked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;granted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;requestExemption&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_keyAsked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'[Battery] Exemption &lt;/span&gt;&lt;span class="si"&gt;${granted ? "granted" : "denied"}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to prompt
&lt;/h3&gt;

&lt;p&gt;Don't ask on app first launch — the user has no context for why you need it. Ask at the moment it matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;startPomodoro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Ask BEFORE the timer starts — user understands why&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;BatteryOptimizationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureExemptedForPomodoro&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// ... start the timer, schedule the alarm&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system dialog is native Android UI — it looks official, not spammy. And because we gate it with &lt;code&gt;_keyAsked&lt;/code&gt;, the user only sees it once.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the user sees
&lt;/h3&gt;

&lt;p&gt;The dialog says something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Let LongGame run in the background?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This app will be able to run in the background, which may increase battery usage.&lt;/p&gt;

&lt;p&gt;[DENY]  [ALLOW]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If they tap "Allow", your app is exempted from the OEM's battery killing. Your &lt;code&gt;exactAllowWhileIdle&lt;/code&gt; alarms now fire reliably even with the screen off.&lt;/p&gt;

&lt;h3&gt;
  
  
  For extra reliability
&lt;/h3&gt;

&lt;p&gt;If your users are on Samsung specifically, you may also want to link them to the device-specific battery settings. Samsung has an additional "Sleeping Apps" list that operates independently of Android's standard battery optimization. The &lt;a href="https://dontkillmyapp.com/" rel="noopener noreferrer"&gt;don't kill my app&lt;/a&gt; project maintains device-specific instructions you can reference in your settings screen.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 11: Custom Notification Sounds
&lt;/h2&gt;

&lt;p&gt;If you want distinct sounds for different notification types — like a bright bell for "focus" and a warm chime for "break" — you need three things.&lt;/p&gt;

&lt;h3&gt;
  
  
  File placement
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;android/app/src/main/res/raw/bell_focus.wav     ← Android reads from res/raw
ios/Runner/bell_focus.wav                        ← iOS reads from app bundle
assets/sounds/bell_focus.wav                     ← Optional: Flutter assets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Notification details with sound
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;NotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;AndroidNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;'channel_focus_bell'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                    &lt;span class="c1"&gt;// Unique channel ID&lt;/span&gt;
    &lt;span class="s"&gt;'Focus Bell'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;sound:&lt;/span&gt; &lt;span class="n"&gt;RawResourceAndroidNotificationSound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'bell_focus'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// No file extension&lt;/span&gt;
    &lt;span class="nl"&gt;playSound:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;DarwinNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;presentSound:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;sound:&lt;/span&gt; &lt;span class="s"&gt;'bell_focus.wav'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                  &lt;span class="c1"&gt;// With file extension&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The channel rule
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Android requires a separate notification channel for each distinct sound.&lt;/strong&gt; Once a channel is created, its sound cannot be changed programmatically — the user would need to clear app data or reinstall. Name your channels carefully the first time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One channel per sound&lt;/span&gt;
&lt;span class="s"&gt;'longgame_focus_bell'&lt;/span&gt;   &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;bell_focus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wav&lt;/span&gt;
&lt;span class="s"&gt;'longgame_break_bell'&lt;/span&gt;   &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;bell_break&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wav&lt;/span&gt;
&lt;span class="s"&gt;'longgame_reminders'&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="n"&gt;sound&lt;/span&gt;

&lt;span class="c1"&gt;// Don't try to reuse a channel with different sounds&lt;/span&gt;
&lt;span class="c1"&gt;// The first sound "wins" and the channel ignores subsequent changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Complete Reference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which mechanism to use
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Repeats?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daily notification (8:00 AM every day)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;zonedSchedule&lt;/code&gt; + &lt;code&gt;DateTimeComponents.time&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Every day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly reminder (Mon at 6:00 PM)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;zonedSchedule&lt;/code&gt; + &lt;code&gt;DateTimeComponents.dayOfWeekAndTime&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Every week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-shot alarm (25 min from now)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;zonedSchedule&lt;/code&gt;, no &lt;code&gt;matchDateTimeComponents&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent indicator (timer running)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_plugin.show()&lt;/code&gt; with &lt;code&gt;ongoing: true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Until cancelled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instant alert (event just happened)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_plugin.show()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Which notification details to use
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Importance&lt;/th&gt;
&lt;th&gt;fullScreenIntent&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daily nudge / reminder&lt;/td&gt;
&lt;td&gt;&lt;code&gt;high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Standard heads-up, doesn't need to wake the screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pomodoro phase transition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alarm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Must wake the screen and break through Samsung battery killing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ongoing timer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stopwatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Persistent but non-intrusive, lives in the notification shade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session reminder&lt;/td&gt;
&lt;td&gt;&lt;code&gt;high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Standard importance, user is likely awake&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What not to use
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What goes wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WorkManager&lt;/code&gt; periodic task&lt;/td&gt;
&lt;td&gt;OS throttles it. 15-minute minimum becomes hours in Doze.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Dart Timer.periodic&lt;/code&gt; for alerts&lt;/td&gt;
&lt;td&gt;Stops when phone locks. App suspended = timer dead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cancel()&lt;/code&gt; then &lt;code&gt;zonedSchedule()&lt;/code&gt; on app open&lt;/td&gt;
&lt;td&gt;Perpetually pushes alarm to "tomorrow." Never fires.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS Background Fetch&lt;/td&gt;
&lt;td&gt;0–2 executions per day. Apple decides when, not you.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Importance.high&lt;/code&gt; for time-critical alarms&lt;/td&gt;
&lt;td&gt;Samsung silently suppresses it. Use &lt;code&gt;max&lt;/code&gt; + &lt;code&gt;alarm&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ignoring battery optimization&lt;/td&gt;
&lt;td&gt;Works on Pixel, dies on Samsung. 70%+ of Android users are on OEM skins.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Android permissions checklist
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;th&gt;When needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Always (Android 13+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RECEIVE_BOOT_COMPLETED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If alarms should survive reboots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SCHEDULE_EXACT_ALARM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exact alarms (API ≤ 33, runtime request on API 34+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USE_EXACT_ALARM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Guaranteed exact alarms (API 33+), no user prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REQUEST_IGNORE_BATTERY_OPTIMIZATIONS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Battery exemption dialog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WAKE_LOCK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep CPU alive during alarm processing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Notification IDs
&lt;/h3&gt;

&lt;p&gt;Use fixed IDs per notification type. &lt;code&gt;zonedSchedule&lt;/code&gt; with the same ID replaces the existing alarm. This is your friend — it's how you update content without breaking repeats.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idMorningIntent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idDriftAlert&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idMirror&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idStreak&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idTimerRunning&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idOngoingTimer&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idPomodoroAlarm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;98&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idPomodoro&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idSessionBase&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// + project hash + weekday&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Bugs To Watch For
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bug 1: The startup hang
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;rescheduleAll()&lt;/code&gt; queries Firestore for personalized content. If no user is signed in, the Firestore SDK with offline persistence doesn't throw — it hangs. Your &lt;code&gt;main()&lt;/code&gt; never reaches &lt;code&gt;runApp()&lt;/code&gt;. The app is stuck on the splash screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Check &lt;code&gt;FirebaseAuth.instance.currentUser != null&lt;/code&gt; before rescheduling. Also make the call fire-and-forget with &lt;code&gt;.catchError()&lt;/code&gt; so a Firestore timeout doesn't block startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: The cancel-reschedule cycle
&lt;/h3&gt;

&lt;p&gt;Covered in Step 5, but worth repeating because it's the most insidious bug. It works perfectly in development (you're always testing right after scheduling), and fails silently in production (the user opens the app after the notification time, so it's always pushed to tomorrow).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Never &lt;code&gt;cancel&lt;/code&gt; before &lt;code&gt;zonedSchedule&lt;/code&gt; for enabled notifications. Only cancel when disabling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: The locked-phone silence
&lt;/h3&gt;

&lt;p&gt;Your Dart timer works nicely in the foreground. You test it, the notification fires after 25 minutes, you ship. Then a user runs a pomodoro session, puts the phone down, and hears nothing. The OS suspended your app. Your timer stopped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Schedule a one-shot OS alarm for the exact phase end time. The Dart timer is for foreground UX; the OS alarm is for reliability. Let them race.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 4: The Samsung battery killer
&lt;/h3&gt;

&lt;p&gt;Everything works on your Pixel. Every alarm fires on time. You ship. Then 70% of your users (Samsung, Xiaomi, Huawei) report that pomodoro notifications never arrive when the screen is off.&lt;/p&gt;

&lt;p&gt;Samsung adds &lt;strong&gt;"Sleeping Apps"&lt;/strong&gt; and &lt;strong&gt;"Deep Sleeping Apps"&lt;/strong&gt; lists on top of Android's standard Doze. Even &lt;code&gt;exactAllowWhileIdle&lt;/code&gt; alarms are suppressed for apps on these lists. Your perfectly scheduled alarm never fires because Samsung killed your process and blocked the &lt;code&gt;AlarmManager&lt;/code&gt; wakeup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix (three layers, use all of them):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Request battery optimization exemption&lt;/strong&gt; — show the system "Allow unrestricted?" dialog on first pomodoro start. This removes you from the standard Android optimization. (Step 10)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use alarm-grade notification details&lt;/strong&gt; — &lt;code&gt;fullScreenIntent: true&lt;/code&gt; + &lt;code&gt;category: alarm&lt;/code&gt; + &lt;code&gt;Importance.max&lt;/code&gt;. The OS treats these like alarm clock notifications and is far less likely to suppress them. (Step 8)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Declare &lt;code&gt;USE_EXACT_ALARM&lt;/code&gt;&lt;/strong&gt; — this permission is always granted on API 33+ without user interaction, giving you a stronger guarantee than &lt;code&gt;SCHEDULE_EXACT_ALARM&lt;/code&gt; alone. (Step 2)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these alone is sufficient. Together, they give you the same delivery reliability as WhatsApp on Samsung.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cost
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Cost per user&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FCM push messages&lt;/td&gt;
&lt;td&gt;$0 — not used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions&lt;/td&gt;
&lt;td&gt;$0 — computed on-device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firestore reads for content&lt;/td&gt;
&lt;td&gt;$0 — bundled with existing app queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local alarm scheduling&lt;/td&gt;
&lt;td&gt;$0 — OS-level, no backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Battery optimization prompt&lt;/td&gt;
&lt;td&gt;$0 — native system dialog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The entire notification system runs at zero marginal cost. The trade-off is that notification content is only as fresh as the user's last app open. For daily reflections and nudges, that's perfectly fine — you're computing tomorrow's content with today's data, and it's accurate enough to be useful.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the system behind &lt;a href="https://long-game-f5520.web.app/" rel="noopener noreferrer"&gt;Long Game&lt;/a&gt;, a life auditing app for people who want to be intentional with their time. The original post explaining the design decisions is &lt;a href="https://www.mxomasuku.com/blog/how-i-built-the-long-game-notification-system-a-journey-into-notifications-and-behavioral-engineering" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>android</category>
      <category>mobiledevelopment</category>
    </item>
    <item>
      <title>The People On The Other Side Of Your Upwork Proposal</title>
      <dc:creator>Mxolisi Masuku</dc:creator>
      <pubDate>Mon, 30 Mar 2026 03:19:47 +0000</pubDate>
      <link>https://dev.to/mxomasuku/the-people-on-the-other-side-of-your-upwork-proposal-4pno</link>
      <guid>https://dev.to/mxomasuku/the-people-on-the-other-side-of-your-upwork-proposal-4pno</guid>
      <description>&lt;p&gt;Upwork is increasingly getting difficult — or so they say. I've had quite a number of friends who created an account and abandoned it, and others curious about whether it's worth starting. The conversation kept repeating itself so I decided to write this.&lt;/p&gt;

&lt;p&gt;If the Internet tells you Upwork is a scam, understand what you are actually hearing: frustration from people who didn't make it, and silence from the thousands quietly building careers on the platform. People who are making money on Upwork eat quietly. The loudest voices are freelancers turned influencers who make money coaching you, and the disgruntled ones on Reddit whose complaints come wrapped in humble brags. "I have a $400k account but Upwork treated me like this." $400k is life-changing money — but if you break the rules, Upwork is judge, jury and executioner. Your account size doesn't matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Numbers Behind The Noise&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Upwork is not a startup hoping to find product-market fit. It is the dominant player in the freelance platform market.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://investors.upwork.com/news-releases/news-release-details/upwork-reports-fourth-quarter-and-full-year-2025-financial" rel="noopener noreferrer"&gt;Record full-year revenue of $788 million in 2025&lt;/a&gt;. Over &lt;strong&gt;$4 billion&lt;/strong&gt; in gross services volume — that's the total amount clients spent on the platform in a single year. More than 18 million registered freelancers across 180+ countries. Around 800,000 active clients. Upwork holds roughly 61% of the freelance platform market share, more than triple Fiverr's and double Toptal's. Fast Company named them &lt;a href="https://investors.upwork.com/news-releases/news-release-details/upwork-named-fast-companys-most-innovative-companies-2025" rel="noopener noreferrer"&gt;one of the Most Innovative Companies of 2025&lt;/a&gt;, and Frost &amp;amp; Sullivan gave them the Global New Product Innovation Award in 2023.&lt;/p&gt;

&lt;p&gt;AI-related work on the platform grew 60% year-over-year in 2024, and freelancers working on AI projects earned 44% more per hour than those who didn't. The average freelancer hourly rate sits around $39. Web and software development accounts for 34% of all work on the platform.&lt;/p&gt;

&lt;p&gt;Why does this matter to you? $4 Billion in circulation means this is not a dying marketplace. The money is real, the clients are real, and the opportunity is real. The question is whether you can position yourself to capture your slice of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. Think People First&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before you get into the Upwork algorithm debate, start here: Who are the people on the other side of your proposals?&lt;/p&gt;

&lt;p&gt;The client landscape has shifted. Three or four years ago, the typical Upwork client had money to spend, no time to look for talent, and needed an automated way to protect their interests. You saw people offering 3-year contracts at $40 an hour.&lt;/p&gt;

&lt;p&gt;Since 2025, the attitude has changed. Everyone is looking to save. But not everyone is looking to save the same way. Here's how I break down the client tiers on Upwork right now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The top-tier client&lt;/strong&gt; understands the value of AI in speeding things up. He has money but is not willing to splash it on a slow freelancer. This client wants the best person on the job who can leverage AI to move fast, and can pay explosive amounts for it. Usually after the first successful job, these people will negotiate a long-term contract with a reliable freelancer. They don't sift through 150 proposals. They go to Upwork and say "find me someone who can do this" and the algorithm feeds them the best and boosted profiles. This is where Connects, profile optimisation and your Upwork ranking all come in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fair-price client&lt;/strong&gt; says "I have this job, I have this amount, let's work." Expect fair treatment here. This kind of client appreciates honesty, &lt;a href="https://www.upwork.com/resources/ways-to-improve-client-communication-skills" rel="noopener noreferrer"&gt;communication&lt;/a&gt; and thoroughness. These are skills you should have or be actively improving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exploitative client&lt;/strong&gt; knows the freelance labour market is full of desperate people right now. So he dangles a carrot. $50 for a hard job. Then he piles on responsibility knowing the developer will do it just to get good reviews. Upwork has &lt;a href="https://www.upwork.com/resources/leading-voices/prepare-for-your-first-client-meeting" rel="noopener noreferrer"&gt;documentation to help you&lt;/a&gt; handle this, but you need to recognise the pattern before you're already trapped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scammer.&lt;/strong&gt; You have to beware of these. They will take you for everything you've got. Steal, use you, hack you or worse. Follow Upwork's guidelines on surviving scammers and go to the unofficial Upwork Reddit channel — &lt;a href="https://www.reddit.com/r/Upwork/" rel="noopener noreferrer"&gt;r/Upwork&lt;/a&gt;. It's unhinged and town-square-like, but it will help you catch up fast with how scams are evolving.&lt;/p&gt;

&lt;p&gt;Pro tip: Read &lt;a href="https://www.reddit.com/r/Upwork/wiki/index/scamguide/generalguide/" rel="noopener noreferrer"&gt;The General Guide To Upwork Scams&lt;/a&gt; on Reddit&lt;/p&gt;

&lt;p&gt;Once you know the people, you know who you need to become to be chosen. You can play the algorithm all you want, but on the other end of everything you type, there is a person who is as interested in gaining value and as afraid of losing money on bad product as you are.&lt;/p&gt;

&lt;p&gt;Think people first. Upwork is a community brought together by the platform. The rules matter as much as the people they serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Play The Long Game But Work Hard On Your Short Plays&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Freelancing is hard and competitive. Upwork, being the top platform, is even more competitive. Whether you are new or a seasoned freelancer looking to reinvent yourself: your patience will be tested and your persistence will be rewarded.&lt;/p&gt;

&lt;p&gt;The long-term goal is being on a stable, well-paying contract. Being a top freelancer that Upwork recommends to its best clients. Getting paid consistently.&lt;/p&gt;

&lt;p&gt;The mid-term goal is building up smaller, shorter contracts. Quick jobs. Building your skill set, networking, studying the market. You don't get to the long-term goal without passing through this one unless you are extremely lucky.&lt;/p&gt;

&lt;p&gt;The immediate goal is your short plays. This is where the real work happens and where most freelancers give up too early.&lt;/p&gt;

&lt;p&gt;In my experience, I got lucky in a sense — I got to Top Rated Plus with just 3 jobs. But those 3 jobs amount to 2,240+ hours. My takeaway: there is more value in sustaining a long-term business relationship than in chasing volume. Your mileage may vary, but there is no substitute for good work and maintaining good relationships.&lt;/p&gt;

&lt;p&gt;As you improve your profile, think of every iteration of profile development as a play that must be analysed and improved. Every proposal is a data point that needs to be dissected and built on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5weexyl2ip1o2l01hsdg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5weexyl2ip1o2l01hsdg.png" alt="Screenshot 2026-03-27 at 23.50.39.png" width="800" height="570"&gt;&lt;/a&gt; &lt;em&gt;Source: &lt;a href="https://www.reddit.com/r/Upwork/comments/1s5ee3s/please_review_my_stats/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button" rel="noopener noreferrer"&gt;r/Upwork&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Look at this from r/Upwork. 49 proposals sent in 90 days. Only 7 were even viewed. 3 interviews. 2 hires. $1,500 earned in the first few months on the platform. This is what the grind looks like. Most of your proposals will disappear into the void. The game is making sure the ones that land are good enough to convert.&lt;/p&gt;

&lt;p&gt;Read job posts. What skills are clients asking for? What's the overall application tone on key skills? It's all about patience, playing the long game and relentlessly looking to improve.&lt;/p&gt;

&lt;p&gt;Right now, in most of the job posts I see, clients are prioritising freelancers who have an AI-first attitude. n8n, Agentic Workflows, Supabase, Firebase, Cursor, Antigravity top almost every post. These can be pointers but they can also get you panicking and chasing the wind. When you see trends like these you can either choose to be a generalist or a specialist. I chose to specialise. Depth beats breadth when a client scans 50 proposals and needs to trust that you've solved their exact problem before.&lt;/p&gt;

&lt;p&gt;In the job posts, listen to what clients are saying about what they need. More importantly, listen to what they're saying about the freelancer they want to work with. Become that person.&lt;/p&gt;

&lt;p&gt;Again. Think people first.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Follow The Rules&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The most nerve-wracking moment in your Upwork journey will be right after a client says &lt;em&gt;"Hey, I saw your proposal. I'm interested. Let's talk."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At this moment, your position must shift: DO NOT THINK PEOPLE FIRST. Think Upwork first.&lt;/p&gt;

&lt;p&gt;Do not be hoodwinked. Your main concern shouldn't be what the other person is saying. It should be: what does Upwork say about this? For every response you send, you must consider the Upwork funnel and &lt;a href="https://www.upwork.com/resources/client-red-flags" rel="noopener noreferrer"&gt;playbook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Failure to do so will result in a ban. Back in the day the ban was effective immediately and non-negotiable, though this may be shifting since Upwork now shows you account health status.&lt;/p&gt;

&lt;p&gt;Things to know: Upwork is ruthless on freelancers who share personal contact details before a contract is finalised. Clients who want to take the conversation off-platform before a contract is signed are a red flag. On top of that, you should also be able to negotiate boldly without putting yourself in the corner, all while studying your client for scammer patterns.&lt;/p&gt;

&lt;p&gt;Have your own rules as a freelancer and follow them. Know Upwork's rules and follow them. Listen to the client's rules and expectations and see if you are prepared to follow them. This is the foundation of your Upwork survival journey.&lt;/p&gt;

&lt;p&gt;The interview and contract negotiation phase is the most dangerous period of your freelance career. When you see people talking about how a single mistake cost them everything, this is the phase they're talking about. This is why the &lt;a href="https://www.reddit.com/r/Upwork/" rel="noopener noreferrer"&gt;unofficial Upwork Reddit channel is a must-visit.&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Protect yourself. Protect your account.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Last Word&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;How do you survive Upwork as a freelancer? This entire article can be distilled into one statement: Know and study the people on Upwork, and follow the rules.&lt;/p&gt;

&lt;p&gt;Link to the You can also check the &lt;a href="https://www.reddit.com/r/UpworkOfficial/" rel="noopener noreferrer"&gt;official Upwork&lt;/a&gt; subreddit, though it's much smaller than &lt;a href="https://www.reddit.com/r/Upwork/" rel="noopener noreferrer"&gt;r/Upwork&lt;/a&gt;. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;My name is Mxolisi Masuku. I write blogs like this and I have a newsletter called &lt;a href="https://www.mxomasuku.com/newsletter" rel="noopener noreferrer"&gt;Systems For Humans&lt;/a&gt; where I talk about the software world beyond the technical considerations. For me, the software world works better when you think about who is on the other end as the guiding principle of whatever you are going to build. &lt;a href="https://www.mxomasuku.com/newsletter" rel="noopener noreferrer"&gt;Subscribe&lt;/a&gt; and follow if you like this.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>upwork</category>
      <category>freelancing</category>
      <category>career</category>
    </item>
    <item>
      <title>Build Your Stripe Checkout Like A Lawyer, Not Just A Developer</title>
      <dc:creator>Mxolisi Masuku</dc:creator>
      <pubDate>Sat, 28 Mar 2026 10:58:13 +0000</pubDate>
      <link>https://dev.to/mxomasuku/build-your-stripe-checkout-like-a-lawyer-not-just-a-developer-67j</link>
      <guid>https://dev.to/mxomasuku/build-your-stripe-checkout-like-a-lawyer-not-just-a-developer-67j</guid>
      <description>&lt;p&gt;Most developers and vibe coders build their Stripe checkout to accept payments. They don't build it to survive disputes with people, their customers. In Stripe's ecosystem, a single ban or badly handled chargeback can cost your business everything. This article is about how to make sure it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Your Refund Policy Is Your First Line of Defence&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before you write a single line of checkout code, you need a refund and chargeback policy. Not a template you copied from the internet. A policy you have actually stress-tested against real scenarios.&lt;/p&gt;

&lt;p&gt;For example, I am working on a platform that sells music beats. These fall under digital products. My thought process was: What happens when a customer pays for a beat, downloads it, then says they don't want it anymore and claims they never used it? What if they didn't download it? How would I even prove that? What if they come back after 2 days? 2 weeks? 2 months?&lt;/p&gt;

&lt;p&gt;These disputes will come. &lt;/p&gt;

&lt;p&gt;Some will be genuine, some will be fraudulent. Your policy is the wall between you keeping that money and Stripe taking it back. &lt;/p&gt;

&lt;p&gt;This policy must be visible to the user. The user must accept it before they complete the purchase. The usual feature on most sites is a checkbox with: "I accept and agree to the terms and conditions." Simple. But if it's not there, you have nothing to stand on.&lt;/p&gt;

&lt;p&gt;When building, you want to spend more of your initial time in the &lt;a href="https://www.youtube.com/watch?v=C-KFUrNw71U" rel="noopener noreferrer"&gt;Stripe Sandbox&lt;/a&gt; where you test all of this before real money is involved. Play both God and customer. Simulate every scenario you can think of. Who has access to the payment gateway and at what stage? How are payments reflecting on your dashboard? Don't take the Sandbox for granted because it is the only environment where mistakes cost you nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Capture Every Event. Prove Everything.&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When a dispute hits, Stripe doesn't take your word for it. They want evidence. The card provider wants evidence. And if you haven't been recording the right events from the start, you have nothing to give them.&lt;/p&gt;

&lt;p&gt;Every interaction between the user and your checkout process needs to be logged. Every click, every purchase, every policy agreement. What IP address did the user have? Was it consistent with their usual IP? Was the purchase made from a valid session? Did the user explicitly agree to the refund policy? Did you deliver the product? You need to be able to prove and demonstrate all of this with timestamps and unique IDs.&lt;/p&gt;

&lt;p&gt;This is the part where you have to be deliberate with your AI. If you are vibe coding your checkout, there is a real chance your AI won't build evidence capture unless you specifically ask for it. AI is great at making the payment flow work. AI is not great at thinking about what happens when a customer disputes that payment six weeks later. You have to stress this requirement yourself.&lt;/p&gt;

&lt;p&gt;Pro tip: Stripe has an AI Assistant for general queries and Agent Skills and Connectors for deeper integration. &lt;a href="https://claude.com/connectors/stripe" rel="noopener noreferrer"&gt;Use these if you are on Claude.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Event logging should be built as a feature not an afterthought. You want to be able to export all the evidence Stripe and card providers require, as a PDF or JSON, with all the required fields populated. When the dispute comes, and it will, you pull the file and submit it. That's the difference between keeping the money and losing it.&lt;/p&gt;

&lt;p&gt;For reference, look at &lt;a href="https://docs.stripe.com/api/disputes/object#dispute_object-evidence" rel="noopener noreferrer"&gt;Stripe's dispute evidence object documentation&lt;/a&gt;. It tells you exactly what fields they expect: customer email, customer IP, product description, proof of delivery, refund policy disclosure, and more. Build your logging around that schema and you won't be scrambling when the first chargeback arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Send Receipts and Invoices. Build Trust Before You Need It.&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You can configure Stripe to send customer emails in Settings → Business → Email → Customer Emails. This isn't a legal requirement but think about it from the human side. You just made a $240 purchase and you don't get a receipt in your email. That's how distrust starts. That's how chargebacks start.&lt;/p&gt;

&lt;p&gt;Receipts, invoices, upcoming deductions, subscription reminders. These are not nice-to-haves. They are evidence that you communicated with your customer. They are proof that the customer knew what they were paying for and when. If a dispute lands, a clean email trail showing receipts sent and subscription reminders delivered tells a very different story from silence.&lt;/p&gt;

&lt;p&gt;If you want to go beyond Stripe's default emails and build custom branded ones, &lt;a href="https://www.youtube.com/watch?v=Avp1OOMH2Z0" rel="noopener noreferrer"&gt;a tool like Resend works well for this&lt;/a&gt;. You create React template emails, hook them to your Stripe webhooks, and fire a custom receipt whenever a &lt;code&gt;checkout.session.completed&lt;/code&gt; event lands. Resend gives you 100 emails a day for free. If you are hitting 100 emails a day, that means you got 100 customers a day and you can afford their $20 a month subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Build For Humans. Defend Against The Worst Ones.&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The thread running through this article is that you should build your software for human behaviour. Defend against malicious intent and protect yourself from legal punishment at the same time. &lt;/p&gt;

&lt;p&gt;Most startups get burned not because their product was bad but because they didn't think about what happens when a real person with real emotions and real incentives interacts with their payment system. People forget what they bought. People regret purchases. People lie. And some people game the system because they know most small developers don't have their evidence in order.&lt;/p&gt;

&lt;p&gt;Your checkout is not just a technical flow. It is a legal document, a communication channel and a defence mechanism. Treat it that way from day one.&lt;/p&gt;

&lt;p&gt;Additional Reading&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stripe as the authority on &lt;a href="https://stripe.com/resources/more/ecommerce-chhttps://stripe.com/resources/more/ecommerce-chargebacks-101argebacks-101" rel="noopener noreferrer"&gt;why chargebacks happen and how to prevent them&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;If you are looking &lt;a href="https://www.reddit.com/r/SaaS/comments/157rorx/are_stripe_disputes_impossible_to_win/" rel="noopener noreferrer"&gt;how rough things can get on chargebacks&lt;/a&gt; check this subReddit. The question: Are Stripe Chargebacks impossible to win.
&lt;/li&gt;
&lt;li&gt;Another Stripe Blog read: &lt;a href="https://stripe.com/blog/can-ai-agents-build-real-stripe-integrations" rel="noopener noreferrer"&gt;Can AI agents build real Stripe integrations? We built a benchmark to find out&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;If you ever decide that Stripe is not for you. &lt;a href="https://www.youtube.com/watch?v=UUsOWyqUCjw" rel="noopener noreferrer"&gt;Here is a video on alternatives.&lt;/a&gt; &lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;My name is Mxolisi Masuku. I am a software engineer and a freelancer on Upwork. If you liked this, subscribe to my newsletter, Systems for Humans, where I write about the software world beyond the technical considerations.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>productdesign</category>
    </item>
  </channel>
</rss>
