Basic Usage
Learn the fundamentals of reactiv - reactive values, observers, and controllers
Basic Usage
Now that you have reactiv installed, let's explore the core concepts that make reactive state management simple and powerful.
Core Concepts
reactiv is built around three main concepts:
- Reactive Values - Store your app's state
- Observers - Watch for changes and update UI
- Controllers - Organize and manage related state
Reactive Values
Reactive values are the foundation of reactiv. They hold your application state and automatically notify listeners when they change.
Creating Reactive Values
import 'package:reactiv/reactiv.dart';
// Basic reactive types
final counter = ReactiveInt(0);
final userName = ReactiveString('Guest');
final isLoggedIn = ReactiveBool(false);
final items = ReactiveList<String>(['apple', 'banana']);
final settings = ReactiveMap<String, dynamic>({'theme': 'light'});
// Generic reactive value
final customObject = ReactiveValue<User>(User.empty());
Using Extension Methods
reactiv provides convenient extension methods for quick reactive value creation:
// These are equivalent to the above
final counter = 0.reactiv;
final userName = 'Guest'.reactiv;
final isLoggedIn = false.reactiv;
final items = ['apple', 'banana'].reactiv;
final settings = {'theme': 'light'}.reactiv;
Reading and Writing Values
// Reading values
print(counter.value); // 0
print(userName.value); // 'Guest'
// Writing values
counter.value = 10;
userName.value = 'John Doe';
isLoggedIn.value = true;
// List operations
items.add('orange');
items.removeAt(0);
// Map operations
settings['theme'] = 'dark';
settings.remove('oldKey');
ReactiveBuilder
ReactiveBuilder widgets watch reactive values and rebuild UI when changes occur. They're the bridge between your reactive state and Flutter widgets.
Basic ReactiveBuilder Usage
class CounterWidget extends StatelessWidget {
final counter = ReactiveInt(0);
Widget build(BuildContext context) {
return ReactiveBuilder(
reactiv: counter,
builder: (context, value) {
return Text('Count: $value');
},
);
}
}
Multiple Reactive Values
class UserProfileWidget extends StatelessWidget {
final userName = ReactiveString('John');
final userAge = ReactiveInt(25);
Widget build(BuildContext context) {
return ReactiveBuilderN(
listenables: [userName, userAge], // Watch multiple values
builder: (context) {
return Column(
children: [
Text('Name: ${userName.value}'),
Text('Age: ${userAge.value}'),
],
);
},
);
}
}
ReactiveBuilder with Listener
For more complex UI logic:
ReactiveBuilder(
reactiv: isLoggedIn,
builder: (context, loggedIn) {
if (loggedIn) {
return DashboardScreen();
} else {
return LoginScreen();
}
},
)
You can also add a listener callback for side effects:
ReactiveBuilder(
reactiv: counter,
builder: (context, count) {
return Text('Count: $count');
},
listener: (count) {
debugPrint('Count changed to $count');
},
)
Controllers
Controllers help organize related state and business logic. They extend ReactiveController and provide automatic lifecycle management.
Creating a Controller
class CounterController extends ReactiveController {
// Reactive state
final count = ReactiveInt(0);
final isEven = ReactiveBool(true);
// Business logic
void increment() {
count.value++;
isEven.value = count.value % 2 == 0;
}
void decrement() {
count.value--;
isEven.value = count.value % 2 == 0;
}
void reset() {
count.value = 0;
isEven.value = true;
}
}
Using Controllers with ReactiveStateWidget
ReactiveStateWidget provides automatic controller lifecycle management:
class CounterScreen extends ReactiveStateWidget<CounterController> {
const CounterScreen({super.key});
BindController<CounterController>? bindController() {
return BindController(controller: () => CounterController());
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ReactiveBuilder(
reactiv: controller.count,
builder: (context, count) {
return Text(
'Count: $count',
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
SizedBox(height: 16),
ReactiveBuilder(
reactiv: controller.isEven,
builder: (context, even) {
return Text(
even ? 'Even number' : 'Odd number',
style: TextStyle(
color: even ? Colors.green : Colors.orange,
),
);
},
),
),
),
],
),
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
onPressed: controller.increment,
child: Icon(Icons.add),
),
SizedBox(height: 8),
FloatingActionButton(
onPressed: controller.decrement,
child: Icon(Icons.remove),
),
],
),
);
}
}
Advanced Features
Computed Reactive Values
Create reactive values that automatically update based on other reactive values:
final firstName = ReactiveString('John');
final lastName = ReactiveString('Doe');
final fullName = ComputedReactive(
() => '${firstName.value} ${lastName.value}',
dependencies: [firstName, lastName],
);
// fullName automatically updates when firstName or lastName changes
firstName.value = 'Jane'; // fullName becomes 'Jane Doe'
Undo/Redo Support
Enable history tracking for any reactive value:
final text = ReactiveString('Hello', enableHistory: true);
text.value = 'World';
text.value = 'Flutter';
// Navigate history
text.undo(); // Back to 'World'
text.redo(); // Forward to 'Flutter'
// Check history state
print(text.canUndo); // true
print(text.canRedo); // false
Debounce and Throttle
Control how often reactive values notify listeners:
final searchQuery = ReactiveString('');
// Debounce - wait for user to stop typing
searchQuery.setDebounce(Duration(milliseconds: 500));
searchQuery.updateDebounced('flutter');
// Throttle - limit update frequency
final scrollPosition = ReactiveInt(0);
scrollPosition.setThrottle(Duration(milliseconds: 100));
scrollPosition.updateThrottled(150);
Best Practices
1. Keep Controllers Focused
// Good: Focused on one feature
class UserProfileController extends ReactiveController {
final name = ReactiveString('');
final email = ReactiveString('');
final avatar = ReactiveString('');
void updateProfile(String name, String email) { /* ... */ }
}
// Avoid: Too many responsibilities
class AppController extends ReactiveController {
// User state
// Navigation state
// Theme state
// etc... (too much!)
}
2. Use Meaningful Names
// Good
final isLoadingUserData = ReactiveBool(false);
final shoppingCartItems = ReactiveList<Product>([]);
// Avoid
final flag = ReactiveBool(false);
final data = ReactiveList([]);
3. Initialize with Sensible Defaults
// Good
final userName = ReactiveString('Guest');
final counter = ReactiveInt(0);
final items = ReactiveList<String>([]);
// Avoid
final userName = ReactiveString(null); // Potential null issues
4. Dispose Controllers Properly
When using controllers manually (not with ReactiveStateWidget):
class MyWidget extends StatefulWidget {
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final CounterController controller;
void initState() {
super.initState();
controller = CounterController();
}
void dispose() {
controller.dispose(); // Important!
super.dispose();
}
Widget build(BuildContext context) {
// Use controller...
}
}
Common Patterns
Loading States
class DataController extends ReactiveController {
final isLoading = ReactiveBool(false);
final data = ReactiveList<Item>([]);
final error = ReactiveString('');
Future<void> loadData() async {
isLoading.value = true;
error.value = '';
try {
final result = await api.fetchData();
data.value = result;
} catch (e) {
error.value = e.toString();
} finally {
isLoading.value = false;
}
}
}
Form Validation
class LoginController extends ReactiveController {
final email = ReactiveString('');
final password = ReactiveString('');
final emailError = ReactiveString('');
final passwordError = ReactiveString('');
bool get isValid => emailError.value.isEmpty && passwordError.value.isEmpty;
void validateEmail() {
if (email.value.isEmpty) {
emailError.value = 'Email is required';
} else if (!email.value.contains('@')) {
emailError.value = 'Invalid email format';
} else {
emailError.value = '';
}
}
void validatePassword() {
if (password.value.length < 6) {
passwordError.value = 'Password must be at least 6 characters';
} else {
passwordError.value = '';
}
}
}
Next Steps
Now that you understand the basics, you're ready to:
- Build Your First Complete Example - Put it all together
- Explore Advanced Patterns - State composition, middleware, and more
- Learn Testing Strategies - How to test reactive state
- Performance Optimization - Best practices for large apps
Troubleshooting
UI Not Updating
Problem: Changes to reactive values don't update the UI
Solution: Make sure you're using ReactiveBuilder to watch the reactive value:
// Wrong - UI won't update
Text('Count: ${counter.value}')
// Correct - UI updates automatically
ReactiveBuilder(
reactiv: counter,
builder: (context, value) {
return Text('Count: $value');
},
)
Memory Leaks
Problem: Controllers not being disposed properly
Solution: Use ReactiveStateWidget or manually dispose in dispose():
// Automatic disposal
class MyScreen extends ReactiveStateWidget<MyController> {
// Controller automatically disposed
}
// Manual disposal
void dispose() {
controller.dispose();
super.dispose();
}
Performance Issues
Problem: Too many rebuilds or expensive operations
Solution: Use computed values and debouncing:
// Expensive computation - cache with ComputedReactive
final expensiveValue = ComputedReactive(
() => heavyComputation(data.value),
dependencies: [data],
);
// Frequent updates - use debouncing
searchField.setDebounce(Duration(milliseconds: 300));
Ready to build something more complex? Continue to the First Example Tutorial to create a complete reactive application!