DEV Community

ROCI
ROCI

Posted on

Why Google Tasks Sync Was Silently Failing Every Hour (And How I Fixed It)

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.

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.

The Mystery: The 60-Minute Disappearing Act

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.

When looking at the logs, three distinct culprits emerged:

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

The result? A broken user experience where data simply stopped syncing without any visual feedback.

The Solution & Architecture

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

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.

To bridge the gap between the auth layer and the UI, I exposed a reactive state flag in our AuthService:

class AuthService extends ChangeNotifier {
  bool _isGoogleTasksTokenExpired = false;

  bool get isGoogleTasksTokenExpired => _isGoogleTasksTokenExpired;

  void setTokenExpired(bool expired) {
    if (_isGoogleTasksTokenExpired != expired) {
      _isGoogleTasksTokenExpired = expired;
      notifyListeners();
    }
  }

  Future<void> refreshAccessToken() async {
    try {
      // Platform-agnostic silent token request logic
      await _silentAuthClient.requestAccess();
      setTokenExpired(false);
    } catch (e) {
      setTokenExpired(true);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Key Lessons for Other Devs

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

Try ROCIs Tasks

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 ROCIs Tasks on Google Play or try the web version at tasks.rocisapps.com. Feedback is always welcome!

Top comments (0)