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
- Providers: The core building blocks in Riverpod. They are used to expose state and logic to the rest of the application.
- Consumer Widgets: Widgets that listen to providers and rebuild when the provider's state changes.
- Scoped Providers: Providers that are only available within a specific part of the widget tree.
- 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:
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 thecounterProvider
. - 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
extendsStateNotifier<List<Todo>>
to manage the list of todos. - StateNotifierProvider:
final todoListProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) { return TodoList(); });
provides theTodoList
state. - ConsumerWidget:
TodoApp
consumes thetodoListProvider
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:
- Add a method in
TodoList
to clear completed todos:
- 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:
- 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, ]; }
- 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), ), ], ),
- 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
- What is Flutter?
- Setting Up the Development Environment
- Understanding Flutter Architecture
- Creating Your First Flutter App
Module 2: Dart Programming Basics
- Introduction to Dart
- Variables and Data Types
- Control Flow Statements
- Functions and Methods
- Object-Oriented Programming in Dart
Module 3: Flutter Widgets
- Introduction to Widgets
- Stateless vs Stateful Widgets
- Basic Widgets
- Layout Widgets
- Input and Form Widgets
Module 4: State Management
Module 5: Navigation and Routing
Module 6: Networking and APIs
- Fetching Data from the Internet
- Parsing JSON Data
- Handling Network Errors
- Using REST APIs
- GraphQL Integration
Module 7: Persistence and Storage
- Introduction to Persistence
- Shared Preferences
- File Storage
- SQLite Database
- Using Hive for Local Storage
Module 8: Advanced Flutter Concepts
- Animations in Flutter
- Custom Paint and Canvas
- Platform Channels
- Isolates and Concurrency
- Performance Optimization
Module 9: Testing and Debugging
Module 10: Deployment and Maintenance
- Preparing for Release
- Building for iOS
- Building for Android
- Continuous Integration/Continuous Deployment (CI/CD)
- Maintaining and Updating Your App