First Example
Build your first complete reactiv application - a todo list with all essential features
Your First reactiv Application
Let's build a complete todo list application using reactiv. This tutorial will show you how to combine reactive values, observers, and controllers to create a fully functional app.
What We're Building
By the end of this tutorial, you'll have a todo app with:
- ✅ Add new todos
- ✅ Mark todos as complete/incomplete
- ✅ Delete todos
- ✅ Filter todos (All, Active, Completed)
- ✅ Clear all completed todos
- ✅ Persistent storage (bonus)
Project Setup
If you haven't already, create a new Flutter project and add reactiv:
flutter create todo_app
cd todo_app
Add reactiv to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
reactiv: ^1.1.0
Run flutter pub get to install the dependencies.
Step 1: Create the Todo Model
First, let's define our todo data structure. This model will represent individual todo items with proper JSON serialization for persistence.
// lib/models/todo.dart
class Todo {
final String id;
final String title;
final bool isCompleted;
final DateTime createdAt;
Todo({
required this.id,
required this.title,
this.isCompleted = false,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();
// Create a copy with modified fields
Todo copyWith({
String? id,
String? title,
bool? isCompleted,
DateTime? createdAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
createdAt: createdAt ?? this.createdAt,
);
}
// Convert to/from JSON for persistence
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'isCompleted': isCompleted,
'createdAt': createdAt.toIso8601String(),
};
}
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
isCompleted: json['isCompleted'],
createdAt: DateTime.parse(json['createdAt']),
);
}
String toString() {
return 'Todo(id: $id, title: $title, isCompleted: $isCompleted)';
}
}
Step 2: Create the Todo Controller
Now let's create a controller to manage our todo state and business logic. This is where the magic of reactiv happens - all state changes will automatically trigger UI updates.
💡 Try it online: You can experiment with reactiv controllers in DartPad by copying the controller code and testing the reactive behavior.
// lib/controllers/todo_controller.dart
import 'package:reactiv/reactiv.dart';
import '../models/todo.dart';
enum TodoFilter { all, active, completed }
class TodoController extends ReactiveController {
// Reactive state
final todos = ReactiveList<Todo>([]);
final currentFilter = ReactiveValue<TodoFilter>(TodoFilter.all);
final newTodoText = ReactiveString('');
// Computed reactive values
late final activeTodos = ComputedReactive(
() => todos.where((todo) => !todo.isCompleted).toList(),
dependencies: [todos],
);
late final completedTodos = ComputedReactive(
() => todos.where((todo) => todo.isCompleted).toList(),
dependencies: [todos],
);
late final filteredTodos = ComputedReactive(
() {
switch (currentFilter.value) {
case TodoFilter.all:
return todos.toList();
case TodoFilter.active:
return activeTodos.value;
case TodoFilter.completed:
return completedTodos.value;
}
},
dependencies: [todos, currentFilter],
);
late final allCompleted = ComputedReactive(
() => todos.isNotEmpty && activeTodos.value.isEmpty,
dependencies: [todos],
);
// Actions
void addTodo() {
final text = newTodoText.value.trim();
if (text.isNotEmpty) {
final todo = Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: text,
);
todos.add(todo);
newTodoText.value = '';
}
}
void toggleTodo(String id) {
final index = todos.indexWhere((todo) => todo.id == id);
if (index != -1) {
final todo = todos[index];
todos[index] = todo.copyWith(isCompleted: !todo.isCompleted);
}
}
void deleteTodo(String id) {
todos.removeWhere((todo) => todo.id == id);
}
void updateTodoTitle(String id, String newTitle) {
final index = todos.indexWhere((todo) => todo.id == id);
if (index != -1 && newTitle.trim().isNotEmpty) {
final todo = todos[index];
todos[index] = todo.copyWith(title: newTitle.trim());
}
}
void toggleAllTodos() {
final shouldComplete = !allCompleted.value;
for (int i = 0; i < todos.length; i++) {
todos[i] = todos[i].copyWith(isCompleted: shouldComplete);
}
}
void clearCompleted() {
todos.removeWhere((todo) => todo.isCompleted);
}
void setFilter(TodoFilter filter) {
currentFilter.value = filter;
}
// Get counts for UI
int get totalCount => todos.length;
int get activeCount => activeTodos.value.length;
int get completedCount => completedTodos.value.length;
}
Step 3: Create UI Components
Let's create reusable components for our todo app:
Todo Item Widget
// lib/widgets/todo_item.dart
import 'package:flutter/material.dart';
import '../models/todo.dart';
class TodoItem extends StatefulWidget {
final Todo todo;
final VoidCallback onToggle;
final VoidCallback onDelete;
final Function(String) onTitleChanged;
const TodoItem({
super.key,
required this.todo,
required this.onToggle,
required this.onDelete,
required this.onTitleChanged,
});
State<TodoItem> createState() => _TodoItemState();
}
class _TodoItemState extends State<TodoItem> {
bool isEditing = false;
late TextEditingController textController;
void initState() {
super.initState();
textController = TextEditingController(text: widget.todo.title);
}
void dispose() {
textController.dispose();
super.dispose();
}
void startEditing() {
setState(() {
isEditing = true;
});
}
void finishEditing() {
if (textController.text.trim().isNotEmpty) {
widget.onTitleChanged(textController.text.trim());
} else {
textController.text = widget.todo.title;
}
setState(() {
isEditing = false;
});
}
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: Checkbox(
value: widget.todo.isCompleted,
onChanged: (_) => widget.onToggle(),
),
title: isEditing
? TextField(
controller: textController,
autofocus: true,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Todo title...',
),
onSubmitted: (_) => finishEditing(),
)
: GestureDetector(
onDoubleTap: startEditing,
child: Text(
widget.todo.title,
style: TextStyle(
decoration: widget.todo.isCompleted
? TextDecoration.lineThrough
: null,
color: widget.todo.isCompleted
? Colors.grey
: null,
),
),
),
trailing: isEditing
? Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.check, color: Colors.green),
onPressed: finishEditing,
),
IconButton(
icon: Icon(Icons.close, color: Colors.red),
onPressed: () {
textController.text = widget.todo.title;
setState(() {
isEditing = false;
});
},
),
],
)
: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: widget.onDelete,
),
),
);
}
}
Filter Buttons Widget
// lib/widgets/filter_buttons.dart
import 'package:flutter/material.dart';
import 'package:reactiv/reactiv.dart';
import '../controllers/todo_controller.dart';
class FilterButtons extends StatelessWidget {
final TodoController controller;
const FilterButtons({super.key, required this.controller});
Widget build(BuildContext context) {
return ReactiveBuilder(
reactiv: controller.currentFilter,
builder: (context, currentFilter) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_FilterButton(
label: 'All',
isSelected: currentFilter == TodoFilter.all,
onPressed: () => controller.setFilter(TodoFilter.all),
),
_FilterButton(
label: 'Active',
isSelected: currentFilter == TodoFilter.active,
onPressed: () => controller.setFilter(TodoFilter.active),
),
_FilterButton(
label: 'Completed',
isSelected: currentFilter == TodoFilter.completed,
onPressed: () => controller.setFilter(TodoFilter.completed),
),
],
);
},
);
}
}
class _FilterButton extends StatelessWidget {
final String label;
final bool isSelected;
final VoidCallback onPressed;
const _FilterButton({
required this.label,
required this.isSelected,
required this.onPressed,
});
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
isSelected ? Theme.of(context).primaryColor : Colors.grey[300],
),
foregroundColor: MaterialStateProperty.all(
isSelected ? Colors.white : Colors.black,
),
),
child: Text(label),
);
}
}
Step 4: Create the Main Todo Screen
Now let's put everything together in the main screen. This is where you'll see how reactiv automatically updates the UI when the state changes - no manual rebuilds needed!
🚀 Interactive Demo: Want to see this in action? Try the complete todo app in DartPad or copy the code below to experiment with reactive UI updates.
// lib/screens/todo_screen.dart
import 'package:flutter/material.dart';
import 'package:reactiv/reactiv.dart';
import '../controllers/todo_controller.dart';
import '../widgets/todo_item.dart';
import '../widgets/filter_buttons.dart';
class TodoScreen extends ReactiveStateWidget<TodoController> {
const TodoScreen({super.key});
BindController<TodoController>? bindController() {
return BindController(controller: () => TodoController());
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('reactiv Todo App'),
backgroundColor: Theme.of(context).primarySwatch,
foregroundColor: Colors.white,
actions: [
ReactiveBuilder(
reactiv: controller.completedTodos,
builder: (context, completedTodos) {
if (completedTodos.isNotEmpty) {
return IconButton(
icon: Icon(Icons.clear_all),
onPressed: controller.clearCompleted,
tooltip: 'Clear completed',
);
}
return SizedBox.shrink();
},
),
],
),
body: Column(
children: [
// Add todo input
Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
ReactiveBuilderN(
listenables: [controller.allCompleted, controller.todos],
builder: (context) {
final allCompleted = controller.allCompleted.value;
final todos = controller.todos.value;
if (todos.isNotEmpty) {
return IconButton(
icon: Icon(
allCompleted ? Icons.check_box : Icons.check_box_outline_blank,
color: allCompleted ? Colors.green : null,
),
onPressed: controller.toggleAllTodos,
tooltip: 'Toggle all',
);
}
return SizedBox(width: 48);
},
),
Expanded(
child: ReactiveBuilder(
reactiv: controller.newTodoText,
builder: (context, text) {
return TextField(
decoration: InputDecoration(
hintText: 'What needs to be done?',
border: OutlineInputBorder(),
),
onChanged: (value) => controller.newTodoText.value = value,
onSubmitted: (_) => controller.addTodo(),
controller: TextEditingController.fromValue(
TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
),
),
);
},
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: controller.addTodo,
),
],
),
),
// Filter buttons
FilterButtons(controller: controller),
SizedBox(height: 16),
// Todo list
Expanded(
child: ReactiveBuilder(
reactiv: controller.filteredTodos,
builder: (context, filteredTodos) {
if (filteredTodos.isEmpty) {
return Center(
child: ReactiveBuilder(
reactiv: controller.todos,
builder: (context, allTodos) {
String message;
if (allTodos.isEmpty) {
message = 'No todos yet.\nAdd one above!';
} else {
message = 'No ${controller.currentFilter.value.name} todos.';
}
return Text(
message,
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
);
},
),
);
}
return ListView.builder(
itemCount: filteredTodos.length,
itemBuilder: (context, index) {
final todo = filteredTodos[index];
return TodoItem(
todo: todo,
onToggle: () => controller.toggleTodo(todo.id),
onDelete: () => controller.deleteTodo(todo.id),
onTitleChanged: (newTitle) =>
controller.updateTodoTitle(todo.id, newTitle),
);
},
);
},
),
),
// Stats
Container(
padding: EdgeInsets.all(16),
child: ReactiveBuilderN(
listenables: [controller.activeTodos, controller.todos],
builder: (context) {
return Text(
'${controller.activeCount} active, ${controller.totalCount} total',
style: TextStyle(color: Colors.grey[600]),
);
},
),
),
),
],
),
);
}
}
Step 5: Update Main App
Finally, update your main app file:
// lib/main.dart
import 'package:flutter/material.dart';
import 'screens/todo_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'reactiv Todo App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TodoScreen(),
debugShowCheckedModeBanner: false,
);
}
}
Step 6: Run Your App
Save all files and run your app:
flutter run
You should now have a fully functional todo app! Try:
- Adding new todos
- Marking todos as complete/incomplete
- Editing todos (double-tap)
- Filtering between All, Active, and Completed
- Toggling all todos at once
- Clearing completed todos
What You've Learned
In this tutorial, you've learned how to:
- ✅ Structure a reactiv app with models, controllers, and widgets
- ✅ Use ReactiveStateWidget for automatic controller lifecycle management
- ✅ Create computed reactive values that automatically update
- ✅ Handle complex state relationships between different reactive values
- ✅ Build reactive UI components that efficiently update when state changes
- ✅ Organize business logic in controllers separate from UI code
Bonus: Add Persistence
Want to save your todos between app sessions? Add this to your controller:
// Add to TodoController class
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
// Add this method to save todos
Future<void> saveTodos() async {
final prefs = await SharedPreferences.getInstance();
final todosJson = todos.map((todo) => todo.toJson()).toList();
await prefs.setString('todos', jsonEncode(todosJson));
}
// Add this method to load todos
Future<void> loadTodos() async {
final prefs = await SharedPreferences.getInstance();
final todosString = prefs.getString('todos');
if (todosString != null) {
final todosList = jsonDecode(todosString) as List;
todos.value = todosList.map((json) => Todo.fromJson(json)).toList();
}
}
// Call saveTodos() after each state change
void addTodo() {
// ... existing code ...
saveTodos();
}
// And so on for other methods...
Don't forget to add shared_preferences to your pubspec.yaml and call loadTodos() when the controller initializes.
Next Steps
Congratulations! You've built your first complete reactiv application. Here are some ideas to extend it further:
- Add Categories - Group todos by category
- Add Due Dates - Set deadlines for todos
- Add Priority Levels - High, medium, low priority todos
- Add Search - Search through todos by title
- Add Animations - Smooth transitions when adding/removing todos
- Add Cloud Sync - Sync todos across devices
What's Next?
Ready to learn more advanced patterns? Check out our Advanced Patterns Guide to learn about middleware, complex state composition, and performance optimization techniques.