reactr 0.2.9
reactr: ^0.2.9 copied to clipboard
Reactr is a state management library for Flutter applications. It provides a simple and efficient way to manage the state of your application using controllers and bindings.
Reactr #
Reactr is a powerful and lightweight state management library for Flutter applications. It provides a simple, efficient, and reactive way to manage application state using controllers, bindings, and reactive data types. Built with performance and developer experience in mind, Reactr offers automatic dependency tracking, lifecycle management, and seamless integration with Flutter's widget system.
✨ Features #
- 🔄 Reactive State Management: Automatic dependency tracking and UI updates
- 🎯 Controller-Based Architecture: Clean separation of business logic and UI
- 🔗 Dependency Injection: Built-in service locator with singleton and lazy singleton support
- 🧭 Navigation Management: Simplified navigation with automatic controller lifecycle management
- 💾 Persistent Storage: Synchronous storage wrapper with SharedPreferences
- 🎨 UI Utilities: SnackBars, BottomSheets, and other UI helpers
- ⚡ Performance Optimized: Efficient reactive updates with minimal rebuilds
- 🛠️ Animation Support: Built-in ticker providers for animations
- 📱 Flutter Material App: Drop-in replacement with enhanced features
📦 Installation #
Add Reactr to your pubspec.yaml:
dependencies:
reactr: ^0.2.9
Then run:
flutter pub get
🚀 Quick Start #
1. Setup Your App #
Replace your MaterialApp with ReactrMaterialApp:
import 'package:flutter/material.dart';
import 'package:reactr/reactr.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ReactrMaterialApp(
title: 'Reactr Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
2. Create a Controller #
import 'package:reactr/reactr.dart';
class CounterController extends ReactrController {
// Reactive variables
final count = RcInt(0);
final name = RcString('Reactr');
final isVisible = RcBool(true);
// Nullable reactive variables
final optionalValue = RcnString(null);
@override
void onInit() {
super.onInit();
// Initialize your controller
print('CounterController initialized');
}
void increment() {
count.value++;
}
void decrement() {
count.value--;
}
void toggleVisibility() {
isVisible.value = !isVisible.value;
}
@override
void onClose() {
super.onClose();
// Clean up resources
print('CounterController disposed');
}
}
3. Create a Binding #
import 'package:reactr/reactr.dart';
import 'counter_controller.dart';
class CounterBinding extends ReactrBinding {
@override
void onBind() {
// Register controller when route is pushed
Reactr.putLazySingleton(() => CounterController());
}
@override
void unBind() {
// Clean up controller when route is popped
Reactr.remove<CounterController>();
}
}
4. Create a View #
import 'package:flutter/material.dart';
import 'package:reactr/reactr.dart';
import 'counter_controller.dart';
class CounterView extends ReactrView<CounterController> {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Reactive widget that rebuilds when count changes
React(
() => Text(
'Count: ${controller.count.value}',
style: Theme.of(context).textTheme.headlineMedium,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: controller.decrement,
child: const Icon(Icons.remove),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
],
),
const SizedBox(height: 20),
// Multi-reactive widget
MultiReact(
values: [controller.count, controller.isVisible],
builder: (values) {
final count = values[0] as int;
final isVisible = values[1] as bool;
return isVisible
? Text('Visible count: $count')
: const SizedBox.shrink();
},
),
],
),
),
);
}
}
5. Navigation #
// Navigate to a new screen
ElevatedButton(
onPressed: () => Reactr.to(
binding: CounterBinding(),
builder: () => const CounterView(),
),
child: const Text('Open Counter'),
),
// Navigate with named route
ElevatedButton(
onPressed: () => Reactr.toNamed(
binding: CounterBinding(),
routeName: '/counter',
),
child: const Text('Open Counter (Named)'),
),
// Replace current screen
Reactr.replace(
binding: CounterBinding(),
builder: () => const CounterView(),
),
// Navigate and clear stack
Reactr.replaceUntil(
binding: CounterBinding(),
builder: () => const CounterView(),
predicate: (route) => false, // Clear all routes
),
📚 Core Concepts #
Reactive Data Types #
Reactr provides several reactive data types that automatically trigger UI updates when their values change:
Basic Types
Rc<T>- Generic reactive containerRcInt- Reactive integerRcDouble- Reactive doubleRcString- Reactive stringRcBool- Reactive booleanRcList<T>- Reactive listRcMap<K, V>- Reactive map
Nullable Types
Rcn<T>- Generic nullable reactive containerRcnInt- Nullable reactive integerRcnDouble- Nullable reactive doubleRcnString- Nullable reactive stringRcnBool- Nullable reactive boolean
class MyController extends ReactrController {
final count = RcInt(0);
final name = RcString('Hello');
final items = RcList<String>(['item1', 'item2']);
final settings = RcMap<String, dynamic>({'theme': 'dark'});
final optionalValue = RcnString(null);
void updateData() {
count.value++;
name.value = 'Updated';
items.value.add('item3');
settings.value['theme'] = 'light';
optionalValue.value = 'Now has value';
}
}
Reactive Widgets #
React Widget
The React widget automatically rebuilds when any reactive variable used within it changes:
React(
() => Text('Count: ${controller.count.value}'),
)
MultiReact Widget
The MultiReact widget can listen to multiple reactive variables:
MultiReact(
values: [controller.count, controller.name, controller.isVisible],
builder: (values) {
final count = values[0] as int;
final name = values[1] as String;
final isVisible = values[2] as bool;
return isVisible
? Text('$name: $count')
: const SizedBox.shrink();
},
)
Controller Lifecycle #
Controllers have three lifecycle methods:
class MyController extends ReactrController {
@override
void onInit() {
super.onInit();
// Called when controller is created
// Initialize variables, load data, etc.
}
@override
void onReady() {
super.onReady();
// Called after the first frame is rendered
// Good place for post-frame initialization
}
@override
void onClose() {
super.onClose();
// Called when controller is disposed
// Clean up resources, cancel subscriptions, etc.
}
}
Dependency Injection #
Reactr provides a simple dependency injection system:
// Register a singleton
Reactr.putSingleton(MyController());
// Register a lazy singleton
Reactr.putLazySingleton(() => MyController());
// Check if registered
if (Reactr.contains<MyController>()) {
// Controller is registered
}
// Get controller instance
final controller = Reactr.find<MyController>();
// Remove controller
Reactr.remove<MyController>();
🎨 UI Utilities #
SnackBars #
Reactr.snackBar(
content: const Text('This is a snackbar'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 3),
action: SnackBarAction(
label: 'Undo',
onPressed: () => print('Undo pressed'),
),
);
Bottom Sheets #
Reactr.bottomSheet(
builder: (context) => Container(
height: 200,
child: const Center(
child: Text('This is a bottom sheet'),
),
),
isScrollControlled: true,
backgroundColor: Colors.white,
);
Context and Media Queries #
// Get context
final context = Reactr.context;
// Check if dark mode
if (Reactr.isDarkMode) {
// Dark mode is active
}
// Get screen dimensions
final width = Reactr.width;
final height = Reactr.height;
// Get navigation arguments
final args = Reactr.arguments;
💾 Storage #
ReactrStorage provides a synchronous wrapper around SharedPreferences:
class UserController extends ReactrController {
final username = RcString('');
@override
void onInit() {
super.onInit();
// Load saved username
username.value = ReactrStorage.getString('username') ?? '';
}
void saveUsername(String name) {
username.value = name;
ReactrStorage.setString('username', name);
}
void clearData() {
ReactrStorage.remove('username');
username.value = '';
}
}
Storage Methods #
// String operations
ReactrStorage.setString('key', 'value');
String? value = ReactrStorage.getString('key');
// Number operations
ReactrStorage.setInt('count', 42);
int? count = ReactrStorage.getInt('count');
ReactrStorage.setDouble('price', 19.99);
double? price = ReactrStorage.getDouble('price');
// Boolean operations
ReactrStorage.setBool('isLoggedIn', true);
bool? isLoggedIn = ReactrStorage.getBool('isLoggedIn');
// List operations
ReactrStorage.setStringList('tags', ['flutter', 'dart']);
List<String>? tags = ReactrStorage.getStringList('tags');
// Utility methods
ReactrStorage.remove('key');
bool hasKey = ReactrStorage.containsKey('key');
Set<String> keys = ReactrStorage.getKeys();
ReactrStorage.clear();
🎭 Animations #
Reactr provides mixins for animation support in controllers:
Single Animation Controller #
class AnimatedController extends ReactrController
with ReactrSingleTickerProviderStateMixin {
late AnimationController animationController;
late Animation<double> animation;
@override
void onInit() {
super.onInit();
animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
animation = Tween<double>(begin: 0, end: 1).animate(animationController);
}
void startAnimation() {
animationController.forward();
}
@override
void onClose() {
animationController.dispose();
super.onClose();
}
}
Multiple Animation Controllers #
class MultiAnimatedController extends ReactrController
with ReactrTickerProviderStateMixin {
late AnimationController fadeController;
late AnimationController slideController;
@override
void onInit() {
super.onInit();
fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
slideController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
@override
void onClose() {
fadeController.dispose();
slideController.dispose();
super.onClose();
}
}
🔧 Advanced Usage #
Custom Reactive Types #
You can create custom reactive types by extending Rc:
class RcUser extends Rc<User> {
RcUser(super.value);
void updateName(String name) {
value = value.copyWith(name: name);
}
bool get isAdult => value.age >= 18;
}
Route Arguments #
Pass data between screens:
// Navigate with arguments
Reactr.to(
binding: UserBinding(),
builder: () => const UserView(),
arguments: {'userId': 123, 'action': 'edit'},
);
// Access arguments in controller
class UserController extends ReactrController {
@override
void onInit() {
super.onInit();
final args = Reactr.arguments as Map<String, dynamic>?;
final userId = args?['userId'] as int?;
if (userId != null) {
loadUser(userId);
}
}
}
Global State Management #
For global state that persists across routes:
class GlobalController extends ReactrController {
final currentUser = Rc<User?>(null);
final theme = RcString('light');
void setTheme(String newTheme) {
theme.value = newTheme;
ReactrStorage.setString('theme', newTheme);
}
}
// Register globally in main()
void main() {
Reactr.putSingleton(GlobalController());
runApp(const MyApp());
}
📱 Example App #
Check out the complete example in the example/ directory:
cd example
flutter run
The example demonstrates:
- Basic counter functionality
- Navigation between screens
- SnackBars and BottomSheets
- Multiple reactive variables
- Controller lifecycle management
🤝 Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License #
This project is licensed under the MIT License - see the LICENSE file for details.
🔗 Links #
Made with ❤️ for the Flutter community