GETTING STARTEDBEGINNER

First Example

Build your first complete reactiv application - a todo list with all essential features

15 min read📅 Updated 11/3/2025
#tutorial#todo-app#complete-example#controller

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:

  1. Add Categories - Group todos by category
  2. Add Due Dates - Set deadlines for todos
  3. Add Priority Levels - High, medium, low priority todos
  4. Add Search - Search through todos by title
  5. Add Animations - Smooth transitions when adding/removing todos
  6. 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.