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
MyCatalog
et MyCart
respectivement). 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, chaqueMyListItem
devrait 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
MyCart
les é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, contents
il doit être dans le parent MyCart
ou supérieur.
//
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}
Il
MyCart
n'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
contents
devrait être dans MyApp
. Chaque fois qu'il change, il reconstruit le MyCart par-dessus (plus à ce sujet plus tard). De cette MyCart
faç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 MyCart
disparaî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
MyListItem
peut ê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, MyCatalog
vous 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
, InheritedModel
et 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,
provider
vous n'avez pas à vous soucier des rappels ou InheritedWidgets
. Mais vous devez comprendre 3 concepts:
- ChangeNotifier
- ChangeNotifierProvider
- Consommateur
ChangeNotifier
ChangeNotifier
Est 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.)
ChangeNotifier
Il provider
existe 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 ChangeNotifier
avec 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 à
ChangeNotifier
est 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 CartModel
est le modèle lui-même et sa logique métier.
ChangeNotifier
fait partie flutter:foundation
et 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
ChangeNotifierProvider
Est un widget qui fournit une instance à ChangeNotifier
ses 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ù CartModel
cela implique quelque chose au MyCart
- dessus et MyCatalog
.
Vous ne voulez pas publier
ChangeNotifierProvider
plus haut que nécessaire (car vous ne voulez pas polluer la portée). Mais dans notre cas, le seul widget qui est fini MyCart
et 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.
ChangeNotifierProvider
suffisamment intelligente pour ne pas se reconstruire à CartModel
moins 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
CartModel
fourni aux widgets dans notre application via la déclaration ChangeNotifierProvider
en 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 provider
ne pourra pas vous aider. provider
est basé sur le type et sans le type, il ne comprendra pas ce que vous voulez.
Le seul argument requis pour le widget
Consumer
est 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 Consumer
appelées.)
Le constructeur est appelé avec trois arguments. Le premier est
context
que vous obtenez également dans chaque méthode de construction.
Le deuxième argument de la fonction de générateur est une instance
ChangeNotifier
... 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
child
qu'il est nécessaire pour l'optimisation. Si vous avez un grand sous-arbre de widgets sous le vôtre Consumer
qui 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
ClearCart
permet à 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.of
avec le paramètre listen
dé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
provider
vous-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 ...