StateNotifierProvider
Managing complex state with methods, immutability, and advanced business logic in Riverpod.
StateNotifierProvider
is the most powerful provider type in Riverpod.
It's designed for managing
complex state
with
methods
,
business logic
, and
immutability
.
🎯 When to Use StateNotifierProvider
Use
StateNotifierProvider
when you need:
- Complex state with multiple fields
- Methods that modify state in specific ways
- Validation and business logic
- Immutable state updates
- Actions like adding, removing, updating items
- Working with lists, forms, or shopping carts
StateNotifierProvider
follows a clear architecture with three main components:
// 1. Define the State (immutable)
class TodoState {
final List<Todo> todos;
final bool isLoading;
const TodoState({required this.todos, required this.isLoading});
}
// 2. Define the StateNotifier
class TodoNotifier extends StateNotifier<TodoState> {
TodoNotifier() : super(TodoState(todos: [], isLoading: false));
// Methods to modify state
void addTodo(String title) {
final newTodo = Todo(title: title);
state = state.copyWith(todos: [...state.todos, newTodo]);
}
}
// 3. Define the Provider
final todoProvider = StateNotifierProvider<TodoNotifier, TodoState>((ref) {
return TodoNotifier();
});
Here's a complete example of a todo list app using
StateNotifierProvider
.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
// 1. Define the Todo model
class Todo {
final String id;
final String title;
final bool isCompleted;
Todo({required this.id, required this.title, this.isCompleted = false});
// Copy method for immutability
Todo copyWith({String? id, String? title, bool? isCompleted}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
// 2. Define the State
class TodoState {
final List<Todo> todos;
final bool isLoading;
final String? error;
const TodoState({required this.todos, required this.isLoading, this.error});
// Factory for initial state
factory TodoState.initial() {
return TodoState(todos: [], isLoading: false, error: null);
}
// Copy method
TodoState copyWith({List<Todo>? todos, bool? isLoading, String? error}) {
return TodoState(
todos: todos ?? this.todos,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}
// 3. Define the StateNotifier
class TodoNotifier extends StateNotifier<TodoState> {
TodoNotifier() : super(TodoState.initial());
// Add a todo
void addTodo(String title) {
if (title.trim().isEmpty) return;
final newTodo = Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title.trim(),
);
state = state.copyWith(todos: [...state.todos, newTodo]);
}
// Toggle completion
void toggleTodo(String id) {
state = state.copyWith(
todos: state.todos.map((todo) {
if (todo.id == id) {
return todo.copyWith(isCompleted: !todo.isCompleted);
}
return todo;
}).toList(),
);
}
// Remove a todo
void removeTodo(String id) {
state = state.copyWith(
todos: state.todos.where((todo) => todo.id != id).toList(),
);
}
// Clear completed todos
void clearCompleted() {
state = state.copyWith(
todos: state.todos.where((todo) => !todo.isCompleted).toList(),
);
}
// Set loading state
void setLoading(bool loading) {
state = state.copyWith(isLoading: loading);
}
// Set error state
void setError(String error) {
state = state.copyWith(error: error);
}
}
// 4. Define the Provider
final todoProvider = StateNotifierProvider<TodoNotifier, TodoState>((ref) {
return TodoNotifier();
});
Here's how to use the
StateNotifierProvider
in your widgets.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'todo_provider.dart';
class TodoScreen extends ConsumerWidget {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch the provider to get the state
final todoState = ref.watch(todoProvider);
// Get the notifier to call methods
final todoNotifier = ref.read(todoProvider.notifier);
return Scaffold(
appBar: AppBar(
title: Text('Todo List'),
actions: [
IconButton(
icon: Icon(Icons.delete_sweep),
onPressed: todoState.todos.any((t) => t.isCompleted)
? () => todoNotifier.clearCompleted()
: null,
),
],
),
body: Column(
children: [
// Input field
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Add a todo...',
border: OutlineInputBorder(),
),
onSubmitted: (value) {
if (value.isNotEmpty) {
todoNotifier.addTodo(value);
_controller.clear();
}
},
),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: () {
if (_controller.text.isNotEmpty) {
todoNotifier.addTodo(_controller.text);
_controller.clear();
}
},
child: Text('Add'),
),
],
),
),
// Todo list
Expanded(
child: todoState.isLoading
? Center(child: CircularProgressIndicator())
: todoState.error != null
? Center(child: Text('Error: ${todoState.error}'))
: todoState.todos.isEmpty
? Center(child: Text('No todos yet!'))
: ListView.builder(
itemCount: todoState.todos.length,
itemBuilder: (context, index) {
final todo = todoState.todos[index];
return ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => todoNotifier.toggleTodo(todo.id),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: null,
),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => todoNotifier.removeTodo(todo.id),
),
);
},
),
),
],
),
);
}
}
Todo
is an immutable data class that represents
a single todo item with an id, title, and completion status.
TodoState
holds the entire state of the feature:
the list of todos, loading state, and any error messages.
TodoNotifier
extends
StateNotifier
and holds methods like
addTodo
,
toggleTodo
, and
removeTodo
.
Each method updates the state immutably.
todoProvider
creates and exposes the
TodoNotifier
to the app.
ref.watch(todoProvider)
gives you the state for reading.
ref.read(todoProvider.notifier)
gives you the notifier for calling methods.
✅ Key Pattern
- State is immutable – Always create a new state object, never mutate the existing one
- Methods are actions – Each method represents a user action or business operation
- State updates are predictable – The state only changes through explicit methods
- Separation of concerns – Business logic lives in the notifier, UI only displays
Immutability
is a key principle of
StateNotifierProvider
.
Instead of modifying the existing state, you create a
new copy
with the changes.
// ❌ MUTABLE - Bad practice
class
BadTodoState
{
List<Todo> todos = [];
// Mutable! Can be modified directly
}
// Can be mutated from anywhere
state.todos.add(newTodo);
// ❌ Hard to track changes
// ✅ IMMUTABLE - Good practice
class
GoodTodoState
{
final
List<Todo> todos;
// Immutable!
const
GoodTodoState({
required
this
.todos});
GoodTodoState
copyWith
({List<Todo>? todos}) {
return
GoodTodoState(
todos: todos ??
this
.todos,
);
}
}
// Always create a new state
state = state.copyWith(todos: [...state.todos, newTodo]);
// ✅ Predictable
💡 Benefits of Immutability
- Predictable – State changes are always explicit and traceable
- Debug-friendly – You can see exactly what changed and when
-
Performance
– Flutter can optimize rebuilds (using
const) - Concurrency-safe – No race conditions from shared mutable state
StateNotifierProvider
- Complex state with multiple fields
- Methods with business logic
- Immutable state
- Validation and error handling
- Perfect for forms, lists, shopping carts
StateProvider
- Simple primitive state
- Direct state assignment
- Mutable state
- No business logic
- Perfect for counters, toggles, strings
Don't do
state.todos.add(newTodo)
. This mutates the state without notifying listeners.
Use
state = state.copyWith(todos: [...state.todos, newTodo])
to create a new state.
If your state has loading and error fields, make sure to set and reset them properly.
Set
isLoading = true
before async operations and reset it after.
Don't put business logic in your widgets. Keep the UI focused on rendering.
All validation, transformation, and logic should be in the
StateNotifier
class.
StateNotifierProvider
works great with async operations. Here's how to handle
fetching data from an API.
class TodoNotifier extends StateNotifier<TodoState> {
final ApiService _api;
TodoNotifier(this._api) : super(TodoState.initial());
// Fetch todos from API
Future<void> fetchTodos() async {
// Set loading state
state = state.copyWith(isLoading: true, error: null);
try {
final todos = await _api.getTodos();
state = state.copyWith(todos: todos, isLoading: false);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
// Add todo to API
Future<void> addTodo(String title) async {
try {
final newTodo = await _api.createTodo(title);
state = state.copyWith(todos: [...state.todos, newTodo]);
} catch (e) {
state = state.copyWith(error: e.toString());
}
}
}
// Provider with dependency injection
final todoProvider = StateNotifierProvider<TodoNotifier, TodoState>((ref) {
final api = ref.watch(apiServiceProvider);
return TodoNotifier(api);
});
🎯 Key Takeaway
StateNotifierProvider
is the most powerful provider for complex state management.
It combines
immutable state
with
clear methods
for business logic.
Use it for any feature that requires multiple state fields, validation, or complex operations.
Remember: state is immutable, methods are actions, and the UI just renders.