preference_flutter 0.1.1 copy "preference_flutter: ^0.1.1" to clipboard
preference_flutter: ^0.1.1 copied to clipboard

Durable local-first storage for native Dart and Flutter apps.

Preference #

pub package

A durable, local-first Flutter/Dart storage engine for app state and typed offline entities—currently in alpha. Use Preference like a typed shared_preferences-style store for settings, then grow into typed collections for work orders, assets, drafts, and cached API responses without adding a second local store.

Alpha-phase notice — 0.1.0

Preference is ready for hands-on evaluation on native platforms. Its file format and parts of the public API may change before 1.0.0. It is not yet a SQL database, a sync service, or an encrypted store. Test upgrades against a copy of important data. For issues, feedback, or alpha-access support, email frostcraftdev@gmail.com.

Contents #

What is included #

  • Durable append-only native file storage with recovery of a truncated write tail.
  • Typed root key-value reads and writes for String, int, double, and bool.
  • Custom serializers for any Dart model.
  • Logical collections with stable record IDs.
  • Atomic root-key batches; a collection write updates its item and declared indexes in one commit.
  • Change streams for keys, collection items, whole collections, and database commits.
  • Logical TTL expiry for flat keys.
  • Collection lookup indexes for direct unique-style and non-unique lookups.
  • Read-only local inspection and Flutter VM Service inspection for debug builds.
  • Compaction to reclaim expired and superseded records.

Platform support #

The 0.1.0 alpha-phase release supports native Dart VM/file platforms:

Platform Status
Android 10+ (API 29+) Supported alpha
iOS Supported alpha
macOS, Windows, Linux Supported alpha
Web Not supported in this alpha

For Flutter, open the database inside a private, persistent application directory. path_provider is a common way to obtain one.

Install #

dependencies:
  preference_flutter: ^0.1.1
  path_provider: ^2.1.0 # Flutter apps: recommended for a safe database path

Then run:

flutter pub get

Open a database #

Flutter #

import 'package:path_provider/path_provider.dart';
import 'package:preference_flutter/preference_flutter.dart';

final directory = await getApplicationSupportDirectory();

final database = await Preference.open(
  '${directory.path}/field_notes.pref',
  appId: 'com.example.field_notes',
  databaseId: 'field_notes',
);

Do not place a database in external/shared storage or a directory the operating system can clear. Close it when the owning service is permanently disposed:

await database.close();

Dart command-line app #

import 'dart:io';
import 'package:preference_flutter/preference_flutter.dart';

final database = await Preference.open(
  '${Directory.current.path}/field_notes.pref',
);

Use case 1: flat key-value storage #

Use flat keys for small application state: settings, feature flags, counters, timestamps represented as strings, and short-lived cached values.

await database.set<String>('settings/theme', 'dark');
await database.set<bool>('settings/biometricsEnabled', true);
await database.set<int>('sync/lastSuccessfulAtMs', 1785524116836);

final theme = await database.get<String>(
  'settings/theme',
  defaultValue: 'system',
);

get<T>() returns null for a missing key unless a defaultValue is supplied. Use the same type for every read and write of a key.

Atomic root-key batch #

await database.batch((batch) {
  batch.set<String>('settings/theme', 'dark');
  batch.set<bool>('settings/biometricsEnabled', true);
  batch.remove('cache/legacyPayload');
});

The batch becomes visible as one commit.

TTL for a flat key #

await database.set<String>(
  'cache/assignment-preview',
  responseBody,
  ttl: const Duration(minutes: 10),
);

After expiry, Preference treats the key as missing. TTL is logical expiry, not secure deletion; call compact() to reclaim expired and superseded file space.

await database.compact();

Use case 2: typed collections #

Use collections for application entities. A work order, asset, draft, or task gets a stable ID and is stored independently from the rest of the collection. Editing one asset does not require rewriting an entire work-order payload.

final assets = database.collection<Asset>('assets');

await assets.set(asset.id, asset);

final asset = await assets.get(assetId);
await assets.remove(assetId);

Collection records are internally namespaced as assets/<id>. Root keys and different collections cannot collide.

For offline-first backend data, a practical shape is:

workOrders/<workOrderUuid>
assets/<assetUuid>
syncOutbox/<operationUuid>
settings/<settingName>

Keep backend IDs as record IDs. Add model fields such as serverRevision, updatedAt, and syncState when your app needs conflict handling later.

Custom model serializers #

Preference deliberately does not guess how to serialize your models. Register a PreferenceSerializer<T> when opening the database. A JSON serializer keeps records understandable in debug inspection tools.

import 'dart:convert';
import 'dart:typed_data';
import 'package:preference_flutter/preference_flutter.dart';

class Asset {
  const Asset({required this.id, required this.title, required this.status});

  final String id;
  final String title;
  final String status;

  Map<String, Object?> toJson() => {
        'id': id,
        'title': title,
        'status': status,
      };

  factory Asset.fromJson(Map<String, dynamic> json) => Asset(
        id: json['id'] as String,
        title: json['title'] as String,
        status: json['status'] as String,
      );
}

class AssetSerializer implements PreferenceSerializer<Asset> {
  @override
  Type get type => Asset;

  @override
  Uint8List encode(Asset asset) =>
      Uint8List.fromList(utf8.encode(jsonEncode(asset.toJson())));

  @override
  Asset decode(Uint8List bytes) =>
      Asset.fromJson(jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>);
}

final database = await Preference.open(
  databasePath,
  serializers: [AssetSerializer()],
);

final assets = database.collection<Asset>('assets');

String, int, double, and bool serializers are built in. Values such as DateTime, lists, maps, and custom models need an application serializer (or an explicit string representation such as ISO-8601 JSON).

Indexes and queries #

Declare indexes when you need direct lookup by a model field. Non-unique indexes return all matching records. A unique: true index provides a single-record lookup; duplicate-value rejection is not yet part of this alpha, so do not use it as a business-rule constraint yet.

final assets = database.collection<Asset>(
  'assets',
  schema: CollectionSchema<Asset>(
    indexes: [
      Index<Asset>(
        name: 'status',
        selector: (asset) => asset.status,
      ),
      Index<Asset>(
        name: 'title',
        unique: true,
        selector: (asset) => asset.title,
      ),
    ],
  ),
);

final openAssets = await assets.find('status', 'open');
final extinguisher = await assets.findUnique('title', 'Fire extinguisher A');

For ad-hoc in-app filtering, use query. It scans that collection, so declare an index for repeated field lookups on larger datasets.

final urgentAssets = await assets.query(
  (asset) => asset.status == 'open',
  limit: 50,
  offset: 0,
);

Reactive updates #

Watch a single flat key:

final subscription = database.watch<String>('settings/theme').listen((theme) {
  print('Theme is now $theme');
});

await database.set<String>('settings/theme', 'dark');
await subscription.cancel();

Watch a single item or a complete collection:

assets.watchItem(assetId).listen((asset) {
  print('Asset changed: $asset');
});

assets.watch().listen((allAssets) {
  print('There are ${allAssets.length} assets');
});

Observe low-level committed changes for logging or future sync triggers:

database.changes.listen((change) {
  print('${change.transactionId}: ${change.type} ${change.key}');
});

Preference Studio and Flutter VM Service #

Preference Studio is coming soon. It is currently in development and testing. Its first public release is intentionally read-only: it is built to browse collections, records, JSON, and runtime activity without changing a user's application data. Record editing and other mutations are not supported yet.

When Studio is publicly available, connect it to a debug Flutter application by copying the Dart VM Service address printed by flutter run. Studio accepts the HTTP address and its WebSocket form, so no application-side server or npm process is required. Until Studio is publicly released, application logs and the database.changes stream remain the most reliable runtime visibility tools.

For a debug build, enable the inspector after opening the database:

final database = await Preference.open(
  databasePath,
  serializers: [AssetSerializer()],
  appId: 'com.example.field_notes',
  databaseId: 'field_notes',
);

database.enableInspector(); // Debug/development builds only.

Run the app with flutter run. Flutter prints a VM Service address similar to:

The Dart VM service is listening on
http://127.0.0.1:5xxxx/2xxxxxxxxxx=/

To connect from Preference Studio, use its WebSocket form:

ws://127.0.0.1:5xxxx/2xxxxxxxxxx=/ws

The port and token are generated per debug run; always use the address Flutter prints for the current session. Studio also accepts the corresponding http address and resolves the local debug-service redirect when possible. Do not enable remote inspection in production builds.

PostgreSQL, MySQL, SQLite/sqflite, and broader source visibility are planned Studio connectors. They are roadmap items, not alpha features.

How Preference compares with shared_preferences #

Preference can be used like shared_preferences for simple settings, but it also supports durable typed collections when your app outgrows flat values.

Need shared_preferences style Preference
Theme, flag, small counter Flat primitive key database.set<T>(key, value)
Read with fallback Getter with fallback database.get<T>(key, defaultValue: value)
Typed model Manual JSON around a string Custom serializer + collection<T>()
Stable entity records Key naming convention Collection ID + record ID
Related root updates Multiple writes Atomic database.batch()
React to a change App-managed state watch, watchItem, collection watch, and changes streams
Expiring cache entry App-managed timestamp Flat-key ttl

Choose shared_preferences when only small platform preference values are needed. Choose Preference when you want those simple keys and a path to typed local application data without introducing a separate store.

Security and data lifecycle #

Preference's alpha file backend is not encrypted at rest. Android/iOS app sandboxing helps protect private application files on ordinary devices, but it is not a replacement for cryptographic database encryption. Do not store passwords, private keys, or long-lived secrets in Preference alone.

Store small secrets and future database-key material in Android Keystore/iOS Keychain through a secure-storage solution. For security questions or responsible disclosure, email frostcraftdev@gmail.com.

Current alpha limits #

  • Web/IndexedDB storage is intentionally unavailable.
  • Encryption at rest and key management are not implemented.
  • Cloud sync, conflict resolution, and database migrations are not implemented.
  • TTL is available for flat keys only; collection set() does not yet accept a TTL parameter.
  • unique: true indexes provide lookup behavior only in this alpha; duplicate values are not rejected yet.
  • Collection query() is predicate-based; it is not SQL or full-text search.
  • Preference Studio is read-only and in active development/testing. Record editing, mutations, and non-Preference connectors are not alpha features.

Roadmap #

  • Security-reviewed authenticated encryption at rest and platform key storage.
  • Collection TTL, migrations, and richer query capabilities.
  • Web storage after it meets the native durability gate.
  • A public, read-only Preference Studio release, then safe editing/mutations, PostgreSQL/MySQL/SQLite-sqflite visibility, and local-first sync primitives.

Built deliberately for local-first apps #

Preference is not a wrapper around shared_preferences, SQLite, or another database. Its Dart-native storage engine owns the compact .pref binary format, append-only write log, transaction model, and recovery behavior.

Writes are committed as atomic batches and flushed to the local file before they complete. Each stored batch is integrity-checked, so Preference can safely recover from an interrupted or truncated write tail when the app opens the database again. Compaction later reclaims superseded and expired data.

We chose explicit serializers, explicit database locations, and a small public API over hidden ORM behavior or global state. The goal is simple: keep application data local, durable, understandable, and ready for better tooling over time. The engine is separate from inspection tooling through a protocol layer, which lets Preference Studio, diagnostics, and future sync evolve without changing how an app stores data.

Example #

See example/preference_example.dart for a small runnable field-note application that combines settings, a typed model, an index, a collection query, and a change stream.

License #

See LICENSE.

2
likes
140
points
60
downloads

Documentation

API reference

Publisher

verified publisherfrostcrafts.in

Weekly Downloads

Durable local-first storage for native Dart and Flutter apps.

Homepage

Topics

#storage #database #flutter #dart

License

unknown (license)

Dependencies

meta, preference_engine, preference_inspector

More

Packages that depend on preference_flutter