Détails sur le package de fournisseur pour Flutter

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 StatelessWidgetpas 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 InheritedWidgetchaque 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 InheritedWidgetpour 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';
      },
    );
  }
}

ScopedModela 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 Modelet 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 StatelessWidgetet 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 myBlocdans 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 closeddè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 Stateen StatefulWidgeta 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


ProviderEst 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 Providerserait 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.

Providerconstruit "avec des widgets, pour des widgets."Providervous 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 Providerpermet é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 Providerconvient 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,Providerest 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 Providerlà où il est nécessaire:

import 'package:provider/provider.dart';

Fournisseur de base

Créez la base Provider à la racine de notre application; cela contiendra une instance de notre modèle:

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

Le paramètre buildercré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 MyWidgetobtient une instance MyModelà l'aide du widget Consommateur . Ce widget nous donne buildercontenant 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 Consumerest utile dans les cas où le code ne peut pas facilement obtenir le lien BuildContext.

Que pensez-vous qu'il adviendra du widget original MyWidgetque 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, Provideril ne pourra pas «voir» que nous avons correctement mis à jour la propriété fooet ordonné la MyWidgetmise à jour du widget en réponse.

ChangeNotifierProvider

Mais il y a de l'espoir! Vous pouvez faire en sorte que notre classe MyModelimplé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é foopar getteret 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 OtherWidgetmet à jour la propriété foodans l'instance MyModel, elle MyWidgetsera automatiquement mise à jour pour refléter ce changement. Cool, non?

Au fait. Vous avez probablement remarqué un gestionnaire de boutons OtherWidgetavec 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 OtherWidgetdès que le modèle changera MyModel. Peut-être que nous n'en avons pas besoin. En fin de compte, il OtherWidgetcontient 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 Providerjuste 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 StreamBuildersi vous avez besoin de consommer un flux dans Flutter. Par exemple, nous écoutons ici le flux onAuthStateChangedfourni 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.

FutureProvider

Similaire à l'exemple ci-dessus, c'est FutureProviderune alternative à la norme FutureBuilderlorsque 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 Consumerque dans l'exemple StreamProviderci-dessus.

ValueListenableProvider

ValueListenable 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 ValueNotifierutilise 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 Consumerimbriquée :ValueListenableProvidercounter

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é à counterpartir 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é à counterpartir 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++;
      },
    );
  }
}

MultiProvider

Si 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(...)
    ) 
  ) 
)

MultiProviderVous 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(...), 
)

ProxyProvider

ProxyProvider 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 ProxyProvideret 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) {
    //value  MyModel
    //value2  int
  },
);

Conclusion


Lorsqu'il est utilisé, InheritedWidget Provideril 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 Providerdevenu un package à essayer sans tarder!

All Articles