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

An offline-first, secure data repository with automatic sync, retry, and encryption for Dart & Flutter.

pub tag buymeacoffee sponsor patreon discord instagram license


Summary #

An offline-first, secure data repository for Flutter. You bring a NetworkAdapter (any REST-ish HTTP client) and pick a StorageAdapter (Sembast, sqflite, or SharedPreferences); ReliableRepository glues them together into an optimistic-write, cache-first read pipeline with retry, exponential backoff, AES-256 at rest, and a durable sync queue that survives app restarts.

  • Optimistic writes. Mutations land in RAM + disk immediately and are queued for sync; the UI never blocks on the network.
  • Cache-first reads. Four CacheStrategy modes (cacheOrElseNetwork, networkOrElseCache, cacheOnly, networkOnly) give you the right freshness vs. responsiveness trade-off per call.
  • Server-push integration. applyServerChange / applyServerDelete let SSE/WebSocket handlers feed into the same cache without enqueueing fake actions.
  • AES-256 at rest. Pluggable ReliableEncryption interface; the bundled ReliableAesEncryption uses a random IV per write.
  • User-scoped queueing. Set the active user and pending actions for other users are skipped until that user is active again.

Installation #

flutter pub add reliable

reliable is a Flutter package (it ships Flutter-only adapters for path_provider / shared_preferences / sqflite). Pure-Dart server code can't import it today — file an issue if you need a pure-Dart core split out.

Quick start #

import 'package:reliable/reliable.dart';

// 1. Implement NetworkAdapter for your HTTP client (Dio, http, custom, ...).
class MyApi implements NetworkAdapter {
  @override
  Future<dynamic> request(
    String endpoint,
    RequestMethod method, {
    dynamic data,
    Map<String, dynamic>? queryParams,
    Map<String, String>? headers,
  }) async {
    // ...call your HTTP client and return the decoded body
  }
}

// 2. Pick a storage backend, initialize, and construct the repository.
final storage = SembastAdapter(dbName: 'app.db');
await storage.init(version: 1, onUpgrade: (db, oldV, newV) async {});

final repo = ReliableRepository(api: MyApi(), storage: storage);
await repo.initialize(); // starts the 5s background sync heartbeat

// 3. Read with a cache strategy.
final todos = await repo.fetch(
  'todos',
  '/v1/todos',
  strategy: CacheStrategy.NETWORK_OR_ELSE_CACHE,
);

// 4. Write optimistically — queues for sync, returns immediately.
await repo.write(
  collection: 'todos',
  endpoint: '/v1/todos',
  method: RequestMethod.POST,
  data: {'title': 'Buy milk'},
);

// 5. Inspect / drain the queue.
final pending = await repo.getPendingCount();
await repo.syncNow(); // force a sync attempt instead of waiting for the heartbeat

A runnable Flutter demo with an "Online/Offline" toggle lives in example/.

Storage backends #

Adapter Best for Notes
SembastAdapter General-purpose, all platforms (mobile, desktop, web) Default choice. Cross-platform via conditional imports.
SharedPreferencesAdapter Tiny caches, key-value-shaped data Lightweight; not suitable for large collections.
SqliteAdapter Mobile-only, large datasets needing SQL Not exported from reliable.dartsqflite pulls in dart:ffi, which breaks web builds. Import directly: import 'package:reliable/src/adapters/sqlite_adapter.dart';

Encryption #

Wrap any adapter with ReliableAesEncryption for transparent at-rest encryption:

final storage = SembastAdapter(dbName: 'app.db');
await storage.init(
  version: 1,
  onUpgrade: (db, oldV, newV) async {},
  encryption: ReliableAesEncryption('your-32-character-secret-key!!!'),
);

The key must be exactly 32 characters (AES-256). Each write uses a fresh random IV.

Local cache migrations #

When your backend's response shape changes between app releases, locally-cached docs from older app versions still live on the user's device. Without migration, the new app code crashes parsing them. Register a DataMigration per collection and reliable brings stale docs forward on the next read:

final repo = ReliableRepository(
  api: MyApi(),
  storage: storage,
  dataMigrations: {
    'users': [
      // v0 → v1: rename `firstName` → `name`.
      DataMigration(
        toVersion: 1,
        migrate: (doc) => {
          ...doc,
          'name': doc['firstName'],
        }..remove('firstName'),
      ),
      // v1 → v2: ensure `verified` is always present.
      DataMigration(
        toVersion: 2,
        migrate: (doc) => {'verified': false, ...doc},
      ),
    ],
  },
);
  • Migrations are forward-only and applied in ascending toVersion order. Every cached doc's version is tracked under ReliableSchema.colSchemaVersion (a reserved internal field stripped from returned maps).
  • Freshly-fetched network docs are tagged with the latest version on write — they're assumed to already match the current schema.
  • A migration that throws drops just that doc and emits a REPO_MIGRATION_FAILED audit event; the rest of the collection still loads. The host can refetch from the network to recover.
  • Collections with no migrations registered cost nothing: the schema-version field is never written and reads never copy.

Using Dio (or any HTTP client) without a hard dependency #

reliable does not depend on Dio, http, or any specific HTTP client. You provide a NetworkAdapter that wraps whatever your app already uses. Canonical Dio wrapper:

import 'package:dio/dio.dart';
import 'package:reliable/reliable.dart';

class DioNetworkAdapter implements NetworkAdapter {
  DioNetworkAdapter(this.dio);
  final Dio dio;

  @override
  Future<dynamic> request(
    String endpoint,
    RequestMethod method, {
    dynamic data,
    Map<String, dynamic>? queryParams,
    Map<String, String>? headers,
  }) async {
    try {
      final response = await dio.request<dynamic>(
        endpoint,
        data: data,
        queryParameters: queryParams,
        options: Options(method: method.name.toUpperCase(), headers: headers),
      );
      return response.data;
    } on DioException catch (e) {
      // 4xx (except 401/408/429) is permanent — don't waste queue retries on it.
      final code = e.response?.statusCode;
      final fatal = code != null &&
          code >= 400 &&
          code < 500 &&
          code != 401 &&
          code != 408 &&
          code != 429;
      throw ReliableNetworkException(
        message: e.message ?? 'request failed',
        statusCode: code,
        isFatal: fatal,
      );
    }
  }
}

The translation rule that matters: convert client/protocol errors into ReliableNetworkException with isFatal set correctly. Fatal errors drop the action from the queue immediately; non-fatal errors get exponential backoff. Returning the decoded body (Map, List, or null) is the only other contract — reliable handles both raw lists and {data: [...]} envelopes.

Working with envelope-style responses #

If your endpoint returns the document as the payload (a translation map keyed by translation key, a flat config blob, etc.), use fetchOneRaw so the internally-injected id field doesn't pollute your lookup namespace:

final translations = await repo.fetchOneRaw(
  'translations',
  '/v1/translations',
  id: 'en_US',
);
// translations is just {'hello': 'Hello', 'world': 'World', ...} — no synthetic 'id' key.

For entity-shaped responses (the body has its own id), use fetchOne as normal.


🔍 For more information, refer to the API reference.


💬 Contributing and Discussions #

This is an open-source project, and we warmly welcome contributions from everyone, regardless of experience level. Whether you're a seasoned developer or just starting out, contributing to this project is a fantastic way to learn, share your knowledge, and make a meaningful impact on the community.

☝️ Ways you can contribute #

  • Find us on Discord: Feel free to ask questions and engage with the community here: https://discord.gg/gEQ8y2nfyX.
  • Share your ideas: Every perspective matters, and your ideas can spark innovation.
  • Help others: Engage with other users by offering advice, solutions, or troubleshooting assistance.
  • Report bugs: Help us identify and fix issues to make the project more robust.
  • Suggest improvements or new features: Your ideas can help shape the future of the project.
  • Help clarify documentation: Good documentation is key to accessibility. You can make it easier for others to get started by improving or expanding our documentation.
  • Write articles: Share your knowledge by writing tutorials, guides, or blog posts about your experiences with the project. It's a great way to contribute and help others learn.

No matter how you choose to contribute, your involvement is greatly appreciated and valued!

☕ We drink a lot of coffee... #

If you're enjoying this package and find it valuable, consider showing your appreciation with a small donation. Every bit helps in supporting future development. You can donate here: https://www.buymeacoffee.com/dev_cetera

LICENSE #

This project is released under the MIT License. See LICENSE for more information.

0
likes
160
points
117
downloads

Documentation

API reference

Publisher

verified publisherdev-cetera.com

Weekly Downloads

An offline-first, secure data repository with automatic sync, retry, and encryption for Dart & Flutter.

Homepage
Repository (GitHub)
View/report issues

Funding

Consider supporting this project:

www.buymeacoffee.com
www.patreon.com
github.com

License

MIT (license)

Dependencies

df_log, encrypt, flutter, path, path_provider, sembast, sembast_web, shared_preferences, sqflite, synchronized, uuid

More

Packages that depend on reliable