43  comments

Riverpod is the response to all the insufficiencies of other dependency injection and state management packages for Dart & Flutter apps.  It's quick to get familiar with, maintainable, testable and it's much less error-prone than the other solutions out there. Let's take a look at the core principles of Riverpod.

How is Riverpod any different?

There are multiple ways to provide dependencies around your Flutter app. Service locators, such as get_it, are great but they aren't directional, for lack of a better word. This means you can access objects in whatever order you want and you're not necessarily guaranteed that all of them are properly initialized unless you're careful. This can result in major headaches especially on large projects with huge amounts of dependencies.

Another solution is to use the Provider package which is a well-known and widely accepted package for providing objects around your Flutter apps. In this tutorial I assume that you've had a chance to use it before. It's great, it's directional but it also has many drawbacks. 

Provider is not perfect

For one, Provider is dependent on Flutter since you're using widgets to provide objects down the widget tree. I've never been very comfortable with mixing the UI code with dependency injection. Provider's usage of widgets also goes hand in hand with all the nesting it creates. Imagine we have MySecondClass that depends on MyFirstClass.

main.dart

class MySecondClass {
  final MyFirstClass myFirstClass;
  MySecondClass(this.myFirstClass);
}

Although I'm not going to explain the Provider package in this tutorial, take a look at the horribly nested UI code needed to properly construct an instance of MySecondClass.

main.dart

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (context) => MyFirstClass(),
      child: ProxyProvider<MyFirstClass, MySecondClass>(
        update: (context, firstClass, previous) => MySecondClass(firstClass),
        child: MyVisibleWidget(),
      ),
    );
  }
}

Secondly, Provider relies only on types to resolve the requested object. If you provide two objects of the same type, you can get only the one closer to the call site.

main.dart

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (context) => 'A String far away.',
      child: Provider(
        create: (context) => 'A String that is close.',
        builder: (context, child) {
          // Displays 'A String that is close.'
          // There's no way to obtain 'A String far away.'
          return Text(Provider.of<String>(context));
        },
      ),
    );
  }
}

Lastly, if you try to access a type which is not provided at all, you're going to get an error only at run-time. This is not ideal since we should always strive to catch as many errors as possible at compile-time.

The basics of Riverpod

Riverpod combines the best things from service locators and Provider which results in a package that is just pleasurable to use . It's compile-time safe, doesn't depend on Flutter, it's still directional and the boilerplate is minimal. Sounds too good to be true? Well, this old adage just doesn't apply in some cases and Riverpod is thankfully one of them.

The first step is to add the package as a dependency to your Flutter project. In this tutorial, we're going to use flutter_riverpod which is the way to go if you want to use pure Flutter.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^0.12.1

If you're using flutter_hooks in your project, you will probably want to also use hooks_riverpod. Lastly, there's the pure Dart riverpod package in case you're developing for something else than the Flutter framework.

Riverpod's Providers aren't placed into the widget tree. They're instead put into global variables which can be inside any file you want. As long they're accessible, you're fine.

main.dart

final greetingProvider = Provider((ref) => 'Hello Riverpod!');

This simplest Provider can expose a read-only value. There are many more types of  Providers for working with Futures, Streams, ChangeNotifiers, StateNotifiers and more.

The ref parameter is of type ProviderReference. As you'll see later on, it's mostly used to resolve dependencies between providers.

While the Provider object is globally accessible, this does not mean that the provided object (in this case a string "Hello Riverpod!") is itself global. Much like with a global function, you can call it from anywhere but the returned value may also be scoped locally. Consider the following code:

function_analogy.dart

String globalFunction() {
  return 'some value';
}

class MyClass {
  void _classMethod() {
    final valueLocalToThisMethod = globalFunction();
  }
}

Where are the widgets!?

Although it's great news that Riverpod's Providers are Flutter-independent, we still need to use the value provided by a Provider object from the widget tree - this is Flutter, after all.

The Riverpod package comes with just a single InheritedWidget that needs to be placed above the whole widget tree called ProviderScope. It's responsible for holding something called a ProviderContainer which is in turn responsible for storing the state of individual Provider objects.

main.dart

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}
For the most part, having a single ProviderScope wrapping the whole widget tree is enough. You can have multiple ones if you want to override certain providers for a part of the widget tree.

Watching a provider

How do we then obtain the string from the greetingProvider so that we can display it using a Text widget? There are actually two ways to do it. Let's showcase both based on the same piece of standard Flutter code. We want to get the provided string into the highlighted widget.

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Tutorial',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Riverpod Tutorial'),
        ),
        body: Center(
          child: Text('greeting goes here'),
        ),
      ),
    );
  }
}

The first way is to change the superclass of our widget to ConsumerWidget which comes from the flutter_riverpod package. This adds a ScopedReader function into the widget's build method. Since we use the ScopedReader to watch the provider and rebuild the widget if any change occurs, the convention is to give this parameter a name "watch".

main.dart

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    // Gets the string from the provider and causes
    // the widget to rebuild when the value changes.
    final greeting = watch(greetingProvider);

    return MaterialApp(
      title: 'Riverpod Tutorial',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Riverpod Tutorial'),
        ),
        body: Center(
          child: Text(greeting),
        ),
      ),
    );
  }
}

The other way of getting the value out of a provider is useful if you want to quickly optimize your widget rebuilds. It's not the end of the world in our case, but we're still rebuilding the whole MaterialApp while, in fact, we only need to rebuild the affected Text widget all the way down the widget tree. This is what the Consumer widget is for.

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Tutorial',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Riverpod Tutorial'),
        ),
        body: Center(
          child: Consumer(
            builder: (context, watch, child) {
              final greeting = watch(greetingProvider);
              return Text(greeting);
            },
          ),
        ),
      ),
    );
  }
}

Reading a provider

Sometimes, it may be impossible to call watch because you're not inside of the build  method. For example, you may want to perform an action when a button is pressed. That's when you can call context.read(). Let's showcase it on another type of a provider - ChangeNotifierProvider. Consider this code:

main.dart

class IncrementNotifier extends ChangeNotifier {
  int _value = 0;
  int get value => _value;

  void increment() {
    _value += 1;
    notifyListeners();
  }
}

final incrementProvider = ChangeNotifierProvider((ref) => IncrementNotifier());

As you've probably seen a thousand times already, we're going to create the classic counter app. A Text widget will watch the provider and get automatically rebuilt if a change occurs and the FloatingActionButton will only read the provider to call the increment() method on it.

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Tutorial',
      home: Scaffold(
        body: Center(
          child: Consumer(
            builder: (context, watch, child) {
              final incrementNotifier = watch(incrementProvider);
              return Text(incrementNotifier.value.toString());
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            context.read(incrementProvider).increment();
          },
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

Stuff you can't do with the "original" Provider

As you could've seen by now, Riverpod's providers objects don't rely on types for resolving their values. This is in contrast with the "original" Provider package. This is awesome because you can have multiple providers of the same type without any issue. 

example.dart

final firstStringProvider = Provider((ref) => 'First');
final secondStringProvider = Provider((ref) => 'Second');

// Somewhere inside a ConsumerWidget
final first = watch(firstStringProvider);
final second = watch(secondStringProvider);

Another conclusion that logically flows out of Riverpod not relying on types is that you cannot possibly try to get a value which is unavailable. You can only watch or read a provider variable that you can access and as long as you can access a provider, its value has to be available, right?

Dependency between providers

Any real-world app has dependencies between classes. For example, you can have a ChangeNotifier that depends on a Repository which in turn depends on a HttpClient. Handling such dependencies with Riverpod is simple and readable.

To keep our example simple, let's just have a FutureProvider depend directly on a simple FakeHttpClient. Getting hold of another provider inside a provider's function is done by calling read on the ProviderReference parameter which is always passed in. If you depend on a provider whose value can change, you can also call watch.

This is the first time we deal with a FutureProvider but I don't think its creation requires much explanation. It looks almost exactly like the most basic Provider but the function that you pass in returns a Future. Just like with aProvider the creation function of a FutureProvider runs as soon as we obtain it from a widget for the first time.

main.dart

class FakeHttpClient {
  Future<String> get(String url) async {
    await Future.delayed(const Duration(seconds: 1));
    return 'Response from $url';
  }
}

final fakeHttpClientProvider = Provider((ref) => FakeHttpClient());
final responseProvider = FutureProvider<String>((ref) async {
  final httpClient = ref.read(fakeHttpClientProvider);
  return httpClient.get('https://resocoder.com');
});

Using values from a FutureProvider from the UI is absolutely delightful. Gone are the days of clumsy FutureBuilders! Riverpod makes building widgets based on a Future easy. It uses freezed unions behind the scenes to break down a Future object into an AsyncValue union which we can easily map to different widgets.

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Tutorial',
      home: Scaffold(
        body: Center(
          child: Consumer(
            builder: (context, watch, child) {
              final responseAsyncValue = watch(responseProvider);
              return responseAsyncValue.map(
                data: (_) => Text(_.value),
                loading: (_) => CircularProgressIndicator(),
                error: (_) => Text(
                  _.error.toString(),
                  style: TextStyle(color: Colors.red),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

Passing arguments to providers

What if we wanted to pass a user-defined URL to the responseProvider? That's what provider families are for! By changing the responseProvider to the following...

main.dart

final responseProvider =
    FutureProvider.family<String, String>((ref, url) async {
  final httpClient = ref.read(fakeHttpClientProvider);
  return httpClient.get(url);
});

...we can now pass the request URL from the UI. Sure, there's no actual user input to keep things simple but you can try changing the hard-coded URL string and you'll see that the FutureProvider reruns its creation function each time you change the passed in string.

main.dart

final responseAsyncValue = watch(responseProvider('https://resocoder.com'));

Automatically disposing of state

Here's an interesting observation - try changing the inputted URL to A then to B and lastly back to A. What happened? When you changed the value back to A, there was no CircularProgressIndicator displayed which means that the value was readily present in the FutureProvider and there was no need to perform yet another fake get request.

This caching of a provider's state is great but sometimes you may want to perform requests even to the same URL over and over again. That's what the autoDispose modifier is for.

main.dart

final responseProvider =
    FutureProvider.autoDispose.family<String, String>((ref, url) async {
  final httpClient = ref.read(fakeHttpClientProvider);
  return httpClient.get(url);
});

This modifier will dispose of the provider's state as soon as the provider is not used. In our case, this happens if we change the argument passed into the provider family. However, autoDispose is useful even if you're not using a family modifier. In such case, disposal will be the started when the ConsumerWidget that watches a provider is disposed.

Conclusion

You've just learned the essence of the Riverpod package. We've gone through the very basics all the way to provider modifiers. Of course, we can go more in-depth because this package has a lot more to offer. If you don't want to miss the upcoming tutorials about building more advanced apps with Riverpod, sign up to the weekly Flutter newsletter below.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter Developer at LeanCode and a developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

Flutter UI Testing with Patrol

Flutter UI Testing with Patrol
  • I heard this can replace get_it, however I still don’t get clearly how to use it to inject/locate repository and other things.

    So if I have these:
    – Repository Class which implements IRepository
    – FakeHttpClient Class which implement IHttpClient
    – FirebaseLogger which implement ILogger
    – Repository that use IHttpClient and ILogger
    – Provider Class that extend ChangeNotifier and use IRepository

    then I have to create:

    final httpClientProvider = Provider((ref) => FakeHttpClient());

    final loggerProvider = Provider((ref) => FirebaseLogger());

    final repositoryProvider = Provider((ref) => Repository(ref.read(httpClientProvider)));

    final provider = ChangeNotifierProvider(
    (ref) => Provider(
    ref.read(loggerProvider),
    ref.read(approvalRepository),
    ));

    Is this right?

  • I have an abstract class LectureRepository implemented by LectureRepositoryImpl. LectureRepositoryImpl initialises SharedPreferences sharedPreferences with

    LectureLocalDataSourceImpl({@required this.sharedPreferences})
    Now I want to use Riverpod to access the repository so I do:

    FutureProvider(
    (ref) async {
    return LectureLocalDataSourceImpl(sharedPreferences: await SharedPreferences.getInstance());
    },
    );
    but now in lectureRepositoryProvider, wenn I read the lectureLocalDataRepositoryProvider I get an AsyncValue in the value localDataSource, which I cannot assign here:

    final lectureRepositoryProvider = FutureProvider((ref) async {
    final localDataSource = ref.read(lectureLocalDataRepositoryProvider);
    return LectureRepositoryImpl(localDataSource: localDataSource);
    });
    How should I handle the AsyncValue?

  • Excellent article! I’d like to see it one day as a part of the official Riverpod documentation in Getting Started section.

  • Its like you read my mind! You appear to know so much about this, like you wrote the book in it or something. I think that you can do with a few pics to drive the message home a little bit, but other than that, this is fantastic blog. A great read. I’ll certainly be back.

  • I loved as much as you’ll receive carried out right here. The sketch is tasteful, your authored material stylish. nonetheless, you command get bought an nervousness over that you wish be delivering the following. unwell unquestionably come more formerly again since exactly the same nearly a lot often inside case you shield this hike.

  • Hello, Neat post. There’s an issue together with your site in internet explorer, would check this텶E still is the marketplace chief and a large element of other folks will leave out your magnificent writing due to this problem.

  • I do believe all the ideas you’ve presented for your post. They are really convincing and will certainly work. Nonetheless, the posts are too short for novices. May just you please lengthen them a little from subsequent time? Thanks for the post.

  • Somebody essentially lend a hand to make significantly articles I’d state. That is the very first time I frequented your website page and up to now? I surprised with the research you made to make this actual submit amazing. Wonderful task!

  • I loved as much as you will receive carried out right here. The sketch is tasteful, your authored subject matter stylish. nonetheless, you command get got an edginess over that you wish be delivering the following. unwell unquestionably come further formerly again as exactly the same nearly very often inside case you shield this hike.

  • Bwer Pipes: The Ultimate Destination for Irrigation Excellence in Iraq: Elevate your farm’s irrigation capabilities with Bwer Pipes’ premium-quality products. Our innovative sprinkler systems and robust pipes are engineered for durability and efficiency, making them the ideal choice for Iraqi farmers striving for success. Learn More

  • Bwer Pipes: Leading the Way in Agricultural Irrigation Technology: Revolutionize your irrigation practices with Bwer Pipes’ innovative solutions. Our cutting-edge sprinkler systems and durable pipes are engineered to withstand the harsh conditions of Iraqi agriculture, ensuring optimal water usage and crop growth. Learn More

  • Optimize Your Farm’s Water Management with Bwer Pipes: Bwer Pipes offers a comprehensive range of irrigation solutions designed to help Iraqi farmers maximize water efficiency. Our reliable sprinkler systems and durable pipes ensure uniform water distribution, promoting healthier crops and sustainable farming practices. Explore Bwer Pipes

  • LDPE Pipes in Iraq Elite Pipe Factory in Iraq offers a comprehensive range of LDPE pipes, which are valued for their flexibility, lightweight nature, and resistance to various chemicals. Our LDPE pipes are engineered to meet high standards of quality, ensuring reliable performance across various applications. Recognized as one of the best and most reliable pipe manufacturers in Iraq, Elite Pipe Factory is committed to delivering products that combine durability with performance. For more information on our LDPE pipes, visit elitepipeiraq.com.

  • HDPE Pipes in Iraq Elite Pipe Factory in Iraq excels in the production of HDPE pipes, which are known for their strength, durability, and resistance to impact and chemicals. Our HDPE pipes are engineered to meet the toughest standards, making them ideal for a wide range of applications, from water distribution to industrial uses. As one of the best and most reliable pipe manufacturers in Iraq, Elite Pipe Factory is dedicated to providing products that deliver outstanding performance and longevity. Discover more about our HDPE pipes and other offerings at elitepipeiraq.com.

  • Clay Pipes in Iraq At Elite Pipe Factory, we take pride in offering high-quality clay pipes, a trusted solution for traditional and modern construction needs in Iraq. Our clay pipes are renowned for their durability, resistance to harsh environmental conditions, and their role in sustainable infrastructure projects. Elite Pipe Factory stands out as one of the best and most reliable manufacturers in Iraq, providing clay pipes that meet rigorous industry standards. For more information about our clay pipes and other products, visit our website at elitepipeiraq.com.

  • أنابيب الألياف الزجاجية والراتنج في العراق تفتخر شركة إيليت بايب في العراق بأنها منتج رائد لأنابيب الألياف الزجاجية والراتنج عالية الجودة، التي توفر أداءً ممتازًا ومتانة لتطبيقات صناعية متنوعة. توفر أنابيب البلاستيك المدعمة بالألياف الزجاجية (FRP)، والمعروفة أيضًا بأنابيب GRP، مقاومة ممتازة للتآكل، وخصائص خفيفة الوزن، وعمر خدمة طويل. تجعل هذه الخصائص منها مثالية للاستخدام في بيئات تتطلب أداءً عالياً مثل معالجة المواد الكيميائية، ومعالجة المياه، وصناعات النفط والغاز. مع التزامنا بالابتكار والجودة، تضمن شركة إيليت بايب أن كل أنبوب يلبي المعايير الصارمة، مما يثبت مكانتنا كواحدة من أفضل وأكثر الموردين موثوقية في العراق. لمزيد من المعلومات، تفضل بزيارة موقعنا على elitepipeiraq.com.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >