tiny_state 2.0.0
tiny_state: ^2.0.0 copied to clipboard
A tiny, zero-dependency, global reactive state manager for Flutter. Feels like a ValueNotifier, but global, keyed, and with computed state built in.
Changelog #
2.0.0 #
A full audit pass. Every finding below was reproduced with a test before it was fixed, and each now has a regression test.
tiny_state is now a zero-dependency package, and the API is stricter
about the things that used to fail silently.
Breaking changes #
shared_preferencesis no longer a dependency, andSharedPreferencesAdapterhas been removed. The package now depends on nothing but Flutter itself.TinyStatePersistenceAdapterremains, and a ready-to-copySharedPreferencesAdapter— with key namespacing and codec support — lives in the README and inexample/lib/src/persistence/shared_preferences_adapter.dart.TinyStatePersistenceAdapterrequires aremove(String key)method. Without it,resetanddeletecould not clear stored values.persistis now a property of the key. Declare it once atwatch(persist: true); thepersist:parameter is gone fromsetandupdate, which now honour the key's own setting. This removes the "I calledsetbut nothing was saved" failure mode entirely.selectrequires aString id. Results are memoized on(key, id), so callingselectfrombuildis now safe.- Unknown keys throw instead of failing silently.
set,update,resetandlistenthrow aStateErrornaming the key and pointing atwatch.deletestays idempotent. - Keys and scope names may not contain
/or be empty, andfutureis reserved as a scope name. This is what makes scoped keys collision-proof. UsetinyState.scope('user').watch('name', ...)instead ofwatch('user/name', ...). - Computed and regular state share one namespace. A key cannot be both;
deleteandlistennow work uniformly on either. clear()no longer resets configuration. It clears state and keeps your adapter,strictTypesandonError— which is what you want in a testsetUp.dispose()still resets everything.resetanddeletenow delete persisted entries for persisted keys.clearanddisposedeliberately do not: semantic operations touch storage, lifecycle operations do not.
Fixed #
selectleaked a listener on every call. It was the only API not memoized by key, so calling it frombuild— as the 1.x example app and README both did — added a new notifier and a new subscription on the source on every rebuild, retained forever. Now memoized on(key, id).resetanddeleteleft persisted values on disk, so the old value came back on the next launch.clear()could throw out of user code. Deleting a key re-evaluated its dependent computeds against the now-missing state, so any builder using!crashed mid-teardown. Teardown no longer re-evaluates, and a builder that throws during a normal re-evaluation is reported throughonErrorwith the previous value kept, instead of propagating out ofset/delete.- A computed could not depend on another computed.
getonly looked at regular state, so reading a computed key silently returnednull. Derived state now composes to any depth. - A nested computed left the outer one permanently stale. Reading another
computed's
.valuewas not tracked; bothget('other')and a direct.valueread now register a dependency. computedwas eager, not lazy as documented. A computed with no listeners re-ran on every dependency change. It is now genuinely lazy while unlistened and eager once a listener attaches.- A computed that read a not-yet-created key never woke up.
watchnow notifies dependents when it creates a key. - There was no way to remove a computed.
delete('key')ignored computeds, so re-registering a key silently returned the old builder.deletenow handles both kinds of key. computedandselectbypassed the type guard, failing with a raw_TypeErrorinstead of a useful message.- Nullable generics tripped the type guard.
watch<int?>followed byset<int>threw. Value-level checks now allow types that overlap in either direction; handing out a typed notifier is still checked exactly. - Scoped keys could collide with global ones.
scope('a').watch('b')andwatch('a/b')resolved to the same entry, as didwatchFuture('x')andscope('future').watch('x'). - Persistence errors vanished. Reads and writes were fire-and-forget with no
error path, so a throwing adapter produced an unhandled async error. All
adapter calls are now caught and reported through
onError. - Concurrent writes to one key could land out of order. Writes are now serialized per key.
get's documentation was wrong. It said a type mismatch returnsnull; it throws.listen(once: true)invoked the listener before unsubscribing, so a listener that changed state could re-enter. It now unsubscribes first.dispose()silently resetstrictTypesas a side effect.
Added #
TinyBuilder— the terse widget form:TinyBuilder<int>('counter', 0, (context, count) => Text('$count')), plusTinyBuilder.listen(...)for aselectorcomputedresult.TinyState.onError— one hook for persistence failures and computed builders that throw. Defaults toFlutterError.reportError.TinyState.flushPersistence()— completes when pending reads and writes have settled. For tests, and for flushing before teardown.MemoryPersistenceAdapter— an in-memory adapter for tests and demos.- A public
TinyState()constructor, for an isolated store.tinyStateremains the global singleton. TinyState.deleteFuture(key)— drops a cachedwatchFutureresult.TinyStateScopegainedwatchFuture,refreshFutureanddeleteFuture, and itsclear()now removes scoped computeds and futures too.
Changed #
- Split into
src/tiny_state.dart,src/persistence.dartandsrc/builder.dartbehind the samepackage:tiny_state/tiny_state.dartimport. - The analyzer runs with
strict-casts,strict-inference,strict-raw-types,public_member_api_docsandunawaited_futures. - The test suite went from 39 tests in one file to 117 across eight.
- CI now checks formatting,
analyze --fatal-infos, the example app, version consistency andpub publish --dry-run, and a tag-driven OIDC publish workflow was added. SeeRELEASING.md. environment.flutteris>=3.32.0, replacing a>=1.17.0constraint that could never have been satisfied alongside Dart 3.8.
Migrating from 1.x #
// Persistence: declare it once, at the watch that creates the key.
- tinyState.watch<int>('count', 0, persist: true);
- tinyState.set<int>('count', 1, persist: true);
+ tinyState.watch<int>('count', 0, persist: true);
+ tinyState.set<int>('count', 1);
// select: give each projection an id.
- tinyState.select<int, bool>('count', (n) => n.isEven);
+ tinyState.select<int, bool>('count', (n) => n.isEven, id: 'isEven');
// Keys with slashes become scopes.
- tinyState.watch<String>('user/name', '');
+ tinyState.scope('user').watch<String>('name', '');
// set/update/listen/reset now require the key to exist.
+ tinyState.watch<int>('count', 0); // call this first
tinyState.set<int>('count', 1);
// Test setUp: clear() keeps your adapter, dispose() drops it.
- setUp(() => tinyState.dispose());
+ setUp(tinyState.clear);
Custom adapters need a remove method:
class MyAdapter extends TinyStatePersistenceAdapter {
@override Future<T?> read<T>(String key) async { /* ... */ }
@override Future<void> write<T>(String key, T value) async { /* ... */ }
+ @override Future<void> remove(String key) async { /* ... */ }
}
If you used SharedPreferencesAdapter, copy
example/lib/src/persistence/shared_preferences_adapter.dart into your project
— it also fixes complex-type round-tripping, which the 1.x built-in could not
do for model classes.
1.1.0 #
- New:
TinyState.dispose()for full teardown (clears all states, computeds, adapter — singleton remains reusable). - New:
watchFuture(refresh: true)andtinyState.refreshFuture(key)to re-run a previously registered future. Stale completions from earlier generations are ignored. - New:
tinyState.strictTypes(defaulttrue) — throws a clearStateErroronset/update/getwith a generic that doesn't match the type the key was originally watched with. - New:
SharedPreferencesAdapteraccepts an optionalonError(key, error)callback for surfacing deserialization failures instead of swallowing them. - Improved: Computed dependency lookup is now O(1) via a reverse index, instead of an O(n_computed) scan on every
set/update/delete. - Fixed:
computed()now correctly tracks dependencies on initial construction. In v1.0.0 the dependency set was always empty, so computed states never recomputed when their inputs changed. - Fixed: Nested
computed()calls now use a stack-based tracker so each level registers its own dependencies independently. - Fixed:
watch(persist: true)no longer overwrites a value the usersetbefore the async load completed — the load is suppressed once the user has taken over. - Fixed:
select()notifiers are now disposed automatically when their source key isdeleted, preventing orphaned listeners on a disposed source. - Docs: Full README rewrite covering every public API, with an Installation section, Quick Start, Persistence walkthrough, and Type Safety note.
- Example: New "Persist" tab in the example app demonstrating a persisted note and a persisted counter side-by-side with an in-memory counter.
1.0.0 #
- Initial release of
tiny_state, a minimalistic and powerful state management library for Flutter. - Core features include
watch,set,get,update,reset, anddelete. - Advanced features include
select,computed,listen,scope,watchFuture, and state persistence. - Added a comprehensive
README.mdwith detailed documentation and examples. - Included a full example application to demonstrate all features.
