accordsync_flutter

Offline-first sync for Flutter apps that stays correct when the network lies.

Your app writes to the phone first and keeps working with no connection. When the network comes back, every device ends up with the same data: each field merges by the rule you declare, counters never lose an increment, and two different decisions made offline are kept for a person to settle instead of being guessed.

This package adds the Flutter parts to accordsync: storage on the device with drift, widgets, and sync that follows the app lifecycle. It syncs with the Accord server, alongside web and React Native devices using the TypeScript packages.

Install

flutter pub add accordsync_flutter drift_flutter

Use

import 'package:accordsync_flutter/accordsync_flutter.dart';
import 'package:drift_flutter/drift_flutter.dart';

// The same schema as your server.
final schema = defineSchema({
  'dossier': {
    'client_name': lww(), // highest clock wins
    'visits': counter(), // every increment counts
    'docs': set(), // add-wins set
    'status': conflict(), // concurrent decisions are kept for a person to settle
  },
});

final accord = await AccordClient.open(
  schema: schema,
  storage: DriftStorage(AccordDatabase(driftDatabase(name: 'accord')), closeDatabase: true),
  transport: HttpTransport(url: 'https://sync.example.com', getToken: auth.currentToken),
);

runApp(
  AccordProvider(
    client: accord,
    child: AccordLifecycle(client: accord, child: const MyApp()),
  ),
);

Read a record, and rebuild when it changes on this device or arrives from the server:

RecordBuilder(
  record: 'dossier:91',
  builder: (context, fields) => Text('${fields?['visits'] ?? 0} visits'),
)

Write: the call completes as soon as the change is saved on the phone, online or not.

await accord.inc('dossier:91', 'visits', 1);
await accord.add('dossier:91', 'docs', 'id-card.pdf');

Show conflicts where someone can decide, and resolve one:

ConflictsBuilder(
  builder: (context, conflicts) => Column(children: [
    for (final c in conflicts)
      for (final v in c.values)
        TextButton(
          onPressed: () => accord.resolve(c.record, c.field, v.value),
          child: Text('Keep ${v.value}'),
        ),
  ]),
)

What's inside

DriftStorage Stores ops, the outbox and snapshots in SQLite through drift. Pass your app's own database (Accord adds four tables prefixed accord_), or AccordDatabase for a separate file. No code generation.
AccordProvider Makes the client available below it (AccordProvider.of(context)).
RecordBuilder Rebuilds when one record changes.
ConflictsBuilder Every conflict() field holding more than one value.
SyncStatusBuilder Pending writes, last sync, last error.
AccordLifecycle Syncs in the foreground, pauses in the background, syncs when online emits true (feed it from connectivity_plus or similar).

Everything from accordsync and accordsync_core is re-exported.

The device database is not encrypted by Accord. Protect it like the rest of your app's data.

Learn more

Libraries

accordsync_flutter
Accord for Flutter: drift storage on the device, widgets for records, conflicts and sync status, and sync that follows the app lifecycle.