Introduction to Riverpod

Riverpod is a state management library for Flutter that aims to provide a more robust and scalable solution compared to other state management techniques. It is built by the same author of the Provider package but offers several improvements, such as better testability, improved performance, and a more declarative API.

Key Concepts

  1. Providers: The core building blocks in Riverpod. They are used to expose state and logic to the rest of the application.
  2. Consumer Widgets: Widgets that listen to providers and rebuild when the provider's state changes.
  3. Scoped Providers: Providers that are only available within a specific part of the widget tree.
  4. State Notifiers: Objects that manage state and notify listeners when the state changes.

Why Use Riverpod?

  • Improved Testability: Riverpod makes it easier to write unit tests for your state management logic.
  • No Context Required: Unlike Provider, Riverpod does not require a BuildContext to access providers.
  • Compile-time Safety: Riverpod provides compile-time safety, reducing runtime errors.
  • Modular and Scalable: Riverpod is designed to be modular and scalable, making it suitable for large applications.

Setting Up Riverpod

To get started with Riverpod, you need to add the flutter_riverpod package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^1.0.0

Then, run flutter pub get to install the package.

Basic Usage

Creating a Provider

A provider is a way to expose state or logic to the rest of your application. Here’s how to create a simple provider:

import 'package:flutter_riverpod/flutter_riverpod.dart';

// Define a provider
final counterProvider = StateProvider<int>((ref) => 0);

Consuming a Provider

To consume a provider, you use the ConsumerWidget:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Counter')),
      body: Center(
        child: Text('Counter: $counter'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: Icon(Icons.add),
      ),
    );
  }
}

Explanation

  • Provider Definition: final counterProvider = StateProvider<int>((ref) => 0); defines a provider that holds an integer state initialized to 0.
  • ConsumerWidget: CounterApp is a widget that consumes the counterProvider.
  • ref.watch: final counter = ref.watch(counterProvider); listens to the provider and rebuilds the widget when the state changes.
  • ref.read: ref.read(counterProvider.notifier).state++ updates the state of the provider.

Practical Example: Todo List

Let's create a simple Todo List application using Riverpod.

Step 1: Define the State

import 'package:flutter_riverpod/flutter_riverpod.dart';

class Todo {
  final String id;
  final String description;
  final bool completed;

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

final todoListProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) {
  return TodoList();
});

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

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

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

  void remove(String id) {
    state = state.where((todo) => todo.id != id).toList();
  }
}

Step 2: Create the UI

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class TodoApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todoList = ref.watch(todoListProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Todo List')),
      body: ListView(
        children: [
          for (final todo in todoList)
            ListTile(
              title: Text(todo.description),
              leading: Checkbox(
                value: todo.completed,
                onChanged: (value) => ref.read(todoListProvider.notifier).toggle(todo.id),
              ),
              trailing: IconButton(
                icon: Icon(Icons.delete),
                onPressed: () => ref.read(todoListProvider.notifier).remove(todo.id),
              ),
            ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _addTodoDialog(context, ref),
        child: Icon(Icons.add),
      ),
    );
  }

  void _addTodoDialog(BuildContext context, WidgetRef ref) {
    final TextEditingController controller = TextEditingController();

    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text('Add Todo'),
          content: TextField(
            controller: controller,
            decoration: InputDecoration(hintText: 'Enter todo description'),
          ),
          actions: [
            TextButton(
              onPressed: () {
                ref.read(todoListProvider.notifier).add(controller.text);
                Navigator.of(context).pop();
              },
              child: Text('Add'),
            ),
          ],
        );
      },
    );
  }
}

Explanation

  • StateNotifier: TodoList extends StateNotifier<List<Todo>> to manage the list of todos.
  • StateNotifierProvider: final todoListProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) { return TodoList(); }); provides the TodoList state.
  • ConsumerWidget: TodoApp consumes the todoListProvider and builds the UI.
  • ListView: Displays the list of todos.
  • FloatingActionButton: Opens a dialog to add a new todo.

Exercises

Exercise 1: Add a Clear Completed Button

Add a button to clear all completed todos.

Solution:

  1. Add a method in TodoList to clear completed todos:
void clearCompleted() {
  state = state.where((todo) => !todo.completed).toList();
}
  1. Add a button in the TodoApp widget:
AppBar(
  title: Text('Riverpod Todo List'),
  actions: [
    IconButton(
      icon: Icon(Icons.clear_all),
      onPressed: () => ref.read(todoListProvider.notifier).clearCompleted(),
    ),
  ],
),

Exercise 2: Add Edit Functionality

Add functionality to edit the description of a todo.

Solution:

  1. Add a method in TodoList to edit a todo:
void edit(String id, String description) {
  state = [
    for (final todo in state)
      if (todo.id == id)
        Todo(
          id: todo.id,
          description: description,
          completed: todo.completed,
        )
      else
        todo,
  ];
}
  1. Add an edit button in the TodoApp widget:
trailing: Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    IconButton(
      icon: Icon(Icons.edit),
      onPressed: () => _editTodoDialog(context, ref, todo),
    ),
    IconButton(
      icon: Icon(Icons.delete),
      onPressed: () => ref.read(todoListProvider.notifier).remove(todo.id),
    ),
  ],
),
  1. Implement the _editTodoDialog method:
void _editTodoDialog(BuildContext context, WidgetRef ref, Todo todo) {
  final TextEditingController controller = TextEditingController(text: todo.description);

  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text('Edit Todo'),
        content: TextField(
          controller: controller,
          decoration: InputDecoration(hintText: 'Enter todo description'),
        ),
        actions: [
          TextButton(
            onPressed: () {
              ref.read(todoListProvider.notifier).edit(todo.id, controller.text);
              Navigator.of(context).pop();
            },
            child: Text('Save'),
          ),
        ],
      );
    },
  );
}

Conclusion

In this section, we explored Riverpod, a powerful state management library for Flutter. We covered the basics of setting up Riverpod, creating and consuming providers, and building a practical Todo List application. By understanding and utilizing Riverpod, you can manage state in your Flutter applications more effectively and efficiently. In the next module, we will dive into navigation and routing in Flutter.

Flutter Development Course

Module 1: Introduction to Flutter

Module 2: Dart Programming Basics

Module 3: Flutter Widgets

Module 4: State Management

Module 5: Navigation and Routing

Module 6: Networking and APIs

Module 7: Persistence and Storage

Module 8: Advanced Flutter Concepts

Module 9: Testing and Debugging

Module 10: Deployment and Maintenance

Module 11: Flutter for Web and Desktop

© Copyright 2024. All rights reserved