Flutter Tutorial

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

WidgetRiverpod refHooks
ConsumerWidgetYesNo
HookWidgetNoYes
HookConsumerWidgetYesYes
HookConsumer(builder: ...)Yes, for part of a widgetYes

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.

Practise in the playground Updated