Flutter.dev: gestion simple de l'état des applications

Bonjour. En septembre, OTUS lance un nouveau cours, "Flutter Mobile Developer" . À la veille du début du cours, nous avons traditionnellement préparé une traduction utile pour vous.








Maintenant que vous connaissez la programmation d'interface utilisateur déclarative et la différence entre l' état éphémère et l'état de l'application , vous êtes prêt à apprendre à gérer facilement l'état de l'application.



Nous utiliserons le package provider. Si vous êtes nouveau sur Flutter et que vous n'avez pas de raison impérieuse de choisir une approche différente (Redux, Rx, hooks, etc.), c'est probablement la meilleure approche pour commencer. Le package provider est facile à apprendre et ne nécessite pas beaucoup de code. Il opère également avec des concepts applicables dans toutes les autres approches.



Cependant, si vous avez déjà beaucoup d'expérience dans la gestion de l'état à partir d'autres frameworks réactifs, vous pouvez rechercher d'autres packages et didacticiels répertoriés sur la page des options .



Exemple







Considérez l'application simple suivante comme exemple.



L'application dispose de deux écrans distincts: catalogue et panier (représentés par des widgets MyCataloget MyCartrespectivement). Dans ce cas, il s'agit d'une application de shopping, mais vous pouvez imaginer la même structure dans une simple application de réseautage social (remplacez le catalogue par «mur» et le panier par «favoris»).



L'écran du catalogue comprend une barre d'applications personnalisable ( MyAppBar) et une vue défilante de plusieurs éléments de liste ( MyListItems).



Voici l'application sous la forme d'un arbre de widgets:







Nous avons donc au moins 5 sous-classes Widget. Beaucoup d'entre eux ont besoin d'un accès pour déclarer qu'ils ne sont pas propriétaires. Par exemple, chaqueMyListItemdevrait pouvoir vous ajouter au panier. Ils peuvent également avoir besoin de vérifier si l'article actuellement affiché est dans le panier.



Cela nous amène à notre première question: où doit-on mettre l'état actuel du bucket?



Condition croissante



Dans Flutter, il est logique de positionner l'état au-dessus des widgets qui l'utilisent.



Pourquoi? Dans les frameworks déclaratifs comme Flutter, si vous voulez changer l'interface utilisateur, vous devez la reconstruire. Vous ne pouvez pas simplement aller écrire MyCart.updateWith(somethingNew). En d'autres termes, il est difficile de forcer le changement du widget de l'extérieur en appelant une méthode dessus. Et même si vous pouviez le faire fonctionner, vous lutteriez contre le cadre au lieu de le laisser vous aider.



// :   
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}




Même si vous faites fonctionner le code ci-dessus, vous devez gérer MyCartles éléments suivants dans le widget :



// :   
Widget build(BuildContext context) {
  return SomeWidget(
//   .
  );
}

void updateWith(Item item) {
// -      UI.
}




Vous devrez prendre en compte l'état actuel de l'interface utilisateur et lui appliquer les nouvelles données. Il sera difficile d'éviter les erreurs ici.



Dans Flutter, vous créez un nouveau widget à chaque fois que son contenu change. Au lieu de MyCart.updateWith(somethingNew)(appel de méthode), vous utilisez MyCart(contents)(constructeur). Étant donné que vous ne pouvez créer de nouveaux widgets que dans les méthodes de construction de leur parent, si vous souhaitez le modifier, contentsil doit être dans le parent MyCartou supérieur.



// 
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}




Il MyCartn'a désormais qu'un seul chemin d'exécution de code pour créer n'importe quelle version de l'interface utilisateur.



// 
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    //     ,    .
    // ···
  );
}




Dans notre exemple, il contentsdevrait être dans MyApp. Chaque fois qu'il change, il reconstruit le MyCart par-dessus (plus à ce sujet plus tard). De cette MyCartfaçon, vous n'avez pas à vous soucier du cycle de vie - il déclare simplement ce qu'il faut afficher pour un contenu donné. Lorsqu'il change, l'ancien widget MyCartdisparaîtra et sera complètement remplacé par le nouveau.







C'est ce que nous voulons dire lorsque nous disons que les widgets sont immuables. Ils ne changent pas - ils sont remplacés.



Maintenant que nous savons où placer l'état du bucket, voyons comment y accéder.



Accès de l'État



Lorsqu'un utilisateur clique sur l'un des articles du catalogue, il est ajouté au panier. Mais puisque la charrette est terminée MyListItem, comment faire cela?



Une option simple consiste à fournir un rappel qui MyListItempeut être appelé en un clic. Les fonctions Dart sont des objets de première classe, vous pouvez donc les transmettre comme vous le souhaitez. Ainsi, en interne, MyCatalogvous pouvez définir les éléments suivants:



@override
Widget build(BuildContext context) {
  return SomeWidget(
   //  ,      .
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}




Cela fonctionne bien, mais pour l'état de l'application que vous devez changer à partir de nombreux endroits différents, vous devrez passer beaucoup de rappels, ce qui devient assez vite ennuyeux.



Heureusement, Flutter dispose de mécanismes qui permettent aux widgets de fournir des données et des services à leurs descendants (en d'autres termes, non seulement à leurs descendants, mais à tous les widgets en aval). Comme on peut s'y attendre d'un Flutter, où tout est un Widget , ces mécanismes sont des sortes de widgets simplement spéciaux: InheritedWidget, InheritedNotifier, InheritedModelet d' autres. Nous ne les décrirons pas ici car ils sont légèrement en décalage avec ce que nous essayons de faire.



Au lieu de cela, nous allons utiliser un package qui fonctionne avec des widgets de bas niveau mais qui est facile à utiliser. Ça s'appelle provider.



Avec, providervous n'avez pas à vous soucier des rappels ou InheritedWidgets. Mais vous devez comprendre 3 concepts:



  • ChangeNotifier
  • ChangeNotifierProvider
  • Consommateur




ChangeNotifier



ChangeNotifierEst une classe simple incluse dans le SDK Flutter qui fournit une notification de changement d'état à ses écouteurs. En d'autres termes, si quelque chose est ChangeNotifier, vous pouvez vous abonner à ses modifications. (Il s'agit d'une forme d'Observable - pour ceux qui ne connaissent pas le terme.)



ChangeNotifierIl providerexiste une façon de résumer l'état de l'application. Pour les applications très simples, vous pouvez vous en tirer avec un seul ChangeNotifier. Dans les plus complexes, vous aurez plusieurs modèles et donc plusieurs ChangeNotifiers. (Vous n'avez pas du tout besoin d'utiliser ChangeNotifieravec provider, mais cette classe est facile à utiliser.)



Dans notre exemple d'application d'achat, nous souhaitons gérer l'état du panier dans ChangeNotifier. Nous créons une nouvelle classe qui l'étend, par exemple:



class CartModel extends ChangeNotifier {
///    .
  final List<Item> _items = [];

  ///     .
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  ///      ( ,      42 ).
  int get totalPrice => _items.length * 42;

  ///  [item]  .   [removeAll] -     .
  void add(Item item) {
    _items.add(item);
    //    ,    ,   .
    notifyListeners();
  }

  ///     .
  void removeAll() {
    _items.clear();
    //    ,    ,   .
    notifyListeners();
  }
}




Le seul morceau de code spécifique à ChangeNotifierest l'appel notifyListeners(). Appelez cette méthode chaque fois que le modèle change de manière à ce qu'il puisse être reflété dans l'interface utilisateur de votre application. Tout le reste CartModelest le modèle lui-même et sa logique métier.



ChangeNotifierfait partie flutter:foundationet ne dépend pas de classes de niveau supérieur dans Flutter. C'est facile à tester (vous n'avez même pas besoin d'utiliser le test de widget pour cela). Par exemple, voici un simple test unitaire CartModel:



test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});




ChangeNotifierProvider



ChangeNotifierProviderEst un widget qui fournit une instance à ChangeNotifierses enfants. Il vient dans un paquet provider.



Nous savons déjà où le placer ChangeNotifierProvider: au-dessus des widgets qui doivent y accéder. Au cas où CartModelcela implique quelque chose au MyCart- dessus et MyCatalog.



Vous ne voulez pas publier ChangeNotifierProviderplus haut que nécessaire (car vous ne voulez pas polluer la portée). Mais dans notre cas, le seul widget qui est fini MyCartet MyCatalog- il MyApp.



void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: MyApp(),
    ),
  );
}




Notez que nous définissons un constructeur qui crée une nouvelle instance CartModel. ChangeNotifierProvidersuffisamment intelligente pour ne pas se reconstruire à CartModelmoins que cela ne soit absolument nécessaire. Il appelle également automatiquement disposer () sur le CartModel lorsque l'instance n'est plus nécessaire.



Si vous souhaitez fournir plus d'une classe, vous pouvez utiliser MultiProvider:



void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: MyApp(),
    ),
  );
}




Consommateur



Maintenant qu'il est CartModelfourni aux widgets dans notre application via la déclaration ChangeNotifierProvideren haut, nous pouvons commencer à l'utiliser.



Cela se fait via un widget Consumer.



return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);




Nous devons spécifier le type de modèle auquel nous voulons accéder. Dans ce cas, nous en avons besoin CartModel, alors nous écrivons Consumer<CartModel>. Si vous ne spécifiez pas generic ( <CartModel>), le package providerne pourra pas vous aider. providerest basé sur le type et sans le type, il ne comprendra pas ce que vous voulez.



Le seul argument requis pour le widget Consumerest builder. Builder est une fonction qui est appelée en cas de changement ChangeNotifier. (En d'autres termes, lorsque vous appelez notifyListeners()votre modèle, toutes les méthodes de générateur de tous les widgets pertinents sont Consumerappelées.)



Le constructeur est appelé avec trois arguments. Le premier est contextque vous obtenez également dans chaque méthode de construction.

Le deuxième argument de la fonction de générateur est une instanceChangeNotifier... C'est ce que nous avons demandé depuis le tout début. Vous pouvez utiliser les données du modèle pour déterminer comment l'interface utilisateur doit ressembler à un point donné.



Le troisième argument est childqu'il est nécessaire pour l'optimisation. Si vous avez un grand sous-arbre de widgets sous le vôtre Consumerqui ne change pas lorsque le modèle change, vous pouvez le créer une fois et l'obtenir via le générateur.



return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
        children: [
          //   SomeExhibitedWidget,    .
          child,
          Text("Total price: ${cart.totalPrice}"),
        ],
      ),
  //    .
  child: SomeExpensiveWidget(),
);




Il est préférable de placer vos widgets Consumer aussi profondément que possible dans l'arborescence. Vous ne voulez pas reconstruire de grandes parties de l'interface utilisateur simplement parce que certains détails ont changé quelque part.



//   
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);




Au lieu de cela:



//  
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);




Provider.of



Parfois, vous n'avez pas vraiment besoin des données du modèle pour modifier l'interface utilisateur, mais vous devez toujours y accéder. Par exemple, un bouton ClearCartpermet à l'utilisateur de tout supprimer du panier. Il n'est pas nécessaire d'afficher le contenu du panier, il suffit d'appeler la méthode clear().



Nous pourrions l'utiliser Consumer<CartModel>pour cela, mais ce serait du gaspillage. Nous demanderions au framework de reconstruire le widget, qui n'a pas besoin d'être reconstruit.



Pour ce cas d'utilisation, nous pouvons utiliser Provider.ofavec le paramètre listendéfini sur false.



Provider.of<CartModel>(context, listen: false).removeAll();




L'utilisation de la ligne ci-dessus dans la méthode de construction ne reconstruira pas ce widget lorsqu'il est appelé notifyListeners .



Mettre tous ensemble



Vous pouvez consulter l'exemple présenté dans cet article. Si vous avez besoin de quelque chose d'un peu plus simple, découvrez à quoi ressemble une application Counter simple créée avec un fournisseur .



Lorsque vous êtes prêt à jouer avec providervous-même, n'oubliez pas d'ajouter d'abord sa dépendance à la vôtre pubspec.yaml.



name: my_name
description: Blah blah blah.

# ...

dependencies:
  flutter:
    sdk: flutter

  provider: ^3.0.0

dev_dependencies:
  # ...




Maintenant vous pouvez 'package:provider/provider.dart'; et commencez à construire ...






All Articles