Open In App

Flutter – Introduction to State Management Using Riverpod

Last Updated : 07 Sep, 2021
Improve
Improve
Like Article
Like
Save
Share
Report

Riverpod is a Reactive State-Management and Dependency Injection framework, it uses different providers to let us access and listen to state changes across our app, it is built by Remi Rousselet. If you don’t know what is state then I’ll recommend you to read this article first as it will be a little hard for you to understand Riverpod without understanding the “state” itself.

What does Riverpod actually do?

Riverpod is a state management helper. It basically makes our state (in clear words, our variables’ values) accessible in all parts of the app, it puts our state at the top of the widget tree and lets us listen to those state changes and update our User Interface accordingly. 

There are many types of providers which Riverpod offers, We’ll go over them one by one.

What are Providers?

Providers are the most important part of a Riverpod application, “A provider is an object that encapsulates a piece of state and allows listening to that state.”

Types of Providers:

  • StateProvider
  • FutureProvider
  • StreamProvider
  • Provider

Let’s start building!

1. Add Riverpod to our app:

Dart




dependencies:
  flutter_riverpod: ^0.14.0+3


2. Wrapping our App:

For providers to work, we must add ProviderScope at the root of our Flutter applications:

Dart




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


3. Create the providers.dart file:

We’ll define all our global providers in this file so that it is easier to maintain the project. Let’s define a StateProvider.

Dart




import 'package:flutter_riverpod/flutter_riverpod.dart';
  
// Instead of String you can use other data types as well,
// we can also use custom data types.
final userNameProvider=StateProvider<String>((ref) {
  
   // we can also return an empty String here, for the sake of simplicity, 
  //  let's return a sample name 
  return "SomeName";
});


4. Reading Providers:

Now that we’ve declared a StateProvider, Let’s learn how can we read providers. To read any provider, we have multiple widgets, in this article, we’ll go over ConsumerWidget() and Consumer().

Using a ConsumerWidget():

A consumer widget is a widget that we can use in place of our Stateful/Stateless widget. It gives us the ability to read/change the states of providers & also listen to them.

Dart




import 'Providers/providers.dart' as providers.dart; //for easy access
import 'package:flutter_riverpod/flutter_riverpod.dart';
  
class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
      
    // Listens to the value exposed by userNameProvider
    // ".state" method lets us get the state easily & directly
    String name = watch(providers.userNameProvider).state;
         
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Center(child: const Text('GFG 🙂 '))),
        body: Center(
            
          // displaying the value
          child: Text('$name'), 
        ),
      ),
    );
  }
}


Output:

Well and good. So now, whenever the value of userNameProvider changes, the Text() will be updated accordingly. But how are we gonna update the value of userNameProvider?

5. Updating/Changing the value of a StateProvider:

To change the value of StateProvider, we need a StateController. So let’s create it.

First, let’s remove the variable “name”. ̶

S̶t̶r̶i̶n̶g̶ ̶n̶a̶m̶e̶ ̶=̶ ̶w̶a̶t̶c̶h̶(̶p̶r̶o̶v̶i̶d̶e̶r̶s̶.̶u̶s̶e̶r̶N̶a̶m̶e̶P̶r̶o̶v̶i̶d̶e̶r̶)̶.̶s̶t̶a̶t̶e̶;̶

//removed as with it we can only listen/get the state not change/mutate the state.

//let's use this StateController instead.

StateController<String> nameController = watch(providers.userNameProvider);

// now we can get / set the state using name.state.

For the sake of simplicity, let’s use a FloatingActionButton, and using it let’s try to mutate(change) the state of userNameProvider.

Dart




// for easy access
import 'Providers/providers.dart' as providers.dart;
  
class Home extends ConsumerWidget {
  int n = 0;
  @override
  Widget build(BuildContext context, ScopedReader watch) {
      
    // Listens to the value exposed by userNameProvider
    StateController<String> nameController = watch(providers.userNameProvider);
  
    // ".state" method lets us get the state easily & directly
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            n++;
            nameController.state =
                
                // now we can set the state using the state setter.
                "New Name $n"
          },
        ),
        appBar: AppBar(title: Center(child: const Text('GFG 🙂 '))),
        body: Center(
          child: Text(
              
            // displaying the value
            '${nameController.state}',
          ), 
        ),
      ),
    );
  }
}


Output:

Now, when we press on the FloatingActionButton(), we’ll see that the name changes every time with a different number. So that’s it. Using this information we can easily use StateProviders, mutate their values and listen to it.

Now, Let’s go over asynchronous providers.

1. FutureProvider:

When we work with asynchronous code we often use some Future based APIs, let’s look into how you can handle Future events using Riverpod. To demonstrate, we will create a method which returns a Future.

Dart




class FutureClass {
    
  // in production, the below method could be any network call
  Future<int> getData(String para) async {
    await Future.delayed(Duration(seconds: 5));
    return 25;
  }
}


Creating a provider for FutureClass:

final futureClass = Provider((ref) => FutureClass());

Now, let’s create our FutureProvider,

Dart




final response = FutureProvider<int>((ref) async {
  final client = ref.read(futureClass);
  return client.getData('some text as a parameter');
});


Now, we can use ConsumerWidget to listen to the state changes and update our UI accordingly.

Consuming a FutureProvider:

Apart from the ConsumerWidget(), we also have a Consumer() widget. The difference is that while ConsumerWidget rebuilds the entire screen when state changes, Consumer() rebuilds only it’s child, making sure we don’t run into performance issues.

Let’s use Consumer() widget to watch(listen) for state changes.

Dart




class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(title: Text("FutureProvider Demo"),),
      body: Center(
        child: Column(
         children: [
            Consumer(
              builder: (context, watch, child) {
                final futureData = watch(response);               
              },
            ),
          ],
        ),
      ),
    );
  }
}


As a Future can have 3 states, i.e, completed, in progress & error, we have the ability to handle those states separately using the .map function.

Dart




Consumer(
  builder: (context, watch, child) {
    final futureData = watch(response);
  
    return  futureData.map(
        
      ,// have data
      data: (data) => Text('${data.value}',)
        
      ,// in progress
      loading: (_) => CircularProgressIndicator()
        
      // has an error
      error: (message) => Text(message.error),
    );
  },
),


If we want to pass a variable/object to the provider as an argument, then we can do that like-

Dart




final response=
    FutureProvider.autoDispose.family<int, String>((ref, i_am_a_param) async {
  final client = ref.read(futureClass);
  return client.getData(i_am_a_param);
});
  
// the family modifier is used to pass a String value,
// which is used to call the get() method.
// The modifier "autoDispose" destroys the state of a provider 
// when it is no longer used, even when the widget state is not yet dispose


Complete Code:

Dart




class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text("FutureProvider Demo"),),
        body: SafeArea(
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Consumer(
                  builder: (context, watch, child) {
                    final futureData = watch(providers.response);
  
                    return futureData.map(
                      data: (data) => Text(
                        '${data.value}',
                      ), // have data
                      loading: (_) => Column(
                        children: [
                          CircularProgressIndicator(),
                          Text(
                            'Fetching data',
                          ),
                        ],
                      ), // in progress
                      error: (message) =>
                          Text(message.error.toString()),
                      // has an error
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}


Output:

2. StreamProvider:

We often use streams in a flutter application, be it fetching data from Firestore or reading data from a file. Let’s learn how we can handle those streams with Riverpod.

Let’s declare a stream first,

final streamProvider = StreamProvider<int>((ref) {
  return Stream.fromIterable([105, 50]);
  //in production this could be a stream of documents from Firestore
});

Consuming StreamProvider:

This time, let’s use ConsumerWidget() to consume StreamProvider’s data, it is fairly similar to that of FutureProvider.

Dart




class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("StreamProvider Demo"),
        ),
        body: SafeArea(
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Consumer(
                  builder: (context, watch, child) {
                    final streamValue = watch(providers.streamProvider);
  
                    return streamValue.when(
                      data: (data) => Text(
                        '${data}',
                      ), // have data
                      loading: () => Column(
                        children: [
                          CircularProgressIndicator(),
                          Text(
                            'Fetching data',
                          ),
                        ],
                      ), // in progress
                      error: (message, e) =>
                          Text(message.toString()),
                      // has an error
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}


Now, every time the stream has new data, our UI will update accordingly.

Notice how we didn’t have to manage different states by ourselves, without Riverpod, the above code would look something like this:

Dart




final stream = Stream.fromIterable([105, 50]);
  
StreamBuilder<int>(
  stream: stream,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.active) {
      if (snapshot.hasData) {
          
         // data
        return SomeWidget(snapshot.data);
      } else if (snapshot.hasError) {
          
        // error state
        return SomeErrorWidget(snapshot.error); 
      } else {
          
        // no data
        return Text('No data'); 
      }
    } else {
        
       // loading state
      return CircularProgressIndicator();
    }
  }
)


That’s it. This should cover almost everything we need to learn to get started with Riverpod .

The complete code for the article above can be found here.



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads