OneStore

A lightweight, zero-boilerplate state persistence library for Flutter. It lets you persist any model to disk with JSON, reactively listen to changes, and rebuild widgets only when selected values change.

OneStore is ideal for apps needing persistent local state without complex databases, streams, or bloated architecture.


๐Ÿš€ Features

  • Persistent Store โ€“ Automatically save/load JSON models to local storage.
  • Selector-Based Rebuilds โ€“ Widgets rebuild only when the selected slice of state changes.
  • Simple API โ€“ Set state, load state, and select data.
  • No Streams Required โ€“ Powered by ValueNotifier.
  • Safe Writes โ€“ Built-in async write locking prevents file corruption.
  • Fully Testable โ€“ Includes widget-friendly selector components and predictable behavior.

๐Ÿ“ฆ Installation

Add the package to your pubspec.yaml:

dependencies:
  one_store: ^1.0.0

Then import it:

import 'package:one_store/one_store.dart';

๐Ÿง  How It Works

You define a model that extends Persistable<T>:

  • toJson() converts the model to a serializable map.
  • fromString() rebuilds the model from a JSON string.

OneStore<T> wraps this model and:

  • Loads saved JSON on startup
  • Provides reactive selectors
  • Saves updated state on widget disposal / state changes

๐Ÿ“ Example: Create a Persistable Model

class CounterState extends Persistable<CounterState> {
  final int count;
  CounterState(this.count);

  @override
  Map<String, dynamic> toJson() => {
    'count': count,
  };

  @override
  CounterState fromString(String jsonString) {
    final decoded = jsonDecode(jsonString);
    return CounterState(decoded['count']);
  }
}

๐Ÿ—ƒ๏ธ Using OneStore

Initialize the store

't load automatically โ€” you control when to load.

final store = OneStore(CounterState(0));
await store.load();

Update state

store.setState(CounterState(store.state!.count + 1));

Read part of the state

final count = store.getState((s) => s?.count ?? 0);

๐Ÿ” Selector-Based UI Updates

OneStore provides createComponent() which only rebuilds when the selected value changes.

store.createComponent<int>(
  (state) => state?.count ?? 0,
  (context, count) => Text('Count: $count'),
)

This ensures:

  • No full widget rebuilds
  • No unnecessary UI updates
  • Better performance for large reactive states

๐Ÿ’พ Persistence Behavior

The library saves the JSON string to a single file:

local_data.txt

Includes:

  • Async write locking โ†’ avoids corrupted files
  • Automatic loading via store.load()

Excludes (on purpose):

  • No background automatic saving
  • No file watching

If you want automatic debounced saving, you can add it manually.


๐Ÿงช Testing

Benchmark save/load

test('Save/load benchmark', () async {
  final store = OneStore(BigState(List.generate(100000, (i) => i)));
  await store.load();

  final sw = Stopwatch()..start();
  await store.state!.saveStringLocally(store.state.toString());
  sw.stop();

  print('Save took: ${sw.elapsedMilliseconds} ms');
});

Selector build frequency

testWidgets('Selector test', (tester) async {
  int builds = 0;
  final store = OneStore(CounterState(0));
  await store.load();

  await tester.pumpWidget(
    store.createComponent((s) => s?.count ?? 0, (c, v) {
      builds++;
      return Text('$v');
    })
  );

  for (var i = 0; i < 10; i++) {
    store.setState(CounterState(i));
    await tester.pump();
  }

  expect(builds <= 15, true);
});

๐Ÿ›ก๏ธ Thread-Safe Writing

OneStore includes a simple async lock:

static bool _isWriting = false;

It prevents overlapping writes that can corrupt JSON.

If you need:

  • Atomic writes
  • Multi-file support
  • Encrypted storage
  • Isolate-based serialization

โ†’ Open an issue, or request a feature.


๐Ÿ“‚ File Storage Path

By default, files are stored at:

project root โ†’ local_data.txt

Inside a real Flutter app, this resolves to the app sandbox directory.

You can also override this behavior if needed.


๐Ÿ”ง Advanced Usage

Custom File Names

You can override save/load paths inside your model.

Debounced Auto-Save

Call saveStringLocally() from inside setState() if you want live syncing.

Split Stores

You can maintain multiple OneStores for different slices of state.


๐Ÿค Contributing

PRs and suggestions are welcome! Feel free to open an issue for bugs, feature requests, or improvements.


๐Ÿ“œ License

MIT License โ€” free to use in commercial and open-source projects.


โค๏ธ Support

If you like this package, please give it a โญ on GitHub and pub.dev!