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.

Libraries

main
stratum