Flutter Riverpod Tutorial

35 min read Riverpod is a modern state management library for Flutter that offers a robust and efficient way to manage state in your applications. Unlike other state management solutions, Riverpod ensures compile-time safety, eliminating many runtime errors and making your code more reliable. It also doesn't require a BuildContext to access providers, simplifying the development process. Riverpod's ability to override providers makes it incredibly easy to write unit tests, enhancing your app's testability. Designed with modularity and scalability in mind, Riverpod is suitable for both small and large applications. June 15, 2024 21:52 Flutter Riverpod Tutorial Flutter Riverpod Tutorial

Flutter Riverpod Tutorial

Managing state in a Flutter application can be complex, especially as the app grows. Riverpod is a state management library that offers several advantages over other state management solutions like Provider, Bloc, or Redux. In this tutorial, we will learn how to manage state in a Flutter application using Riverpod, create a counter app with increment, decrement, and reset functionalities, and navigate to a second page while maintaining state across pages.

Why Riverpod?

Riverpod is a modern state management library for Flutter that provides several benefits:

  • Compile-Time Safety: Riverpod ensures that providers are created correctly at compile time, reducing runtime errors.
  • No Need for BuildContext: Unlike Provider, Riverpod does not require a BuildContext to access providers, making it easier to use.
  • Easier Testing: Riverpod's providers can be overridden for testing purposes, making it easier to write unit tests.
  • Modular and Scalable: Riverpod is designed to be modular and scalable, allowing you to manage state efficiently in large applications.

Tips and Tricks with Riverpod

  • Use Providers for Global State: Use StateNotifierProvider and StateProvider to manage global state that needs to be accessed across the app.
  • Override Providers for Testing: Use ProviderScope and overrides to inject mock providers in tests.
  • Avoid Rebuilding Unnecessary Widgets: Use Consumer and ConsumerWidget to rebuild only the parts of the UI that depend on specific providers.

Step 1: Set Up Your Flutter Project

  1. Create a new Flutter project:
    flutter create counter_app
    cd counter_app
  2. Add Riverpod to your pubspec.yaml file:
    dependencies:
      flutter:
        sdk: flutter
      flutter_riverpod: ^2.0.0
  3. Run flutter pub get to install the dependencies.

Step 2: Create a State Management File

Create a file named counter_provider.dart in the lib directory:

import 'package:flutter_riverpod/flutter_riverpod.dart';

// StateNotifier for managing the counter
class CounterNotifier extends StateNotifier {
  CounterNotifier() : super(0); // Initialize the counter to 0

  // Method to increment the counter by a given step
  void increment(int step) => state += step;

  // Method to decrement the counter if it is greater than 0
  void decrement() {
    if (state > 0) state--;
  }

  // Method to reset the counter to 0
  void reset() => state = 0;
}

// Provider for the counter
final counterProvider = StateNotifierProvider((ref) {
  return CounterNotifier();
});

// StateNotifier for managing the step size
class StepNotifier extends StateNotifier {
  StepNotifier() : super(1); // Initialize the step size to 1

  // Method to set a new step size
  void setStep(int newStep) => state = newStep;
}

// Provider for the step size
final stepProvider = StateNotifierProvider((ref) {
  return StepNotifier();
});

Step 3: Update the Main Application File

Update the main.dart file in the lib directory:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'counter_provider.dart'; // Ensure this is the correct import path
import 'second_page.dart'; // Import the second page

void main() {
  runApp(const ProviderScope(child: MyApp())); // Wrap MyApp with ProviderScope
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends ConsumerStatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  ConsumerState createState() => _MyHomePageState();
}

class _MyHomePageState extends ConsumerState
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 50),
      vsync: this,
    );
    _animation = CurvedAnimation(parent: _controller, curve: Curves.bounceOut);
  }

  void _incrementCounter() {
    final step = ref.read(stepProvider);
    ref.read(counterProvider.notifier).increment(step);
    _controller.forward().then((_) => _controller.reverse());
  }

  void _decrementCounter() {
    ref.read(counterProvider.notifier).decrement();
    _controller.forward().then((_) => _controller.reverse());
  }

  void _resetCounter() {
    ref.read(counterProvider.notifier).reset();
    _controller.reset();
  }

  void _navigateToSecondPage() {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (context) => const SecondPage()),
    );
  }

  @override
  Widget build(BuildContext context) {
    final counter = ref.watch(counterProvider);
    final step = ref.watch(stepProvider);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You have pushed the button this many times:'),
            AnimatedBuilder(
              animation: _animation,
              builder: (context, child) {
                return Transform.scale(
                  scale: _animation.value + 1,
                  child: child,
                );
              },
              child: Text(
                '$counter',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FloatingActionButton.extended(
                  onPressed: counter > 0 ? _decrementCounter : null,
                  tooltip: 'Decrement',
                  icon: const Icon(Icons.remove),
                  label: const Text('Decrement'),
                  backgroundColor: counter > 0 ? Colors.blue : Colors.grey,
                ),
                const SizedBox(width: 20),
                FloatingActionButton.extended(
                  onPressed: _resetCounter,
                  tooltip: 'Reset',
                  icon: const Icon(Icons.refresh),
                  label: const Text('Reset'),
                ),
                const SizedBox(width: 20),
                FloatingActionButton.extended(
                  onPressed: _incrementCounter,
                  tooltip: 'Increment',
                  icon: const Icon(Icons.add),
                  label: const Text('Increment'),
                ),
              ],
            ),
            const SizedBox(height: 20),
            Text('Increment Step: $step'),
            Slider(
              value: step.toDouble(),
              min: 1,
              max: 10,
              divisions: 9,
              label: step.toString(),
              onChanged: (value) {
                ref.read(stepProvider.notifier).setStep(value.toInt());
              },
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _navigateToSecondPage,
              child: const Text('Go to Second Page'),
            ),
          ],
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Step 4: Create the Second Page

Create a new file named second_page.dart in the lib directory:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'counter_provider.dart'; // Ensure this is the correct import path

class SecondPage extends ConsumerWidget {
  const SecondPage({super.key});

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

    void _incrementCounter() {
      final step = ref.read(stepProvider);
      ref.read(counterProvider.notifier).increment(step);
    }

    void _decrementCounter() {
      ref.read(counterProvider.notifier).decrement();
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Counter on second page:'),
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FloatingActionButton.extended(
                  onPressed: _decrementCounter,
                  tooltip: 'Decrement',
                  icon: const Icon(Icons.remove),
                  label: const Text('Decrement'),
                ),
                const SizedBox(width: 20),
                FloatingActionButton.extended(
                  onPressed: _incrementCounter,
                  tooltip: 'Increment',
                  icon: const Icon(Icons.add),
                  label: const Text('Increment'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Step 5: Run the Application

  1. Run the application:
    flutter run
  2. Interact with the application:
    • Use the increment and decrement buttons to change the counter value on the main page.
    • Use the reset button to reset the counter on the main page.
    • Adjust the step size using the slider on the main page.
    • Click the "Go to Second Page" button to navigate to the second page.
    • Use the increment and decrement buttons on the second page to change the counter value.
    • Navigate back to the main page to see the updated counter value.

Conclusion

In this tutorial, we learned how to manage state in a Flutter application using Riverpod. We created a simple counter app with increment, decrement, and reset functionalities. Additionally, we added a slider to adjust the increment step size, animated the counter text with a bounce effect, and added navigation to a second page where the counter can also be modified. The state is maintained across pages, demonstrating the power and flexibility of Riverpod for state management in Flutter applications.

User Comments (0)

Add Comment
We'll never share your email with anyone else.