Stokky The app
FR EN MG
Under the hood

Offline first, synced with nothing lost.

Stokky is a Flutter app, written in Dart, with Bloc for state management. This page covers the technical choices holding it together: local storage as the source of truth, a sync layer that resolves conflicts instead of overwriting them, and fixes shipped without going through an app store.

Flutter Dart Bloc / Cubit Local SQL database Realtime TDD

Offline first

Local storage isn't a cache — it is the source of truth. The app opens with no account and no network, and every read and every write goes through an SQL database embedded on the device. The UI never waits on a round trip; the network, when there is one, only catches up with what was already decided locally.

  • Starts instantly, no sign-up
  • Writes commit locally, then travel
  • Local-only mode stays usable indefinitely

Synchronisation

Every change enters a durable queue that survives the app being closed. Pulling is incremental: a cursor remembers where the last pass stopped, so only rows that actually changed travel. Failures retry on a growing delay, and the state stays visible — a pending-change counter, a banner when sync is blocked. Photos take their own path, separate from text, because they share neither its size nor its urgency.

  • Queue persisted, replayed on restart
  • Cursor-based incremental pull, never a full reload
  • Backed-off retries, no silent loss
  • Photos sync on a path of their own

Conflict handling

A write never blindly replaces a row. It carries the version it believes it is amending; if the row moved elsewhere in the meantime, the server refuses, the app captures both states and files a conflict. A resolution screen shows the difference field by field — mine, remote — separating what changed from what didn't. Where no merge would mean anything, such as the zones drawn on a photo of a piece of furniture, the last write simply wins: asking would have cost the user more than it gave them.

  • Conditional write: a per-row version check
  • The conflict is set aside; the app keeps working
  • Field-level resolution, an explicit choice
  • Last write wins where arbitration would be absurd
The rest of the machinery

What makes the first three sustainable.

Realtime

When several people share a household, changes arrive without a refresh. A realtime subscription wakes the sync layer as soon as a row moves on the server, instead of polling in a loop.

Over-the-air updates

A fix reaches already-installed devices without going through app store review — hours instead of days. Every patch is validated on a pre-production channel before it ships.

Architecture

Feature-first MVVM, with abstract repositories as the only seam. Dependencies point one way: view → cubit → repository. That seam is what lets the web build run without a local database, and every cubit test run without one at all.

Predictable state

State lives in Cubits, as immutable state classes rather than loading booleans scattered about. Anything that wouldn't survive a screen rebuild stays a plain setState: the boundary is written down somewhere, not guessed at each screen.

Tests on four layers

Cubit, repository, widget, integration. The cycle is red-green-refactor: the test fails first, or it proves nothing. The phone screenshots on the landing page are produced by those same tests — they cannot lie about the app.

Additive schema

A migration adds a column; it never renames or drops one. An older build therefore keeps working against a newer server — a hard requirement once you accept that not everyone updates on the same day.

Design system

Colours, spacing, radii and type are tokens, never values hand-written into a widget. A catalogue of shared components shows them in isolation, in light and dark. This page uses the same tokens as the app.

One codebase, six targets

Android, iOS, the web and the three desktops all come from the same code. The home-screen widget is app UI rasterised to an image, so changing its layout stays Dart, not native code written twice.

Three languages, none hardcoded

French, English and Malagasy, without a single string hardcoded into a widget. The framework ships no Malagasy for its own built-in labels, so those fall back to French rather than English — the useful fallback here.

Shipping

Three environments install side by side on the same phone, each with its own local database, so a build can be tested without touching real data. Nothing reaches production without going through staging first.

0 Platforms
0 Languages
0 Test layers
0 Trackers

None of this is visible. That is the point.

You open the app, add an item, put the phone away. The rest is our problem.

Back to the app