State Management

State management is how you share data between widgets. Flutter offers several approaches, from simple to complex.

setState (Simple)

For local widget state only:

Dart
class TodoList extends StatefulWidget {
  const TodoList({super.key});

  @override
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final List<String> _todos = [];
  final _controller = TextEditingController();

  void _addTodo() {
    if (_controller.text.isNotEmpty) {
      setState(() {
        _todos.add(_controller.text);
        _controller.clear();
      });
    }
  }

  void _removeTodo(int index) {
    setState(() {
      _todos.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          children: [
            Expanded(child: TextField(controller: _controller)),
            IconButton(onPressed: _addTodo, icon: Icon(Icons.add)),
          ],
        ),
        Expanded(
          child: ListView.builder(
            itemCount: _todos.length,
            itemBuilder: (context, index) => ListTile(
              title: Text(_todos[index]),
              trailing: IconButton(
                icon: Icon(Icons.delete),
                onPressed: () => _removeTodo(index),
              ),
            ),
          ),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
YAML
# pubspec.yaml
dependencies:
  provider: ^6.1.0
Dart
// models/todo.dart
class Todo {
  final String id;
  final String title;
  bool completed;

  Todo({required this.id, required this.title, this.completed = false});
}

// providers/todo_provider.dart
import 'package:flutter/foundation.dart';

class TodoProvider extends ChangeNotifier {
  final List<Todo> _todos = [];

  List<Todo> get todos => List.unmodifiable(_todos);
  int get count => _todos.length;
  int get completedCount => _todos.where((t) => t.completed).length;

  void add(String title) {
    _todos.add(Todo(id: DateTime.now().toString(), title: title));
    notifyListeners();  // Tells all listeners to rebuild
  }

  void toggle(String id) {
    final todo = _todos.firstWhere((t) => t.id == id);
    todo.completed = !todo.completed;
    notifyListeners();
  }

  void remove(String id) {
    _todos.removeWhere((t) => t.id == id);
    notifyListeners();
  }
}
Dart
// main.dart — provide the state at the top
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => TodoProvider(),
      child: const MyApp(),
    ),
  );
}

// In any widget — read the state
class TodoList extends StatelessWidget {
  const TodoList({super.key});

  @override
  Widget build(BuildContext context) {
    final todos = context.watch<TodoProvider>();  // Rebuilds on changes

    return ListView.builder(
      itemCount: todos.count,
      itemBuilder: (context, index) {
        final todo = todos.todos[index];
        return ListTile(
          title: Text(
            todo.title,
            style: TextStyle(
              decoration: todo.completed ? TextDecoration.lineThrough : null,
            ),
          ),
          leading: Checkbox(
            value: todo.completed,
            onChanged: (_) => context.read<TodoProvider>().toggle(todo.id),
          ),
        );
      },
    );
  }
}

Riverpod (Modern Provider)

YAML
dependencies:
  flutter_riverpod: ^2.4.0
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Define a provider
final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
  return TodoNotifier();
});

class TodoNotifier extends StateNotifier<List<Todo>> {
  TodoNotifier() : super([]);

  void add(String title) {
    state = [...state, Todo(id: DateTime.now().toString(), title: title)];
  }

  void toggle(String id) {
    state = [
      for (final todo in state)
        if (todo.id == id) todo..completed = !todo.completed else todo,
    ];
  }
}

// Widget uses ConsumerWidget
class TodoPage extends ConsumerWidget {
  const TodoPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todoProvider);
    return ListView(
      children: [for (final todo in todos) ListTile(title: Text(todo.title))],
    );
  }
}

Comparison

Approach Complexity Best For
setState Very low Local widget state
InheritedWidget Low Simple dependency injection
Provider Medium Small-medium apps
Riverpod Medium Medium-large apps
BLoC High Large enterprise apps

💡 Tip: Start with Provider. It's simple, well-documented, and scales well for most apps. Migrate to Riverpod or BLoC only when you outgrow it.

Next: Navigation and Routing