sling_gql_sqflite
Persist sling_gql's normalized cache in
SQLite with sqflite: the app's second
launch paints from the previous run's data, with no skeleton and no request
(or a background revalidation under maxAge).
Reads stay synchronous and in memory — they happen inside build(). The
database is a durable write-behind copy: loaded once before the client
starts, then kept up to date with small deltas. Memory is the persisted
size, so the store is bounded (by age and entity count).
Docs: https://tpucci.github.io/sling_gql/guides/persistence/ · Source: https://github.com/tpucci/sling_gql
flutter pub add sling_gql_sqflite
import 'package:sling_gql_sqflite/sling_gql_sqflite.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final persistence = await SqflitePersistence.open(
'sling_cache.db', // relative: in sqflite's getDatabasesPath()
schema: slingSchema,
);
final client = SlingClient<Query>(
endpoint: Uri.parse(endpoint),
schema: slingSchema,
cache: persistence.cache,
// Restored data carries no fetch time: under maxAge it is shown, then
// revalidated in the background. Plain cacheFirst never refetches it.
maxAge: const Duration(minutes: 5),
);
runApp(SlingScope(client: client, child: const App()));
}
// Logout: empty the cache (every query refetches) and the database.
await persistence.clear();
How it saves
openreturns a persistence already listening tocache.onChange: there is nothing to attach.- Every save is
cache.changesSince(savedVersion)— the entities changed or removed since the last stored save, never a whole snapshot — written in one transaction (batch.commit(noResult: true)). - Saves start
debounce(1 s) after the last change, at mostmaxWait(5 s) after the first one, when the app is hidden, paused or detached (AppLifecycleListener), and onflush(). They run one after another, so an older delta never lands after a newer one; a failed save (reported toonError) leaves its changes pending for the next one. - One row per entity (
sling_entities:key,json,updated_at), one row perROOT_QUERYfield (sling_root_fields:field,json,updated_at): a response rewrites the root fields it touched, not the whole root.ROOT_MUTATIONandROOT_SUBSCRIPTIONare never stored.
Bounds
Applied when the store is opened (null disables one):
maxAge(7 days): root fields whose row was last written longer ago are dropped;maxEntities(10 000): while the entities the remaining root fields reach are more, the oldest root fields are dropped;- entities no kept root field reaches are dropped (what
cache.gc()does).
Dropped rows are deleted before open returns; persistence.loaded reports
the counts and timings. maxEntities counts entities only: inline data under
root fields is bounded by maxAge alone. A store whose rows cannot be decoded
is wiped at open (reported to onError).
Invalidation
The database stores a format version, the key field and slingSchema.hash
(a fingerprint sling_gql_gen emits of the generated file). When any of them
differs at open — the schema, --key-field, a --scalar mapping or the
generator changed — the store is wiped and the app starts cold.
Options
| Option | Default | |
|---|---|---|
databaseFactory |
sqflite's | sqflite_common_ffi for desktop/tests, sqflite_common_ffi_web, an SQLCipher factory for encryption |
maxAge / maxEntities |
7 days / 10 000 | bounds applied at open |
debounce / maxWait |
1 s / 5 s | save timing |
hydrateInIsolate |
false |
decode and build the cache in a background isolate (compute) |
flushOnLifecycle |
true |
save when the app goes to the background |
onError |
FlutterError.reportError |
background save failures, undecodable stores (wiped) |
Numbers
Host benchmark (flutter test benchmark/persistence_bench_test.dart, JIT,
macOS, sqflite_common_ffi; ~600 B of JSON per entity):
| 1 000 entities (0.6 MB) | 10 000 entities (5.6 MB) | |
|---|---|---|
| open: read rows | 4 ms | 14 ms |
| open: decode + prune + hydrate | 6 ms | 57–65 ms |
same with hydrateInIsolate (off the UI thread) |
7 ms | 63–80 ms |
changesSince: new root field + 20 changed entities |
0.1 ms | 0.8 ms |
| save of that delta | 2 ms | 4 ms |
| first save, or a full delta (every entity) | 38 ms | 180–190 ms |
Rows written cost what changed; changesSince also copies the whole
ROOT_QUERY when one of its fields changed (the 0.1 → 0.8 ms). A full delta —
after clear(), or once the cache forgot its removals (a gc of more than a
thousand entities) — rewrites every entity.
Libraries
- sling_gql_sqflite
- sling_gql's cache persisted in SQLite through sqflite.