State management with provider

State management with provider

History

When I started exploring Flutter, there was (and there still is) a lot of discussion about state management. At that time, Google promoted Inherited Widget as the way to propagate information down the widget tree. I tried Inherited Widget, and indeed with some extra code, you could use it to make your state accessible to your widgets. But it involved quite some boilerplate, which was not easy to read and understand. It felt overly complex and likely to introduce errors that are difficult to spot.

After that, I tried get_it. That was easy and simple. It looked a lot that I was used to when I was working in C#. But I was not sure. I found a lot of happy users of get_it. But there were also a lot of people calling the foundation of get_it an anti-pattern. Although I did not really understand the discussion at that time, the choice was made for me by Google. They explicitly recommended Provider in their talk Pragmatic State Management in Flutter (Google I/O’19)

Provider

With Provider, you can wrap your class with one statement into an Inherited Widget.

In the code below, a Provider of an S080StateService is created. When this S080StateService is created, it’s dependencies are injected via its constructor. Provider is used to find these dependencies in the tree above.

  Provider<S080StateService>(
    create: (BuildContext context) {
      return S080StateService(
        Provider.of<LoggingService>(context, listen: false),
        Provider.of<LadenLossenBoxService>(context, listen: false),
        Provider.of<ConfiguratieBoxService>(context, listen: false),
        Provider.of<UnloadingStateService>(context, listen: false),
        Provider.of<CommunicatieBoxService>(context, listen: false),
        Provider.of<NotInteractiveCommunicationService>(context, listen: false),
        Provider.of<NavigatorService>(context, listen: false),
      );
    },
  )

Below this provider, the instance of the S080StateService is accessible by:

var state = Provider.of<S080StateService>(context);

The way this works is readable and easy to understand until you have a lot of services, that depends on a lot of other services.

Avoid super-nesting

When your code is dependent on many services, the code gets easily unreadable. Every Provider creates a parent-child relationship and therefore indents your code.

The indentation can easily be solved by using the MultiProvider widget. When you supply a list of widgets to the MultiProvider widget, the MultiProvider will take care of the parent-child relationship at runtime.

In code:

  • MultiProvider
    • Provider1
    • Provider2
    • Provider3
    • Provider4

At runtime:

  • MultiProvider
    • Provider1
      • Provider2
        • Provider3
          • Provider4

This improves readability a lot.

Another improvement can be made to move the list of Providers that you feed to the MultiProvider to a separate file.

class S080UnloadingSignaturePage extends StatefulWidget {
  static get Route => RouteEx<S080UnloadingSignaturePage>(
        builder: (context) => S080UnloadingSignaturePage(),
      );
  @override
  _S080UnloadingSignaturePageState createState() =>
      _S080UnloadingSignaturePageState();
}

class _S080UnloadingSignaturePageState
    extends State<S080UnloadingSignaturePage> {
  final SignatureController _controller =
      SignatureController(penColor: CompanyColor.companyBlue);

  @override
  Widget build(BuildContext context) {
    double bottomHeight = 80.0;
    return MultiProvider(
19 => providers: providers,
      child: Builder(builder: (BuildContext context) {
        var unloadingStateService = Provider.of<UnloadingStateService>(context);
        var state = Provider.of<S080StateService>(context);
        return Scanner(

Here the list of providers is just one line, on line 19. The actual list of providers is in a separate file:

List<SingleChildWidget> providers = [
  Provider<DialogService>(
    create: (BuildContext context) {
      return DialogService(
        context,
        Provider.of<ValidatorService>(context, listen: false),
        Provider.of<DatawedgeService>(context, listen: false),
        Provider.of<SoundService>(context, listen: false),
      );
    },
  ),
  Provider<S080StateService>(
    create: (BuildContext context) {
      return S080StateService(
        Provider.of<LoggingService>(context, listen: false),
        Provider.of<LadenLossenBoxService>(context, listen: false),
        Provider.of<ConfiguratieBoxService>(context, listen: false),
        Provider.of<UnloadingStateService>(context, listen: false),
        Provider.of<CommunicatieBoxService>(context, listen: false),
        Provider.of<NotInteractiveCommunicationService>(context, listen: false),
        Provider.of<NavigatorService>(context, listen: false),
      );
    },
  ),
];

Although I am not convinced that this is the very best way, it gets the job done. The code is readable and understandable. And, if you make a mistake, the error message is usually so well detailed, that finding and fixing the issue is easy.