PowerSync Dart SDK
repository·main·Indexed 42 days ago
https://github.com/powersync-ja/powersync.dartA 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.
What's inside PowerSync
- 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.
Build sqlite3.wasm files for PowerSync Dart SDK
mainThis directory provides the tools necessary to buildsqlite3.wasmfiles that are compatible with the PowerSync Dart SDK. The build process is based onpackage:sqlite3but includes specific patches to statically link the PowerSync SQLite extension.Use PowerSync Attachments Helper for Dart/Flutter
mainThe
powersync_attachments_helperpackage 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.dartlibrary 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.Understand the PowerSync Benchmark app metrics
mainThe PowerSync Benchmark app is designed to measure two key performance metrics:
- Initial sync time: Measures the time taken to sync bulk data to the client. This provides an estimate of sync throughput.
- 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.
Deprecation of powersync_flutter_libs
mainThe
powersync_flutter_libspackage 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.How attachments work in PowerSync
mainInstead 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.
- Attachment Identification: Each attachment is identified by a unique
Implement optional sync using local-only tables and viewName overrides
mainTo allow users to use an app without signing in and then sync their data later, use a combination of
localOnlytables and theviewNameproperty. This prevents app queries from needing to change logic when the user's authentication state changes.The Strategy
- Local-only State: Define tables with the
localOnlyflag. Use theviewNameproperty to override their names to match your standard synced table names (e.g.,lists). This allows your queries (likePowerSync.getAll("SELECT * FROM lists")) to work immediately. - Transition to Synced State: When the user signs in:
- Call
updateSchemato change theviewNameof the synced tables from their inactive names (e.g.,inactive_synced_lists) to their active names (e.g.,lists). - Copy data from the
localOnlytables to the newly active synced tables. - Delete the data from the
localOnlytables to save space.
- Call
- Consistency: By using
viewNameoverrides, 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
updateSchemaConstraints:updateSchemacannot be called inside a transaction. It is recommended to perform schema updates when the database is not connected.watchMethod Behavior: Thewatchmethod may not correctly track tables altered byupdateSchema. To resolve this, executerefreshSchemaafter updating the schema.
- Local-only State: Define tables with the
Listen to real-time data changes with watch queries
mainThe PowerSync SDK provides a
watchmethod on the database executor. This allows you to create aStreamthat emits new events whenever the underlying synced data changes. This is ideal for updating Flutter UI components using aStreamBuilder.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; }); }Configure credentials in the demo app
mainThe demo app requires specific project credentials to connect to your backend. You must manually populate the
lib/app_config.dartfile (created from the template) with your unique credentials for both Supabase and PowerSync.// Edit lib/app_config.dart with your credentialsRun the PowerSync + Django Flutter Demo
mainThis 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:
- Navigate to the demo directory:
cd demos/django-todolist - Bootstrap dependencies:
melos bootstrap - Create your configuration file:
cp lib/app_config_template.dart lib/app_config.dart - Configure
lib/app_config.dartwith your Django URL and PowerSync credentials (see configuration sections below). - 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- Navigate to the demo directory:
Implement a custom AttachmentQueue
mainTo use the attachments helper, you must extend
AbstractAttachmentQueueand implement three core methods to define how files are handled and how the sync process is triggered:saveFile(String fileId, int size, {mediaType}): Defines how a new file is queued for upload. You create anAttachmentobject with the appropriateAttachmentState.queuedUploadand callattachmentsService.saveAttachment().deleteFile(String fileId): Defines how a file is queued for deletion. You create anAttachmentobject withAttachmentState.queuedDeleteand callattachmentsService.saveAttachment().watchIds({String fileExtension}): A watcher that monitors the PowerSync database for new IDs that need processing. When new IDs are detected (e.g., a newphoto_idin auserstable), you should callsyncingService.processIds(relevantIds, fileExtension)to trigger the sync.
Note: The
Attachmentobject requires anid,filename,state, and (for uploads)localUri,size, andmediaType.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); }); } }Connect Android to local PowerSync via adb reverse
mainIf you are running the PowerSync service on your local machine and testing on an Android device, use
adb reverseto route traffic from the device to your localhost.adb reverse tcp:8080 tcp:8080 adb reverse tcp:6060 tcp:6060