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 StatelessWidget
no “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 InheritedWidget
whenever 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 InheritedWidget
to 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 ScopedModel
in the root of our application:ScopedModel<MyModel>(
model: MyModel(),
child: MyApp(...)
)
Now any descendant widgets will be able to access MyModel
using 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';
},
);
}
}
ScopedModel
gained popularity in Flutter as a tool for state management, but its use is limited to the provision of objects that inherit the class Model
and 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 StatelessWidget
and 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 myBloc
in the above example? How do we call dispose()
to get rid of him? Streams require use StreamController
, which should be closed
as 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 State
in StatefulWidget
has 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
Provider
Is 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 Provider
will 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
.Provider
built “with widgets, for widgets.”Provider
allows you to place any object with a state in the widget tree and open access to it for any other widget (child). It also Provider
helps 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 Provider
is 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,Provider
comes 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 Provider
where it is needed:import 'package:provider/provider.dart';
Base providerCreate the base Provide
r in the root of our application; this will contain an instance of our model:Provider<MyModel>(
builder: (context) => MyModel(),
child: MyApp(...),
)
The parameter builder
creates 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 MyWidget
gets an instance MyModel
using the Consumer widget . This widget gives us builder
containing 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 Consumer
is useful in cases when the code cannot easily get the link BuildContext
.What do you think will happen to the original widget MyWidget
that 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, Provider
it will not be able to “see” that we have properly updated the property foo
and ordered the widget to be MyWidget
updated in response.ChangeNotifierProviderBut there is hope! You can make our class MyModel
implement 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 foo
with getter
and 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 OtherWidget
updates the property foo
in the instance MyModel
, it MyWidget
will automatically update to reflect this change. Cool, right?By the way. You probably noticed a button handler OtherWidget
with which we used the following syntax:final model = Provider.of<MyModel>(context);
By default, this syntax will automatically cause instance rebuilding OtherWidget
as soon as the model changes MyModel
. Perhaps we do not need this. In the end, it OtherWidget
simply 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 Provider
just like that.StreamProviderAt first glance, it is not clear why it is needed StreamProvider
. In the end, you can just use the usual StreamBuilder
if you need to consume a stream in Flutter. For example, here we listen to the stream onAuthStateChanged
provided 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 StreamProvider
at 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.FutureProviderSimilar to the above example, it FutureProvider
is an alternative to the standard FutureBuilder
when 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 Consumer
as in the example StreamProvider
above.ValueListenableProviderValueListenable 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 ValueNotifier
uses 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 Consumer
nested ValueListenableProvider
listening 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 counter
from 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 counter
from 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++;
},
);
}
}
MultiProviderIf 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(...)
)
)
)
MultiProvider
allows 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(...),
)
ProxyProviderProxyProvider
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) {
},
);
Conclusion
When used, InheritedWidget
Provider
it 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 Provider
has become a package worth trying without delay!