Skip to main content

Command Palette

Search for a command to run...

The Day My Calendar Lied by One

Updated
5 min read
D
Flutter developer documenting real-world mobile engineering problems, architecture decisions, and the occasional debugging battle. I enjoy building production-ready apps, experimenting with clean architecture, and learning something new from every bug I fix.

Today was one of those Flutter days where everything looked fine until it didn’t, and the bug felt personal. The app was letting users create events, and the calendar was calmly insisting those events happened a day earlier than expected. No crash. No red screen. Just a calendar quietly gaslighting me.

It got weirder because everything else in the UI felt right. The create event screen showed the correct date. The event list showed the correct date. But the calendar view? Always one day behind for several entries. It felt like a phantom timezone I hadn’t invited.

Where the truth started to surface

The API response was the first real clue. The server was returning timestamps like 2026-05-29T22:00:00.000Z for an event I had picked as May 30. That’s not “wrong” per se, but it is the classic symptom of a UTC/local mismatch: midnight local gets stored as late night UTC, and the calendar is reading the day from the wrong side of midnight.

So the issue wasn’t a missing UI update. It was how time values were being created, stored, and rehydrated.

On the client side, I was serializing a date-only value using a local DateTime, then sending it as ISO. The backend developer had been explicit about expectations: “Send dates in UTC. When we return, they’ll be UTC.” That sounded straightforward… until I realized I was not actually forcing UTC on send.

The two-part fix

  1. Send UTC, always.

Anywhere I was sending a date/time payload, I updated it to explicitly use UTC. The core pattern became:

final iso = myDate.toUtc().toIso8601String();

This applied to event dates, task dates, schedules, and any other time fields going into API requests.

  1. Parse UTC as local before grouping by day.

The calendar was doing day grouping like this (simplified):

final parsed = DateTime.parse(eventDateString);
final dayKey = DateTime(parsed.year, parsed.month, parsed.day);

That’s fine until the parsed date is UTC. Then you’re chopping the UTC day, not the user’s day. I fixed it with a tiny conversion step:

DateTime? parseUtc(String value) {
  final parsed = DateTime.tryParse(value);
  if (parsed == null) return null;
  return parsed.isUtc ? parsed.toLocal() : parsed;
}

That single line—parsed.isUtc ? parsed.toLocal() : parsed—was the difference between “calendar lies” and “calendar behaves.”

But then the app got slower

After the UTC fix, the home screen started feeling sluggish. I wasn’t surprised. I had just introduced timezone lookups into requests that run on app start, and those can easily block the UI if done repeatedly.

The API layer was calling FlutterTimezone.getLocalTimezone() for both dashboard and events. That’s two native calls back-to-back on every load. I fixed this by caching the timezone result the first time and sharing the same in-flight lookup if two requests happen at once.

The end result was a tiny memoization + timeout guard:

static String? _cachedTimezone;
static Future<String?>? _timezoneLookup;

Future<String?> _getTimezone() async {
  if (_cachedTimezone != null) return _cachedTimezone;
  final lookup = _timezoneLookup ??= _loadTimezone();
  final tz = await lookup;
  if (identical(_timezoneLookup, lookup)) _timezoneLookup = null;
  return tz;
}

The lookup itself gets a short timeout; if it fails, we simply skip adding the timezone parameter for that request. The app feels fast again, and the home screen doesn’t wait on a slow plugin call.

The cache that wasn’t really a cache

Then came another subtle issue: calendar caching. The calendar page had a 5‑minute in-memory cache, but only if you open it with a cacheUserId. One entry point was passing it; another wasn’t. So half the time the calendar always reloaded, which made it feel like caching didn’t exist.

I fixed that by threading the user ID into the “My Events” path too. But the bigger realization was this: the cache didn’t know about mutations. If I created a new event or task, the calendar could still happily show the stale cached list for up to five minutes.

So I added a small cache invalidation hook that fires on event creation, task creation, task deletion, event edits—anything that changes what the calendar should show. It wasn’t glamorous, but it made the app feel honest again.

One surprise I didn’t expect

I hit a nasty debug warning about _StretchController scheduling builds during layout. It popped up after a few navigation hops and was tied to overscroll stretch behavior in a SingleChildScrollView. It wasn’t a functional bug, but it was noise and made the app feel fragile. The fix is either disabling stretch overscroll globally or clamping scroll physics on the specific views. That’s on my list for tomorrow because today was already crowded.

What I’m taking from this

  • Date-only values are never really date-only once you serialize them.

  • UTC isn’t a suggestion. It’s a contract.

  • Caching without invalidation is just a delayed bug.

  • Plugin calls on startup are easy to overlook until your app feels sluggish.

This was one of those tasks that started with “why is the day off by one?” and ended with a handful of changes across serialization, parsing, caching, and request plumbing. Not a massive refactor—just a string of small, sharp edges that finally lined up.

It’s not exactly fun when a calendar lies to you. But it is satisfying when you trace it back to a single UTC conversion and fix the rest of the chain with intention. That’s the kind of day Flutter gives you. Not dramatic, just weirdly precise.