Bonjour, Habr!Nos plans à moyen terme incluent la sortie du livre Flutter. En ce qui concerne le langage de Dart en tant que sujet, nous adoptons toujours une position plus prudente, nous allons donc essayer d'évaluer sa pertinence en fonction des résultats de cet article. Il se concentrera sur le package Provider et, par conséquent, sur la gestion des états dans Flutter.Le fournisseur est un progiciel de gestion de l'état écrit par Remy Rusle et adopté par Google et la communauté Flutter. Mais qu'est-ce que la gestion de l'État? Pour commencer, qu'est-ce qu'une condition? Permettez-moi de vous rappeler que l'état n'est que des données pour représenter l'interface utilisateur dans votre application. La gestion des états est une approche pour créer ces données, y accéder, les manipuler et les éliminer. Pour mieux comprendre le package Provider, nous décrivons brièvement l'historique de la gestion des états dans Flutter.1. StatefulWidget
StatelessWidget est un simple composant d'interface utilisateur qui s'affiche uniquement lorsqu'il contient des données. Il n'y a StatelessWidget
pas de «mémoire»; il est créé et détruit si nécessaire. Flutter a également un StatefulWidget , dans lequel il y a une mémoire, grâce à lui un satellite à longue durée de vie - l'objet State . Cette classe a une méthode setState()
, lorsqu'elle est appelée, un widget est lancé qui reconstruit l'état et l'affiche sous une nouvelle forme. Il s'agit de la forme la plus simple de gestion de l'état Flutter fournie dès le départ. Voici un exemple avec un bouton qui affiche toujours l'heure de la dernière pression: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());
},
);
}
}
Quel est donc le problème avec cette approche? Supposons que votre application ait un état global stocké dans la racine StatefulWidget
. Il contient des données destinées à être utilisées dans diverses parties de l'interface utilisateur. Ces données sont partagées et transmises à chaque widget enfant sous forme de paramètres. Tout événement au cours duquel il est prévu de modifier ces données s'affiche alors sous la forme de rappels. Ainsi, à travers tous les widgets intermédiaires, un grand nombre de paramètres et de rappels sont transmis, ce qui peut rapidement conduire à la confusion. Pire encore, toute mise à jour de la racine susmentionnée entraînera une reconstruction de l'arborescence entière du widget, ce qui est inefficace.2. InheritedWidget
InheritedWidget est un widget spécial dont les descendants peuvent y accéder sans lien direct. Juste en se tournant vers InheritedWidget
, un widget consommateur peut s'inscrire pour une reconstruction automatique, qui se produira lors de la reconstruction d'un widget ancêtre. Cette technique vous permet d'organiser plus efficacement la mise à jour de l'interface utilisateur. Au lieu de reconstruire d'énormes morceaux de l'application en réponse à un petit changement d'état, vous ne pouvez sélectionner sélectivement que les widgets spécifiques qui doivent être reconstruits. Vous avez déjà travaillé avec InheritedWidget
chaque fois que vous avez utilisé MediaQuery.of(context)
ou Theme.of(context)
. Certes, il est moins probable que vous ayez implémenté votre propre InheritedWidget avec conservation de l'état. Le fait est que leur mise en œuvre correcte n'est pas facile.3. ScopedModel
ScopedModel est un package créé en 2017 par Brian Egan, qui le rend facile à utiliser InheritedWidget
pour stocker l'état de l'application. Vous devez d'abord créer un objet d'état qui hérite de Model , puis l'appeler notifyListeners()
lorsque ses propriétés changent. La situation n'est pas sans rappeler l'implémentation de l'interface PropertyChangeListener en Java.class MyModel extends Model {
String _foo; String get foo => _foo;
void set foo(String value) {
_foo = value;
notifyListeners();
}
}
Pour fournir notre objet d'état, nous enveloppons cet objet dans un widget ScopedModel
à la racine de notre application:ScopedModel<MyModel>(
model: MyModel(),
child: MyApp(...)
)
Désormais, tous les widgets descendants pourront accéder MyModel
à l'aide du widget ScopedModelDescendant . L'instance de modèle est passée au paramètre builder
:class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<MyModel>(
builder: (context, child, model) => Text(model.foo),
);
}
}
Tout widget descendant pourra également mettre à jour le modèle, ce qui provoquera automatiquement une reconstruction de tout ScopedModelDescendants
(à condition que notre modèle appelle correctement 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
a gagné en popularité dans Flutter en tant qu'outil de gestion des états, mais son utilisation est limitée à la fourniture d'objets qui héritent de la classe Model
et utilisent ce modèle de notification des modifications.4. BLoC
Lors de la conférence Google I / O '18 , le modèle BLoC ( Business Logic Component ) a été introduit , qui constitue un autre outil pour extraire l'état des widgets. Les classes BLoC sont des composants non UI à longue durée de vie qui préservent l'état et l'exposent en tant que flux et récepteurs. En prenant la logique d'état et d'entreprise au-delà de l'interface utilisateur, vous pouvez implémenter le widget de manière simple StatelessWidget
et utiliser StreamBuilder pour la reconstruction automatique. En conséquence, le widget "devient stupide", et il devient plus facile à tester.Exemple de classe BLoC: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) {
});
}
Le problème avec le modèle BLoC est qu'il n'est pas évident de créer et de détruire des objets BLoC. Comment l'instance a-t-elle été créée myBloc
dans l'exemple ci-dessus? Comment appelons-nous dispose()
pour nous débarrasser de lui? Les flux nécessitent une utilisation StreamController
, ce qui devrait être closed
dès qu'elle devient inutile - cela est fait pour éviter les fuites de mémoire. (Il n'y a pas une telle chose comme destructor de classe à Dart, seule une classe State
en StatefulWidget
a une méthode dispose()
). De plus, il n'est pas clair comment partager ce BLoC entre plusieurs widgets. Il est souvent difficile pour les développeurs de maîtriser BLoC. Il existe plusieurs packages qui tentent de simplifier cela.5. Fournisseur
Provider
Est un package écrit en 2018 par Remy Rusle, similaire à ScopedModel
, mais dont les fonctions ne sont pas limitées à, fournissant une sous-classe de Model. C'est également un wrapper qui conclut InheritedWidget
, mais le fournisseur peut fournir tous les objets d'état, y compris BLoC, les flux, les futurs et autres. Étant donné que le fournisseur est si simple et flexible, Google a annoncé lors de la conférence Google I / O '19 qu'à l'avenir, il Provider
serait le package préféré pour la gestion de l'état. Bien sûr, d'autres packages sont également autorisés, mais si vous avez des doutes, Google recommande de s'arrêter à Provider
.Provider
construit "avec des widgets, pour des widgets."Provider
vous permet de placer n'importe quel objet avec un état dans l'arborescence des widgets et d'ouvrir l'accès à celui-ci pour tout autre widget (enfant). Il Provider
permet également de gérer la durée de vie des objets d'état en les initialisant avec des données et en effectuant un nettoyage après leur suppression de l'arborescence des widgets. Par conséquent, il Provider
convient même pour la mise en œuvre de composants BLoC ou peut servir de base à d'autres solutions de gestion d'état! Ou tout simplement utilisé pour implémenter des dépendances - un terme sophistiqué qui signifie transférer des données vers des widgets d'une manière qui vous permet de relâcher la connexion et d'améliorer la testabilité du code. Finalement,Provider
est livré avec un ensemble de classes spécialisées, grâce auxquelles il est encore plus pratique à utiliser. Ensuite, nous examinerons de plus près chacune de ces classes.- Fournisseur de base
- ChangeNotifierProvider
- StreamProvider
- Futureprovider
- ValueListenableProvider
- MultiProvider
- Proxyprovider
Installation
Pour l'utiliser Provider
, ajoutez d'abord une dépendance à notre fichier pubspec.yaml
:provider: ^3.0.0
Ensuite, nous importons le paquet Provider
là où il est nécessaire:import 'package:provider/provider.dart';
Fournisseur de baseCréez la base Provide
r à la racine de notre application; cela contiendra une instance de notre modèle:Provider<MyModel>(
builder: (context) => MyModel(),
child: MyApp(...),
)
Le paramètre builder
crée une instance MyModel
. Si vous souhaitez lui passer une instance existante, utilisez le constructeur ici Provider.value
.Ensuite, vous pouvez consommer cette instance du modèle n'importe où dans MyApp
, en utilisant le widget Consumer
:class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<MyModel>(
builder: (context, value, child) => Text(value.foo),
);
}
}
Dans l'exemple ci-dessus, la classe MyWidget
obtient une instance MyModel
à l'aide du widget Consommateur . Ce widget nous donne builder
contenant notre objet dans le paramètre value
.Maintenant, que devons-nous faire si nous voulons mettre à jour les données de notre modèle? Disons que nous avons un autre widget où, lorsqu'un bouton est cliqué, la propriété doit être mise à jour 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';
},
);
}
}
Notez la syntaxe spécifique utilisée pour accéder à l'instance MyModel
. Fonctionnellement, cela équivaut à accéder au widget Consumer
. Le widget Consumer
est utile dans les cas où le code ne peut pas facilement obtenir le lien BuildContext
.Que pensez-vous qu'il adviendra du widget original MyWidget
que nous avons créé plus tôt? Une nouvelle signification y sera-t-elle affichée bar
? Malheureusement non . Il n'est pas possible d'écouter les changements dans les anciens objets Dart traditionnels (au moins sans réflexion, ce qui n'est pas fourni dans Flutter). Ainsi, Provider
il ne pourra pas «voir» que nous avons correctement mis à jour la propriété foo
et ordonné la MyWidget
mise à jour du widget en réponse.ChangeNotifierProviderMais il y a de l'espoir! Vous pouvez faire en sorte que notre classe MyModel
implémente une impureté ChangeNotifier
. Il faudra un peu pour changer l'implémentation de notre modèle et appeler une méthode spéciale notifyListeners()
chaque fois que l'une de nos propriétés change. Cela fonctionne à peu près de la même manière ScopedModel
, mais dans ce cas, il est bien que vous n'ayez pas besoin d'hériter d'une classe particulière du modèle. Il suffit de réaliser le mélange ChangeNotifier
. Voici à quoi ça ressemble:class MyModel with ChangeNotifier {
String _foo; String get foo => _foo;
void set foo(String value) {
_foo = value;
notifyListeners();
}
}
Comme vous pouvez le voir, nous avons remplacé notre propriété foo
par getter
et setter
, soutenu par la variable privée _foo. De cette façon, nous pouvons «intercepter» toutes les modifications apportées à la propriété foo et faire savoir à nos auditeurs que notre objet a changé.Maintenant, de l'extérieur Provider
, nous pouvons changer notre implémentation afin qu'elle utilise une classe différente appelée ChangeNotifierProvider
:ChangeNotifierProvider<MyModel>(
builder: (context) => MyModel(),
child: MyApp(...),
)
Comme ça! Maintenant, lorsque notre OtherWidget
met à jour la propriété foo
dans l'instance MyModel
, elle MyWidget
sera automatiquement mise à jour pour refléter ce changement. Cool, non?Au fait. Vous avez probablement remarqué un gestionnaire de boutons OtherWidget
avec lequel nous avons utilisé la syntaxe suivante:final model = Provider.of<MyModel>(context);
Par défaut, cette syntaxe entraînera automatiquement la reconstruction de l'instance OtherWidget
dès que le modèle changera MyModel
. Peut-être que nous n'en avons pas besoin. En fin de compte, il OtherWidget
contient simplement un bouton qui ne change pas du tout lorsque la valeur change MyModel
. Pour éviter la reconstruction, vous pouvez utiliser la syntaxe suivante pour accéder à notre modèle sans vous inscrire à la reconstruction:final model = Provider.of<MyModel>(context, listen: false);
C'est un autre charme fourni dans le package Provider
juste comme ça.StreamProviderÀ première vue, il n'est pas clair pourquoi il est nécessaire StreamProvider
. En fin de compte, vous pouvez simplement utiliser l'habituel StreamBuilder
si vous avez besoin de consommer un flux dans Flutter. Par exemple, nous écoutons ici le flux onAuthStateChanged
fourni par FirebaseAuth
:@override
Widget build(BuildContext context {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot snapshot){
...
});
}
Pour faire de même avec de l'aide Provider
, nous pourrions fournir notre flux StreamProvider
à la racine de notre application:StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged,
child: MyApp(...),
}
Consommez ensuite le widget enfant, comme c'est généralement le cas avec Provider
:@override
Widget build(BuildContext context) {
return Consumer<FirebaseUser>(
builder: (context, value, child) => Text(value.displayName),
);
}
Non seulement notre code de widget est devenu beaucoup plus propre, mais il fait également abstraction du fait que les données proviennent du flux. Si nous décidons de changer l'implémentation de base, par exemple en FutureProvider
, alors aucune modification du code du widget ne sera nécessaire. Comme vous le verrez, cela s'applique à tous les autres fournisseurs indiqués ci-dessous.FutureProviderSimilaire à l'exemple ci-dessus, c'est FutureProvider
une alternative à la norme FutureBuilder
lorsque vous travaillez avec des widgets. Voici un exemple:FutureProvider<FirebaseUser>.value(
value: FirebaseAuth.instance.currentUser(),
child: MyApp(...),
);
Pour consommer cette valeur dans le widget enfant, nous utilisons la même implémentation Consumer
que dans l'exemple StreamProvider
ci-dessus.ValueListenableProviderValueListenable est une interface Dart implémentée par la classe ValueNotifier qui prend une valeur et avertit les écouteurs lorsqu'elle passe à une autre valeur. Il est possible, par exemple, d'encapsuler un compteur entier dans une classe de modèle simple:class MyModel {
final ValueNotifier<int> counter = ValueNotifier(0);
}
Lorsque vous travaillez avec des types complexes, il ValueNotifier
utilise l'opérateur de l' ==
objet qui y est stocké pour déterminer si la valeur a changé.Créons le plus simple Provider
, qui contiendra notre modèle principal, et il sera suivi d'une propriété d'écoute Consumer
imbriquée :ValueListenableProvider
counter
Provider<MyModel>(
builder: (context) => MyModel(),
child: Consumer<MyModel>(builder: (context, value, child) {
return ValueListenableProvider<int>.value(
value: value.counter,
child: MyApp(...)
}
}
}
Veuillez noter que ce fournisseur imbriqué est de type int
. Il peut y en avoir d'autres. Si vous avez plusieurs fournisseurs du même type enregistrés, le fournisseur retournera le «plus proche» (ancêtre le plus proche).Voici comment écouter une propriété à counter
partir de n'importe quel widget enfant:class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<int>(
builder: (context, value, child) {
return Text(value.toString());
},
);
}
}
Mais voici comment mettre à jour une propriété à counter
partir d'un autre widget. Veuillez noter: nous devons avoir accès à la copie originale 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++;
},
);
}
}
MultiProviderSi vous utilisez de nombreux widgetsProvider
, à la racine de l'application, vous obtenez une structure laide à partir de nombreuses pièces jointes:Provider<Foo>.value(
value: foo,
child: Provider<Bar>.value(
value: bar,
child: Provider<Baz>.value(
value: baz ,
child: MyApp(...)
)
)
)
MultiProvider
Vous permet de les déclarer tous au même niveau. C'est juste du sucre syntaxique: au niveau intra-système, ils restent tous imbriqués de toute façon.MultiProvider(
providers: [
Provider<Foo>.value(value: foo),
Provider<Bar>.value(value: bar),
Provider<Baz>.value(value: baz),
],
child: MyApp(...),
)
ProxyProviderProxyProvider
est une classe intéressante ajoutée dans la troisième version du packageProvider
. Il vous permet de déclarer des fournisseurs qui eux-mêmes peuvent dépendre d'autres fournisseurs, jusqu'à six sur un. Dans cet exemple, la classe Bar est spécifique à l'instanceFoo
. Cela est utile lors de la compilation d'un ensemble racine de services qui dépendent eux-mêmes les uns des autres.MultiProvider (
providers: [
Provider<Foo> (
builder: (context) => Foo(),
),
ProxyProvider<Foo, Bar>(
builder: (context, value, previous) => Bar(value),
),
],
child: MyApp(...),
)
Le premier argument de type générique est le type dont dépend le vôtre ProxyProvider
et le second est le type qu'il renvoie.Comment écouter de nombreux fournisseurs en même temps
Et si nous voulons qu'un seul widget écoute de nombreux fournisseurs et se reconstruise lorsque l'un d'eux change? Vous pouvez écouter jusqu'à 6 fournisseurs en même temps à l'aide des options de widget Consumer
. Nous recevrons des instances en tant que paramètres de méthode supplémentaires builder
.Consumer2<MyModel, int>(
builder: (context, value, value2, child) {
},
);
Conclusion
Lorsqu'il est utilisé, InheritedWidget
Provider
il vous permet de gérer l'état comme il est de coutume dans Flutter. Il permet aux widgets d'accéder aux objets d'état et de les écouter de manière à ce que le mécanisme de notification sous-jacent soit abstrait. Il est plus facile de gérer la durée de vie des objets d'état en créant des points d'ancrage pour créer ces objets selon vos besoins et vous en débarrasser en cas de besoin. Ce mécanisme peut être utilisé pour implémenter facilement des dépendances et même comme base pour des options de gestion d'état plus avancées. Avec la bénédiction de Google et le soutien croissant de la communauté Flutter, c'est Provider
devenu un package à essayer sans tarder!