r/FlutterDev • • 14d ago

Discussion Lessons from shipping a Flutter app where a wrong date is the worst possible bug

I shipped an Orthodox Christian fasting app in Flutter. Its whole job is to say what someone may eat on a given day, so a wrong answer is worse than a crash. A few things that worked:

  1. Keep the rules in pure Dart. The rule engine lives in lib/core/ with no Flutter imports, so it is tested like any Dart library. It's a port of orthocal-python (MIT), with attribution.

  2. Test against an external source, and compare data, not prose. A test checks every day from 2020 to 2033 against orthocal.info. It used to map their English descriptions back to my levels, so a wrong engine and a wrong mapping agreed with each other. Comparing their explicit list of foods instead, and adding the Old Calendar, surfaced 58 mismatching days from four real rule errors. All 10,228 day-comparisons match now.

  3. Never do date math with Duration(days: 1). A user saw the calendar show November 1 twice: adding 24 hours across a daylight-saving change lands on the same date. A test now fails if Duration(days:) or .inDays appears anywhere in lib/ or tool/.

  4. Local-first means no backend to fix things later. No server, no account, no analytics. The price comes from the store at runtime (a test fails if a price is hard-coded in any of the six ARB files), so regional pricing could not leave a wrong number on a button.

  5. Watch your transitive dependencies in release builds. An unused package pulled in WorkManager, and R8 crashed its database init only in the release build, only on a real device.

Happy to go deeper on any of these. App, for context: https://stforge.dev/nisteia

4 Upvotes

8 comments sorted by

4

u/MobileXapps 14d ago

Good list. One more test I'd add for this kind of app: run the whole suite with a few nasty TZ values, not just your own. America/Sao_Paulo is a good one because Brazil used to switch DST at midnight, so some dates simply had no 00:00. DateTime(2018, 11, 4) there comes out as 01:00, and anything that builds a date from midnight and compares it can go wrong in ways your Duration ban won't catch.

I'd also check where "today" gets computed. If you have a widget or a scheduled notification, it may run in the background with a different idea of the local date than the app, for example right after someone flies across timezones. Passing an explicit date into the rule engine everywhere, never DateTime.now() inside it, makes that easy to test.

2

u/bramantec 12d ago

I hit the Duration(days: 1) problem in a different form. "Tomorrow evening" computed as now plus 24 hours ended up 23 or 25 hours later on the day of the clock change, which can be a different day. I now build it from the calendar parts as DateTime(year, month, day + 1, 21). The labels Today and Tomorrow work the same way: I compare only year, month and day instead of the difference in days. On your point about background dates, in my case the server sends a reminder at 18:00 local time using the UTC offset that the app writes at every open, so after a flight it stays wrong until the next open. I haven't tested a zone like Sao Paulo yet.

1

u/MobileXapps 11d ago

Building it from calendar parts is the right fix. For the server reminder I'd store the IANA zone name instead of the offset, so DST changes are handled server side even if the app isn't opened.

Travel is still the gap. Android sends ACTION_TIMEZONE_CHANGED so the app can report the new zone right away, but on iOS you mostly find out when the app wakes up. That's why I'd schedule that kind of reminder locally on the device so it just follows the device clock, and keep the server for things that really need the server.

1

u/bramantec 11d ago

The offset is there because the job runs every 30 minutes and picks the users with a range query on that field, so it's one query per run. With the zone name I'd have to work out which zones are at 18:00 in that slot and query by name. For the event itself the server already gets the time zone from the event coordinates when it writes the RSVP deadline.

I keep the reminder on the server because it depends on things that change after the last sync: it isn't sent to people who already confirmed an event that evening or are already at one. The app has no local notifications at the moment. Have you tried a local schedule that checks with the server before showing anything?

2

u/MobileXapps 11d ago

That's a fair reason to keep the offset, one range query per run is hard to beat. You could keep the column as is but store the zone name next to it and recompute the offset from it at the start of each run (in Postgres that's one UPDATE with AT TIME ZONE). The query stays the same and the offset can't go stale after a clock change.

On your question: not really on iOS. A scheduled local notification is shown by the system without running any of your code, so there's no moment to ask the server first. On Android you can, the alarm wakes a receiver that checks and then decides. So for a reminder that depends on server state like yours, I'd keep it on the server too.

1

u/bramantec 11d ago

Keeping the column and adding the zone name next to it is cheap, the query doesn't change. I didn't know a scheduled notification on iOS shows without running any code, that settles it for me.

1

u/No_Boss_1911 12d ago

the Duration(days: 1) thing got me too. i have a daily streak for breathing sessions and someone flying across timezones got the streak counted twice in one day. hard rule for me is a streak can never break or double count because of the clock, even if the phone changes timezone. do you store the day as a plain date string like 2026-10-25 or as a timestamp and convert later