flutter_crdt_sync_kit 0.1.0 copy "flutter_crdt_sync_kit: ^0.1.0" to clipboard
flutter_crdt_sync_kit: ^0.1.0 copied to clipboard

An offline-first, CRDT-based local data layer for Flutter. Write while offline, merge automatically and conflict-free when back online, with pluggable Supabase and REST backend adapters.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_crdt_sync_kit/flutter_crdt_sync_kit.dart';

import 'demo_backend.dart';
import 'device_panel.dart';
import 'manual_connectivity_monitor.dart';
import 'todo_item.dart';

/// sync_kit's killer demo, as a single-process Flutter app: two
/// independent "devices" (their own [InMemoryLocalStore], [HlcClock] and
/// [SyncEngine]) edit the *same* to-do item, a vote counter, and a tag set
/// while offline. Flip a device to airplane mode, edit both, flip it back
/// online, and watch the merge happen live — with no manual conflict
/// resolution.
void main() {
  runApp(const SyncKitDemoApp());
}

const sharedDocId = 'demo_todo_1';

class SyncKitDemoApp extends StatelessWidget {
  const SyncKitDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'sync_kit demo',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const _DemoHome(),
    );
  }
}

class _DemoHome extends StatefulWidget {
  const _DemoHome();

  @override
  State<_DemoHome> createState() => _DemoHomeState();
}

class _DemoHomeState extends State<_DemoHome> {
  // One shared in-memory "server" both devices sync through. Swap
  // DemoSyncAdapter for SupabaseSyncAdapter or RestSyncAdapter to point
  // this demo at a real backend — that's the entire change needed.
  final backend = DemoBackend();
  late final Future<(DeviceSession, DeviceSession)> _sessions =
      _createSessions();

  Future<(DeviceSession, DeviceSession)> _createSessions() async {
    final a = await DeviceSession.create(
      name: 'Device A',
      nodeId: 'device-a',
      backend: backend,
    );
    final b = await DeviceSession.create(
      name: 'Device B',
      nodeId: 'device-b',
      backend: backend,
    );
    return (a, b);
  }

  @override
  void dispose() {
    _sessions.then((s) {
      s.$1.dispose();
      s.$2.dispose();
    });
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('sync_kit — offline-first CRDT sync')),
      body: FutureBuilder<(DeviceSession, DeviceSession)>(
        future: _sessions,
        builder: (context, snapshot) {
          final sessions = snapshot.data;
          if (sessions == null) {
            return const Center(child: CircularProgressIndicator());
          }
          return Padding(
            padding: const EdgeInsets.all(12),
            child: Column(
              children: [
                const _Explainer(),
                const SizedBox(height: 12),
                Expanded(
                  child: LayoutBuilder(
                    builder: (context, constraints) {
                      final wide = constraints.maxWidth > 700;
                      final panelA = DevicePanel(session: sessions.$1);
                      final panelB = DevicePanel(session: sessions.$2);
                      return wide
                          ? Row(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Expanded(child: panelA),
                                const SizedBox(width: 12),
                                Expanded(child: panelB),
                              ],
                            )
                          : ListView(
                              children: [
                                panelA,
                                const SizedBox(height: 12),
                                panelB,
                              ],
                            );
                    },
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

class _Explainer extends StatelessWidget {
  const _Explainer();

  @override
  Widget build(BuildContext context) {
    return Card(
      color: Theme.of(context).colorScheme.surfaceContainerHighest,
      child: const Padding(
        padding: EdgeInsets.all(12),
        child: Text(
          'Both devices edit the same record. Flip a device offline, edit both '
          'devices differently, then flip it back online — the merge happens '
          'automatically, with no lost writes. Expand "sync_kit inspector" on '
          'either panel to see the op log driving it.',
        ),
      ),
    );
  }
}

/// Everything one simulated device needs: its own local store, clock,
/// connectivity switch and sync engine, plus the three CRDT-backed values
/// this demo shows off (a document, a counter, a set).
class DeviceSession {
  final String name;
  final LocalStore store;
  final ManualConnectivityMonitor connectivity;
  final SyncEngine engine;
  final SyncedDoc<TodoItem> doc;
  final SyncedCounter votes;
  final SyncedSet<String> tags;

  DeviceSession._({
    required this.name,
    required this.store,
    required this.connectivity,
    required this.engine,
    required this.doc,
    required this.votes,
    required this.tags,
  });

  static Future<DeviceSession> create({
    required String name,
    required String nodeId,
    required DemoBackend backend,
  }) async {
    final store = InMemoryLocalStore();
    await store.init();
    final clock = HlcClock(nodeId);
    final connectivity = ManualConnectivityMonitor();
    final engine = SyncEngine(
      store: store,
      adapter: DemoSyncAdapter(backend, id: 'demo'),
      connectivity: connectivity,
      pollInterval: const Duration(seconds: 5),
      writeDebounce: const Duration(milliseconds: 200),
    );

    final doc = await SyncedDoc.open<TodoItem>(
      docId: sharedDocId,
      docType: 'todo',
      store: store,
      clock: clock,
      codec: todoCodec,
      empty: TodoItem.empty,
      engine: engine,
    );
    final votes = await SyncedCounter.open(
      docId: '${sharedDocId}_votes',
      store: store,
      clock: clock,
      engine: engine,
    );
    final tags = await SyncedSet.open<String>(
      docId: '${sharedDocId}_tags',
      store: store,
      clock: clock,
      engine: engine,
    );

    engine.start();

    return DeviceSession._(
      name: name,
      store: store,
      connectivity: connectivity,
      engine: engine,
      doc: doc,
      votes: votes,
      tags: tags,
    );
  }

  Future<void> dispose() async {
    await engine.stop();
    await doc.dispose();
    await votes.dispose();
    await tags.dispose();
  }
}
0
likes
140
points
84
downloads

Documentation

API reference

Publisher

verified publisherehsanur.com

Weekly Downloads

An offline-first, CRDT-based local data layer for Flutter. Write while offline, merge automatically and conflict-free when back online, with pluggable Supabase and REST backend adapters.

Repository (GitHub)
View/report issues

Topics

#offline-first #crdt #sync #database #supabase

License

MIT (license)

Dependencies

collection, connectivity_plus, cryptography, drift, flutter, http, meta, path, path_provider, sqlite3, sqlite3_flutter_libs, supabase, uuid

More

Packages that depend on flutter_crdt_sync_kit