Readme
Stratum
A Flutter state management system that makes state lifetime a first-class concept.
Stratum provides three primitives aligned to lifetime:
Atom<T> for local widget state, FlowController<S, E, SE> for route-scoped flows,
and AppStore<S> for app-wide state, with a unified AsyncValue<T> for async data.
Changelog
See CHANGELOG.md.
Example
One Page Demo (Stratum APIs)
import 'package:flutter/material.dart';
import 'package:stratum/stratum.dart';
void main() {
Stores.initialize([
MapEntry<String, AppStore<dynamic>>('counter', CounterStore()),
]);
runApp(
StoreScope(
stores: [Stores.get<CounterState>('counter')],
child: const StratumDemoApp(),
),
);
}
class StratumDemoApp extends StatelessWidget {
const StratumDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stratum Demo',
home: FlowScope<CheckoutFlow, CheckoutState, CheckoutEvent, CheckoutEffect>(
create: CheckoutFlow.new,
onSideEffect: (context, effect) {
switch (effect) {
case CheckoutEffectShowSnack(:final message):
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
case CheckoutEffectNavigateSuccess():
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const SuccessPage(),
),
);
}
},
child: const DemoPage(),
),
);
}
}
class CounterState {
const CounterState(this.value);
final int value;
}
class CounterStore extends AppStore<CounterState> {
CounterStore() : super(const CounterState(0));
void increment() => setState(CounterState(state.value + 1));
}
sealed class CheckoutEvent {
const CheckoutEvent();
}
class CheckoutEventLoad extends CheckoutEvent {
const CheckoutEventLoad();
}
class CheckoutEventSubmit extends CheckoutEvent {
const CheckoutEventSubmit();
}
sealed class CheckoutState {
const CheckoutState();
}
class CheckoutIdle extends CheckoutState {
const CheckoutIdle();
}
class CheckoutLoading extends CheckoutState {
const CheckoutLoading();
}
class CheckoutSuccess extends CheckoutState {
const CheckoutSuccess();
}
sealed class CheckoutEffect {
const CheckoutEffect();
}
class CheckoutEffectShowSnack extends CheckoutEffect {
const CheckoutEffectShowSnack(this.message);
final String message;
}
class CheckoutEffectNavigateSuccess extends CheckoutEffect {
const CheckoutEffectNavigateSuccess();
}
class CheckoutFlow
extends FlowController<CheckoutState, CheckoutEvent, CheckoutEffect> {
CheckoutFlow() : super(const CheckoutIdle());
@override
Stream<CheckoutState> handle(CheckoutEvent event) async* {
switch (event) {
case CheckoutEventLoad():
yield const CheckoutLoading();
await Future<void>.delayed(const Duration(milliseconds: 300));
yield const CheckoutIdle();
case CheckoutEventSubmit():
yield const CheckoutLoading();
await Future<void>.delayed(const Duration(milliseconds: 300));
emit(const CheckoutEffectShowSnack('Order placed'));
emit(const CheckoutEffectNavigateSuccess());
yield const CheckoutSuccess();
}
}
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
final Atom<int> localCount = Atom(0);
final Atom<AsyncValue<String>> asyncAtom =
Atom<AsyncValue<String>>(const AsyncLoading<String>());
late final Atom<String> label;
VoidCallback? _effectDispose;
@override
void initState() {
super.initState();
label = computed<String>(() => 'Local: ${localCount.value}');
_effectDispose = effect(() {
final value = localCount.value;
debugPrint('Local count changed: $value');
});
_loadAsync();
}
Future<void> _loadAsync() async {
asyncAtom.value = const AsyncLoading<String>();
await Future<void>.delayed(const Duration(milliseconds: 200));
asyncAtom.value = const AsyncData<String>('Async ready');
}
@override
void dispose() {
_effectDispose?.call();
label.dispose();
localCount.dispose();
asyncAtom.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final flowState = context.watchFlow<CheckoutFlow, CheckoutState>();
final counterStore = Stores.getStore<CounterStore>('counter');
final counterState = context.watchStore(counterStore);
return Scaffold(
appBar: AppBar(title: const Text('Stratum Demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text('AppStore count: ${counterState.value}'),
const SizedBox(height: 12),
ElevatedButton(
onPressed: counterStore.increment,
child: const Text('Increment AppStore'),
),
const Divider(height: 32),
AtomBuilder(
builder: (context) {
final display = label.value;
final local = localCount.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(display),
const SizedBox(height: 8),
Text('Local count: $local'),
],
);
},
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => localCount.update((v) => v + 1),
child: const Text('Increment Local Atom'),
),
const Divider(height: 32),
AtomBuilder(
builder: (context) {
return asyncAtom.value.when(
loading: () => const Text('AsyncValue: loading...'),
data: (data) => Text('AsyncValue: $data'),
errorCallback: (error, _) => Text('AsyncValue: $error'),
);
},
),
const Divider(height: 32),
Text('Flow state: ${flowState.runtimeType}'),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => context
.readFlow<CheckoutFlow>()
.add(const CheckoutEventLoad()),
child: const Text('Start Flow Load'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => context
.readFlow<CheckoutFlow>()
.add(const CheckoutEventSubmit()),
child: const Text('Submit Flow'),
),
],
),
);
}
}
class SuccessPage extends StatelessWidget {
const SuccessPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: Text('Success')),
);
}
}
Installing
Add stratum to your pubspec.yaml:
dependencies:
stratum: ^0.1.0
Then run flutter pub get.
Versions
Current version: 0.1.0
Scores
Not applicable.
Admin
Maintainers: project owners.
Activity log
Design: see DESIGN.md.
Implementation plan: see IMPLEMENTATION.md.