Details about the Provider package for Flutter

Hello, Habr!

Our medium-term plans include the release of the Flutter book. Regarding the language of Dart as a topic, we still take a more cautious position, so we will try to evaluate its relevance according to the results of this article. It will focus on the Provider package and, therefore, on state management in Flutter.

Provider is a state management package written by Remy Rusle and adopted by Google and the Flutter community. But what is state management? For starters, what is a condition? Let me remind you that state is just data for representing the UI in your application. State management is an approach to creating this data, accessing, handling and disposing of it. To better understand the Provider package, we briefly outline the history of state management in Flutter.

1. StatefulWidget


StatelessWidget is a simple UI component that displays only when it has data. There is StatelessWidgetno “memory"; it is created and destroyed as necessary. Flutter also has a StatefulWidget , in which there is a memory, thanks to it a long-lived satellite - the State object . This class has a method setState(), when called, a widget is launched that rebuilds the state and displays it in a new form. This is the simplest form of Flutter state management provided out of the box. Here is an example with a button that always displays the time it was last pressed:

class _MyWidgetState extends State<MyWidget> {
  DateTime _time = DateTime.now();  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text(_time.toString()),
      onPressed: () {
        setState(() => _time = DateTime.now());
      },
    );
  }
}

So what is the problem with this approach? Suppose your application has some global state stored in the root StatefulWidget. It contains data that is intended for use in various parts of the UI. This data is shared and passed to each child widget in the form of parameters. Any events at which it is planned to change this data then pop up in the form of callbacks. Thus, through all intermediate widgets, a lot of parameters and callbacks are transmitted, which can soon lead to confusion. Worse, any updates to the aforementioned root will lead to a rebuild of the entire widget tree, which is inefficient.

2. InheritedWidget


InheritedWidget is a special widget whose descendants can access it without a direct link. Just by turning to InheritedWidget, a consuming widget can register for an automatic rebuild, which will occur when rebuilding an ancestor widget. This technique allows you to more efficiently organize the UI update. Instead of rebuilding huge pieces of the application in response to a small change in state, you can selectively select only those specific widgets that need to be rebuilt. You have already worked with InheritedWidgetwhenever you used MediaQuery.of(context)or Theme.of(context). True, it is less likely that you have implemented your own InheritedWidget with state preservation. The fact is that correctly implementing them is not easy.

3. ScopedModel


ScopedModel is a package created in 2017 by Brian Egan, which makes it easy to use InheritedWidgetto store application state. First you need to create a state object that inherits from Model , and then call notifyListeners()it when its properties change. The situation is reminiscent of the implementation of the PropertyChangeListener interface in Java.

class MyModel extends Model {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

To provide our state object, we wrap this object in a widget ScopedModelin the root of our application:

ScopedModel<MyModel>(
  model: MyModel(),
  child: MyApp(...)
)

Now any descendant widgets will be able to access MyModelusing the ScopedModelDescendant widget . The model instance is passed to the parameter builder:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<MyModel>(
      builder: (context, child, model) => Text(model.foo),
    );
  }
}

Any descendant widget will also be able to update the model, which will automatically provoke a rebuild of any ScopedModelDescendants(provided that our model correctly calls notifyListeners()):

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = ScopedModel.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

ScopedModelgained popularity in Flutter as a tool for state management, but its use is limited to the provision of objects that inherit the class Modeland use this pattern of notification of changes.

4. BLoC


At the Google I / O '18 conference, the Business Logic Component (BLoC) pattern was introduced , which serves as yet another tool for pulling state from widgets. BLoC classes are long-lived non-UI components that preserve state and expose it as streams and receivers. Taking state and business logic beyond the UI, you can implement the widget as simple StatelessWidgetand use StreamBuilder for automatic rebuilding. As a result, the widget "gets dumb", and it becomes easier to test.

Example BLoC class:

class MyBloc {
  final _controller = StreamController<MyType>();  Stream<MyType> get stream => _controller.stream;
  StreamSink<MyType> get sink => _controller.sink;
  
  myMethod() {
    //  
    sink.add(foo);
  }  dispose() {
    _controller.close();
  }
}
 ,   BLoC:
@override
Widget build(BuildContext context) {
 return StreamBuilder<MyType>(
  stream: myBloc.stream,
  builder: (context, asyncSnapshot) {
    //  
 });
}

The problem with the BLoC pattern is that it is not obvious how to create and destroy BLoC objects. How was the instance created myBlocin the above example? How do we call dispose()to get rid of him? Streams require use StreamController, which should be closedas soon as it becomes unnecessary - this is done to prevent memory leaks. (There is no such thing as a class destructor in Dart; only a class Statein StatefulWidgethas a method dispose()). In addition, it is not clear how to share this BLoC between multiple widgets. It is often difficult for developers to master BLoC. There are several packages that attempt to simplify this.

5. Provider


ProviderIs a package written in 2018 by Remy Rusle, similar to ScopedModel, but whose functions are not limited to, providing a subclass of Model. This is also a wrapper that concludes InheritedWidget, but the provider can provide any state objects, including BLoC, streams, futures and others. Since the provider is so simple and flexible, Google announced at the Google I / O '19 conference that in the future it Providerwill be the preferred package for managing state. Of course, other packages are also allowed, but if you have any doubts, Google recommends stopping at Provider.

Providerbuilt “with widgets, for widgets.”Providerallows you to place any object with a state in the widget tree and open access to it for any other widget (child). It also Providerhelps to manage the lifetime of state objects by initializing them with data and performing a cleanup after they are removed from the widget tree. Therefore, it Provideris suitable even for implementing BLoC components or can serve as a basis for other state management solutions! Or simply used to implement dependencies - a fancy term that means transferring data to widgets in a way that allows you to loosen the connection and improve the testability of the code. Finally,Providercomes with a set of specialized classes, thanks to which it is even more convenient to use. Next, we will take a closer look at each of these classes.

  • Basic Provider
  • ChangeNotifierProvider
  • StreamProvider
  • Futureprovider
  • ValueListenableProvider
  • MultiProvider
  • Proxyprovider

Installation


To use it Provider, first add a dependency to our file pubspec.yaml:

provider: ^3.0.0

Then we import the package Providerwhere it is needed:

import 'package:provider/provider.dart';

Base provider

Create the base Provider in the root of our application; this will contain an instance of our model:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

The parameter buildercreates an instance MyModel. If you want to pass an existing instance to it, use the constructor here Provider.value.

Then you can consume this instance of the model anywhere in MyApp, using the widget Consumer:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyModel>(
      builder: (context, value, child) => Text(value.foo),
    );
  }
}

In the above example, the class MyWidgetgets an instance MyModelusing the Consumer widget . This widget gives us buildercontaining our object in the parameter value.

Now, what should we do if we want to update the data in our model? Let's say we have another widget where, when a button is clicked, the property should be updated foo:

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

Note the specific syntax used to access the instance MyModel. Functionally, this is equivalent to accessing the widget Consumer. The widget Consumeris useful in cases when the code cannot easily get the link BuildContext.

What do you think will happen to the original widget MyWidgetthat we created earlier? Will a new meaning be displayed in it bar? Unfortunately, no . It is not possible to listen to changes in old traditional Dart objects (at least without reflection, which is not provided in Flutter). Thus, Providerit will not be able to “see” that we have properly updated the property fooand ordered the widget to be MyWidgetupdated in response.

ChangeNotifierProvider

But there is hope! You can make our class MyModelimplement an impurity ChangeNotifier. It will take a bit to change the implementation of our model and call a special method notifyListeners()whenever one of our properties changes. It works in approximately the same way ScopedModel, but in this case it’s nice that you don’t need to inherit from a particular model class. It is enough to realize the admixture ChangeNotifier. Here's what it looks like:

class MyModel with ChangeNotifier {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

As you can see, we replaced our property foowith getterand setter, backed by the _foo private variable. This way we can “intercept” any changes made to the foo property and let our listeners know that our object has changed.

Now, from the outside Provider, we can change our implementation so that it uses a different class called ChangeNotifierProvider:

ChangeNotifierProvider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

Like this! Now, when our OtherWidgetupdates the property fooin the instance MyModel, it MyWidgetwill automatically update to reflect this change. Cool, right?

By the way. You probably noticed a button handler OtherWidgetwith which we used the following syntax:

final model = Provider.of<MyModel>(context);

By default, this syntax will automatically cause instance rebuilding OtherWidgetas soon as the model changes MyModel. Perhaps we do not need this. In the end, it OtherWidgetsimply contains a button that does not change at all when the value changes MyModel. To avoid rebuilding, you can use the following syntax to access our model without registering for rebuilding:

final model = Provider.of<MyModel>(context, listen: false);

This is another charm provided in the package Providerjust like that.

StreamProvider

At first glance, it is not clear why it is needed StreamProvider. In the end, you can just use the usual StreamBuilderif you need to consume a stream in Flutter. For example, here we listen to the stream onAuthStateChangedprovided by FirebaseAuth:

@override
Widget build(BuildContext context {
  return StreamBuilder(
   stream: FirebaseAuth.instance.onAuthStateChanged, 
   builder: (BuildContext context, AsyncSnapshot snapshot){ 
     ...
   });
}

To do the same with help Provider, we could provide our stream through StreamProviderat the root of our application:

StreamProvider<FirebaseUser>.value(
  stream: FirebaseAuth.instance.onAuthStateChanged,
  child: MyApp(...),
}

Then consume the child widget, as is usually done with Provider:

@override
Widget build(BuildContext context) {
  return Consumer<FirebaseUser>(
    builder: (context, value, child) => Text(value.displayName),
  );
}

Not only has our widget code become much cleaner, it also abstracts the fact that the data came from the stream. If we ever decide to change the base implementation, for example, to FutureProvider, then no changes to the widget code will be required. As you will see, this applies to all other providers shown below.

FutureProvider

Similar to the above example, it FutureProvideris an alternative to the standard FutureBuilderwhen working with widgets. Here is an example:

FutureProvider<FirebaseUser>.value(
  value: FirebaseAuth.instance.currentUser(),
  child: MyApp(...),
);

To consume this value in the child widget, we use the same implementation Consumeras in the example StreamProviderabove.

ValueListenableProvider

ValueListenable is a Dart interface implemented by the ValueNotifier class that takes a value and notifies listeners when it changes to another value. It is possible, for example, to wrap an integer counter in a simple model class:

class MyModel {
  final ValueNotifier<int> counter = ValueNotifier(0);  
}

When working with complex types, it ValueNotifieruses the operator of the ==object stored in it to determine if the value has changed.
Let's create the simplest one Provider, which will contain our main model, and it will be followed by a Consumernested ValueListenableProviderlistening property counter:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: Consumer<MyModel>(builder: (context, value, child) {
    return ValueListenableProvider<int>.value(
      value: value.counter,
      child: MyApp(...)
    }
  }
}

Please note that this nested provider is of type int. There may be others. If you have several providers of the same type registered, the Provider will return the “closest” (closest ancestor).

Here's how to listen to a property counterfrom any child widget:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<int>(
      builder: (context, value, child) {
        return Text(value.toString());
      },
    );
  }
}

But here's how to update a property counterfrom another widget. Please note: we need access to the original copy MyModel.

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.counter.value++;
      },
    );
  }
}

MultiProvider

If you use many widgetsProvider, then in the root of the application you get an ugly structure from many attachments:

Provider<Foo>.value( 
  value: foo, 
  child: Provider<Bar>.value( 
    value: bar, 
    child: Provider<Baz>.value( 
      value: baz , 
      child: MyApp(...)
    ) 
  ) 
)

MultiProviderallows you to declare them all on the same level. It’s just syntactic sugar: at the intra-system level, they all remain nested anyway.

MultiProvider( 
  providers: [ 
    Provider<Foo>.value(value: foo), 
    Provider<Bar>.value(value: bar), 
    Provider<Baz>.value(value: baz), 
  ], 
  child: MyApp(...), 
)

ProxyProvider

ProxyProvider is an interesting class added in the third package releaseProvider. It allows you to declare providers that themselves may depend on other providers, up to six on one. In this example, the Bar class is instance-specificFoo. This is useful when compiling a root set of services that are themselves dependent on each other.

MultiProvider ( 
  providers: [ 
    Provider<Foo> ( 
      builder: (context) => Foo(),
    ), 
    ProxyProvider<Foo, Bar>(
      builder: (context, value, previous) => Bar(value),
    ), 
  ], 
  child: MyApp(...),
)

The first generic type argument is the type that yours depends on ProxyProvider, and the second is the type that it returns.

How to listen to many providers at the same time


What if we want a single widget to listen to many providers and rebuild when any of them changes? You can listen to up to 6 providers at the same time using widget options Consumer. We will receive instances as additional method parameters builder.

Consumer2<MyModel, int>(
  builder: (context, value, value2, child) {
    //value  MyModel
    //value2  int
  },
);

Conclusion


When used, InheritedWidget Providerit allows you to manage the state as is customary in Flutter. It allows widgets to access state objects and listen to them in such a way that the underlying notification mechanism is abstracted. It’s easier to manage the lifetime of state objects by creating anchor points to create these objects as needed and get rid of them when needed. This mechanism can be used to easily implement dependencies and even as a basis for more advanced state management options. With the blessing of Google and growing support in the Flutter community, it Providerhas become a package worth trying without delay!

All Articles