<?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: ROCI</title>
    <description>The latest articles on DEV Community by ROCI (@rocisapps).</description>
    <link>https://dev.to/rocisapps</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1115082%2F0cbca8f4-907b-4522-a217-4120285c6926.jpg</url>
      <title>DEV Community: ROCI</title>
      <link>https://dev.to/rocisapps</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rocisapps"/>
    <language>en</language>
    <item>
      <title>Offline-First Flutter Architecture</title>
      <dc:creator>ROCI</dc:creator>
      <pubDate>Sun, 06 Sep 2026 13:03:25 +0000</pubDate>
      <link>https://dev.to/rocisapps/offline-first-flutter-architecture-558l</link>
      <guid>https://dev.to/rocisapps/offline-first-flutter-architecture-558l</guid>
      <description>&lt;p&gt;Deep dive into offline caching with Hive and SQLite.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>android</category>
    </item>
    <item>
      <title>Why Your Flutter Background Isolates Crash on Android (And How I Fixed It)</title>
      <dc:creator>ROCI</dc:creator>
      <pubDate>Sat, 05 Sep 2026 17:02:02 +0000</pubDate>
      <link>https://dev.to/rocisapps/why-your-flutter-background-isolates-crash-on-android-and-how-i-fixed-it-5cka</link>
      <guid>https://dev.to/rocisapps/why-your-flutter-background-isolates-crash-on-android-and-how-i-fixed-it-5cka</guid>
      <description>&lt;p&gt;Building an offline-first task manager with Flutter and native Android widgets means dancing on the edge of two worlds. Recently, while shipping update &lt;code&gt;0.2.2+63&lt;/code&gt; for &lt;strong&gt;ROCIs Tasks&lt;/strong&gt;, I hit a classic multi-threading trap that’s easy to stumble into when dealing with background isolates, platform channels, and cloud sync.&lt;/p&gt;

&lt;p&gt;Here is how I debugged it, solved it, and finally closed the loop on true two-way Google Tasks syncing.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Mystery: Background Crashes and Broken Tests
&lt;/h3&gt;

&lt;p&gt;While working on background task completion handlers, my app started throwing intermittent runtime errors on Android. The culprit? &lt;code&gt;WidgetsBinding.instance&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've ever tried to trigger state updates, platform channel calls, or bindings initialization inside a detached background isolate, you know the pain. Flutter expects a UI binding to be initialized on the main thread. When a background handler fires asynchronously, &lt;code&gt;WidgetsBinding.instance&lt;/code&gt; can be null or completely unstable depending on the lifecycle state and platform.&lt;/p&gt;

&lt;p&gt;To make matters worse, my test suite was throwing a couple of red flags: a regex validation failure on my new version string format (&lt;code&gt;0.2.2+63&lt;/code&gt;), and a Mocktail type-casting error because I hadn't stubbed &lt;code&gt;getGoogleAccessToken()&lt;/code&gt; during startup synchronization tests. Standard indie dev Tuesday.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Root Cause
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Isolate Trap&lt;/strong&gt;: &lt;code&gt;WidgetsBinding.instance.platformDispatcher&lt;/code&gt; relies heavily on an active UI view and bound Flutter engine lifecycle. In background execution contexts (like handling quick actions or background sync triggers), that binding simply isn't guaranteed to exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mocking Blind Spots&lt;/strong&gt;: As features grow, mocking external auth tokens in unit tests becomes brittle if your initialization sequence suddenly demands them before the mock is set up.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  The Solution &amp;amp; Code Architecture
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Ditching &lt;code&gt;WidgetsBinding&lt;/code&gt; for Background Tasks
&lt;/h4&gt;

&lt;p&gt;The fix for the background isolate issue was surprisingly clean. Instead of reaching for the UI-bound dispatcher, drop down to the global singleton &lt;code&gt;PlatformDispatcher&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;// Before (Unstable in background isolates)&lt;/span&gt;
&lt;span class="c1"&gt;// final dispatcher = WidgetsBinding.instance.platformDispatcher;&lt;/span&gt;

&lt;span class="c1"&gt;// After (Safe globally, even off-main-thread)&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PlatformDispatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple swap completely stabilized my background handlers without touching the main UI lifecycle.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Bulletproofing Google Tasks Back-Sync
&lt;/h4&gt;

&lt;p&gt;With background stability restored, I tackled true two-way sync. Users expect changes made directly inside Google Tasks (completions, uncompletions, and deletions) to reflect instantly in ROCIs Tasks.&lt;/p&gt;

&lt;p&gt;I expanded the &lt;code&gt;GoogleTasksService&lt;/code&gt; with paginated task retrieval, ensuring we pull completed and hidden items, and implemented &lt;code&gt;syncGoogleTasksToLocal()&lt;/code&gt; inside the &lt;code&gt;TaskProvider&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="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;syncGoogleTasksToLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GoogleTask&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;remoteTasks&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;for&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;remote&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;remoteTasks&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;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_localTasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firstWhereOrNull&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;googleId&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&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;remote&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isCompleted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;isCompleted&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="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_markLocalCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;remote&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isCompleted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;isCompleted&lt;/span&gt; &lt;span class="o"&gt;==&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_markLocalActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&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;span class="c1"&gt;// Handle deletions: tasks present locally with a Google ID but missing remotely&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_reconcileDeletedTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteTasks&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;I hooked this reconciliation logic directly into app startup (&lt;code&gt;syncWithCloud&lt;/code&gt;), manual settings triggers, and lifecycle tab-switches on both mobile and web (&lt;code&gt;home_screen.dart&lt;/code&gt; / &lt;code&gt;web_home_screen.dart&lt;/code&gt;).&lt;/p&gt;




&lt;h3&gt;
  
  
  Key Lessons for Other Devs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beware of Isolate Assumptions&lt;/strong&gt;: Never assume UI-bound singletons like &lt;code&gt;WidgetsBinding&lt;/code&gt; are available outside the main thread. Use &lt;code&gt;PlatformDispatcher.instance&lt;/code&gt; for thread-safe global access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Your Version Strings&lt;/strong&gt;: If you adopt build number formats like &lt;code&gt;+63&lt;/code&gt;, update your validation regexes &lt;em&gt;before&lt;/em&gt; CI/CD complains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stub Everything Early&lt;/strong&gt;: Keep your Mocktail default stubs updated when expanding authentication flows to prevent mysterious startup crashes in tests.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Try ROCIs Tasks
&lt;/h3&gt;

&lt;p&gt;ROCIs Tasks is built completely offline-first with native Kotlin home screen widgets and seamless cloud sync. If you want to check it out, grab it on Google Play or try the web version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Play&lt;/strong&gt;: &lt;a href="https://play.google.com/store/apps/details?id=com.rocisapps.tasks" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.rocisapps.tasks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web App&lt;/strong&gt;: &lt;a href="https://tasks.rocisapps.com" rel="noopener noreferrer"&gt;https://tasks.rocisapps.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>android</category>
      <category>indiedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why Google Tasks Sync Was Silently Failing Every Hour (And How I Fixed It)</title>
      <dc:creator>ROCI</dc:creator>
      <pubDate>Sat, 05 Sep 2026 16:51:51 +0000</pubDate>
      <link>https://dev.to/rocisapps/why-google-tasks-sync-was-silently-failing-every-hour-and-how-i-fixed-it-1cao</link>
      <guid>https://dev.to/rocisapps/why-google-tasks-sync-was-silently-failing-every-hour-and-how-i-fixed-it-1cao</guid>
      <description>&lt;p&gt;Building an offline-first task app sounds peaceful until you introduce cloud synchronization. Suddenly, you're knee-deep in OAuth scopes, token expirations, and mysterious silent failures that only happen when you step away from your desk for an hour.&lt;/p&gt;

&lt;p&gt;In ROCIs Tasks—my offline-first Android task and calendar app built with Flutter and native Kotlin home screen widgets—I recently ran into a multi-layered Google Tasks sync puzzle. Users were experiencing sudden sync dropouts, and worse, the app wasn't telling them why. Let's break down what went wrong and how I built a robust, self-healing auth recovery flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Mystery: The 60-Minute Disappearing Act
&lt;/h3&gt;

&lt;p&gt;Everything worked great during initial testing. You log in, tasks sync beautifully between local SQLite databases and Google Tasks, and native widgets update seamlessly. But after an hour of idle time, background sync would quietly die.&lt;/p&gt;

&lt;p&gt;When looking at the logs, three distinct culprits emerged:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Cloud Console Blindspot&lt;/strong&gt;: The Google Tasks API simply wasn't enabled in the Google Cloud Console for the project. Classic rookie configuration error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 60-Minute Token Wall&lt;/strong&gt;: OAuth access tokens expire after 60 minutes. When the API threw a &lt;code&gt;GoogleTokenExpiredException&lt;/code&gt;, my &lt;code&gt;TaskProvider&lt;/code&gt; was catching it, silently ignoring it, and failing indefinitely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Web Silent Refresh Gap&lt;/strong&gt;: While mobile had some token handling, the web platform lacked proper silent scope authorization requests, forcing users to completely re-authenticate manually.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result? A broken user experience where data simply stopped syncing without any visual feedback.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution &amp;amp; Architecture
&lt;/h3&gt;

&lt;p&gt;To fix this permanently, I needed a multi-pronged approach: fixing the backend configuration, upgrading token handling, and adding graceful UI recovery.&lt;/p&gt;

&lt;p&gt;First, I enabled the API in Google Cloud. Next, I tackled the token lifecycle inside our authentication service. I implemented platform-agnostic silent scope authorization requests to ensure tokens could refresh seamlessly without throwing hard errors.&lt;/p&gt;

&lt;p&gt;To bridge the gap between the auth layer and the UI, I exposed a reactive state flag in our &lt;code&gt;AuthService&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthService&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ChangeNotifier&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;_isGoogleTasksTokenExpired&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="kt"&gt;bool&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;isGoogleTasksTokenExpired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_isGoogleTasksTokenExpired&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;setTokenExpired&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;expired&lt;/span&gt;&lt;span class="p"&gt;)&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;_isGoogleTasksTokenExpired&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_isGoogleTasksTokenExpired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;notifyListeners&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="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;refreshAccessToken&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Platform-agnostic silent token request logic&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_silentAuthClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestAccess&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="n"&gt;setTokenExpired&lt;/span&gt;&lt;span class="p"&gt;(&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;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;setTokenExpired&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="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;Finally, instead of hiding the failure, I brought it to the user's attention gracefully. Using our Glassmorphism design system rules, I added contextual warning banners at the top of the Tasks tab on Mobile and inside the sidebar on Web. With a single tap on 'Reconnect', users trigger a self-healing re-authorization flow right where they are.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Lessons for Other Devs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never silently swallow auth exceptions&lt;/strong&gt;: If an API call fails due to an expired token, surface that state immediately to your state management layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for expiration&lt;/strong&gt;: OAuth tokens &lt;em&gt;will&lt;/em&gt; expire. Always build your UI assuming connectivity and auth states can drop at any moment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-healing UI wins&lt;/strong&gt;: Instead of locking users out or showing generic error toasts, give them a prominent, one-tap path to re-authenticate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Try ROCIs Tasks
&lt;/h3&gt;

&lt;p&gt;Building ROCIs Tasks as an indie developer has been an incredible journey into bridging Flutter cross-platform power with native Android widgets. If you want to check out an offline-first productivity app with deep calendar integration, take a look at &lt;a href="https://play.google.com/store/apps/details?id=com.rocisapps.tasks" rel="noopener noreferrer"&gt;ROCIs Tasks on Google Play&lt;/a&gt; or try the web version at &lt;a href="https://tasks.rocisapps.com" rel="noopener noreferrer"&gt;tasks.rocisapps.com&lt;/a&gt;. Feedback is always welcome!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>android</category>
      <category>indiedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why My Flutter App Was Spamming Google Sign-In Prompts (And How I Fixed It)</title>
      <dc:creator>ROCI</dc:creator>
      <pubDate>Sat, 05 Sep 2026 16:48:26 +0000</pubDate>
      <link>https://dev.to/rocisapps/why-my-flutter-app-was-spamming-google-sign-in-prompts-and-how-i-fixed-it-496h</link>
      <guid>https://dev.to/rocisapps/why-my-flutter-app-was-spamming-google-sign-in-prompts-and-how-i-fixed-it-496h</guid>
      <description>&lt;p&gt;As an indie developer building an offline-first task and calendar app (ROCIs Tasks) using Flutter with native Android Kotlin widgets, nothing gives you sudden micro-heart attacks quite like user bug reports about authentication loops. &lt;/p&gt;

&lt;p&gt;Recently, I hit a nasty regression: users were getting hammered with multiple Google sign-in prompts. Tap the Google Tasks sync toggle? Prompt. Tap 'Create Task'? Another prompt. It felt less like a productivity app and more like an aggressive security guard.&lt;/p&gt;

&lt;p&gt;Here is the breakdown of why this happened and how I fixed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Mystery: Death by a Thousand Prompts
&lt;/h3&gt;

&lt;p&gt;Users trying to sync their tasks were seeing back-to-back sign-in overlays. Even worse, generic email/password users were occasionally getting swept into the credential flow. That is a textbook way to tank your app's retention on Google Play.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root Cause Analysis
&lt;/h3&gt;

&lt;p&gt;Digging into the codebase revealed three distinct culprits conspiring against my users:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decoupled Auth in Google Sign-In v7:&lt;/strong&gt; Authentication (ID token) and Authorization (Access token/scopes) are now separate steps. My code was calling &lt;code&gt;attemptLightweightAuthentication()&lt;/code&gt; inside &lt;code&gt;getGoogleAccessToken()&lt;/code&gt; whenever the cache missed. On Android, this doesn't always fail silently—it can trigger a Credential Manager overlay chooser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Amnesiac In-Memory Session:&lt;/strong&gt; The authenticated &lt;code&gt;GoogleSignInAccount&lt;/code&gt; object was never stored in memory. Every single token request started completely from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redundant Scopes:&lt;/strong&gt; My mobile build was requesting the Google Calendar scope (&lt;code&gt;https://www.googleapis.com/auth/calendar&lt;/code&gt;). But on mobile, ROCIs Tasks uses &lt;code&gt;device_calendar&lt;/code&gt; for native OS calendar integration. Asking for the web calendar scope via Google Sign-In triggered redundant, scary permission consent prompts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Solution &amp;amp; Architecture
&lt;/h3&gt;

&lt;p&gt;To clean this up, I refactored the &lt;code&gt;AuthService&lt;/code&gt; with a strict caching and platform-aware scope strategy.&lt;/p&gt;

&lt;p&gt;First, I introduced an in-memory session cache and hooked into the authentication event stream on startup:&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;class&lt;/span&gt; &lt;span class="nc"&gt;AuthService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;GoogleSignInAccount&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_googleUser&lt;/span&gt;&lt;span class="p"&gt;;&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;initialize&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;_googleSignIn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authenticationEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&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;event&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;GoogleSignInAccountEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_googleUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&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;await&lt;/span&gt; &lt;span class="n"&gt;_restoreGoogleSignInSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Smart startup restoration - only for users who actually linked Google Tasks&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;_restoreGoogleSignInSession&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="n"&gt;_userHasLinkedGoogleTasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_googleUser&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;_googleSignIn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attemptLightweightAuthentication&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;Next, I split the requested scopes by platform. Web needs the Calendar API; mobile handles it natively via the OS device calendar:&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="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getScopesForPlatform&lt;/span&gt;&lt;span class="p"&gt;()&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;kIsWeb&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="p"&gt;[&lt;/span&gt;
      &lt;span class="s"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'https://www.googleapis.com/auth/tasks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'https://www.googleapis.com/auth/calendar'&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Mobile uses device_calendar for native OS integration&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'https://www.googleapis.com/auth/tasks'&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;Finally, I unified SharedPreferences token caching across platforms so that access tokens survive app restarts without forcing unnecessary network calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Lessons for Other Devs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never assume silent auth is silent:&lt;/strong&gt; Modern Android credential managers can surface UI when you least expect it. Cache your user state locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit your OAuth scopes:&lt;/strong&gt; Don't request web-only API scopes on mobile if your app handles things natively via platform channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect your users' intent:&lt;/strong&gt; Generic email/password accounts should never touch Google OAuth logic on startup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;Building ROCIs Tasks as a solo indie dev means obsessing over details like smooth auth flows so the app feels as native and polished as possible. If you want to check out an offline-first task manager with native Kotlin widgets built in Flutter, take a look at &lt;a href="https://play.google.com/store/apps/details?id=com.rocisapps.tasks" rel="noopener noreferrer"&gt;ROCIs Tasks on Google Play&lt;/a&gt; or visit &lt;a href="https://tasks.rocisapps.com" rel="noopener noreferrer"&gt;tasks.rocisapps.com&lt;/a&gt;. Happy coding!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>android</category>
      <category>indiedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How I Fixed a Firestore Stream Race Condition That Reverted Task Toggles</title>
      <dc:creator>ROCI</dc:creator>
      <pubDate>Thu, 18 Jun 2026 08:56:24 +0000</pubDate>
      <link>https://dev.to/rocisapps/how-i-fixed-a-firestore-stream-race-condition-that-reverted-task-toggles-1k64</link>
      <guid>https://dev.to/rocisapps/how-i-fixed-a-firestore-stream-race-condition-that-reverted-task-toggles-1k64</guid>
      <description>&lt;p&gt;I'm building an Android task manager (&lt;a href="https://play.google.com/store/apps/details?id=com.rocisapps.tasks" rel="noopener noreferrer"&gt;ROCIs Tasks&lt;/a&gt;) with Flutter. The app uses an offline-first architecture: tasks live in Hive locally and sync to Firestore when online.&lt;br&gt;
Users reported a maddening bug — they'd tap to complete a task, the checkbox would animate, and then... it would snap back to incomplete. Here's what was happening and how I fixed it.&lt;br&gt;
The Race Condition&lt;br&gt;
The app listens to a Firestore stream for real-time updates:&lt;br&gt;
_tasksSubscription = _firestoreService.getActiveTasksStream().listen(&lt;br&gt;
  (events) async {&lt;br&gt;
    for (final event in events) {&lt;br&gt;
      final cloudTask = event.task;&lt;br&gt;
      // process cloud task...&lt;br&gt;
    }&lt;br&gt;
  },&lt;br&gt;
);&lt;br&gt;
When a user toggles a task, the app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updates the local Hive store immediately&lt;/li&gt;
&lt;li&gt;Fires notifyListeners() so the UI updates&lt;/li&gt;
&lt;li&gt;Sends the write to Firestore asynchronously
The problem: the Firestore query filters isCompleted == false. The moment the write reaches Firestore, the task disappears from the stream — which emits a removed event. Meanwhile, the Firestore write hasn't fully propagated, so the snapshot data is stale. The listener processes the stale snapshot, overwrites the local state, and the UI reverts.
Here's the timeline:
User taps "complete"
→ Local Hive: isCompleted = true ✓
→ UI updates ✓
→ Firestore write sent (async)
→ Firestore stream emits: task removed from "active" query
→ Listener processes stale snapshot: isCompleted = false
→ Overwrites local Hive with false ✗
→ UI reverts ✗
The entire round-trip happens in milliseconds. The user sees a flash of completion followed by an instant revert.
The Fix: _pendingLocalWrites Guard
I added a map that tracks recently toggled tasks and their intended state:
final Map _pendingLocalWrites = {};
When a task is toggled, I record the intended state before writing to Firestore:
Future toggleTaskCompletion(Task task) async {
task.isCompleted = !task.isCompleted;
task.completedAt = task.isCompleted ? DateTime.now() : null;
// Record the intended state so the Firestore stream doesn't revert it
_pendingLocalWrites[task.id] = task.isCompleted;
notifyListeners();
await _source.addTask(task);
_firestoreService.updateTask(task).catchError((e, s) {
_errorHandlingService.logError(e, s,
    reason: 'Background cloud updateTask failed');
}).whenComplete(() {
// Allow stream to handle this task again after Firestore write settles
Future.delayed(const Duration(seconds: 3), () {
  _pendingLocalWrites.remove(task.id);
});
});
}
In the stream listener, I check the guard before processing each event:
final pendingState = _pendingLocalWrites[cloudTask.id];
if (pendingState != null) {
// We recently toggled this task — don't let stale data revert it
if (event.type == SyncEventType.removed &amp;amp;&amp;amp; pendingState) {
// We just completed this task — the removed event is expected.
// Update Hive to reflect completion from the latest snapshot.
final (latestTask, isMissing) =
    await _firestoreService.fetchTaskById(cloudTask.id);
if (latestTask != null &amp;amp;&amp;amp; latestTask.isCompleted) {
  await _source.addTask(latestTask);
  await _cancelTaskNotificationsById(latestTask.id);
  needsUpdate = true;
}
continue;
}
if (event.type != SyncEventType.removed &amp;amp;&amp;amp; !pendingState) {
// We just uncompleted this task — ignore the stale cloud event.
continue;
}
}
After 3 seconds (enough for Firestore to propagate), the guard is removed and normal stream processing resumes.
Why 3 Seconds?
Firestore writes typically propagate in 100-500ms. I used 3 seconds as a conservative buffer that covers:&lt;/li&gt;
&lt;li&gt;Slow network connections&lt;/li&gt;
&lt;li&gt;Firestore replication lag under load&lt;/li&gt;
&lt;li&gt;The stream batching multiple events
It's a tradeoff — during those 3 seconds, genuine cloud updates to that task from other devices are also suppressed. For a single-user task manager, that's acceptable. For a collaborative app, you'd want a more sophisticated conflict resolution strategy.
Key Takeaways&lt;/li&gt;
&lt;li&gt;Firestore streams are great, but they're not instant. The gap between a local write and the stream reflecting it is a race condition window.&lt;/li&gt;
&lt;li&gt;Offline-first needs local-first UI updates. Updating Hive before sending to Firestore ensures the UI never feels laggy. The stream is for background sync, not for driving the UI.&lt;/li&gt;
&lt;li&gt;Simple guard maps work. You don't need a full state machine or optimistic concurrency control for most cases. A Map with a TTL is enough.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Test with bad network conditions. This bug was invisible on fast WiFi. It appeared on cellular with 200ms+ latency.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  If you're building offline-first Flutter apps with Firestore, this pattern is worth knowing. The full app is in open beta on the Play Store — happy to answer questions about the architecture.
&lt;/h2&gt;

&lt;p&gt;If you have an idea on how to optimize the process, I would love to know!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
