ods-pages

ODS Flutter Local — Architecture

Contributor-level internals of the Flutter renderer. For the product overview see product.md; for the spec format see spec.md; for the workspace-level picture see ../../ARCHITECTURE.md.

Code layout

lib/
├── main.dart                     Entry point, MaterialApp, root routing
├── models/                       Plain Dart model classes — no Flutter deps
├── parser/                       spec.json → models, with validation
├── engine/                       Runtime state + business logic
├── renderer/                     Model → Flutter widgets (no state owned here)
├── loader/                       File picker + URL loading + clipboard paste
├── screens/                      Top-level screens (Welcome, Settings, Admin
│                                  setup, Login, Tour, Help, etc.)
├── widgets/                      Shared widget primitives (theme picker,
│                                  color picker, framework user list)
└── debug/                        Debug panel overlay

The layering rule: modelsparserenginerenderer. Renderer reads engine; engine doesn’t know the renderer exists. That discipline is what made a headless conformance driver possible in the first place.

Key classes

AppEngine (lib/engine/app_engine.dart)

The centerpiece. A ChangeNotifier that owns:

Exposes methods the renderer calls to mutate state: loadSpec, navigateTo, setFormField, clearFormStates, dispatchAction.

Owns but delegates to:

DataStore (lib/engine/data_store.dart)

The SQLite abstraction. One database file per app (named ods_<app_slug>.db) under the user-chosen storage folder. Schema:

Methods: insert, update, delete, query, queryWithOwnership, ensureTable, createUser, assignRole, getUserRoles, etc.

AuthService and FrameworkAuthService (lib/engine/auth_service.dart, lib/engine/framework_auth_service.dart)

Two services, one responsibility split:

Password hashing is SHA-256 + salt (lib/engine/password_hasher.dart) — pure Dart, no platform bindings.

SettingsStore (lib/engine/settings_store.dart)

Framework-level preferences: theme mode, default theme, backup settings, multi-user flag, default-app id, and the user-chosen storage folder. Persists to ods_settings.json inside that folder.

The storage-folder bootstrap is the interesting bit. SettingsStore resolves the data directory via:

  1. Explicit customPath parameter (if caller passes one)
  2. ods_bootstrap.json in getApplicationSupportDirectory() (the AppData side on Windows, not OneDrive-redirected)
  3. Default <Documents>/One Does Simply

First run: no bootstrap exists → default is used → a dialog asks the user to confirm or pick a custom folder → chosen path is written to the bootstrap. Subsequent runs: bootstrap is authoritative. “Move Data” in Framework Settings uses moveStorageFolder / resetStorageFolder to copy files + retarget. See docs/TROUBLESHOOTING.md for why this indirection exists.

ActionHandler (lib/engine/action_handler.dart)

Interprets OdsAction lists. Actions are declarative ({"action": "submit", "target": "addForm", "dataSource": "tasks"}); ActionHandler runs them in order, short-circuiting on the first failure (except showMessage which is always allowed to fire after a successful chain).

Reads form state + current user context from AppEngine, writes to DataStore.

Renderer layer (lib/renderer/)

Rule: components read engine state via context.watch<AppEngine>(); they never call DataStore directly. Mutations go through engine methods or dispatchAction.

State flow — a form submit

User types "Buy milk" in the Title field
  ↓
TextField.onChanged
  ↓
AppEngine.setFormField('addForm', 'title', 'Buy milk')
  ↓
AppEngine.notifyListeners()
  ↓ (React)
FormComponent rebuilds with new value

User clicks Save button
  ↓
OdsButtonWidget.onPressed
  ↓
AppEngine.dispatchAction(onClickActions, formId: 'addForm')
  ↓
ActionHandler.execute(actions, ctx)
  ├── submit → DataStore.insert('tasks', formValues)
  ├── showMessage → AppEngine._lastMessage = 'Saved!'
  └── notifyListeners()
  ↓
ListComponent sees new row; SnackbarHelper fires toast.

Every step is idempotent and inspectable. The engine is the single mutation point; state shape is the contract.

Testing

Tests live under test/ with the convention:

Current totals and skip status are in ../../REGRESSION_LOG.md. CI for this framework is tracked on TODO.md — there’s no workflow yet; React side has one.

Platforms

Off-ramp

One of the framework’s design goals is “the builder can export their app if they outgrow ODS.” Two mechanisms exist:

Generated code currently uses getApplicationDocumentsDirectory() directly (doesn’t honor the bootstrap mechanism). That’s deliberate — generated apps run on an end-user’s machine with no ODS context — but flagged on TODO.md for eventual consistency.