Flutter: Comparing GetIt, Provider and Riverpod

Today we’re going to look at 3 of the more popular libraries for basic state management in Flutter: GetIt, Provider and riverpod.

For each of the libraries, we’ll look at how you can perform data-binding, data-injection, and how you might mock them for testing.

The setup…

There are many types of data to share in Flutter: Futures, Streams, ValueNotifiers etc. In this example, we’ll go with a classic ChangeNotifier based manager with 2 mutable properties. It will look something like:

class AppManager extends ChangeNotifier {
    ...
    int _count1 = 0;
    int get count => _count;
    set count1(int value){
      _count = value;
      notifyListener();
    }

    int _count2 = 0;
    set count2(int value){
      _count2 = value;
      notifyListener();
    }
}

For the sake of example, imagine we have two other objects to pass around: SettingsManager and FileService. Also, let’s assume that the settings and app, both need to use the file service (this will let us demonstrate shared objects accessing each other). To follow along with the examples below, check out the code on github.

Provider

Provider passes data around using parent:child relationships within the widget tree. A MultiProvider widget can be used to pass multiple objects to the widget tree below:

class ProviderExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider(create: (_) => FileService()),
        ChangeNotifierProvider(create: (context) {
          return SettingsManager(() => context.read<FileService>());
        }),
        ChangeNotifierProvider(create: (context) {
          return AppManager(() => context.read<FileService>());
        }),
      ],
      child: ...,
    );
  }
}

From within the view layer a context.read extension can be used to access data, and a context.watch or context.select can be used to bind to it:

class View1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    void handleTap() => context.read<AppManager>().count1++;
    int count = context.select((AppManager m) => m.count1);
    ...
  }
}

The select call here is binding our widget to the ChangeNotifier, it will now rebuild whenever the .count1 property is changed, while ignoring changes to count2.

Testing with Provider

To test with Provider, you can extend your data objects to create a mock, and provide that above a widget you would like to test:

class MockFileService extends FileService { ... }

return MultiProvider(
  providers: [
    Provider(create: (_) => MockFileService()),
    ...
  ],
  child: ...,
);

Unfortunately Provider does not have any system to allow you to override dependencies from “outside” which can make things a bit harder to test.

Limitations of Provider

As mentioned, `Provider` is limited in it’s testability because it has no built-in override system. It also can be hard to work with outside of the widget tree, due to the required context for every lookup. Finally, it has issues when trying to provide multiple objects of the same type.

GetIt + GetItMixin

GetIt is an implementation of the classic Service Locator pattern, providing various static methods to register singletons or factories:

final sl = GetIt.instance;
sl.registerSingleton(FileService());
sl.registerSingleton(SettingsManager(() => sl.get<FileService>()));
sl.registerSingleton(AppManager(() => sl.get<FileService>()));

A get method can be used to read data, and the GetItMixin package provides various ways to bind to it within your views. In this case, to bind to a property on a ChangeNotifier we can use watchOnly which we get from the mixin:

class View1 extends StatelessWidget with GetItMixin {
  @override
  Widget build(BuildContext context) {
    void handleTap() => GetIt.I.get<AppManager>().count1++;
    int count = watchOnly((AppManager m) => m.count1);
    ...
  }
}

Testing with GetIt

To test with GetIt, you can use the unregister and register methods to provide a mock class at any time, from anywhere, which makes testing a breeze:

GetIt.I.unregister(GetIt.I.get<FileService>());
GetIt.I.register(MockFileService());

You can also use the pushNewScope and popScope to easily setup and teardown a set of registered objects:

GetIt.I.pushNewScope();
GetIt.I.register(MockFileService());
// do tests
GetIt.I.popScope();

Limitations of GetIt

GetIt also suffers from the issue of providing instances of the same type, however it does expose a name field when registering, which offers a reasonable workaround for the issue. Another potential weakness is that you may have to manually dispose or unregister items as they are not intrinsically part of the widget tree. Lastly, some developers may not like the global nature of GetIt singletons, preferring the more restrictive scoping models of riverpod or Provider, while other developers may view this as a benefit.

Riverpod

With riverpod, you define globally accessible “providers”, and then you use a ref object to read them. A ProviderScope must also be placed around the widget tree:

final fileServiceProvider = Provider((_) => FileService());
final settingsManagerProvider = ChangeNotifierProvider((ref) {
  return SettingsManager(() => ref.read(fileServiceProvider));
});
final appManagerProvider = ChangeNotifierProvider((ref) {
  return AppManager(() => ref.read(fileServiceProvider));
});

class RiverpodExample extends StatelessWidget {
  const RiverpodExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      child: ...,
    );
  }
}

With your providers declared, you can subclass the ConsumerWidget and declare a slightly modified build method, to get a ref which will then allow you to read the providers:

class View1 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    void handleTap() => ref.read(appManagerProvider).count1++;
    int count = ref.watch(appManagerProvider.select((p) => p.count1));
    ...
  }
}

Testing with riverpod

riverpod provides a simple override mechanism that can be used to force a mock into any provider:

ProviderScope  overrides: [
    fileServiceProvider.overrideWithValue(MockFileService())
  ],
  child: ...,
);

Limitations of riverpod

Feature wise there are not many limitations with riverpod, it’s highly testable and allows for easy injection of objects with the same type. It’s biggest drawback may be the slightly awkward usage. It requires a custom widget, a modified build method signature, and all reads require a ref. While this feels a little annoying at first, it is not an issue once you get used to it. To ease this further riverpod also comes with a set of code snippets for Android Studio and VSCode.

Closing thoughts…

Really you can’t go wrong with any of these state-management libraries. Which you choose is mainly a matter of personal taste. GetIt comes with the most keep-it-simple approach and provides a hard de-coupling of logical objects and the widget tree. riverpod introduces a powerful twist on the service locator formula with its ‘many-locators’ + scoping approach, but is not quite as easy to learn as the others. Last but not least (well ok, it’s basically least) Provider gives you the no frills, traditional “inherited widget” style approach with a very simple API that works great inside of a widget tree, but not so great outside of it.

If you have any questions (or corrections!), please let us know below.

Flutter: Deep dive into the new `skeleton` app template

For many years Flutter has provided the basic “Counter App” each time you created a new project. While that example provides a nice introduction to the foundational concepts of StatefulWidget and basic font/asset management, it does not really resemble the structure of a “complete app”. This has left developers without guidance for anything beyond a basic single page app.

Continue reading →

Flutter: Hit-test outside parent bounds with `DeferPointer`

One thing that has always felt a little limiting in Flutter for us has been its inability to perform hit-testing for a button or gesture detector that is outside the parents bounds. This has been a popular issue in the Flutter bug-base over the years, getting something around 150+ upvotes if you add up all incarnations of the issue.

Continue reading →

Alpha video in HTML5

Alpha video in HTML5 should be easy right? Not quite, certainly not as easy as Flash was. In an article a long long time ago, from an internet far far away … I wrote about alpha video in Flash 8. (remember Flash?). Back then alpha video was a huge new feature that allowed developers to create a .flv with a transparent background that worked in all browsers. Allowing us to do all sorts of fancy effects. With Flash being a thing of the past, modern browsers are not in agreement on what video format we should use on the web. It makes things a little more muddled today.

Continue reading →

XD to Flutter v3.0

I’m very excited to announce the release of v3.0 of the “XD to Flutter” plugin, with a number of powerful new developer features.

Prior to v1.0, the primary goal was just to output as much of the content in Adobe XD to Flutter as possible: Vector graphics, text, images, fills, blurs, blend modes, etc. Version 1 tackled responsive layout, and v2.0 built on that with support for stacks, scroll groups, and padding. Version 2 also included the ability to export null-safe code, a critical developer feature for working with Flutter 2.

In v3.0 we’ve doubled down on improving the workflow for developers, including providing new ways to clean up the exported code and integrate dynamic content.

Continue reading →