?? Riverpod in Flutter: The Only Guide You Need
Muhammad Ismail

?? Riverpod in Flutter: The Only Guide You Need

70% of Flutter devs still struggle with state management—Riverpod fixes that!

Introduction

  • Riverpod is a reactive state management and dependency injection framework that is the enhanced version of the provider
  • complete rewrite of the provider
  • Developed by Remi Rousselet

Why Riverpod

  1. Catch programming errors at compile time.
  2. Remove nesting for listening/combining objects.
  3. Create, Use, and combine Provider when no longer used.
  4. Ensure the code is testable.

Riverpid Installation

  1. flutter_riverpod : Riverpod for flutter
  2. hooks_riveroid : If application uses flutter_hooks
  3. riverpod : For dart projects

Types of Riverpod Provider

  1. Provider
  2. SateProvider
  3. StateNotifierProvider
  4. FutureProvider
  5. StreamProvider
  6. ChangeNotifierProvider

Initial Setup

  • Insall Riverpod in your Flutter applicaiton

flutter pub add flutter_riverpod
        

  • Setup Your main file of the project

 import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}        

ProviderScope

  • In Riverpod, ProviderScope is a fundamental widget that acts as the container for all providers in a Flutter app
  • It ensures that providers can be accessed throughout the widget tree and manages their lifecycle efficiently.
  • ProviderScope is the entry point for using Riverpod in a Flutter app
  • It stores and manages the state of all registered providers.
  • Without ProviderScope, providers won't be accessible in your app.

Now let's deep dive into each type of Riverpod Provider so sit back relax and enjoy the article

1. Provider


  • The Provider in Riverpod is the most basic and read-only provider.
  • It is used when you need to expose immutable data (i.e., data that does not change) throughout your Flutter app.

How Provider Works?


  • It creates and exposes a value but does not allow modifications.
  • Best used for constants, configurations, or computed values.
  • Does not rebuild widgets when the value changes.

Provider Syntax & Usage

import 'package:flutter_riverpod/flutter_riverpod.dart';

// A simple provider that returns a static string
final greetingProvider = Provider<String>((ref) {
  return "Hello, Riverpod!";
});
        

Consume the Provider in a Widget

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

class HomeScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Access the provider's value
    final greeting = ref.watch(greetingProvider);

    return Scaffold(
      body: Center(
        child: Text(greeting), // Displays "Hello, Riverpod!"
      ),
    );
  }
}
        

We have another way through which we can consume only the necessary widgets to rebuild

import 'package:flutter_riverpod/flutter_riverpod.dart';

// Provider
final greetingProvider = Provider<String>((ref) => "Hello, Riverpod!");

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Consumer(
          builder: (context, ref, child) {
            final greeting = ref.watch(greetingProvider);
            return Text(greeting); // "Hello, Riverpod!"
          },
        ),
      ),
    );
  }
}        

Now if we have a Stateful widget and we want to read the provider value what do we have to do in that case? let's dive into it.

class HomeScreen extends ConsumerStatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends ConsumerState<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    final greeting = ref.watch(greetingProvider); // Can use ref directly

    return Scaffold(
      body: Center(
        child: Text(greeting), // "Hello, Riverpod!"
      ),
    );
  }
}
        

  • If only a specific part of the widget needs access to the provider, wrap it with Consumer:

import 'package:flutter_riverpod/flutter_riverpod.dart'; // Simple Provider
final greetingProvider = Provider<String>((ref) => "Hello, Riverpod!");
class HomeScreen extends StatefulWidget {
@override
 HomeScreenState createState() => HomeScreenState()
}        


class HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Consumer(
builder: (context, ref, child) {
final greeting = ref.watch(greetingProvider);
return Text(greeting); // "Hello, Riverpod!"
),);
 }
}        

2. StateProvider

  • StateProvider is a simple way to manage mutable state
  • Similar to StatfulWidget allowing you to update single-value

When to Use StateProvider?

  • When you needa temporary state (like UI selections, toggles, or form inputs).

Example

In this example of counter, we will see how to increment and update the UI and then reset the counter

 import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}        
import 'package:flutter_riverpod/flutter_riverpod.dart';

// StateProvider for managing the counter
final counterProvider = StateProvider<int>((ref) => 0);

        
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/counter_provider.dart';

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

    return Scaffold(
      appBar: AppBar(title: Text("Riverpod Counter"),
	action[
	IconButton(onPressed: (){
		ref.invalidate(counterProvider); // First way to reset counter
		ref.referesh(counterProvider); //second way to reset counter
		 )
		}
	 , icon: Icon(Icons.referesh)
	]),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Counter: $counter',
              style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    ref.read(counterProvider.notifier).state++; // Incrementation fist Way
		    ref.read(counterProvider.notifier).update((state)=> state+1); // Incrementaion second way
                  },
                  child: Text("Increment"),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    ref.read(counterProvider.notifier).state--; // Decrement
                  },
                  child: Text("Decrement"),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    ref.read(counterProvider.notifier).state = 0; // Reset
                  },
                  child: Text("Reset"),
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
        

3.StateNotifierProvider

  • StateNotifierProvider is a more powerful provider in Riverpod that allows managing complex state logic and provides immutable state updates.
  • It is the recommended way to handle state changes in a professional Flutter app.

lib/

│── main.dart // Entry point of the app

│── providers/counter_notifier.dart // Counter logic using StateNotifier

│── views/counter_screen.dart // User interface screen for the counter

import 'package:flutter_riverpod/flutter_riverpod.dart';

// StateNotifier to manage counter state
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0); // Initial counter value is 0

  void increment() => state++; // Increment counter
  void decrement() => state--; // Decrement counter
  void reset() => state = 0; // Reset counter
}

// Define a provider using StateNotifierProvider
final counterProvider =
    StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());
        
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/counter_notifier.dart';

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

    return Scaffold(
      appBar: AppBar(title: Text("Riverpod Counter with StateNotifier")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Counter: $counter',
              style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    ref.read(counterProvider.notifier).increment();
                  },
                  child: Text("Increment"),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    ref.read(counterProvider.notifier).decrement();
                  },
                  child: Text("Decrement"),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    ref.read(counterProvider.notifier).reset();
                  },
                  child: Text("Reset"),
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}        

4.FutureProvider

  • FutureProvider is used when we need to fetch asynchronous data in Flutter, such as API calls, database queries, or local storage operations.
  • It automatically handles states like loading, success, and error.

Example 1

  • Let's build an example where we Fetch data from a mock API (simulated with Future.delayed).
  • Display a loading indicator while fetching data.
  • Show an error message if the fetch fails.
  • Display the fetched data when available.

lib/
│── main.dart                          // Entry point
│── providers/
│   │── data_provider.dart             // FutureProvider for fetching data
│── views/
│   │── home_screen.dart               // UI screen        
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';

// Simulated API call
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 2)); // Simulating network delay
  return "Hello from FutureProvider!";
}

// FutureProvider to fetch data asynchronously
final dataProvider = FutureProvider<String>((ref) async {
  return fetchData();
});
        
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/data_provider.dart';

class HomeScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncData = ref.watch(dataProvider); // Watching data state

    return Scaffold(
      appBar: AppBar(title: Text("FutureProvider Example")),
      body: Center(
        child: asyncData.when(
          data: (data) => Text(
            data,
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          loading: () => CircularProgressIndicator(), // Show while fetching
          error: (err, stack) => Text("Error loading data"), // Show on error
        ),
      ),
    );
  }
}
        

Example 2

Great! Let's implement FutureProvider with a real API call using JSONPlaceholder (a free REST API). We'll fetch a random post from their /posts/1 endpoint.

lib/
│── main.dart                          // Entry point
│── providers/
│   │── post_provider.dart              // FutureProvider for API call
│── models/
│   │── post_model.dart                 // Post model
│── views/
│   │── home_screen.dart                // UI screen        
class PostModel {
  final int id;
  final String title;
  final String body;

  PostModel({required this.id, required this.title, required this.body});

  // Factory method to convert JSON response into a Dart object
  factory PostModel.fromJson(Map<String, dynamic> json) {
    return PostModel(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}        
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/post_model.dart';

// Function to fetch post data from API
Future<PostModel> fetchPost() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));

  if (response.statusCode == 200) {
    final jsonData = json.decode(response.body);
    return PostModel.fromJson(jsonData);
  } else {
    throw Exception("Failed to load post");
  }
}

// FutureProvider to manage API call state
final postProvider = FutureProvider<PostModel>((ref) async {
  return fetchPost();
});
        
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/post_provider.dart';

class HomeScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final postAsync = ref.watch(postProvider); // Watching API call state

    return Scaffold(
      appBar: AppBar(title: Text("Fetch API with FutureProvider")),
      body: Center(
        child: postAsync.when(
          data: (post) => Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  post.title,
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 10),
                Text(
                  post.body,
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 18),
                ),
              ],
            ),
          ),
          loading: () => CircularProgressIndicator(), // Show loader
          error: (err, stack) => Text("Error: ${err.toString()}"), // Show error
        ),
      ),
    );
  }
}        

5. StreamProvider

StreamProvider in Riverpod is used to listen to a stream of data and automatically rebuild the UI whenever the stream emits a new value. It's useful for real-time updates, such as:

  • Live API data fetching (e.g., stock prices, weather updates)
  • Firebase Firestore real-time updates.
  • WebSocket connections Live sensor data from devices.

Example

lib/
│── main.dart                          // Entry point
│── providers/
│   │── number_provider.dart            // StreamProvider for real-time updates
│── views/
│   │── home_screen.dart                 // UI screen        
import 'dart:async';
import 'dart:math';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Function that generates a new random number every 2 seconds
Stream<int> randomNumberStream() async* {  
  final random = Random();
  while (true) {
    await Future.delayed(Duration(seconds: 2));
    yield random.nextInt(100); // Generate a random number (0-99)
  }
}

// StreamProvider to manage the random number stream
final numberProvider = StreamProvider<int>((ref) {
  return randomNumberStream();
});
        
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/number_provider.dart';

class HomeScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final numberStream = ref.watch(numberProvider); // Watching stream updates

    return Scaffold(
      appBar: AppBar(title: Text("Live Stream with StreamProvider")),
      body: Center(
        child: numberStream.when(
          data: (number) => Text(
            "Live Number: $number",
            style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
          ),
          loading: () => CircularProgressIndicator(), // Show loader
          error: (err, stack) => Text("Error: ${err.toString()}"), // Show error
        ),
      ),
    );
  }
}
        

6.ChangeNotifierProvider

  • ChangeNotifierProvider is used to provide and listen to a ChangeNotifier class in Riverpod.
  • It is useful when managing complex state that requires notifying listeners when changes occur.

Example

import 'package:flutter/material.dart';

class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;  //getter

  void increment() {
    _count++;
    notifyListeners(); // Notify UI about state change
  }

  void reset() {
    _count = 0;
    notifyListeners(); // Reset counter and update UI
  }
}


// Create a provider for CounterNotifier
final counterProvider = ChangeNotifierProvider((ref) => CounterNotifier());
        
class CounterScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(title: Text("Counter with ChangeNotifierProvider")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("Counter: ${counter.count}", style: TextStyle(fontSize: 24)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => ref.read(counterProvider).increment(),
              child: Text("Increment"),
            ),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => ref.read(counterProvider).reset(),
              child: Text("Reset"),
            ),
          ],
        ),
      ),
    );
  }
}
        

Real Project Example

create a Flutter project that includes: ? Toggle Button – Change UI state (e.g., Dark/Light Mode) ? User Name from Firestore – Fetch user's name from Firestore ? Data from API – Fetch external data (e.g., Posts from JSONPlaceholder) ? Theme Change Support – Allow user to switch between light and dark themes

Project Structure

/lib
│-- /models
│   ├── user_model.dart            # User model
│-- /services
│   ├── api_service.dart           # Fetch data from API
│   ├── firestore_service.dart     # Fetch user data from Firestore
│-- /providers
│   ├── theme_provider.dart        # Theme management
│   ├── user_provider.dart         # Firestore user provider
│   ├── api_provider.dart          # API fetch provider
│-- /views
│   ├── home_screen.dart           # Main screen
│-- /widgets
│   ├── toggle_button.dart         # Toggle button widget
│-- main.dart                      # Entry point
        
// UserModel

class User {
  final String id;
  final String name;

  User({required this.id, required this.name});

  factory User.fromMap(Map<String, dynamic> data) {
    return User(
      id: data['id'],
      name: data['name'],
    );
  }
}
        
//Firestore Service

import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/user_model.dart';

class FirestoreService {
  final FirebaseFirestore _db = FirebaseFirestore.instance;

  Future<User> getUserData(String userId) async {
    final doc = await _db.collection('users').doc(userId).get();
    return User.fromMap(doc.data()!);
  }
}
        
// API service

import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiService {
  Future<List<String>> fetchPosts() async {
    final response =
        await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));

    if (response.statusCode == 200) {
      final List data = jsonDecode(response.body);
      return data.map((post) => post['title'].toString()).toList();
    } else {
      throw Exception('Failed to load posts');
    }
  }
}
        
// User Provider

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/firestore_service.dart';
import '../models/user_model.dart';

final userProvider = FutureProvider.family<User, String>((ref, userId) {
  return FirestoreService().getUserData(userId);
});
        
//API provider



import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/api_service.dart';

final apiProvider = FutureProvider<List<String>>((ref) {
  return ApiService().fetchPosts();
});
        
//Theme Provider


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

class ThemeNotifier extends StateNotifier<ThemeMode> {
  ThemeNotifier() : super(ThemeMode.light);

  void toggleTheme() {
    state = state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
  }
}

final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeMode>((ref) {
  return ThemeNotifier();
});

        
// Main File



import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'views/home_screen.dart';
import 'providers/theme_provider.dart';

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeProvider);

    return MaterialApp(
      title: 'Riverpod Demo',
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      themeMode: themeMode,
      home: HomeScreen(),
    );
  }
}

        
//Home Screen UI





import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/user_provider.dart';
import '../providers/api_provider.dart';
import '../providers/theme_provider.dart';
import '../widgets/toggle_button.dart';

class HomeScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider('123'));
    final postsAsync = ref.watch(apiProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text("Riverpod Demo"),
        actions: [
          IconButton(
            icon: Icon(Icons.brightness_6),
            onPressed: () => ref.read(themeProvider.notifier).toggleTheme(),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            // ?? Firestore User Data
            userAsync.when(
              data: (user) => Text("User: ${user.name}", style: TextStyle(fontSize: 20)),
              loading: () => CircularProgressIndicator(),
              error: (err, _) => Text("Error: $err"),
            ),
            SizedBox(height: 20),

            // ?? Toggle Button
            ToggleButtonWidget(),

            SizedBox(height: 20),

            // ?? API Data
            Expanded(
              child: postsAsync.when(
                data: (posts) => ListView.builder(
                  itemCount: posts.length,
                  itemBuilder: (context, index) => ListTile(title: Text(posts[index])),
                ),
                loading: () => Center(child: CircularProgressIndicator()),
                error: (err, _) => Text("Error loading posts"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
        

How Riverpod Handles Disposal?

  • StateProvider & FutureProvider: Automatically disposed of when no longer used.
  • StateNotifierProvider & ChangeNotifierProvider: You must override the dispose() method if necessary.
  • StreamProvider: Disposed when no longer listened to.

 @override
  void dispose() {
    print("CounterNotifier Disposed!");
    super.dispose();
  }        
final timerProvider = Provider.autoDispose((ref) {
  final timer = Timer.periodic(Duration(seconds: 1), (timer) {
    print("Tick...");
  });

  ref.onDispose(() {
    print("Timer Disposed!");
    timer.cancel();
  });

  return timer;
});
        

Understanding .family in Riverpod

  • The .family modifier in Riverpod allows us to create parameterized providers.
  • This is useful when we need to fetch data dynamically based on a parameter, such as an ID, user input, or configuration.

Example

final userProvider = FutureProvider.family<UserModel, String>((ref, userId) async {
  final response = await http.get(Uri.parse('https://api.example.com/users/$userId'));
  final data = jsonDecode(response.body);
  return UserModel.fromJson(data);
});


Consumer(
  builder: (context, ref, child) {
    final userId = "123"; // This could come from a route or user selection
    final userAsync = ref.watch(userProvider(userId));

    return userAsync.when(
      data: (user) => Text("User: ${user.name}"),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text("Error: $err"),
    );
  },
);

        

Wrapping Up: Mastering Riverpod in Flutter ??

Riverpod is a game-changer in Flutter state management, providing a scalable, testable, and efficient way to handle app states. In this article, we explored:

? The different types of Riverpod providers and their use cases. ? How to efficiently fetch and manage data using FutureProvider & StreamProvider. ? Advanced concepts like StateNotifierProvider, ChangeNotifierProvider, and Family. ? Best practices for maintaining and disposing of providers effectively.

What’s Next? Let’s Discuss! ??

Have you tried Riverpod in your projects? Which provider do you use the most? Let’s share insights in the comments! ??

If you found this article helpful, give it a like, share it with your fellow developers, and follow me for more Flutter content! ????

?? Further Reading:

?? State management is a journey, not a destination. Keep coding, and keep building! ???

要查看或添加评论,请登录

Muhammad Ismail的更多文章

社区洞察

其他会员也浏览了