Riverpod in Flutter: A Complete Guide

Introduction

Riverpod is a powerful and modern state management solution for Flutter. It builds upon the concepts of Provider but removes limitations like context dependency, making it more scalable and testable.

In this guide, we’ll cover:

  • Why use Riverpod?

  • Installation

  • Core concepts

  • Different types of providers

  • State management with Riverpod

  • Dependency injection

  • Async programming

  • Notifiers and state modifiers

  • Example project


Why Use Riverpod?

No BuildContext Required – Unlike Provider, Riverpod doesn’t require context.watch or context.read.
Improved Performance – It ensures widgets only rebuild when necessary.
Testability – Riverpod makes writing tests simpler.
Scalability – Ideal for small to large-scale applications.


Installation

To use Riverpod in your Flutter app, add the following dependency in pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.0.0
  

Then, run:


flutter pub get
  

1. Setting Up Riverpod

Riverpod requires a ProviderScope at the root of your application.


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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}
  

2. Understanding Providers

Riverpod provides different types of providers for different use cases. Let’s explore them.

a) Provider (Read-Only State)

Used for providing immutable values.


final helloProvider = Provider((ref) => "Hello, Riverpod!");

class HomeScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final message = ref.watch(helloProvider);
    return Scaffold(
      body: Center(child: Text(message)),
    );
  }
}
  

b) StateProvider (Mutable State)

Used for simple state changes.


final counterProvider = StateProvider((ref) => 0);

class CounterScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Scaffold(
      body: Center(child: Text("Count: $count")),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: Icon(Icons.add),
      ),
    );
  }
}
  

c) FutureProvider (Asynchronous Data)

Used for handling async operations.


final dataProvider = FutureProvider((ref) async {
  await Future.delayed(Duration(seconds: 2));
  return "Data loaded!";
});

class AsyncScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncData = ref.watch(dataProvider);
    return Scaffold(
      body: Center(
        child: asyncData.when(
          data: (data) => Text(data),
          loading: () => CircularProgressIndicator(),
          error: (err, _) => Text("Error: $err"),
        ),
      ),
    );
  }
}
  

d) StreamProvider (Stream Handling)

Used for handling real-time data.


final streamProvider = StreamProvider((ref) async* {
  for (int i = 0; i < 10; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
});

class StreamScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final streamData = ref.watch(streamProvider);
    return Scaffold(
      body: Center(
        child: streamData.when(
          data: (data) => Text("Count: $data"),
          loading: () => CircularProgressIndicator(),
          error: (err, _) => Text("Error: $err"),
        ),
      ),
    );
  }
}
  

3. Riverpod Notifiers

a) StateNotifierProvider (For Complex States)

Used for handling complex states.


class CounterNotifier extends StateNotifier {
  CounterNotifier() : super(0);
  void increment() => state++;
}

final counterNotifierProvider =
    StateNotifierProvider((ref) => CounterNotifier());

class NotifierScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterNotifierProvider);
    return Scaffold(
      body: Center(child: Text("Count: $count")),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterNotifierProvider.notifier).increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  

4. Dependency Injection with Riverpod

Riverpod makes it easy to inject dependencies.


final apiServiceProvider = Provider((ref) => ApiService());

class ApiService {
  String fetchData() => "Fetched Data";
}

class DependencyScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final apiService = ref.watch(apiServiceProvider);
    return Scaffold(
      body: Center(child: Text(apiService.fetchData())),
    );
  }
}
  

5. Persisting State in Riverpod

Use shared_preferences or hive for local storage.


final sharedPrefsProvider = Provider((ref) {
  throw UnimplementedError();
});

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final sharedPreferences = await SharedPreferences.getInstance();
  runApp(
    ProviderScope(
      overrides: [sharedPrefsProvider.overrideWithValue(sharedPreferences)],
      child: MyApp(),
    ),
  );
}
  

Conclusion

Riverpod is a game-changer for Flutter state management. Whether you need simple state handling, async operations, or dependency injection, Riverpod provides a clean and scalable solution.

If you're building a Flutter app, give Riverpod a try! 🚀

Comments

Popular posts from this blog

Setting Up Flutter Environment on Windows: A Complete Guide

Flutter State Management: A Beginner's Guide