Using transactions to group changes
Read and write transactions present a context where multiple changes can be made then finally committed to the DB or rolled back. This ensures that either all the changes get persisted, or no change is made to the DB (in the case of a rollback or exception).
The writeTransaction(callback) method combines all writes into a single transaction, only committing to persistent storage once.
deleteList(SqliteDatabase db, String id) async {
await db.writeTransaction((tx) async {
// Delete the main list
await tx.execute('DELETE FROM lists WHERE id = ?', [id]);
// Delete any children of the list
await tx.execute('DELETE FROM todos WHERE list_id = ?', [id]);
});
}
Also see readTransaction(callback) .
Subscribe to changes in data
Use watch to watch for changes to the dependent tables of any SQL query.
StreamBuilder(
// You can watch any SQL query
stream: db.watch('SELECT * FROM customers order by id asc'),
builder: (context, snapshot) {
if (snapshot.hasData) {
// TODO: implement your own UI here based on the result set
return ...;
} else {
return const Center(child: CircularProgressIndicator());
}
},
)
Insert, update, and delete data in the local database
Use execute to run INSERT, UPDATE or DELETE queries.
FloatingActionButton(
onPressed: () async {
await db.execute(
'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)',
['Fred', 'fred@example.org'],
);
},
tooltip: '+',
child: const Icon(Icons.add),
);
Send changes in local data to your backend service
Override uploadData to send local updates to your backend service.
@override
Future<void> uploadData(PowerSyncDatabase database) async {
final batch = await database.getCrudBatch();
if (batch == null) return;
for (var op in batch.crud) {
switch (op.op) {
case UpdateType.put:
// Send the data to your backend service
// Replace `_myApi` with your own API client or service
await _myApi.put(op.table, op.opData!);
break;
default:
// TODO: implement the other operations (patch, delete)
break;
}
}
await batch.complete();
}
Use SyncStatus and register an event listener with statusStream to listen for status changes to your PowerSync instance.
class _StatusAppBarState extends State<StatusAppBar> {
late SyncStatus _connectionState;
StreamSubscription<SyncStatus>? _syncStatusSubscription;
@override
void initState() {
super.initState();
_connectionState = db.currentStatus;
_syncStatusSubscription = db.statusStream.listen((event) {
setState(() {
_connectionState = db.currentStatus;
});
});
}
@override
void dispose() {
super.dispose();
_syncStatusSubscription?.cancel();
}
@override
Widget build(BuildContext context) {
final statusIcon = _getStatusIcon(_connectionState);
return AppBar(
title: Text(widget.title),
actions: <Widget>[
...
statusIcon
],
);
}
}
Widget _getStatusIcon(SyncStatus status) {
if (status.anyError != null) {
// The error message is verbose, could be replaced with something
// more user-friendly
if (!status.connected) {
return _makeIcon(status.anyError!.toString(), Icons.cloud_off);
} else {
return _makeIcon(status.anyError!.toString(), Icons.sync_problem);
}
} else if (status.connecting) {
return _makeIcon('Connecting', Icons.cloud_sync_outlined);
} else if (!status.connected) {
return _makeIcon('Not connected', Icons.cloud_off);
} else if (status.uploading && status.downloading) {
// The status changes often between downloading, uploading and both,
// so we use the same icon for all three
return _makeIcon('Uploading and downloading', Icons.cloud_sync_outlined);
} else if (status.uploading) {
return _makeIcon('Uploading', Icons.cloud_sync_outlined);
} else if (status.downloading) {
return _makeIcon('Downloading', Icons.cloud_sync_outlined);
} else {
return _makeIcon('Connected', Icons.cloud_queue);
}
}
Wait for the initial sync to complete
Use the hasSynced property (available since version 1.5.1 of the SDK) and register a listener to indicate to the user whether the initial sync is in progress.
// Example of using hasSynced to show whether the first sync has completed
/// Global reference to the database
final PowerSyncDatabase db;
bool hasSynced = false;
StreamSubscription? _syncStatusSubscription;
// Use the exposed statusStream
Stream<SyncStatus> watchSyncStatus() {
return db.statusStream;
}
@override
void initState() {
super.initState();
_syncStatusSubscription = watchSyncStatus.listen((status) {
setState(() {
hasSynced = status.hasSynced ?? false;
});
});
}
@override
Widget build(BuildContext context) {
return Text(hasSynced ? 'Initial sync completed!' : 'Busy with initial sync...');
}
// Don't forget to dispose of stream subscriptions when the view is disposed
void dispose() {
super.dispose();
_syncStatusSubscription?.cancel();
}
For async use cases, see the waitForFirstSync method which returns a promise that resolves once the first full sync has completed.
Report sync download progress
You can show users a progress bar when data downloads using the downloadProgress
property from the
SyncStatus class.
downloadProgress.downloadedFraction
gives you a value from 0.0 to 1.0 representing the total sync progress. This is especially useful for long-running initial syncs.
As an example, this widget renders a progress bar when a download is active:
import 'package:flutter/material.dart';
import 'package:powersync/powersync.dart' hide Column;
class SyncProgressBar extends StatelessWidget {
final PowerSyncDatabase db;
/// When set, show progress towards the [BucketPriority] instead of towards
/// the full sync.
final BucketPriority? priority;
const SyncProgressBar({
super.key,
required this.db,
this.priority,
});
@override
Widget build(BuildContext context) {
return StreamBuilder<SyncStatus>(
stream: db.statusStream,
initialData: db.currentStatus,
builder: (context, snapshot) {
final status = snapshot.requireData;
final progress = switch (priority) {
null => status.downloadProgress,
var priority? => status.downloadProgress?.untilPriority(priority),
};
if (progress != null) {
return Center(
child: Column(
children: [
const Text('Busy with sync...'),
LinearProgressIndicator(value: progress?.downloadedFraction),
Text(
'${progress.downloadedOperations} out of ${progress.totalOperations}')
],
),
);
} else {
return const SizedBox.shrink();
}
},
);
}
}
Also see: