PowerSync Dart SDK

repository·main·Indexed 42 days ago

https://github.com/powersync-ja/powersync.dart

A synchronization engine for Dart and Flutter applications that keeps a local SQLite database in sync with a backend database, enabling offline-first capabilities and real-time updates. Version 2.4.0 includes pre-compiled Dart-Wasm bundles.

Tokens
14.1K
Snippets
41
Records
62
Agent score
53%

What's inside PowerSync

  1. Overview of PowerSync SDK for Dart and Flutter

    main
    PowerSync is a synchronization layer that maintains a local client-side SQLite database in sync with your backend database (Postgres, MongoDB, MySQL, or SQL Server). It enables real-time data updates across devices, instant user interactions, and full offline capabilities. The Dart/Flutter SDK allows you to build applications that work seamlessly regardless of connectivity.
  2. Use PowerSync Attachments Helper for Dart/Flutter

    main

    The powersync_attachments_helper package helps synchronize files (uploads, downloads, and deletes) between local and remote storage.

    Warning: This package is being deprecated in favor of the new powersync_/attachments/attachments.dart library included in the core PowerSync package. The new library offers improved APIs and allows for custom local storage implementations. For new projects, it is recommended to use the core library. New features will only be added to the core library, while this package will only receive bug fixes.

  3. Understand the PowerSync Benchmark app metrics

    main

    The PowerSync Benchmark app is designed to measure two key performance metrics:

    1. Initial sync time: Measures the time taken to sync bulk data to the client. This provides an estimate of sync throughput.
    2. Incremental sync latency: Measures the end-to-end latency of a specific data lifecycle:
      • A row is created on the client.
      • The row is uploaded to the server via a NodeJS backend.
      • Postgres applies a default value to a column.
      • The row is synced back to the client.
      • The client detects the change via a watched query.
      • The client updates the row, completing the cycle.
  4. Deprecation of powersync_flutter_libs

    main

    The powersync_flutter_libs package is deprecated and no longer performs any functions. It was previously used to link the PowerSync SQLite Core extension for Flutter apps.

    Modern PowerSync SDK installations use build hooks to handle SQLite Core linking automatically, making this package unnecessary. If you are migrating or setting up a new project, you do not need to manually include powersync_flutter_libs.

  5. How attachments work in PowerSync

    main

    Instead of embedding large binary data (like images) directly in your source database, PowerSync uses an attachments model. You store references (IDs) to binary data in your regular database schema, while the actual files are stored in a secondary data store (e.g., Supabase Storage, S3) and cached locally on the device.

    Key Concepts

    • Attachment Identification: Each attachment is identified by a unique id.
    • Immutability: Attachments are immutable once created.
    • Referential Integrity: Relational data should reference attachments using a foreign key.
    • Automatic Cleanup: The sync process manages the lifecycle. If no relational data in your database references an attachment, the SDK will eventually delete that orphaned attachment from local storage.
    • Alpha Status: The attachments API is currently marked as @experimental. It is intended for testing and may undergo breaking changes. Do not use in production.
  6. Implement optional sync using local-only tables and viewName overrides

    main

    To allow users to use an app without signing in and then sync their data later, use a combination of localOnly tables and the viewName property. This prevents app queries from needing to change logic when the user's authentication state changes.

    The Strategy

    1. Local-only State: Define tables with the localOnly flag. Use the viewName property to override their names to match your standard synced table names (e.g., lists). This allows your queries (like PowerSync.getAll("SELECT * FROM lists")) to work immediately.
    2. Transition to Synced State: When the user signs in:
      • Call updateSchema to change the viewName of the synced tables from their inactive names (e.g., inactive_synced_lists) to their active names (e.g., lists).
      • Copy data from the localOnly tables to the newly active synced tables.
      • Delete the data from the localOnly tables to save space.
    3. Consistency: By using viewName overrides, the SQL queries used throughout the app remain identical regardless of whether the data is currently being served from a local-only table or a synced table.

    Important Implementation Notes

    • updateSchema Constraints: updateSchema cannot be called inside a transaction. It is recommended to perform schema updates when the database is not connected.
    • watch Method Behavior: The watch method may not correctly track tables altered by updateSchema. To resolve this, execute refreshSchema after updating the schema.
  7. Listen to real-time data changes with watch queries

    main

    The PowerSync SDK provides a watch method on the database executor. This allows you to create a Stream that emits new events whenever the underlying synced data changes. This is ideal for updating Flutter UI components using a StreamBuilder.

    When a query matches updated data, the stream emits an event, allowing you to map the new rows to your application's domain models.

    // Example of watching a query and mapping to models
    Stream<List<Workspace>> watchWorkspacesByUser({required String userId}) {
      return client.getDBExecutor().watch('''
              SELECT * FROM workspace WHERE userId = ?
               ''', parameters: [userId]).asyncMap((event) async {
          List<Workspace> workspaces = event.map((row) => Workspace.fromRow(row)).toList();
          // ... perform additional async lookups if needed
          return workspaces;
        });
    }
  8. Run the PowerSync + Django Flutter Demo

    main

    This demo showcases the PowerSync SDK for Flutter integrated with a Django backend. To run the application locally, you must have melos installed.

    Follow these steps:

    1. Navigate to the demo directory: cd demos/django-todolist
    2. Bootstrap dependencies: melos bootstrap
    3. Create your configuration file: cp lib/app_config_template.dart lib/app_config.dart
    4. Configure lib/app_config.dart with your Django URL and PowerSync credentials (see configuration sections below).
    5. Launch the app: flutter run

    Test Credentials:

    • Username: testuser
    • Password: testpassword
    cd demos/django-todolist
    melos bootstrap
    cp lib/app_config_template.dart lib/app_config.dart
    # Edit lib/app_config.dart before running
    flutter run
  9. Implement a custom AttachmentQueue

    main

    To use the attachments helper, you must extend AbstractAttachmentQueue and implement three core methods to define how files are handled and how the sync process is triggered:

    1. saveFile(String fileId, int size, {mediaType}): Defines how a new file is queued for upload. You create an Attachment object with the appropriate AttachmentState.queuedUpload and call attachmentsService.saveAttachment().
    2. deleteFile(String fileId): Defines how a file is queued for deletion. You create an Attachment object with AttachmentState.queuedDelete and call attachmentsService.saveAttachment().
    3. watchIds({String fileExtension}): A watcher that monitors the PowerSync database for new IDs that need processing. When new IDs are detected (e.g., a new photo_id in a users table), you should call syncingService.processIds(relevantIds, fileExtension) to trigger the sync.

    Note: The Attachment object requires an id, filename, state, and (for uploads) localUri, size, and mediaType.

    class PhotoAttachmentQueue extends AbstractAttachmentQueue {
      PhotoAttachmentQueue(db, remoteStorage)
          : super(db: db, remoteStorage: remoteStorage);
    
      @override
      Future<Attachment> saveFile(String fileId, int size, {mediaType = 'image/jpeg'}) async {
        String filename = '$fileId.jpg';
        Attachment photoAttachment = Attachment(
          id: fileId,
          filename: filename,
          state: AttachmentState.queuedUpload.index,
          mediaType: mediaType,
          localUri: getLocalFilePathSuffix(filename),
          size: size,
        );
    
        return attachmentsService.saveAttachment(photoAttachment);
      }
    
      @override
      Future<Attachment> deleteFile(String fileId) async {
        String filename = '$fileId.jpg';
        Attachment photoAttachment = Attachment(
            id: fileId,
            filename: filename,
            state: AttachmentState.queuedDelete.index);
    
        return attachmentsService.saveAttachment(photoAttachment);
      }
    
      @override
      StreamSubscription<void> watchIds({String fileExtension = 'jpg'}) {
        return db.watch('''
          SELECT photo_id FROM users
          WHERE photo_id IS NOT NULL
        ''').map((results) {
          return results.map((row) => row['photo_id'] as String).toList();
        }).listen((ids) async {
          List<String> idsInQueue = await attachmentsService.getAttachmentIds();
          List<String> relevantIds =
              ids.where((element) => !idsInQueue.contains(element)).toList();
          syncingService.processIds(relevantIds, fileExtension);
        });
      }
    }