Swasti · mForm V2→V3

frappe-mobile-sdkom/offline_improvement deep-dive

UPDATE 2026-06-02 — Sync bootstrap is on the SDK’s pull pipeline. The Swasti app’s post-login bootstrap used to bypass SyncService.pullSync (the SDK’s connectivity probe rejected adb reverse tunnels). After diagnosing the gate properly, the bypass was lifted and the bootstrap now drives sdk.sync.pullSync(doctype:) per doctype. The visible win on subsequent logins: surveyors only refetch rows whose modified exceeds the previous sync (delta cursors), with crash-resilient resume mid-pull and 2× page lookahead for free. The non-visible win: zero // ignore: implementation_imports directives in the app, ~250 LOC of bypass code retired. Two pull requests are open against the SDK develop branch on dhwani-ris/frappe-mobile-sdk: PR #68 adds an isOnlineOverride hook to SyncService (default null = current behaviour, dev builds set () async => true to keep the bench reachable through adb reverse); PR #69 exports the canonical DependsOnEvaluator on the SDK barrel so consumers that own their own form layer can apply mandatory_depends_on semantics without crossing the src/ line. The app builds against both branches today via a local merge branch in the SDK clone.

UPDATE 2026-05-27 — MERGED. om/offline_improvement is now merged into develop (tip 5dc2a94, 2026-05-22). Swasti now tracks develop. develop is a ~31-commit advance over the old feature head — the sync layer was refactored (new schema/parent_schema.dart, child_schema.dart, system_tables.dart) with ALTER TABLE ADD COLUMN migration wrappers, which is the fix for the docs__ table rebuild / data-loss issue (old behaviour dropped + recreated docs__<doctype> on a metaJson schema diff, wiping unsynced local rows and churning mobile_uuid). Pointing the app at develop needs a compatibility pass (public API moved). The deep-dive below is still accurate on concepts; file paths may have moved on develop.

Companion to 02-reliance-frappe-context.md (rel-mis Frappe shape) and 04-mform-to-frappe-skill.md (Doctype generation). The SDK is the mobile-side runtime the Swasti Flutter app is built on.

Source: ~/Dhwani/swasti-mform-migration/raw/sdk/frappe-mobile-sdk/ — track branch develop (was om/offline_improvement head 5bd0a93). Pub package name frappe_mobile_sdk. Server companion (REQUIRED): frappe-mobile-control, branch develop (was om/mobile-sync-bulk-fetch). See §9 and note 08.


1. What the SDK is

A Flutter package that turns a Frappe site into a metadata-driven mobile backend. It provides:

  • Direct Frappe API access — auth (4 flows), CRUD, file upload, custom whitelisted method calls.
  • Dynamic form rendering — every form is generated from Frappe DocType metadata (doctype_meta + doc_field); no hand-written form widgets.
  • Offline-first data layer — SQLite (via sqflite) with bi-directional sync. Toggleable server-side per deployment (see §8).
  • Ready-made screens — login, doctype list, document list, form, sync status, sync errors, migration blocked, force-update guard.
  • Server-driven app controlMobile App Control doctype on the server gates app status (active / under-maintenance / force-update).

Versioning: pub.dev frappe_mobile_sdk: ^<latest> for releases; git: ref: for branch pinning. rel-mis pins to ~/.pub-cache/git/frappe-mobile-sdk-7d27451ced0fb88f9007e5df70f4948b3afe0a27/ (per rel-mis/docs/archive/plans/2026-03-05-table-multiselect-impl.md:473); Swasti will pin to the merge commit of om/offline_improvementdevelop.


2. Architecture

┌──────────────────────── App layer (Swasti Flutter app) ────────────────────────┐
│  AppGuard → Login → MobileHomeScreen ─ DoctypeList ─ DocumentList ─ FormScreen │
└───────────────────────────────┬────────────────────────────────────────────────┘
                                │ public API (FrappeSDK + FrappeFormBuilder + screens)
┌───────────────────────────────▼────────────────────────────────────────────────┐
│                              FrappeSDK (lib/src/sdk/frappe_sdk.dart)           │
│  Holds: client, database, services, resolver, sessionUser, syncController      │
├─────────────────────────────────────────────────────────────────────────────────┤
│  Services (lib/src/services/)                                                  │
│    AuthService  MetaService  PermissionService  TranslationService             │
│    SyncService  OfflineRepository  OfflineTransitionService                    │
│    LinkOptionService  LinkFieldCoordinator  ClosureBuilder  AppStatusService   │
│    LocalWriter  AtomicWipe  MetaDiffer  MetaMigration  RetryPriority           │
│    SessionUserService  WorkflowService  BulkWatermarkProbe                     │
├─────────────────────────────────────────────────────────────────────────────────┤
│  Sync engine (lib/src/sync/) — push & pull, cursor, conflict, attachments      │
│    PullEngine ↔ PullPageFetcher ↔ PullApply                                    │
│    PushEngine ↔ PayloadAssembler ↔ ResponseWriteback ↔ AttachmentPipeline      │
│    SyncStateNotifier ↔ SyncState ↔ Cursor ↔ ThreeWayMerge ↔ UUIDRewriter       │
│  Query (lib/src/query/) — UnifiedResolver, FilterParser, LinkDecorator         │
│  Concurrency (lib/src/concurrency/) — SyncMutex, WriteQueue, ConcurrencyPool   │
│                                       ConnectivityWatcher, DeviceTier          │
├─────────────────────────────────────────────────────────────────────────────────┤
│  Persistence (lib/src/database/)                                               │
│    AppDatabase (sqflite)                                                       │
│    System tables: auth_token, doctype_meta, doctype_permission, sdk_meta,      │
│                   link_options, outbox, pending_attachments                    │
│    Per-doctype tables: docs__<doctype> (parent) + docs__<child_doctype>        │
│    SchemaApplier — DDL per doctype, idempotent, max 7 indexes                  │
├─────────────────────────────────────────────────────────────────────────────────┤
│  API layer (lib/src/api/) — FrappeClient, AuthApi, DoctypeService,             │
│    DocumentService, AttachmentService, QueryBuilder, RestHelper, OAuth2Helper  │
└────────────────────────────────────┬────────────────────────────────────────────┘
                                     │ HTTP
                            ┌────────▼─────────┐
                            │ Frappe + mobile_ │
                            │ control + your   │
                            │ Frappe app       │
                            └──────────────────┘

Two operating modes (set per-deployment via server flag, immutable per session):

Online mode (offline_enabled=false, default in 2.1+)Offline mode (offline_enabled=true)
ReadsUnifiedResolver._onlinePassthroughfrappe.client.get_listLocalWriter / per-doctype tables, fallthrough to network refresh
WritesDirect RESTOutbox queue → push when connectable
Sync engineNo-op (SyncResult.empty())Active
docs__<doctype> tablesNot createdCreated per-doctype on demand
outbox / pending_attachmentsEmptyUsed

The mode is read once at boot from sdk_meta.offline_enabled and cannot change mid-session. Switching requires logout/login.


3. Public API surface

lib/frappe_mobile_sdk.dart is the barrel. Stable exports:

SymbolSourcePurpose
FrappeSDKsrc/sdk/frappe_sdk.dartOne-stop init; holds all services.
FrappeClient, FrappeDocument, QueryBuildersrc/api/*Direct Frappe HTTP access.
AuthServicesrc/services/auth_service.dartAll login flows.
MetaServicesrc/services/meta_service.dartDocType metadata fetch + cache.
PermissionServicesrc/services/permission_service.dartCheck & cache per-doctype permissions.
TranslationServicesrc/services/translation_service.dartPull mobile_auth.get_translations.
SyncService + SyncControllersrc/services/sync_service.dart, src/sync/...Imperative sync surface.
OfflineRepositorysrc/services/offline_repository.dartRead/write through the offline cache.
OfflineTransitionServicesrc/services/offline_transition_service.dartDrain/wipe when toggling modes (P3).
LinkOptionService, LinkFieldCoordinatorsrc/services/...Resolve Link field options + cache.
SessionUser, SessionUserServicesrc/models/session_user.dart, src/services/session_user_service.dartPersisted user identity, roles, perms.
LinkFilterBuilder, FieldChangeHandlersrc/models/link_filter_result.dart, hooks in src/ui/Per-doctype customisation hooks (see §5–§7).
FrappeException, AuthException, ApiException, NetworkException, ValidationExceptionsrc/api/exceptions.dartTyped errors.
DependsOnEvaluatorsrc/utils/depends_on_evaluator.dartFrappe depends_on / mandatory_depends_on evaluator.
MigrationNeedsNetworkExceptionsrc/sdk/frappe_sdk.dart:23Thrown by runV1ToV2MigrationIfNeeded when offline.
Screens: MobileHomeScreen, DoctypeListScreen, DocumentListScreen, FormScreen, FrappeAppGuard, SyncStatusBar, SyncProgressScreen, SyncErrorsScreen, MigrationBlockedScreensrc/screens/..., src/ui/...Drop-in UI.

Init pattern (from README):

final sdk = FrappeSDK(baseUrl: 'https://your-frappe-site.com/');
await sdk.initialize(true);   // autoRestoreAndSync=true → restore session + initial sync

4. Auth flow

lib/src/services/auth_service.dart (~720 lines) — ALL login flows funnel through mobile_auth.* whitelisted endpoints on the server:

FlowEndpointTokens persistedSessionUser populated from
Username + passwordmobile_auth.loginaccess_token (24h) + refresh_token (30d)login response
Mobile OTPmobile_auth.send_login_otpverify_login_otpsamelogin response
API keyloginWithApiKeyapi_key + api_secret in secure storagemobile_auth.me
OAuth 2.0 + PKCEOAuth2Helper.exchangeCodeForToken (full flow)bearer + refresh + clientId/clientSecretmobile_auth.me
Social login (Google etc.)mobile_auth.get_social_login_providersget_social_authorize_url → OAuthsame as OAuthmobile_auth.me

Refresh: mobile_auth.refresh_token rotates the refresh token on every refresh (30-day rolling window). The offline_enabled flag is only updated on full login, not refresh.

Storage: flutter_secure_storage for tokens + API keys + OAuth credentials. auth_token table mirrors for SDK use. No biometric layer in the SDK itself — wire local_auth in the consumer app if needed.

Multi-account: not first-class. Logout fully wipes session; AtomicWipe (lib/src/services/atomic_wipe.dart) is used during force-logout, schema reset, and offline→online drain.


5. Doctype binding & form rendering

No code generation. Every Doctype is pulled at runtime as DocTypeMeta (lib/src/models/doc_type_meta.dart) and rendered by FrappeFormBuilder (lib/src/ui/form_renderer_helper.dart + per-field widgets in lib/src/ui/widgets/fields/).

Field-type → widget mapping lives in lib/src/database/field_type_mapping.dart and lib/src/constants/field_types.dart. Documented in doc/FIELD_TYPES.md:

Frappe fieldtypeWidgetNotes
Data, Small Text, Text, Long TextTextField variantsper-fieldtype lengths, regex from server validators
Int, Float, Currency, PercentNumericFielddecimal precision honoured
Date, Datetime, TimeDateFieldmin/max from mandatory_depends_on etc.
CheckCheckFieldbool with proper 1/0 emission
SelectSelectFieldinline options
LinkLinkField (uses SearchableSelect)resolves via LinkOptionService; supports server-side options + LinkFilterBuilder overrides
Table MultiSelectTableMultiSelectFieldrequires Link child + Option Master (the rel-mis anti-pattern, see §15)
Attach, Attach Imageupload via AttachmentServiceoffline → pending_attachments
GeolocationGeolocationFieldGeoJSON, reads from device GPS
PhonePhoneFieldintl_phone_field
ButtonButtonFieldtriggers server method

Skip-logic / depends_on: DependsOnEvaluator (lib/src/utils/depends_on_evaluator.dart) interprets Frappe’s JS-flavoured expressions (eval:doc.field == "X", ===, !==, etc.). Recent commit 5bd0a93 added paren handling for depends_on. The SDK side handles all depends_on / mandatory_depends_on / read_only_depends_on — the consumer app does not implement skip logic.

fetch_from: declared on the Doctype field; the SDK auto-fetches the linked parent on Link selection. This is how the PM design sheet’s “Auto populate from Member Form” rows materialise — the consumer app does not wire prefill manually (cross-reference 02-reliance-frappe-context.md anti-pattern: “don’t add Dart handlers for geography”).

Computed fields: read-only, computed in server validate() + mirrored in client script. Validation _id "5" in the conversion skill (04-mform-to-frappe-skill.md §4.2) → these arrive as read_only=1 fields with eval: expressions.


6. Geography handling

The SDK auto-cascades geography Link chains based on fetch_from declarations on the parent Link’s target Doctype. There is no Dart-side handler to write — the rel-mis “Geography Column Break breaking GP autofill” and “double-clear” bugs documented in 02-reliance-frappe-context.md are exactly what happens when consumers try to override the cascade manually.

For Swasti (per feedback_swasti_rel_mis_reference.md and the PM design sheet’s “Lookup for the Member with this Name for the Location of the User”):

  • The Member doctype carries a single Link to Geography (state/district/block/GP/village hierarchy).
  • Child program forms have a Link to Member; fetch_from pulls geography fields automatically.
  • Surveyor’s location filtering on Member list = a LinkFilterBuilder registered on MobileHomeScreen for the Member doctype, scoping by session_user.user_defaults.location or similar.
  • Do not lift rel-mis’s User Program Assignment / UPA Geography infrastructure.

7. Offline / sync model — what om/offline_improvement adds

This branch is the second-generation offline-first layer. The first generation (the version rel-mis uses, ref 7d27451) had a simpler write-then-sync flow with known issues around concurrent push/pull, attachment relinking, and toggle/migration paths. The new branch ships in 6 phased commits (P1–P6, see git log):

CommitPhaseWhat it adds
11aba92P1Persist server-driven offline_enabled flag → sdk_meta
c9f751cP2Gate read/write paths on the flag (online thin client mode)
89036dcP3Drain/wipe transition handler + UI when toggling modes
162f238P4–P6Wire pull, push, write-path, UX scaffolding
04c6674SyncMutex, push/pull coordination (SIG-2), dispose + SIG-1/9/12 fixes
3a1c8ba / ceadfd8Replace orphan sentinel docname with v16 relink flow for child-row attachments
5bd0a93UnifiedResolver for list reads, count API, depends_on parens

Pull (offline mode)

PullEngine (lib/src/sync/pull_engine.dart) → PullPageFetcherPullApply:

  • Cursor-based pagination keyed on (modified, name). Pages fetched modified asc, name asc. Persisted with complete:false marker; flips to complete:true on the final short page.
  • Resume skips rows <= cursor to avoid double-applying tie-group rows.
  • Batch pull via pullSyncMany(doctypes:..., concurrency: 4) runs up to 45 doctypes through a 4-worker pool. Per-doctype failures don’t abort the batch.
  • Server endpoint: mobile_sync.get_docs_with_children (in frappe-mobile-control). Accepts {doctype, names: [...]} (max 200/call) — returns parent + embedded child rows in one trip. Falls back to per-name GET if endpoint missing (slow).

Push (offline mode)

PushEnginePayloadAssemblerResponseWritebackAttachmentPipeline:

  • Outbox (outbox table, lib/src/database/daos/outbox_dao.dart) stores pending writes (create/update/delete) with mobile_uuid as the local primary key.
  • UUID → server-name resolution on push: server returns the assigned name; SDK rewrites the local row + relinks dependents via UUIDRewriter (lib/src/sync/uuid_rewriter.dart).
  • Three-way merge (lib/src/sync/three_way_merge.dart) on conflict — base + local + remote → diff. ConflictAction.keepRemote / keepLocalAndRetry.
  • Delete cascade previewSyncController.previewDeleteCascade returns a plan listing blockedBy rows; acceptDeleteCascade resets to pending and re-pushes.
  • SyncMutex (lib/src/concurrency/sync_mutex.dart) prevents concurrent push/pull (SIG-2 fix) — push-then-pull semantics enforced per-cycle.

SyncController API

final ctrl = sdk.syncController;
await ctrl.syncNow();                  // full pull + push cycle
await ctrl.pause(); await ctrl.resume();
await ctrl.cancelInitialSync();
await ctrl.retryRow(outboxId);
await ctrl.resolveConflict(outboxId, ConflictAction.keepRemote);
final plan = await ctrl.previewDeleteCascade(outboxId);
await ctrl.acceptDeleteCascade(outboxId);
final SyncState current = ctrl.state;
final Stream<SyncState> stream = ctrl.state$;

SyncState carries isPaused, isInitialSync, perDoctype: Map<String, DoctypeProgress>. Drives the SyncStatusBar / SyncProgressScreen / SyncErrorsScreen UI.

Triggers (corrected, per om/offline_improvement design spec — doc/OFFLINE_FIRST.md)

  • Post-login _initialMetaAndDataSync.
  • Post-save when online: app calls pushSync(doctype:) after OfflineRepository.createDocument etc.
  • Manual user trigger from SyncStatusBar.
  • No 5-minute background timer. No OrderedSyncService — push order is application-controlled.

Local cache: SQLite via sqflite

  • One DB per app (AppDatabase, lib/src/database/app_database.dart).
  • System tables (always): auth_tokens, doctype_meta, doctype_permission, sdk_meta, link_options, outbox, pending_attachments.
  • Per-doctype tables (offline mode only): docs__<doctype> for parent + docs__<child_doctype> for each child table. Created on first pull via SchemaApplier (lib/src/database/schema_applier.dart), idempotent — skips if exists. Max 7 indexes per table. Enforces is_child_table flag in doctype_meta.
  • Migrations: lib/src/database/migrations/v1_to_v2.dart — runs once when an upgrade detects v1 schema. Requires network — throws MigrationNeedsNetworkException if offline; consumer shows MigrationBlockedScreen and retries.

8. Server-driven offline-mode toggle (P1+P2+P3)

doc/OFFLINE_MODE_TOGGLE.md is the canonical reference. Headline:

  • Server has a Mobile Configuration single Doctype with an offline_enabled Check field (default 0).
  • Login responses (every flow, except token refresh) include the flag at the top level.
  • SDK persists it to sdk_meta; reads at next initialize(); immutable for the session.
  • A missing key → defaults to false (online mode). Older mobile_control versions therefore get online-mode behaviour for free.
  • Switching modes mid-deployment triggers OfflineTransitionService to drain pending writes (online→offline gives nothing to drain; offline→online uploads outbox and wipes per-doctype tables).

For Swasti: Decide upfront. The Swasti V3 plan implies offline-first (field workforce, often offline). Default the flag to 1 after the data-import pipeline lands; until then keep it 0 to avoid sync-engine churn while the schema is in flux.


9. Bulk fetch & frappe-mobile-control

Server endpoint mobile_sync.get_docs_with_children lives in the companion app frappe-mobile-control, branch om/mobile-sync-bulk-fetch. The SDK calls it from PullPageFetcher. Required for any doctype with child tables to pull efficiently — without it the SDK falls back to per-name fetch which is unusable at Swasti’s expected member counts (533+ per surveyor as of kickoff screenshot 31).

The companion app also provides:

  • mobile_control.attachment_relink.relink_mobile_files — needed to attach SDK-uploaded files to child-row Attach fields (parent Attach fields work via stock Frappe).
  • mobile_auth.* whitelisted methods (login, OTP, refresh, permissions, translations, social login).
  • Mobile Configuration Doctype (carries offline_enabled).

Swasti’s Frappe app must include frappe-mobile-control from the om/mobile-sync-bulk-fetch branch (which will merge to develop) — pinning here matters as much as on the SDK side.


10. File uploads / attachments

lib/src/services/attachment_pipeline.dart (referenced; full path under lib/src/sync/attachment_pipeline.dart).

Online: direct AttachmentService POST to /api/method/upload_file and link to the doctype.

Offline: queued in pending_attachments table — rows hold:

  • parent_uuid / parent_doctype — the row that owns the Attach field (parent OR child).
  • top_parent_uuid / top_parent_doctype — the outbox row that carries it to the server (always the parent doc UUID, even for child-row attachments).
  • local_path, file_name, mime_type, is_private, state (pending|uploading|done|failed), retry/error metadata.

Push flow: files upload before the parent doc is pushed → server returns file URLs → relink hook attaches files to child rows by mobile_uuid → parent doc push includes the resolved file refs.

Failure modes:

  • Upload retries: 2s/5s/10s × 3 → failed → outbox row goes blocked. User reattaches; SDK clears the failed row, re-queues.
  • Parent push permanently fails: file row stays orphan, all attached_to_* NULL. Frappe’s stock cleanup_unattached_files cron reaps on schedule.
  • mobile_control not installed → child-row Attach fields stay orphan. Parent-only Attach still works.

11. Notifications / push

Not in the SDK. Wire FCM (or your preferred provider) in the consumer app. Follow-up reminders (per Swasti decision D6 — push notifications as paid feature enhancement) are a separate workstream — would integrate with FCM topic subscriptions keyed on user roles + due dates.


12. Audit log support (Swasti D3)

The SDK does not synthesize Frappe’s document change log into a structured audit-log Doctype. For Swasti’s “replace mForm self-linking with Frappe-native audit log” (D3), the audit-log Doctype lives server-side (per 02-reliance-frappe-context.md — rel-mis already has Submission Approval Log + workflow_utils.log_workflow_action). The SDK pulls it like any other Doctype and renders it via the standard list/form screens.

Implication for Swasti: the followup_log Doctype is built server-side; the Flutter app reads it through OfflineRepository like Member or any other doctype. No SDK changes needed.


13. Error handling / logging

  • Typed exceptions: FrappeException (base), AuthException, ApiException, NetworkException, ValidationException (lib/src/api/exceptions.dart).
  • API tracer (api_tracer) for HTTP request/response logging in debug.
  • No bundled Sentry / Crashlytics — wire in the consumer app.
  • Codeburn (org telemetry per the new memory entry) is not wired at SDK level — handled by the harness, not the runtime.
  • Push errors: PushError (lib/src/sync/push_error.dart) carries outbox_id, error_code, retryable, validation_payload — surfaced in SyncErrorsScreen.

14. Testing

test/ has ~30 test files covering:

  • Concurrency: concurrency_pool, connectivity_watcher, device_tier, isolate_parser, sync_mutex, write_queue.
  • Database: app_database_v3, field_type_mapping, migrations/, normalize_for_search, schema_applier, sdk_meta_dao, sdk_meta_migration, table_name, wipe_offline_document_tables, all DAOs.
  • Models: dep_graph, document, meta_diff, offline_mode, session_user.
  • Query: filter_parser_basic/like/null_parity/or, query_builder.
  • Form: depends_on_evaluator, form_builder_submit, mobile_home_screen, meta_service, migration_integration, frappe_sdk.

Patterns: heavy use of @visibleForTesting getters on FrappeSDK (e.g. offlineModeForTesting); SQL fakes via sqflite_common_ffi. widget_test.dart in example/ for the demo app. Swasti should add: full integration test against a staging Frappe instance running frappe-mobile-control.


15. Migration notes — what changed since rel-mis

rel-mis pins frappe-mobile-sdk ref 7d27451 (per its plan archives). Differences vs om/offline_improvement head 5bd0a93:

Arearel-mis (old)om/offline_improvement (new)
Offline toggleNone — always offline-firstoffline_enabled flag, online-mode is the new default for 2.1+
Mode transitionsManual, no drainOfflineTransitionService with drain/wipe + MigrationBlockedScreen
Push/pull coordinationRace conditions on overlapSyncMutex enforces single-cycle semantics (SIG-2)
Attachments (child rows)Orphan sentinel docnamev16 relink flow via mobile_control.attachment_relink
List readsDirect query of local tableUnifiedResolver (online passthrough OR offline cache, with background refresh)
Count APINoneNew count operation in resolver
depends_on parserSubstring-prone (== vs ===)Strict-equality + paren handling
Hooks for appFieldChangeHandler only+ LinkFilterBuilder (PR #35)
SessionUserPartialComplete — populated for ALL auth paths via mobile_auth.me
Schema migrationv1 onlyv1→v2 path, network-required, throws MigrationNeedsNetworkException
Version1.x2.1.0

Breaking for rel-mis if upgraded: the offline_enabled=false default. rel-mis assumes offline-first; upgrading without flipping the server flag would silently switch it to thin-client mode and drop the offline cache. Swasti starts fresh on 2.1, so not a concern — just be aware when reviewing rel-mis code patterns.


16. Known limitations / open issues (mined from commit msgs + branch docs)

  1. Token refresh does not re-emit offline_enabled. The flag only updates on full login. If admin flips it server-side, users won’t pick up the change until next login — by design (in-session immutability).
  2. previewDeleteCascade requires the dependency graph to be fresh. If the user makes local edits between preview and accept, the cascade plan may be stale.
  3. pullSyncMany concurrency=4 is conservative. Tunable, but bumping past 4 has race-condition risk with link option caching (LinkOptionService).
  4. SearchableSelect / TableMultiSelectdisease_category-style child Doctypes need a Link child + Option Master populated server-side. The conversion skill (04-mform-to-frappe-skill.md) handles this; consumer code that hand-rolls Table MultiSelect will fail with silent ValidationError.
  5. SyncMutex dispose ordering — fixed in 04c6674 (SIG-1/9/12) but worth attention when adding new sync paths.
  6. Attachments without mobile_control — child-row Attach fields stay orphan. Hard requirement.
  7. No biometric auth, no multi-account — wire in consumer app if needed.

17. How a Swasti Flutter form gets wired up

Worked example: 5.1 Health Screening form (V2 form 1010, redesigned per PM Health_Screening tab → ~138-row spec).

Server-side prerequisite (one-time)

  1. Conversion skill produces health_screening Doctype + .py + .js + .dart (4-files-in-parity, per 04-mform-to-frappe-skill.md §1).
  2. Skill encodes BMI/HB/BP/RBS auto-calc as read_only=1 fields with eval: expressions; closure rules become server-side validate() raises.
  3. Doctype is registered in Mobile Configuration so it appears in mobile_form_names for the surveyor role.

App-side (no widget code beyond hooks)

// 1. Boot
final sdk = FrappeSDK(baseUrl: AppConfig.baseUrl);
await sdk.initialize(true);

// 2. Wrap app in guard
MaterialApp(
  home: FrappeAppGuard(
    baseUrl: AppConfig.baseUrl,
    child: MobileHomeScreen(
      sdk: sdk,
      // Per-doctype hook: when 'member' link is picked on Health Screening,
      // patch member-derived fields. (Almost never needed — fetch_from on
      // the Doctype handles auto-populate. Use only when the design sheet
      // says "Auto Calculate" or there's a derivation the server can't do.)
      fieldChangeHandlerFor: (doctype) {
        if (doctype == 'Health Screening') {
          return (snapshot, fieldname) {
            // example: derive age from dob (only if not server-side)
            return null;
          };
        }
        return null;
      },
      // Filter Member dropdown to current surveyor's location
      linkFilterBuilderFor: (doctype) {
        if (doctype == 'Health Screening') {
          return (lookup, fieldname, formData) {
            if (fieldname == 'member') {
              return LinkFilterResult.replace([
                ['location', '=', sdk.sessionUser?.userDefaults?['location']],
              ]);
            }
            return null;  // SDK uses meta filters
          };
        }
        return null;
      },
    ),
  ),
);

What Swasti developers do NOT write

  • Form widgets (the SDK renders from Doctype meta).
  • Skip-logic dispatchers (DependsOnEvaluator does it).
  • Geography cascade handlers (fetch_from does it).
  • Sync triggers (sync runs post-login + post-save).
  • Outbox / conflict UI (SyncStatusBar / SyncProgressScreen / SyncErrorsScreen are drop-in).
  • Login screen (LoginScreen from the SDK; configurable styles per doc/CUSTOMIZATION.md).

What Swasti developers DO write

  • Theme / branding / config (example/lib/config/app_config.dart template).
  • Per-doctype hooks (LinkFilterBuilder, FieldChangeHandler) only when the server-side / fetch_from / depends_on aren’t enough.
  • The Livelihood Doctype Dart-side companion file (one of the 4-files-in-parity) — but the conversion skill generates it; manual touches go in only for genuinely Dart-only logic.
  • FCM notification glue (Swasti push-notifications feature).

18. Open questions for SDK author (Om / Sunandan)

  1. offline_enabled=false as 2.1 default — for Swasti’s offline-heavy field workforce, should we ship with offline_enabled=true from day 1, or stage in via online-mode first then flip after the import pipeline stabilises?
  2. SyncMutex granularity — is it global, per-doctype, or per-connection? Concurrency limits for pullSyncMany interplay with this.
  3. MigrationNeedsNetworkException retry UX — does MigrationBlockedScreen auto-retry on connectivity recovery, or does the consumer poll? (Code reading suggests user-triggered retry only.)
  4. v1_to_v2.dart migration — Swasti starts at v2 fresh, so no migration runs. Confirm there’s no half-state where a fresh install touches the v1 path.
  5. Translation pull cadencemobile_auth.get_translations is pulled once on login. If A14a (Swasti Kannada/Telugu) ships post-launch, do users need to log out / log in to get new strings?
  6. Bulk fetch limitmobile_sync.get_docs_with_children accepts max 200 names per call. Documented; is the SDK already chunking, or does the consumer app need to?
  7. LinkFilterBuilder precedence with link_filters JSON-in-JSON escaping — rel-mis paid for this anti-pattern. Has the new branch added a guard / lint?
  8. closedItself import — when we collapse mForm 1011 self-linked chains into single records + audit-log entries, is there an SDK-side helper to backfill audit-log rows, or does the import pipeline write them directly to the server?
  9. Geography hierarchy for Swasti — design says “single Link to Geography on Member”, which then fetches state/district/block/GP/village via fetch_from. Confirm the cascade depth limit (rel-mis hit the “Geography Column Break breaking GP autofill” — is the fix in 2.1?).
  10. Workflow doctype detection (doc/WORKFLOWS.md) — does it auto-detect any Doctype with a workflow_state field, or does the Doctype need to be flagged?

19. References

  • Branch: ~/Dhwani/swasti-mform-migration/raw/sdk/frappe-mobile-sdk/ @ om/offline_improvement (head 5bd0a93)
  • Canonical docs in the SDK repo:
    • doc/OFFLINE_FIRST.md — sync architecture
    • doc/OFFLINE_MODE_TOGGLE.md — server-driven flag
    • doc/FIELD_TYPES.md — widget mapping
    • doc/LINK_FILTER_BUILDER.md — runtime Link filter hook
    • doc/FIELD_CHANGE_HANDLER.md — patch-map hook
    • doc/WORKFLOWS.md — workflow detection
    • doc/CUSTOMIZATION.md — theming + screen overrides
    • doc/SETUP.md, doc/QUICK_TEST.md, doc/TESTING.md, doc/DOCUMENTATION.md
  • Server companion: frappe-mobile-control branch om/mobile-sync-bulk-fetch (will merge to develop)
  • Cross-references in this kickoff:
    • 02-reliance-frappe-context.md — rel-mis Frappe shape; SDK anti-patterns from CODEBASE_GUIDE
    • 04-mform-to-frappe-skill.md — Doctype + .py + .js + .dart generation, validation _id table
    • 05-pm-design-doc.md — design authority (HS form, Livelihood, BP New Logic)
    • feedback_swasti_rel_mis_reference.md — don’t lift UPA/multi-geography model

Last updated 2026-05-04