Flutter Intermediate Lesson 50 of 135 3 min read
hooks_riverpod Tutorial: Riverpod with Flutter Hooks
Combine Riverpod and Flutter Hooks with hooks_riverpod: HookConsumerWidget, ref.watch and useTextEditingController in an easy runnable to-do app.
On this page
hooks_riverpod is Riverpod plus Flutter Hooks in one package. You use:
- Riverpod for shared app state (the to-do list, the user, API data).
- Hooks for local widget state (a text controller, an animation, a toggle).
Both in the same widget: a HookConsumerWidget.
Install
flutter pub add hooks_riverpod flutter_hooks
Use hooks_riverpod instead of flutter_riverpod, not as well.
The counter
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
void main() => runApp(const ProviderScope(child: MaterialApp(home: CounterPage())));
class CounterPage extends HookConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // shared state (Riverpod)
final tapped = useState(false); // local state (hook)
return Scaffold(
appBar: AppBar(title: const Text('hooks_riverpod')),
body: Center(
child: Text(
tapped.value ? '$count' : 'Tap +',
style: const TextStyle(fontSize: 48),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
tapped.value = true;
ref.read(counterProvider.notifier).increment();
},
child: const Icon(Icons.add),
),
);
}
}
A to-do list
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class Todo {
const Todo(this.title, {this.done = false});
final String title;
final bool done;
Todo toggled() => Todo(title, done: !done);
}
class TodosNotifier extends Notifier<List<Todo>> {
@override
List<Todo> build() => const [Todo('Learn Riverpod'), Todo('Learn hooks')];
void add(String title) => state = [...state, Todo(title)];
void toggle(int index) => state = [
for (var i = 0; i < state.length; i++)
if (i == index) state[i].toggled() else state[i],
];
}
final todosProvider = NotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);
// A derived provider: recalculated only when the list changes.
final remainingProvider = Provider<int>(
(ref) => ref.watch(todosProvider).where((t) => !t.done).length,
);
void main() => runApp(const ProviderScope(child: MaterialApp(home: TodoPage())));
class TodoPage extends HookConsumerWidget {
const TodoPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todosProvider);
final remaining = ref.watch(remainingProvider);
final controller = useTextEditingController(); // disposed for you
void add() {
final text = controller.text.trim();
if (text.isEmpty) return;
ref.read(todosProvider.notifier).add(text);
controller.clear();
}
return Scaffold(
appBar: AppBar(title: Text('To-do ($remaining left)')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: controller,
onSubmitted: (_) => add(),
decoration: InputDecoration(
hintText: 'New task',
suffixIcon: IconButton(icon: const Icon(Icons.add), onPressed: add),
),
),
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, i) => CheckboxListTile(
title: Text(todos[i].title),
value: todos[i].done,
onChanged: (_) => ref.read(todosProvider.notifier).toggle(i),
),
),
),
],
),
);
}
}
Widget types
| Widget | Riverpod ref | Hooks |
|---|---|---|
ConsumerWidget | Yes | No |
HookWidget | No | Yes |
HookConsumerWidget | Yes | Yes |
HookConsumer(builder: ...) | Yes, for part of a widget | Yes |
When to choose it
Pick hooks_riverpod if you like Riverpod and are tired of writing StatefulWidget just to hold a TextEditingController. Many production Riverpod apps use it.
Try it yourself
Add a filter (All / Active / Done) as a second NotifierProvider, and keep the currently selected tab index in a useState hook.