DataLocal
DataLocal 2 is document-style local persistence for Flutter applications that need more structure than direct preference calls without the weight of a SQL schema.
It provides:
- map and typed documents with IDs, timestamps, and optimistic revisions
- immutable filters, nested field paths, sorting, cursors, and aggregates
- reactive query snapshots and commit-ordered change events
- serialized writes, logical batches, and crash-recovery journaling
- a
SharedPreferencesAsyncadapter for small datasets - optional AES-256-GCM encryption with platform secure-key storage
- resumable migration from DataLocal 1.x
DataLocal is not a replacement for SQLite when you need large datasets, indexes, joins, cross-process transactions, or query pushdown.
Install
dependencies:
datalocal: ^2.0.0
Open an encrypted database
import 'package:datalocal/datalocal.dart';
const databaseName = 'my_app';
final keys = DataLocalSecureStorageKeyProvider(
databaseName: databaseName,
);
final database = await DataLocalDatabase.open(
name: databaseName,
storage: DataLocalSharedPreferencesAsyncStorage(),
encryption: DataLocalAesGcmEncryptionProvider(
keyProvider: keys,
),
);
DataLocalDatabase.open defaults to explicit plaintext mode for caches and
tests. Pass the AES-GCM provider above when confidentiality at rest is required.
On Apple platforms, configure Keychain Sharing as required by
flutter_secure_storage. On web, secure storage requires HTTPS or localhost.
CRUD
final notes = database.mapCollection('notes');
final inserted = await notes.insert({
'title': 'DataLocal 2',
'completed': false,
'author': {'id': 'user-1'},
});
final updated = await notes.patch(
inserted.id,
{'completed': true},
expectedRevision: inserted.revision,
);
final sameNote = await notes.require(updated.id);
await notes.delete(sameNote.id, expectedRevision: sameNote.revision);
get(id) returns null for a missing document. require(id) throws
DataLocalNotFoundException.
Query and watch
final query = notes
.query()
.where('completed', isEqualTo: false)
.where('author.id', isEqualTo: 'user-1')
.orderBy('title')
.limit(20);
final snapshot = await query.get();
final subscription = query.watch().listen((snapshot) {
print('${snapshot.documents.length} matching notes');
});
Queries execute in memory for the SharedPreferences adapter. Cursor ordering is stable and uses the document ID as a deterministic tie-breaker.
Batch
await database.writeBatch((batch) {
batch.insert(notes, {'title': 'first'}, id: 'first');
batch.insert(notes, {'title': 'second'}, id: 'second');
});
The SharedPreferences adapter provides one logical, recoverable commit inside a database instance. It does not claim cross-process ACID transactions.
Typed documents
final users = database.collection<User>(
'users',
codec: DataLocalFunctionalCodec<User>(
encode: (user) => user.toJson(),
decode: User.fromJson,
),
);
Migration from 1.x
Migration is explicit, resumable, and preserves legacy values until cleanup:
final migrator = DataLocalLegacyV1Migrator();
final report = await migrator.migrate(
databaseName: databaseName,
stateName: 'notes',
preferences: DataLocalSharedPreferencesAsyncClient(),
target: database.mapCollection('notes'),
);
if (report.complete) {
await migrator.cleanupLegacy(
databaseName: databaseName,
stateName: 'notes',
preferences: DataLocalSharedPreferencesAsyncClient(),
);
}
Read the migration guide before upgrading an existing application.
Security boundary
AES-GCM protects stored document contents and authenticates database, collection, document ID, and schema context. It does not protect plaintext while your process is running, and it cannot defeat an attacker who fully controls the device or application runtime.
See security and limitations and the E2E matrix.
Always close databases and cancel subscriptions you no longer need:
await subscription.cancel();
await database.close();
Libraries
- datalocal
- Encrypted document-style local persistence for Flutter.
- datalocal_core
- Pure Dart domain primitives for the DataLocal 2.0 API.