Counter App
A minimal counter application that demonstrates the core Reacton concepts: creating state with reacton(), deriving state with computed(), reading reactively with context.watch(), and modifying state with context.update() and context.set().
Full Source
import 'package:flutter/material.dart';
import 'package:flutter_reacton/flutter_reacton.dart';
// 1. Create reactons (top-level declarations)
final counterReacton = reacton(0, name: 'counter');
// 2. Derived state with computed
final doubleCountReacton = computed(
(read) => read(counterReacton) * 2,
name: 'doubleCount',
);
final isEvenReacton = computed(
(read) => read(counterReacton) % 2 == 0,
name: 'isEven',
);
void main() {
// 3. Wrap app with ReactonScope
runApp(ReactonScope(child: const CounterApp()));
}
class CounterApp extends StatelessWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Reacton Counter',
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
),
home: const CounterPage(),
);
}
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
// 4. Watch reactons - rebuilds automatically when values change
final count = context.watch(counterReacton);
final doubleCount = context.watch(doubleCountReacton);
final isEven = context.watch(isEvenReacton);
return Scaffold(
appBar: AppBar(
title: const Text('Reacton Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$count',
style: Theme.of(context).textTheme.displayLarge,
),
const SizedBox(height: 16),
Text(
'Double: $doubleCount',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
isEven ? 'Even' : 'Odd',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: isEven ? Colors.green : Colors.orange,
),
),
],
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'increment',
// 5. Update state
onPressed: () => context.update(counterReacton, (c) => c + 1),
child: const Icon(Icons.add),
),
const SizedBox(height: 12),
FloatingActionButton(
heroTag: 'decrement',
onPressed: () => context.update(counterReacton, (c) => c - 1),
child: const Icon(Icons.remove),
),
const SizedBox(height: 12),
FloatingActionButton(
heroTag: 'reset',
onPressed: () => context.set(counterReacton, 0),
child: const Icon(Icons.refresh),
),
],
),
);
}
}Walkthrough
Step 1: Declare Reactons
final counterReacton = reacton(0, name: 'counter');reacton() creates a WritableReacton<int> with an initial value of 0. The name parameter is optional but recommended for debugging and DevTools. Reactons are declared at the top level -- they are lightweight identity objects, not instances of state.
Step 2: Derive State with Computed
final doubleCountReacton = computed(
(read) => read(counterReacton) * 2,
name: 'doubleCount',
);
final isEvenReacton = computed(
(read) => read(counterReacton) % 2 == 0,
name: 'isEven',
);computed() creates a ReadonlyReacton that automatically recomputes when its dependencies change. The read function is used to access other reactons -- dependencies are tracked automatically. There is no need to declare a dependency list.
doubleCountReactonderives its value fromcounterReactonby multiplying by 2isEvenReactonderives a boolean fromcounterReacton
Both recompute automatically whenever counterReacton changes.
Step 3: Provide the Store
void main() {
runApp(ReactonScope(child: const CounterApp()));
}ReactonScope creates a ReactonStore and makes it available to the entire widget tree. Every descendant can use context.watch(), context.read(), context.set(), and context.update().
Step 4: Watch Reactons in Widgets
final count = context.watch(counterReacton);
final doubleCount = context.watch(doubleCountReacton);
final isEven = context.watch(isEvenReacton);context.watch() reads the current value and subscribes the widget to changes. When any watched reacton's value changes, the widget rebuilds automatically. No setState, no ChangeNotifier, no streams.
Step 5: Modify State
// Update with a function (increment/decrement)
onPressed: () => context.update(counterReacton, (c) => c + 1),
// Set directly (reset)
onPressed: () => context.set(counterReacton, 0),context.update()reads the current value, applies the function, and writes the resultcontext.set()writes a new value directly
Both trigger reactive propagation: counterReacton changes, which causes doubleCountReacton and isEvenReacton to recompute, which causes the widget to rebuild with new values.
Key Takeaways
- Reactons are declared at the top level -- They are identity objects, not state containers. The store holds the actual values.
- Computed reactons auto-track dependencies -- No need to manually specify what they depend on.
- Widgets are plain StatelessWidget -- No special base class needed. Just use
context.watch(). - State changes propagate automatically -- Modify a source reacton and everything downstream updates.
What's Next
- Todo App -- CRUD operations with filtering and computed counts
- Authentication -- State machine patterns
- Form Validation -- Complex forms with per-field validation