Reactiv
Production-Ready Reactive State Management for Flutter
Lightweight β’ Powerful β’ Type-Safe β’ Production-Ready
Get Started β’ Documentation β’ Examples β’ Features
π What is Reactiv?
Reactiv is a production-ready reactive state management solution for Flutter that combines simplicity with powerful features. Built with developer experience in mind, it offers:
- β Zero boilerplate - Write less code, do more
- β Type-safe - Compile-time checks prevent runtime errors
- β Zero memory leaks - Smart dependency injection with automatic cleanup
- β Undo/Redo - Built-in history tracking
- β Computed values - Auto-updating derived state
- β Debounce/Throttle - Performance optimization built-in
- β Robust Logger - Production-ready logging framework
- β 100% tested - Battle-tested in production apps
Why Choose Reactiv?
| Feature | Reactiv | GetX | Provider | Riverpod | BLoC |
|---|---|---|---|---|---|
| Lines of Code | βββββ | ββββ | βββ | βββ | ββ |
| Learning Curve | βββββ | ββββ | ββββ | βββ | ββ |
| Type Safety | β | β οΈ | β | β | β |
| Dependency Injection | β | β | β | β οΈ | β |
| Undo/Redo | β | β | β | β | β |
| Computed Values | β | β | β | β | β |
| Logger Framework | β | β | β | β | β |
| Package Size | ~150 APIs | 2400+ APIs | Medium | Large | Large |
π¦ Installation
Add to your pubspec.yaml:
dependencies:
reactiv: ^1.1.0
Then run:
flutter pub get
β‘ Quick Start
Step 1: Create a Reactive Controller
Create a controller that extends ReactiveController and define reactive variables:
import 'package:reactiv/reactiv.dart';
class CounterController extends ReactiveController {
// Define a reactive integer variable
final count = ReactiveInt(0);
// Method to increment the counter
void increment() {
count.value++;
}
}
Step 2: Inject the Controller
In your widget's initState method, inject the controller using the dependency injection system:
@override
void initState() {
super.initState();
// Inject the controller instance
Dependency.put<CounterController>(CounterController());
}
Step 3: Use ReactiveBuilder Widget
Use the ReactiveBuilder widget to listen to reactive variables and rebuild the UI when they change:
import 'package:flutter/material.dart';
import 'package:reactiv/reactiv.dart';
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
@override
void initState() {
super.initState();
Dependency.put<CounterController>(CounterController());
}
@override
Widget build(BuildContext context) {
final controller = Dependency.find<CounterController>();
return Scaffold(
appBar: AppBar(
title: const Text('Counter App'),
),
body: Center(
child: ReactiveBuilder<int>(
reactiv: controller.count,
builder: (context, count) {
return Text(
'Count: $count',
style: const TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}
What Just Happened?
-
ReactiveController: You created a controller that manages your application state. The
ReactiveIntvariable automatically notifies listeners when its value changes. -
Dependency Injection: You used
Dependency.put()to register the controller instance andDependency.find()to retrieve it. This ensures you're using the same instance throughout your app. -
ReactiveBuilder Widget: The
ReactiveBuilderwidget listens to thecountreactive variable. Whenevercount.valuechanges, only the ReactiveBuilder widget rebuildsβnot the entire screen. The builder receives the unwrapped value directly. -
State Update: When you call
controller.increment(), it updatescount.value, which automatically triggers the ReactiveBuilder to rebuild with the new value.
Alternative: Using ReactiveStateWidget
Reactiv provides ReactiveStateWidget as a convenient alternative to StatefulWidget that handles controller lifecycle management automatically.
With ReactiveStateWidget (Recommended)
import 'package:flutter/material.dart';
import 'package:reactiv/reactiv.dart';
class CounterScreen extends ReactiveStateWidget<CounterController> {
const CounterScreen({super.key});
@override
BindController<CounterController>? bindController() {
// Automatically injects and disposes the controller
return BindController(controller: () => CounterController());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter App'),
),
body: Center(
child: ReactiveBuilder(
reactiv: controller.count,
builder: (context, count) {
return Text(
'Count: $count',
style: const TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}
Benefits:
- Controller is automatically injected via
bindController() - Controller is automatically disposed when the widget is removed
- Access controller via
controllergetter (no need forDependency.find()) - Cleaner, less boilerplate code
Using ReactiveState with StatefulWidget
If you prefer StatefulWidget, you can use ReactiveState:
import 'package:flutter/material.dart';
import 'package:reactiv/reactiv.dart';
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends ReactiveState<CounterScreen, CounterController> {
@override
BindController<CounterController>? bindController() {
return BindController(controller: () => CounterController());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter App'),
),
body: Center(
child: ReactiveBuilder<int>(
reactiv: controller.count,
builder: (context, count) {
return Text(
'Count: $count',
style: const TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}
When to use ReactiveState:
- When you need StatefulWidget lifecycle methods
- When you need to manage additional local state
- When integrating with existing StatefulWidget code
π Key Features
1οΈβ£ Conditional Rebuilds (NEW in v1.1.0)
Control when widgets rebuild and listeners fire with buildWhen and listenWhen:
// Only rebuild on even numbers
ReactiveBuilder<int>(
reactiv: counter,
builder: (context, count) => Text('Count: $count'),
buildWhen: (prev, current) => current % 2 == 0,
listenWhen: (prev, current) => current > 10,
listener: (count) {
showDialog(context: context, builder: (_) => Alert('High: $count'));
},
)
// Conditional rebuild for multiple reactives
MultiReactiveBuilder(
reactives: [user, settings],
builder: (context) => UserProfile(user.value, settings.value),
buildWhen: () => user.value.isActive, // Only when user is active
listenWhen: () => settings.value.hasChanges, // Only on actual changes
listener: () => saveSettings(),
)
Benefits:
- β‘ Performance optimization - avoid unnecessary rebuilds
- π― Fine-grained control over UI updates
- π‘ Clean separation of concerns
2οΈβ£ Nullable Reactive Types
Full support for nullable values with dedicated types:
// Nullable reactive types
final username = ReactiveN<String>(null);
final age = ReactiveIntN(null);
final price = ReactiveDoubleN(null);
final isEnabled = ReactiveBoolN(null);
// Update values
username.value = 'john_doe';
age.value = 25;
price.value = 99.99;
isEnabled.value = true;
// Set back to null
username.value = null;
// Use with ReactiveBuilder
ReactiveBuilder<String?>(
reactiv: username,
builder: (context, name) {
return Text(name ?? 'Anonymous');
},
)
3οΈβ£ Undo/Redo Support (NEW in v1.0.0)
final text = ReactiveString('Hello', enableHistory: true);
text.value = 'World';
text.value = 'Flutter';
text.undo(); // Back to 'World'
text.redo(); // Forward to 'Flutter'
print(text.canUndo); // true
print(text.canRedo); // false
4οΈβ£ Computed Values (NEW in v1.0.0)
Auto-updating derived state:
final firstName = 'John'.reactiv;
final lastName = 'Doe'.reactiv;
final fullName = ComputedReactive(
() => '${firstName.value} ${lastName.value}',
dependencies: [firstName, lastName],
);
firstName.value = 'Jane';
// fullName automatically updates to 'Jane Doe'!
5οΈβ£ Debounce & Throttle (NEW in v1.0.0)
Perfect for search inputs and scroll events:
// Debounce - wait for user to stop typing
final searchQuery = ReactiveString('');
searchQuery.setDebounce(Duration(milliseconds: 500));
searchQuery.updateDebounced('flutter'); // Waits 500ms
// Throttle - limit update frequency
final scrollPosition = ReactiveInt(0);
scrollPosition.setThrottle(Duration(milliseconds: 100));
scrollPosition.updateThrottled(150); // Max once per 100ms
6οΈβ£ Lazy Dependency Injection (NEW in v1.0.0)
Controllers created only when needed:
// Register lazy builder - controller NOT created yet
Dependency.lazyPut(() => ExpensiveController());
// Controller created here, on first access
final controller = Dependency.find<ExpensiveController>();
7οΈβ£ Ever & Once Listeners (NEW in v1.0.0)
// Called on every change
count.ever((value) => print('New value: $value'));
// Called only once, then auto-removed
count.once((value) => showWelcomeDialog());
8οΈβ£ Multiple Reactive Variables
Observe multiple reactive variables:
// Use MultiReactiveBuilder for multiple reactives
MultiReactiveBuilder(
reactives: [firstName, lastName, age],
builder: (context) {
return Text('${firstName.value} ${lastName.value}, ${age.value}');
},
listener: () {
debugPrint('User info changed');
},
)
9οΈβ£ Type-Safe Reactive Types
Built-in types for common use cases:
// Non-nullable reactive types
final name = ReactiveString('John');
final age = ReactiveInt(25);
final score = ReactiveDouble(98.5);
final isActive = ReactiveBool(true);
final items = ReactiveList<String>(['A', 'B', 'C']);
final tags = ReactiveSet<String>({'flutter', 'dart'});
// Nullable reactive types
final username = ReactiveN<String>(null); // Generic nullable
final count = ReactiveIntN(null); // Nullable int
final price = ReactiveDoubleN(null); // Nullable double
final isEnabled = ReactiveBoolN(null); // Nullable bool
// Or use generic for custom types
final user = Reactive<User>(User());
final optionalUser = ReactiveN<User>(null);
Using with ReactiveBuilder:
// Non-nullable
ReactiveBuilder<int>(
reactiv: age,
builder: (context, value) => Text('Age: $value'),
)
// Nullable
ReactiveBuilder<int?>(
reactiv: count,
builder: (context, value) => Text('Count: ${value ?? 0}'),
)
π Smart Dependency Management
// Register with overwrite warning
Dependency.put(MyController());
// Conditional registration
Dependency.putIfAbsent(() => MyController());
// Check if registered
if (Dependency.isRegistered<MyController>()) {
// Use it
}
// Fenix mode - auto-recreate after deletion
Dependency.put(MyController(), fenix: true);
// Reset all dependencies
Dependency.reset();
1οΈβ£1οΈβ£ Robust Logger Framework (NEW in v1.0.1)
Production-ready logging with multiple levels, JSON formatting, and performance tracking:
// Configure for your environment
Logger.config = kReleaseMode
? LoggerConfig.production // Minimal logging
: LoggerConfig.development; // Verbose logging
// Multiple log levels
Logger.v('Verbose trace information');
Logger.d('Debug diagnostic info');
Logger.i('General information');
Logger.w('Warning - potential issue');
Logger.e('Error occurred', error: e, stackTrace: stack);
Logger.wtf('Critical failure!');
// Pretty JSON logging
Logger.json({
'user': 'John',
'preferences': {'theme': 'dark'},
});
// Performance timing
final data = await Logger.timed(
() => api.fetchUsers(),
label: 'API Call',
);
// Table formatting
Logger.table([
{'name': 'John', 'age': 30},
{'name': 'Jane', 'age': 25},
]);
// Headers and dividers
Logger.header('USER REGISTRATION');
Logger.divider();
Logger Features:
- π¨ 6 Log Levels: Verbose, Debug, Info, Warning, Error, WTF
- π Pretty JSON: Automatic indentation and formatting
- β±οΈ Performance Timing: Measure async/sync function execution
- π Table Formatting: Display structured data beautifully
- π― Stack Traces: Full error debugging support
- π ANSI Colors: Color-coded terminal output
- βοΈ Configurable: Dev/Production/Testing presets
- π Custom Handlers: Integrate with analytics services
Quick Configuration:
Logger.config = LoggerConfig(
enabled: true,
minLevel: LogLevel.debug,
showTimestamp: true,
prettyJson: true,
);
π Full Logger Documentation
π Documentation
Core Concepts
- π Getting Started Guide - Step-by-step tutorial
- π API Reference - Complete API documentation
- π Advanced Patterns - Best practices & patterns
- β‘ Quick Reference - Cheat sheet
- π What's New in v1.0.0 - New features guide
- π Logger Framework - Complete logging documentation
Migration & Upgrading
- β From 0.3.x to 1.0.0: No changes needed! 100% backward compatible
- β From GetX: Similar API, easier to learn
- β From Provider/Riverpod: Less boilerplate, same power
π‘ Examples
Basic Counter
final count = ReactiveInt(0);
ReactiveBuilder<int>(
reactiv: count,
builder: (context, value) => Text('Count: $value'),
)
Search with Debounce
class SearchController extends ReactiveController {
final searchQuery = ReactiveString('');
final results = ReactiveList<String>([]);
SearchController() {
searchQuery.setDebounce(Duration(milliseconds: 500));
searchQuery.ever((query) => _performSearch(query));
}
void updateQuery(String query) {
searchQuery.updateDebounced(query);
}
Future<void> _performSearch(String query) async {
if (query.isEmpty) {
results.clear();
return;
}
final data = await api.search(query);
results.value = data;
}
}
Form with Undo/Redo
class FormController extends ReactiveController {
final name = ReactiveString('', enableHistory: true);
final email = ReactiveString('', enableHistory: true);
bool get canUndo => name.canUndo || email.canUndo;
void undoAll() {
if (name.canUndo) name.undo();
if (email.canUndo) email.undo();
}
}
Shopping Cart with Computed Total
class CartController extends ReactiveController {
final items = ReactiveList<CartItem>([]);
late final ComputedReactive<double> total;
CartController() {
total = ComputedReactive(
() => items.fold(0.0, (sum, item) => sum + item.price),
dependencies: [items],
);
}
}
Logger with Performance Tracking
class ApiController extends ReactiveController {
final users = ReactiveList<User>([]);
Future<void> fetchUsers() async {
// Automatically logs execution time
await Logger.timed(
() async {
Logger.i('Fetching users from API', tag: 'Network');
try {
final response = await api.getUsers();
// Log response as pretty JSON
Logger.json(response, tag: 'Network');
users.value = response.users;
Logger.i('Successfully loaded ${users.length} users');
} catch (e, stack) {
// Log error with full stack trace
Logger.e(
'Failed to fetch users',
tag: 'Network',
error: e,
stackTrace: stack,
);
}
},
label: 'Fetch Users API',
tag: 'Network',
);
}
}
π See Full Examples
β The GetX alternative that does it right
- Explicit Listening - ReactiveBuilder knows exactly what it's listening to (no magic)
- Compile-Time Safety - Red lines if you forget to specify what to listen to
- Optimized by Design - Encourages writing performant code
- Focused Package - ~150 APIs vs GetX's 2400+ APIs
- Better Performance - Batched updates, optimized rebuilds
- Production Features - Undo/redo, computed values, debounce built-in
Code Comparison
Reactiv:
ReactiveBuilder<int>(
reactiv: controller.count, // Explicit - you know what updates this
builder: (context, count) => Text('$count'),
)
GetX:
Obx(() {
// Implicit - any reactive var here triggers rebuild
final count = controller.count.value;
return Text('$count');
})
π― Best Practices
β Do's
// β
Use ReactiveBuilder for single reactives
ReactiveBuilder<int>(
reactiv: count,
builder: (context, val) => Text('$val'),
)
// β
Use MultiReactiveBuilder for multiple reactives
MultiReactiveBuilder(
reactives: [firstName, lastName],
builder: (context) => Text('${firstName.value} ${lastName.value}'),
)
// β
Enable history only when needed
final text = ReactiveString('', enableHistory: true);
// β
Use computed for derived state
final total = ComputedReactive(() => items.sum(), [items]);
// β
Debounce expensive operations
searchQuery.setDebounce(Duration(milliseconds: 500));
// β
Use lazy loading for heavy controllers
Dependency.lazyPut(() => HeavyController());
// β
Configure Logger for production
Logger.config = kReleaseMode
? LoggerConfig.production
: LoggerConfig.development;
// β
Use appropriate log levels
Logger.d('User navigated to screen');
Logger.e('Error occurred', error: e, stackTrace: stack);
β Don'ts
// β Don't use ReactiveBuilder at root of entire page
ReactiveBuilder(
reactiv: controller.anything,
builder: (context, _) => EntirePageWidget(), // Rebuilds everything!
)
// β Don't enable history on everything
final x = ReactiveInt(0, enableHistory: true); // Unless you need it
// β Don't forget to dispose
// Always call Dependency.delete() or use autoDispose
// β Don't log sensitive data
Logger.info('User password: $password'); // NEVER!
// β Don't use verbose logging in production
Logger.config = LoggerConfig.development; // In release builds
π§ Configuration
Logger Configuration for Different Environments
import 'package:flutter/foundation.dart';
import 'package:reactiv/reactiv.dart';
void main() {
// Configure logger based on environment
if (kReleaseMode) {
Logger.config = LoggerConfig.production; // Minimal logging
} else if (kProfileMode) {
Logger.config = LoggerConfig.testing; // Warnings & errors
} else {
Logger.config = LoggerConfig.development; // Full logging
}
runApp(MyApp());
}
Custom Logger Configuration
Logger.config = LoggerConfig(
enabled: true,
minLevel: LogLevel.debug,
showTimestamp: true,
showLevel: true,
prettyJson: true,
customHandler: (level, message, {tag}) {
// Send to your analytics service
if (level.index >= LogLevel.error.index) {
crashlytics.log(message);
}
},
);
---
## π Performance
Reactiv is designed for performance:
- β
**Batched Updates**: Multiple mutations = single rebuild
- β
**Optimized Observers**: No unnecessary widget rebuilds
- β
**Smart Memory Management**: Automatic cleanup, zero leaks
- β
**Lazy Loading**: Create controllers only when needed
- β
**Debounce/Throttle**: Built-in performance optimization
---
## π€ Contributing
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
### Contributors
Thanks to all contributors! β€οΈ
---
## π License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
---
## π Show Your Support
If you like Reactiv, please:
- β Star the repo on [GitHub](https://github.com/therdm/reactiv)
- π Like the package on [pub.dev](https://pub.dev/packages/reactiv)
- π’ Share with the Flutter community
- π Report issues or suggest features
---
## π Support & Community
- π [Issue Tracker](https://github.com/therdm/reactiv/issues)
- π¬ [Discussions](https://github.com/therdm/reactiv/discussions)
- π§ Email: [Your Email]
- π¦ Twitter: [Your Twitter]
---
<div align="center">
**Made with β€οΈ for the Flutter Community**
[Get Started](#-quick-start) β’ [View on GitHub](https://github.com/therdm/reactiv) β’ [pub.dev](https://pub.dev/packages/reactiv)
</div>
Libraries
- controllers/reactive_controller
- dependency_management/dependency
- reactiv
- state_management/core/reactive_notifier
- state_management/reactive_types
- state_management/reactive_types/iterator/reactive_map
- state_management/widgets/observer
- state_management/widgets/reactive_builder
- utils/exceptions
- Custom exceptions for the Reactiv package
- utils/extensions
- utils/extensions/int_extention
- utils/extensions/reactiv_ext
- utils/logger
- views/bind_controller
- views/reactive_state
- views/reactive_state_widget