Skip to content

A-bv/tiny-flutter-notes

Repository files navigation

Field Notes — a tiny offline-first notes app, built test-first

Field Notes

A deliberately small Flutter app that does everything a real app does — offline storage, background sync, networking, theming, accessibility, the full test pyramid — and nothing it doesn't. Every line was written test-first, one behavior per commit, so the git history reads as a step-by-step build you can follow.

CI Flutter 3.44 Platform iOS | Android Coverage at least 85 percent style: very good analysis License: MIT

What it does

Write a note; it saves on the device instantly and uploads in the background. Go offline, keep writing — changes queue and flush the moment you reconnect. Delete a note and it disappears at once, reconciling with the server later. Reinstall and your notes come back.

Notes list, each note synced from the server     The new-note screen

Run it

flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter run

Out of the box the app uses an in-memory fake server, so it runs anywhere with zero setup. Point it at a real backend with a build-time define:

flutter run --dart-define=API_URL=https://your-server

Nothing above the NoteApi seam changes — the fake and the real dio client are interchangeable.

Architecture

Two layers plus a shared domain, dependencies pointing inward (the official Flutter guidance — MVVM):

  ui/      views + Riverpod view models   (what to show, how state changes)
   │
   ▼
  data/    repository + services          (the rules, the sources of truth)
   │        ├─ DatabaseService (Drift)    ← the source of truth
   │        ├─ NoteApi (Fake | Http/dio)  ← the sync seam
   │        └─ ConnectivityService
   ▼
  domain/  Note, SyncStatus               (pure Dart, no Flutter)

And the same layers as a runtime flow — how a note travels, offline-first:

   tap "Save"
       │
       ▼
   view model ──▶ repository ──┬──▶ Drift DB        saved at once as "pending";
                               │       │            the UI re-renders from the
                               │       └─▶ stream    database it watches
                               │
                               └──▶ NoteApi ──▶ server   in the background:
                                    (dio)                "pending" ➜ "synced"
                                    skipped offline, retried on reconnect

Three rules hold it together: the local database is the source of truth (the UI only ever watches it, which is what makes the app work offline); dependencies point one way (ui → data → domain); and widgets hold no logic (a view decides what to show, a view model how state changes, the repository the rules).

How it was built with TDD

TDD means building in tiny steps, each the same three-part cycle. The git history makes every step visible:

  • 🔴 test: — write one small failing test for a behavior you don't have yet. Run it. It fails, because the code isn't there.
  • 🟢 feat: — write the least code that makes it pass.
  • 🔵 refactor: — clean up while the tests stay green.

Nothing is written unless a test asked for it. Infrastructure commits (scaffold, CI, docs) are tagged (non-TDD) so the test-first spine is unambiguous.

Here is one full cycle — "a sync must do nothing while offline":

🔴 the failing test:

test('syncPending does nothing while offline', () async {
  connectivity.isOnline = false;
  final repo = makeRepository();
  await repo.createNote('Buy milk');

  await repo.syncPending();

  expect(api.uploaded, isEmpty);
  expect((await onlyNote()).syncStatus, SyncStatus.pending);
});

🟢 the code that makes it pass:

Future<void> _syncOnce() async {
  if (!_connectivity.isOnline) return; // ← added
  ...
}

Each collaborator (the note API, connectivity) enters the codebase only in the cycle whose test first needs it — so the design is pulled into existence by the tests, not sketched up front.

📖 Read the full build log → — every commit classified by layer, with the framework each one introduced. It's the clearest way to see how this was built.

Testing

The full pyramid, all runnable with flutter test:

Level Tool What it covers
Unit flutter_test + real in-memory Drift domain, database, repository, DTO, view models
Widget WidgetTester every screen state, delete, banners, a11y live regions
Golden Alchemist the list item's look, per sync status
E2E integration_test create-a-note through the real app

CI runs format → analyze → test, and gates coverage at 85% on hand-written source (generated *.g.dart excluded).

Trade-offs

  • Riverpod over Bloc — less boilerplate at this scale, and it does DI and state in one tool. Bloc earns its place on a large team that wants explicit event rules.
  • Drift for local storage — Isar is abandoned and Hive stalled; Drift is the maintenance-safe reactive database.
  • A fake backend as the default — the app is the artifact, not a server. The real dio client is built and tested; --dart-define switches to it.
  • Last-write-wins conflicts, additive pull — cross-device deletion needs server-side tombstones, out of scope here and documented in decisions.md.

More

About

A tiny offline-first Flutter notes app built test-first (TDD). MVVM + Riverpod + Drift + dio, with a full unit/widget/golden/E2E test pyramid and a gated CI.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages