Flutter Reactive
Flutter Reactive is a lightweight reactive state package for Flutter. It gives you local widget state, shared stores, derived values, and automatic rebuilds — without code generation or extra boilerplate.
Documentation
- Official docs: flutterreactive.com
- Docs source: doc/
- Source and examples: example/
Features
- Observable values with
Reactive<T>andReactiveN<T> - Local widget state with
react()andreactN() - Automatic rebuilds with
ReactiveBuilderandReactiveStateBuilder - Shared stores with
ReactiveDependencyandRxDep - Derived values with
as,compute, andcombine - Transactions, validators, save/restore checkpoints, and stream listeners
- Helper extensions for numbers, booleans, strings, iterables, lists, and maps
Installation
dart pub add flutter_reactive
import 'package:flutter_reactive/flutter_reactive.dart';
Quick Start
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
late final counter = react(0);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Counter: ${counter.value}'),
const SizedBox(height: 12),
ElevatedButton(
onPressed: counter.increment,
child: const Text('Increment'),
),
],
);
}
}
react() creates a Reactive<T> and binds it to the current State, so updates trigger setState() automatically.
Widget Binding
Use ReactiveBuilder when you want a widget to rebuild from reactive reads:
ReactiveBuilder(() {
return Text('Counter: ${counter.value}');
});
Or watch a specific reactive explicitly:
ReactiveBuilder.watch(counter, (value) {
return Text('Counter: $value');
});
ReactiveBuilder.stream(...) is also available when you prefer Flutter's StreamBuilder API.
For local widget state machines, use ReactiveStateBuilder:
ReactiveStateBuilder<bool>(
initialState: false,
states: {
false: (state) => ElevatedButton(
onPressed: state.enable,
child: const Text('Open'),
),
true: (state) => ElevatedButton(
onPressed: state.disable,
child: const Text('Close'),
),
},
);
Derived State
final price = 100.rx;
final quantity = 2.rx;
final total = compute(() => price.value * quantity.value);
final label = total.as((value) => 'Total: \$${value.toStringAsFixed(0)}');
final summary = combine2(
price,
quantity,
(p, q) => '$q × \$${p.toStringAsFixed(0)}',
);
compute, combine, and the typed helpers (combine2 to combine5) return read-only reactives. Trying to set a value on them throws a state error.
Values can be accessed via .value, .v, or by calling the instance directly counter().
Side Effects
final counter = 0.rx;
final sub = counter.listen((value) {
debugPrint('Counter changed: $value');
}, true);
sub.cancel(); // Unsubscribe when no longer needed
counter.once((value) {
debugPrint('First value: $value');
});
counter.when((value) => value == 10, (value) {
debugPrint('Reached $value');
});
listen works independently from the widget tree. It is useful for logging, syncing, analytics, or imperative side effects.
Shared Dependencies
class UserStore extends ReactiveDependency {
final name = 'Alice'.rx;
void rename(String value) => name.value = value;
}
final store = UserStore().dep;
store.rename('Bob');
store.dispose();
dep registers a single instance per type, which makes it convenient for shared stores and service-like objects.
Mutable Models
class User {
User(this.name, this.age);
String name;
int age;
}
final user = User('Alice', 30).rx
.require((value) => value.age >= 0, 'Age cannot be negative')
.require((value) => value.name.trim().isNotEmpty, 'Name cannot be empty');
user.mutate((value) {
value.name = 'Bob';
value.age = 31;
});
Use mutate when you intentionally update an object in place. For immutable updates, prefer assigning a new value instead.
Transactions, Validation, and Checkpoints
final stock = <String, int>{'Latte': 3}.rx;
final sold = <String>[].rxNonStrict;
await rxRun(() {
stock.put('Latte', stock.get('Latte')! - 2);
sold.add('ticket-1');
});
If an error is thrown, changes are rolled back by default. You can also disable automatic rollback and handle it manually through the returned transaction.
Save and restore checkpoints when you want lightweight state snapshots:
final counter = 0.rx;
counter.save('draft');
counter.increment(5);
counter.restore('draft');
Collection And Primitive Helpers
flutter_reactive.dart exports helpers for common reactive data types:
num:increment,decrement,inc,dec,clamp,roundTobool:toggle,enable,disableString:trim,append,prepend,toUpper,toLowerIterable/List:add,addFirst,addAll,remove,sort,transform,at,atOrNullMap:put,remove,get,has
A quick example:
final name = ' Flutter '.rx;
name.trim();
name.toUpper();
name.append(' Reactive');
final items = <int>[2, 5, 1].rxNonStrict;
items.addFirst(9);
items.sort();
final settings = <String, dynamic>{}.rx;
settings.put('theme', 'dark');
Best Practices
- Prefer immutable updates whenever possible.
- Use
rxNonStrictfor mutable collections or repeated equal values that should still notify listeners. - Use
mutateonly when in-place mutation is intentional. - Dispose long-lived reactives or shared stores when they are no longer needed.
License
MIT. See LICENSE.
Example
See example/ for a complete Flutter sample app.