Boost Flutter State Management with Riverpod

Boost Flutter State Management with Riverpod

We will go further into Flutter State Management with Riverpod in this article. We’ll review every crucial aspect of Riverpod, potential drawbacks, and practical ways to get around them. Let’s start now!

Let’s begin by understanding why Riverpod is necessary in the first place. A provider has issues that cannot be resolved, such as complex and tedious syntax from the proxy provider, multiple providers lacking the same return data type, and the infamous provider not found an exception.

Nevertheless, since Riverpod’s creation, many design patterns, including Flutter Singleton, dependency injection, and service locators, have been replaced by it to address these issues. Additionally, riverpod aids in fetching caching, canceling network requests, and handling error cases. To get started, go to the pubspec.yaml file and ensure you have the most recent version of flutter riverpod installed.

You can develop a cli using Riverpod since it is broken into three packages: hooks_riverpod, flutter_riverpod, and riverpod. Hooks_riverpod is used with Flutter, flutter_riverpod with the Flutter SDK, and riverpod with dart programs.

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

After doing it, you can go to the main. The providerscope Widget, a regular part of the flutter riverpod package, is wrapped by a dart file around your root widget and passed in the run app. This provider scope keeps track of all the providers and ensures that there is no State leakage despite the providers being declared globally.

In general, global declaration of variables or providers should be problematic due to mutability issues, i.e., if a non-constant variable like this is declared outside of any function and of any class. For example, I have a variable named “title” that I’ve declared globally.

import 'package:flutter/material.dart';

String title="riverpod";

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

The title has been defined, and any function can alter its value. For instance, I can alter the title’s value here if I have the primary function.

 void _incrementCounter() {
    setState(() {
      title="flutter_riverpod"; 
      _counter++;
    });
  }


I can type “flutter_riverpod” for my title, so if you could change this for hundreds of thousands of files, do you think that would be a good idea? It would be a bad idea because it would be very difficult to determine which function changed the title’s value, whether we wanted it to or not, and finding it would be a big task. If it were illegal, why would providers or riverpod even do this?

This is because all providers in Riverpod, except for one, have been declared worldwide as immutable, so let’s talk about all of the providers sequentially.

Provider

The first kind of provider is the provider itself. Riverpod has several providers, each of which can give us access to the desired state in Flutter development. Since each provider serves a different role, “provider” refers to the first kind of provider.

As implied by the name, if you are coming from a provider package, you are already familiar with it.

It is an object that serves as a data source for widgets or other providers; it is a read-only widget and cannot be used to edit its internal value; nonetheless, it can be used to offer primitive and non-primitive data types, as well as instances of classes, to build a provider.

import 'package:flutter_riverpod/flutter_riverpod.dart';

final myProvider = Provider<int>((_) => 24);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Consumer(
            builder: (context, watch, _) {
              final myValue = watch(myProvider);
              return Text('My value is $myValue');
            },
          ),
        ),
      ),
    );
  }
}

A Riverpod Provider is beneficial for providing values and services to other components of your application, but it does not come with an integrated system for managing the state. The StateProvider comes in at this point.

StateProvider

To update the value from outside, which is impossible using the provider, we have the second form of provider, the State provider. This type of provider is an upgrade over the standard provider.

A provider called a StateProvider enables you to expose a value so that it may be read and changed across your application. Any Flutter widgets watching the value will be rebuilt to reflect the modified value when you change it.

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

class Counter extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final count = watch(countProvider).state;

    return Scaffold(
      body: Center(
        child: Text('$count'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read(countProvider).state++,
        child: Icon(Icons.add),
      ),
    );
  }
}

Currently, a third type of Provider called StateNotifierProvider is available. It is an improvement over the state provider, primarily used for elementary values like the string we had over here, integer values, double values, and Boolean values. However, what about complex values? I’m not saying we can’t do that with the state provided; it’s possible, but our logic will lie around in our widgets. As Flutter programmers, we want our logic to be together in a single place and, most likely, a class. For instance, if you want to modify or manipulate the values inside of a class, what if you want to update a map? There will be many functions.

StateNotifierProvider

StateNotifierProvider is helpful in this scenario. We can centrally store all the logic for updating a complicated state by managing it with a StateNotifier class. We may split our concerns this way, keeping our code well-structured and maintainable.

Consider a User class, for instance, with attributes like name, email, age, and address. To control the User object’s status and offer a clear and organized technique to change and access its attributes across the app, we might use a StateNotifierProvider.

Here is an illustration of how we can manage the state of a User object using a StateNotifierProvider:

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

class User {
  String name;
  DateTime dateOfBirth;

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

class UserNotifier extends StateNotifier<User> {
  UserNotifier() : super(User(name: 'John Doe', dateOfBirth: DateTime.now()));

  void setName(String name) {
    state = User(name: name, dateOfBirth: state.dateOfBirth);
  }
}

final userProvider = StateNotifierProvider<UserNotifier, User>((ref) => UserNotifier());

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      child: MaterialApp(
        title: 'User Example',
        home: Scaffold(
          appBar: AppBar(
            title: Text('User Example'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Consumer(
                  builder: (context, watch, child) {
                    final user = watch(userProvider);
                    return Text(user.name);
                  },
                ),
                SizedBox(height: 20),
                FlatButton(
                  onPressed: () {
                    context.read(userProvider.notifier).setName('Shirsh Shukla');
                  },
                  child: Text('Change Name'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

The changeNotifierProvider is the following sort of provider; if you now use a provider, then you are already familiar with it since it directly derives from Riverpod and enables quick migration from provider to Riverpod because many of the techniques used in the provider have changed to utilize the Notifier class.

ChangeNotifierProvider

To clarify, ChangeNotifierProvider is a widget in the Flutter Riverpod state management library that gives its descendants access to an instance of a ChangeNotifier. Its function is to monitor the specified ChangeNotifier instance for changes and alert its descendants to rebuild when a change occurs.

Here’s an illustration of how to take advantage of Riverpod’s ChangeNotifierProvider:

import 'package:flutter_riverpod/flutter_riverpod.dart';

// Define a custom class that extends ChangeNotifier
class User extends ChangeNotifier {
  String name;
  DateTime dateOfBirth;

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

  void setName(String newName) {
    name = newName;
    notifyListeners();
  }
}

// Create a ChangeNotifierProvider for the User class
final userProvider = ChangeNotifierProvider<User>((ref) {
  return User(name: 'Shirsh Shukla', dateOfBirth: DateTime(1995, 4, 5));
});

// Use the User class in a widget
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('My App')),
        body: Center(
          child: Consumer(
            builder: (context, watch, child) {
              final user = watch(userProvider);

              return Text('Name: ${user.name}');
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            // Use the setName method to update the user's name
            context.read(userProvider).setName('Shirsh Shukla');
          },
          child: Icon(Icons.edit),
        ),
      ),
    );
  }
}

After learning a little bit about the change notifier provider, let’s move on to the next sort of provider, which is the future provider. If you have a provider-used project that is very large to convert, you can use it, but it is not advised, and I advise you not to do so.

FutureProvider

FutureProvider is a provider based on HTTP calls, asynchronous code, or even Firebase calls when used for futures, as the name indicates.

Therefore, utilizing the future provider would simplify your life if you have asynchronous code. It is a replacement for future Builder and has less code.

FutureProvider enables you to provide values that might not be accessible right away but will be at some point in the future. This is useful if you must fetch data from a server or carry out another asynchronous action to get a value.

For instance, you might provide user data to your app by using FutureProvider to retrieve it from a server. The asynchronous task of getting the data and updating the UI once it is available would be handled by the FutureProvider.

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

final apiProvider = FutureProvider.autoDispose((ref) async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));

  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
});

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FutureProvider Example'),
      ),
      body: Center(
        child: Consumer(
          builder: (context, watch, child) {
            final apiData = watch(apiProvider);

            return apiData.when(
              data: (data) => Text(data),
              loading: () => CircularProgressIndicator(),
              error: (error, stackTrace) => Text('Error: $error'),
            );
          },
        ),
      ),
    );
  }
}

Let’s move on to the last sort of provider in the Riverpod package, the stream provider, now that we have this future provider.

StreamProvider

When you need to listen to an event stream, such as user interactions or database updates, a StreamProvider might be helpful.

import 'dart:async';

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

final streamProvider = StreamProvider<int>((ref) {
  return Stream.periodic(Duration(seconds: 1), (i) => i).take(10);
});

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod StreamProvider Example',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Riverpod StreamProvider Example'),
        ),
        body: Consumer(builder: (context, watch, child) {
          final stream = watch(streamProvider).data?.value ?? Stream.empty();
          return StreamBuilder<int>(
            stream: stream,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Center(
                  child: Text('${snapshot.data}'),
                );
              } else {
                return Center(
                  child: CircularProgressIndicator(),
                );
              }
            },
          );
        }),
      ),
    );
  }
}

What features are accessible to us, and they’re not only for future or stream providers; they’re open to all types of providers, as we see all different kinds of providers.

I’m referring to the modifiers, a function that may be used to change or improve a Provider’s behavior. Modifiers provide Providers with new features like caching, slow initialization, or filtering.

Here are a few Riverpod instances of modifiers:

.family: This modifier is used to group Providers with similar behavior into a family. You may develop a group of HTTP Clients using the same primary URL.

.autoDispose: When a Provider is no longer in use, it may be automatically disposed of using the autoDispose modifier. For providers who use resources like database connections, this may be helpful.

.overrideWithvalue: Use the modifier.overrideWithValue to replace a Provider’s value with a different one. Depending on the situation, this can serve as test or mock data.

.state: Using the.state modifier, a Provider may be created to manage changeable state values. Managing UI state, such as whether a button is active or disabled, may be done with the help of this.

For instance, the following line of code illustrates how to use the.autoDispose modification in a StreamProvider:

final streamProvider = StreamProvider<int>((ref) {
  return Stream.periodic(Duration(seconds: 1), (i) => i).take(5);
}, autoDispose: true);

class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    AsyncValue<int> data = watch(streamProvider);
    return data.when(
      data: (value) => Text('Stream value: $value'),
      loading: () => CircularProgressIndicator(),
      error: (error, stackTrace) => Text('Error: $error'),
    );
  }
}


In this case, a stream of integers that emits a new value every second for five seconds is exposed by the StreamProvider. When the stream subscription is no longer required, such as when the Widget using it is removed from the widget tree, the provider will automatically dispose of it because the.autoDispose modifier is set to true. By doing this, resource waste and memory leaks are reduced.

Mixing modifiers to make more sophisticated Providers with unique behaviors is possible. By utilizing the.autoDispose and.cached modifiers, you could develop a Provider that automatically destroys itself and caches its value.

We have now examined all six of the different types of providers. To give you a refresher, a provider is a read-only object, so you can use it to provide values of any type, including non-primitive data types like list maps and instances of classes and primitive data types like integer and boolean strings. If you merely want to change a specific State, such as an integer value or a Boolean, the second state provider is like an upgrade of the standard provider.

The third option is the StateNotifier provider, typically used when dealing with more complicated states, such as the user class when we wish to edit their properties or something similar. A changeNotifierProvider is the fourth one. If you have a more extensive code base, it will assist you move from the provider to Riverpod, but if you’re starting from new or working on a smaller project, I recommend against using it due to its dynamic state. The fifth one is the FutureProvider, so you may use it if you have asynchronous code and want to avoid the drawbacks of the future builder and async snapshot. The sixth and final provider is StreamProvider, which can be used if you have a stream and want to do away with the stream Builder and async snapshot once more. It is reusable and with, and it should greatly assist you now. After knowing all of the providers, there are some ref methods that I want to discuss.

ProviderObserver

The last item I want to discuss in this flutter riverpod package is a ProviderObserver. As I previously mentioned, access to a ProviderObserver makes it easier to understand where a provider is being added, removed, or listened to. However, with access to a ProviderObserver, you can log these things out.

The Riverpod Flutter state management library ProviderObserver widget enables you to observe changes to a provider and respond appropriately.

Here is some sample code that demonstrates how to display a counter using ProviderObserver:

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('Counter')),
          body: Center(
            child: ProviderObserver(
              provider: counterProvider,
              // This function will be called whenever the counterProvider changes
              onChange: (BuildContext context, int value) {
                // The value argument is the new value of the counterProvider
                print('Counter changed to $value');
              },
              // This builder function will be called whenever the Widget is built
              builder: (BuildContext context, int value, Widget? child) {
                return Text('Counter: $value');
              },
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              // Increment the counterProvider by 1
              context.read(counterProvider).state++;
            },
            child: Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}

For clarity, I’ve written everything here on one page, but you could make this with a different class as well. We’re using a StateProvider to build a provider that stores an integer here. After that, we create a widget that shows the provider’s value using a ConsumerWidget. We override the provider’s state with a new value of 42 using ProviderScope, and we use ref.watch(myProvider) to keep track of changes to the provider’s state. We utilize the to access the provider’s current state within the constraints of the ProviderScope; we utilize the ScopedProviderRef. The overridden provider is also automatically disposed of when it is no longer required using the.autoDispose feature.

Flutter Developers from Flutter Agency

Conclusion

I wanted to tell you everything I knew about Riverpod. Riverpod provides a better option than Provider for maintaining the state in Flutter. As a consequence, it takes care of Provider’s drawbacks and makes it possible for your Flutter application to have an attractive architecture, letting you create a dependable Flutter application from scratch up. Contact the top Flutter app development company to use Riverpod state management solutions.

Frequently Asked Questions (FAQs)

1. Is Riverpod the state management?

A framework for both dependency injection and reactive state management is called Riverpod. It is also referred to as the successor to provider state management, which the same author created with distinct goals. It uses several services to enable us to listen to and access state changes across our app.

2. Why is Riverpod used in Flutter?

Riverpod is a reactive caching framework for Flutter/Dart. It can handle your failures and automatically retrieve, cache, combine, and recompute network requests. It lowers dependencies because the Provider package has been rebuilt.

3. What does the future state provider Riverpod do?

A Configuration object built by reading a JSON file might be conveniently exposed using FutureProvider. When the Future is finished, this will automatically rebuild the user interface. If many widgets request configurations at once, the asset will only be decoded once.

Reference Article link: https://link.medium.com/0RtITYJCZAb

Book Your Flutter Developer Now

Abhishek Dhanani

Written by Abhishek Dhanani

Abhishek Dhanani, a skilled software developer with 3+ years of experience, masters Dart, JavaScript, TypeScript, and frameworks like Flutter and NodeJS. Proficient in MySQL, Firebase, and cloud platforms AWS and GCP, he delivers innovative digital solutions.

Leave a comment

Your email address will not be published. Required fields are marked *


ready to get started?

Fill out the form below and we will be in touch soon!

"*" indicates required fields

✓ Valid number ✕ Invalid number