Flutter: La réutilisation de la logique d'état est soit trop verbeuse, soit trop difficile

Créé le 2 mars 2020  ·  420Commentaires  ·  Source: flutter/flutter

.

Lié à la discussion autour des crochets #25280

TL;DR : Il est difficile de réutiliser la logique State . Soit nous nous retrouvons avec une méthode build complexe et profondément imbriquée, soit nous devons copier-coller la logique sur plusieurs widgets.

Il n'est ni possible de réutiliser une telle logique à travers des mixins ni des fonctions.

Problème

Réutiliser une logique State sur plusieurs StatefulWidget est très difficile, dès lors que cette logique repose sur plusieurs cycles de vie.

Un exemple typique serait la logique de création d'un TextEditingController (mais aussi AnimationController , des animations implicites, et bien d'autres). Cette logique se compose de plusieurs étapes :

  • définir une variable sur State .
    dart TextEditingController controller;
  • créant le contrôleur (généralement dans initState), avec potentiellement une valeur par défaut :
    dart <strong i="25">@override</strong> void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • supprimé le contrôleur lorsque le State est supprimé :
    dart <strong i="30">@override</strong> void dispose() { controller.dispose(); super.dispose(); }
  • faire ce que nous voulons avec cette variable dans build .
  • (facultatif) exposez cette propriété sur debugFillProperties :
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

Ceci, en soi, n'est pas complexe. Le problème commence lorsque nous voulons étendre cette approche.
Une application Flutter typique peut avoir des dizaines de champs de texte, ce qui signifie que cette logique est dupliquée plusieurs fois.

Copier-coller cette logique partout "fonctionne", mais crée une faiblesse dans notre code :

  • il peut être facile d'oublier de réécrire l'une des étapes (comme oublier d'appeler dispose )
  • ça rajoute beaucoup de bruit dans le code

Le problème de Mixin

La première tentative de factorisation de cette logique serait d'utiliser un mixin :

mixin TextEditingControllerMixin<T extends StatefulWidget> on State<T> {
  TextEditingController get textEditingController => _textEditingController;
  TextEditingController _textEditingController;

  <strong i="11">@override</strong>
  void initState() {
    super.initState();
    _textEditingController = TextEditingController();
  }

  <strong i="12">@override</strong>
  void dispose() {
    _textEditingController.dispose();
    super.dispose();
  }

  <strong i="13">@override</strong>
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty('textEditingController', textEditingController));
  }
}

Ensuite utilisé de cette façon:

class Example extends StatefulWidget {
  <strong i="17">@override</strong>
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example>
    with TextEditingControllerMixin<Example> {
  <strong i="18">@override</strong>
  Widget build(BuildContext context) {
    return TextField(
      controller: textEditingController,
    );
  }
}

Mais cela a différents défauts :

  • Un mixin ne peut être utilisé qu'une seule fois par cours. Si notre StatefulWidget besoin de plusieurs TextEditingController , alors nous ne pouvons plus utiliser l'approche mixin.

  • L'"état" déclaré par le mixin peut entrer en conflit avec un autre mixin ou le State lui-même.
    Plus précisément, si deux mixins déclarent un membre utilisant le même nom, il y aura un conflit.
    Dans le pire des cas, si les membres en conflit ont le même type, cela échouera silencieusement.

Cela rend les mixins à la fois non idéaux et trop dangereux pour être une vraie solution.

Utiliser le modèle « constructeur »

Une autre solution peut être d'utiliser le même modèle que StreamBuilder & co.

Nous pouvons créer un widget TextEditingControllerBuilder , qui gère ce contrôleur. Ensuite, notre méthode build peut l'utiliser librement.

Un tel widget serait généralement implémenté de cette façon :

class TextEditingControllerBuilder extends StatefulWidget {
  const TextEditingControllerBuilder({Key key, this.builder}) : super(key: key);

  final Widget Function(BuildContext, TextEditingController) builder;

  <strong i="12">@override</strong>
  _TextEditingControllerBuilderState createState() =>
      _TextEditingControllerBuilderState();
}

class _TextEditingControllerBuilderState
    extends State<TextEditingControllerBuilder> {
  TextEditingController textEditingController;

  <strong i="13">@override</strong>
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(
        DiagnosticsProperty('textEditingController', textEditingController));
  }

  <strong i="14">@override</strong>
  void dispose() {
    textEditingController.dispose();
    super.dispose();
  }

  <strong i="15">@override</strong>
  Widget build(BuildContext context) {
    return widget.builder(context, textEditingController);
  }
}

Puis utilisé comme tel :

class Example extends StatelessWidget {
  <strong i="19">@override</strong>
  Widget build(BuildContext context) {
    return TextEditingControllerBuilder(
      builder: (context, controller) {
        return TextField(
          controller: controller,
        );
      },
    );
  }
}

Cela résout les problèmes rencontrés avec les mixins. Mais cela crée d'autres problèmes.

  • L'utilisation est très verbeuse. C'est effectivement 4 lignes de code + deux niveaux d'indentation pour une seule déclaration de variable.
    C'est encore pire si nous voulons l'utiliser plusieurs fois. Bien que nous puissions créer un TextEditingControllerBuilder dans un autre une fois, cela diminue considérablement la lisibilité du code :

    <strong i="28">@override</strong>
    Widget build(BuildContext context) {
    return TextEditingControllerBuilder(
      builder: (context, controller1) {
        return TextEditingControllerBuilder(
          builder: (context, controller2) {
            return Column(
              children: <Widget>[
                TextField(controller: controller1),
                TextField(controller: controller2),
              ],
            );
          },
        );
      },
    );
    }
    

    C'est un code très indenté juste pour déclarer deux variables.

  • Cela ajoute des frais généraux car nous avons une instance supplémentaire de State et Element .

  • Il est difficile d'utiliser le TextEditingController dehors de build .
    Si nous voulons qu'un cycle de vie State effectue une opération sur ces contrôleurs, alors nous aurons besoin d'un GlobalKey pour y accéder. Par exemple:

    class Example extends StatefulWidget {
    <strong i="43">@override</strong>
    _ExampleState createState() => _ExampleState();
    }
    
    class _ExampleState extends State<Example> {
    final textEditingControllerKey =
        GlobalKey<_TextEditingControllerBuilderState>();
    
    <strong i="44">@override</strong>
    void didUpdateWidget(Example oldWidget) {
      super.didUpdateWidget(oldWidget);
    
      if (something) {
        textEditingControllerKey.currentState.textEditingController.clear();
      }
    }
    
    <strong i="45">@override</strong>
    Widget build(BuildContext context) {
      return TextEditingControllerBuilder(
        key: textEditingControllerKey,
        builder: (context, controller) {
          return TextField(controller: controller);
        },
      );
    }
    }
    
P5 crowd framework passed first triage proposal new feature

Commentaire le plus utile

J'ajouterai quelques réflexions du point de vue de React.
Excusez-moi s'ils ne sont pas pertinents, mais je voulais expliquer brièvement ce que nous pensons de Hooks.

Les crochets "cachent" définitivement des choses. Ou, selon la façon dont vous le regardez, encapsulez-les. En particulier, ils encapsulent l'état et les effets locaux (je pense que nos "effets" sont les mêmes choses que les "jetables"). L'"implicite" réside dans le fait qu'ils attachent automatiquement la durée de vie au composant à l'intérieur duquel ils sont appelés.

Cette implicite n'est pas inhérente au modèle. Vous pouvez imaginer qu'un argument soit explicitement enfilé dans tous les appels - du composant lui-même aux Hooks personnalisés, jusqu'à chaque Hook primitif. Mais dans la pratique, nous avons trouvé que c'était bruyant et pas vraiment utile. Nous avons donc créé l'état global implicite du composant en cours d'exécution. Ceci est similaire à la façon dont throw dans une VM recherche vers le haut le bloc catch le plus proche au lieu de vous passer autour de errorHandlerFrame dans le code.

D'accord, ce sont donc les fonctions avec un état caché implicite à l'intérieur, cela semble mauvais ? Mais dans React, les composants le sont aussi en général. C'est tout l'intérêt des composants. Ce sont des fonctions auxquelles est associée une durée de vie (qui correspond à une position dans l'arborescence de l'interface utilisateur). La raison pour laquelle les composants eux-mêmes ne sont pas une arme à feu en ce qui concerne l'état est que vous ne les appelez pas simplement à partir d'un code aléatoire. Vous les appelez à partir d'autres composants. Leur durée de vie a donc du sens car vous restez dans le contexte du code de l'interface utilisateur.

Cependant, tous les problèmes ne sont pas liés à des composants. Les composants combinent deux capacités : état+effets et une durée de vie liée à la position de l'arbre. Mais nous avons constaté que la première capacité est utile en soi. Tout comme les fonctions sont utiles en général car elles vous permettent d'encapsuler du code, il nous manquait une primitive qui nous permettrait d'encapsuler (et de réutiliser) les bundles état+effets sans nécessairement créer un nouveau nœud dans l'arborescence. C'est ce que sont les crochets. Composants = Hooks + UI renvoyée.

Comme je l'ai mentionné, une fonction arbitraire cachant un état contextuel est effrayante. C'est pourquoi nous appliquons une convention via un linter. Les Hooks ont une "couleur" — si vous utilisez un Hook, votre fonction est aussi un Hook. Et le linter impose que seuls les composants ou autres Hooks peuvent utiliser des Hooks. Cela supprime le problème des fonctions arbitraires masquant l'état contextuel de l'interface utilisateur, car elles ne sont désormais plus implicites que les composants eux-mêmes.

Conceptuellement, nous ne considérons pas les appels Hook comme des appels de fonction simples. Comme useState() est plus use State() si nous avions la syntaxe. Ce serait une fonction linguistique. Vous pouvez modéliser quelque chose comme des crochets avec effets algébriques dans des langages qui ont un suivi des effets. Donc, dans ce sens, il s'agirait de fonctions régulières, mais le fait qu'elles "utilisent" l'État ferait partie de leur signature de type. Ensuite, vous pouvez considérer React lui-même comme un "gestionnaire" pour cet effet. Quoi qu'il en soit, c'est très théorique mais je voulais souligner l'art antérieur en termes de modèle de programmation.

Concrètement, il y a quelques choses ici. Tout d'abord, il convient de noter que les Hooks ne sont pas une API "supplémentaire" pour React. Ils sont l' API React pour écrire des composants à ce stade. Je pense que je serais d'accord pour dire qu'en tant que fonctionnalité supplémentaire, ils ne seraient pas très convaincants. Donc, je ne sais pas s'ils ont vraiment du sens pour Flutter, qui a un paradigme global sans doute différent.

Quant à ce qu'ils permettent, je pense que la caractéristique clé est la possibilité d'encapsuler l'état + la logique efficace, puis de les enchaîner comme vous le feriez avec une composition de fonction normale. Parce que les primitives sont conçues pour composer, vous pouvez prendre une sortie Hook comme useState() , la passer comme entrée à un useGesture(state) , puis la passer comme entrée à plusieurs useSpring(gesture) appels qui vous donnent des valeurs échelonnées, et ainsi de suite. Chacune de ces pièces ignore complètement les autres et peut être écrite par des personnes différentes, mais elles se composent bien ensemble car l'état et les effets sont encapsulés et « attachés » au Composant englobant. Voici une petite démo de quelque chose comme ça, et un article où je récapitule brièvement ce que sont les Hooks.

Je tiens à souligner qu'il ne s'agit pas de réduire le passe-partout mais de la possibilité de composer dynamiquement des pipelines de logique encapsulée avec état. Notez qu'il est entièrement réactif, c'est-à-dire qu'il ne s'exécute pas une seule fois, mais qu'il réagit à tous les changements de propriétés au fil du temps. Une façon de les considérer est qu'ils sont comme des plugins dans un pipeline de signaux audio. Bien que je reçoive totalement la méfiance à propos des "fonctions qui ont des souvenirs" dans la pratique, nous n'avons pas trouvé que cela soit un problème car elles sont complètement isolées. En fait, cet isolement est leur principale caractéristique. Il s'effondrerait sinon. Ainsi, toute codépendance doit être exprimée explicitement en retournant et en transmettant des valeurs à l'élément suivant de la chaîne. Et le fait que n'importe quel Hook personnalisé puisse ajouter ou supprimer un état ou des effets sans casser (ou même affecter) ses consommateurs est une autre caractéristique importante du point de vue des bibliothèques tierces.

Je ne sais pas si cela a été utile du tout, mais j'espère que cela jette un peu de perspective sur le modèle de programmation.
Au plaisir de répondre à d'autres questions.

Tous les 420 commentaires

cc @dnfield @Hixie
Comme demandé, voici les détails complets sur les problèmes résolus par les crochets.

Je crains que toute tentative pour rendre cela plus facile dans le cadre masque en fait la complexité à laquelle les utilisateurs devraient penser.

Il semble qu'une partie de cela pourrait être améliorée pour les auteurs de bibliothèques si nous tapions fortement les classes qui doivent être supprimées avec une sorte de abstract class Disposable . Dans un tel cas, vous devriez être capable d'écrire plus facilement une classe plus simple comme celle-ci si vous le souhaitez :

class AutomaticDisposingState<T> extends State<T> {
  List<Disposable> _disposables;

  void addDisposable(Disposable disposable) {
    assert(!_disposables.contains(disposable));
    _disposables.add(disposable);
  }

  <strong i="8">@override</strong>
  void dispose() {
    for (final Disposable disposable in _disposables)
      disposable.dispose();
    super.dispose();
  }
}

Ce qui supprime quelques lignes de code répétées. Vous pouvez écrire une classe abstraite similaire pour les propriétés de débogage, et même une classe qui combine les deux. Votre état d'initialisation pourrait ressembler à quelque chose comme :

<strong i="12">@override</strong>
void initState() {
  super.initState();
  controller = TextEditingController(text: 'Hello world');
  addDisposable(controller);
  addProperty('controller', controller);
}

Manquons-nous simplement de fournir de telles informations de frappe pour les classes jetables ?

Je crains que toute tentative pour rendre cela plus facile dans le cadre masque en fait la complexité à laquelle les utilisateurs devraient penser.

Les widgets cachent la complexité à laquelle les utilisateurs doivent penser.
Je ne suis pas sûr que ce soit vraiment un problème.

En fin de compte, c'est aux utilisateurs de le factoriser comme ils le souhaitent.


Le problème ne concerne pas seulement les produits jetables.

Cela oublie la partie mise à jour du problème. La logique d'étape pourrait également s'appuyer sur des cycles de vie tels que didChangeDependencies et didUpdateWidget.

Quelques exemples concrets :

  • SingleTickerProviderStateMixin qui a une logique à l'intérieur de didChangeDependencies .
  • AutomaticKeepAliveClientMixin, qui repose sur super.build(context)

Il existe de nombreux exemples dans le framework où nous souhaitons réutiliser la logique d'état :

  • StreamBuilder
  • TweenAnimationBuilder
    ...

Ce ne sont rien d'autre qu'un moyen de réutiliser l'état avec un mécanisme de mise à jour.

Mais ils souffrent du même problème que ceux évoqués dans la partie "constructeur".

Cela pose de nombreux problèmes.
Par exemple, l'un des problèmes les plus courants sur Stackoverflow est que les gens essaient d'utiliser StreamBuilder pour des effets secondaires, comme "push a route on change".

Et finalement leur seule solution est d'"éjecter" StreamBuilder.
Cela implique:

  • conversion du widget en stateful
  • écouter manuellement le flux dans initState+didUpdateWidget+didChangeDependencies
  • annuler l'abonnement précédent sur didChangeDependencies/didUpdateWidget lorsque le flux change
  • résilier l'abonnement lors de la disposition

C'est _beaucoup de travail_, et ce n'est effectivement pas réutilisable.

Problème

Réutiliser une logique State sur plusieurs StatefulWidget est très difficile, dès lors que cette logique repose sur plusieurs cycles de vie.

Un exemple typique serait la logique de création d'un TextEditingController (mais aussi AnimationController , des animations implicites, et bien d'autres). Cette logique se compose de plusieurs étapes :

  • définir une variable sur State .
    dart TextEditingController controller;
  • créant le contrôleur (généralement dans initState), avec potentiellement une valeur par défaut :
    dart <strong i="19">@override</strong> void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • a supprimé le contrôleur lorsque le State est supprimé :
    dart <strong i="24">@override</strong> void dispose() { controller.dispose(); super.dispose(); }
  • faire ce que nous voulons avec cette variable dans build .
  • (facultatif) exposez cette propriété sur debugFillProperties :
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

Ceci, en soi, n'est pas complexe. Le problème commence lorsque nous voulons étendre cette approche.
Une application Flutter typique peut avoir des dizaines de champs de texte, ce qui signifie que cette logique est dupliquée plusieurs fois.

Copier-coller cette logique partout "fonctionne", mais crée une faiblesse dans notre code :

  • il peut être facile d'oublier de réécrire l'une des étapes (comme oublier d'appeler dispose )
  • ça rajoute beaucoup de bruit dans le code

J'ai vraiment du mal à comprendre pourquoi c'est un problème. J'ai écrit beaucoup d'applications Flutter, mais cela ne semble vraiment pas être un problème? Même dans le pire des cas, il faut quatre lignes pour déclarer une propriété, l'initialiser, la supprimer et la rapporter aux données de débogage (et c'est généralement moins, car vous pouvez généralement la déclarer sur la même ligne que vous l'initialisez, les applications en général n'avez pas à vous soucier d'ajouter un état aux propriétés de débogage, et beaucoup de ces objets n'ont pas d'état qui doit être supprimé).

Je suis d'accord qu'un mixin par type de propriété ne fonctionne pas. Je suis d'accord que le modèle de constructeur n'est pas bon (il utilise littéralement le même nombre de lignes que le pire des cas décrit ci-dessus).

Avec NNBD (en particulier avec late final pour que les initialiseurs puissent référencer this ) nous pourrons faire quelque chose comme ceci :

typedef Initializer<T> = T Function();
typedef Disposer<T> = void Function(T value);

mixin StateHelper<T extends StatefulWidget> on State<T> {
  bool _active = false;
  List<Property<Object>> _properties = <Property<Object>>[];

  <strong i="8">@protected</strong>
  void registerProperty<T>(Property<T> property) {
    assert(T != Object);
    assert(T != dynamic);
    assert(!_properties.contains(property));
    _properties.add(property);
    if (_active)
      property._initState();
  }

  <strong i="9">@override</strong>
  void initState() {
    _active = true;
    super.initState();
    for (Property<Object> property in _properties)
      property._initState();
  }

  <strong i="10">@override</strong>
  void dispose() {
    for (Property<Object> property in _properties)
      property._dispose();
    super.dispose();
    _active = false;
  }

  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    for (Property<Object> property in _properties)
      property._debugFillProperties(properties);
  }
}

class Property<T> {
  Property(this.owner, this.initializer, this.disposer, [ this.debugName ]) {
    owner.registerProperty(this);
  }

  final StateHelper<StatefulWidget> owner;
  final Initializer<T> initializer;
  final Disposer<T> disposer;
  final String debugName;

  T value;

  void _initState() {
    if (initializer != null)
      value = initializer();
  }

  void _dispose() {
    if (disposer != null)
      disposer(value);
    value = null;
  }

  void _debugFillProperties(DiagnosticPropertiesBuilder properties) {
    properties.add(DiagnosticsProperty(debugName ?? '$T property', value));
  }
}

Vous l'utiliseriez comme ceci :

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  <strong i="14">@override</strong>
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with StateHelper<MyHomePage> {
  late final Property<int> _counter = Property<int>(this, null, null);
  late final Property<TextEditingController> _text = Property<TextEditingController>(this,
    () => TextEditingController(text: 'button'),
    (TextEditingController value) => value.dispose(),
  );

  void _incrementCounter() {
    setState(() {
      _counter.value += 1;
    });
  }

  <strong i="15">@override</strong>
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the ${_text.value.text} this many times:',
            ),
            Text(
              '${_counter.value}',
              style: Theme.of(context).textTheme.headline4,
            ),
            TextField(
              controller: _text.value,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Cela ne semble pas vraiment améliorer les choses. C'est toujours quatre lignes.

Les widgets cachent la complexité à laquelle les utilisateurs doivent penser.

Que cachent-ils ?

Le problème n'est pas le nombre de lignes, mais ce que sont ces lignes.

StreamBuilder peut contenir à peu près autant de lignes que stream.listen + setState + subscription.close .
Mais écrire un StreamBuilder peut se faire sans aucune réflexion, pour ainsi dire.
Il n'y a aucune erreur possible dans le processus. Il s'agit simplement de "passer le flux et de créer des widgets à partir de celui-ci".

Alors qu'écrire le code manuellement implique beaucoup plus de réflexions :

  • Le flux peut-il changer au fil du temps ? Si nous avons oublié de gérer cela, nous avons un bug.
  • Avons-nous oublié de fermer l'abonnement? Un autre bug
  • Quel nom de variable dois-je utiliser pour l'abonnement ? Ce nom peut ne pas être disponible
  • Et les tests ? Dois-je dupliquer le test ? Avec StreamBuilder , il n'est pas nécessaire d'écrire des tests unitaires pour écouter le flux, ce serait redondant. Mais si on l'écrit tout le temps manuellement, il est tout à fait possible de se tromper
  • Si nous écoutons deux flux à la fois, nous avons maintenant plusieurs variables avec des noms très similaires qui polluent notre code, cela peut provoquer une certaine confusion.

Que cachent-ils ?

  • FutureBuilder/StreamBuilder masque le mécanisme d'écoute et garde une trace de l'instantané actuel.
    La logique de basculement entre deux Future est également assez complexe, étant donné qu'elle n'a pas de subscription.close() .
  • AnimatedContainer masque la logique de faire une interpolation entre les valeurs précédentes et nouvelles.
  • Listview masque la logique de "monter un widget tel qu'il apparaît"

les applications n'ont généralement pas à se soucier d'ajouter un état aux propriétés de débogage

Ils ne le font pas, car ils ne veulent pas gérer la complexité de la maintenance de la méthode debugFillProperties.
Mais si nous disions aux développeurs « Voudriez-vous que tous vos paramètres et propriétés d'état soient prêts à l'emploi sur le devtool de Flutter ? » Je suis sûr qu'ils diraient oui

De nombreuses personnes m'ont exprimé leur désir d'un véritable équivalent au devtool de React. L'outil de développement de Flutter n'est pas encore là.
Dans React, on peut voir tout l'état d'un widget + ses paramètres, et l'éditer, sans rien faire.

De même, les gens ont été assez surpris quand je leur ai dit que lorsqu'ils utilisaient provider + certains de mes autres packages, par défaut, l'état complet de leur application leur était visible, sans avoir à faire quoi que ce soit ( modulo ce bogue devtool ennuyeux )

Je dois admettre que je ne suis pas un grand fan de FutureBuilder, cela cause beaucoup de bugs car les gens ne pensent pas à quand déclencher le Future. Je pense qu'il ne serait pas déraisonnable pour nous d'abandonner notre soutien. StreamBuilder est ok, je suppose, mais je pense que les flux eux-mêmes sont trop compliqués (comme vous le mentionnez dans votre commentaire ci-dessus) alors...

Pourquoi faut-il penser à la complexité de créer des Tweens ?

ListView ne cache pas vraiment la logique de montage d'un widget tel qu'il apparaît ; c'est une grande partie de l'API.

Le problème n'est pas le nombre de lignes, mais ce que sont ces lignes.

Je ne comprends vraiment pas le souci ici. Les lignes ressemblent à peu près à un simple passe-partout. Déclarer la chose, initialiser la chose, disposer de la chose. Si ce n'est pas le nombre de lignes, alors quel est le problème ?

Je suis d'accord avec vous pour dire que FutureBuilder est problématique.

C'est un peu hors sujet, mais je suggérerais qu'en développement, Flutter déclenche un faux rechargement à chaud toutes les quelques secondes. Cela mettrait en évidence les abus de FutureBuilder, des clés et bien d'autres.

Pourquoi faut-il penser à la complexité de créer des Tweens ?

ListView ne cache pas vraiment la logique de montage d'un widget tel qu'il apparaît ; c'est une grande partie de l'API.

Nous sommes d'accord là-dessus. Mon point était que nous ne pouvons pas critiquer quelque chose comme les crochets avec "ça cache la logique", car ce que font les crochets est strictement équivalent à ce qu'un TweenAnimationBuilder / AnimatedContainer /... fait.
La logique n'est pas cachée

Au final, je pense que les animations sont une bonne comparaison. Les animations ont ce concept d'implicite vs explicite.
Les animations implicites sont appréciées pour leur simplicité, leur composabilité et leur lisibilité.
Les animations explicites sont plus flexibles, mais plus complexes.

Lorsque nous traduisons ce concept en écoute de flux, StreamBuilder est une _écoute implicite_, alors que stream.listen est _explicite_.

Plus précisément, avec StreamBuilder vous _ne pouvez pas_ oublier de gérer le scénario où le flux change, ou oublier de fermer l'abonnement.
Vous pouvez également combiner plusieurs StreamBuilder ensemble

stream.listen est légèrement plus avancé et plus sujet aux erreurs.

Les constructeurs sont puissants pour simplifier l'application.
Mais comme nous en avons convenu précédemment, le modèle Builder n'est pas idéal. C'est à la fois verbeux à écrire et à utiliser.
Ce problème, et ce que les hooks résolvent, concerne une syntaxe alternative pour *Builders

Par exemple, flutter_hooks a un équivalent strict à FutureBuilder et StreamBuilder :

Widget build(context) {
  final AsyncSnapshot<T> snapshot = useStream(stream);
}

Dans la suite, AnimatedContainer & similaires pourraient être représentés par un useAnimatedSize / useAnimatedDecoractedBox / ... tel que nous avons :

double opacity;

Widget build(context) {
  final double animatedOpacity = useAnimatedDouble(opacity, duration: Duration(milliseconds: 200));
  return Opacity(
    opacity: animatedOpacity,
    child: ...,
  );
}

Mon point était que nous ne pouvons pas critiquer quelque chose comme les crochets avec "ça cache la logique",

Ce n'est pas l'argument. L'argument est "cela cache la logique à laquelle les développeurs devraient penser ".

Avez-vous un exemple d'une telle logique auquel les développeurs devraient réfléchir ?

Comme, à qui appartient le TextEditingController (qui le crée, qui en dispose).

Comme avec ce code ?

Widget build(context) {
  final controller = useTextEditingController();
  final focusNode = useFocusNode();
}

Le crochet le crée et s'en débarrasse.

Je ne sais pas ce qui n'est pas clair à ce sujet.

Oui, exactement. Je n'ai aucune idée du cycle de vie du contrôleur avec ce code. Cela dure-t-il jusqu'à la fin de la portée lexicale ? La durée de vie de l'État ? Autre chose? A qui appartient-il ? Si je le passe à quelqu'un d'autre, peut-il en prendre possession ? Rien de tout cela n'est évident dans le code lui-même.

Il semble que votre argument soit davantage causé par un manque de compréhension de ce que font les crochets que par un véritable problème.
Ces questions ont une réponse clairement définie qui est cohérente avec tous les crochets :

Je n'ai aucune idée du cycle de vie du contrôleur avec ce code

Vous n'avez pas non plus à y penser. Ce n'est plus la responsabilité du développeur.

Cela dure-t-il jusqu'à la fin de la portée lexicale ? La durée de vie de l'État

La durée de vie de l'État

A qui appartient-il ?

Le crochet possède le contrôleur. Il fait partie de l'API de useTextEditingController qu'il possède le contrôleur.
Ceci s'applique à useFocusNode , useScrollController , useAnimationController , ...

D'une certaine manière, ces questions s'appliquent à StreamBuilder :

  • Nous n'avons pas à penser aux cycles de vie du StreamSubscription
  • L'abonnement dure toute la vie de l'Etat
  • le StreamBuilder possède le StreamSubscription

En général, vous pouvez penser à :

final value = useX(argument);

comme un équivalent strict de :

XBuilder(
  argument: argument,
  builder: (context, value) {

  },
);

Ils ont les mêmes règles et le même comportement.

Ce n'est plus la responsabilité du développeur

Je pense que fondamentalement c'est le désaccord ici. Avoir une API de type fonction qui renvoie une valeur qui a une durée de vie définie qui n'est pas claire est, à mon humble avis, fondamentalement très différent d'une API basée sur la transmission de cette valeur à une fermeture.

Je n'ai aucun problème avec quelqu'un qui crée un package qui utilise ce style, mais c'est un style contraire à celui que je voudrais inclure dans l'API flutter de base.

@Hixie
Je ne pense pas que @rrousselGit disait qu'ils sont la même chose mais juste qu'ils ont "les mêmes règles et le même comportement" concernant le cycle de vie ? Correct?

Ils ne résolvent pas les mêmes problèmes cependant.

Peut-être que je me trompe, mais l'automne dernier, en essayant le flutter, je pense que si j'avais eu besoin de trois de ces constructeurs dans un widget, cela aurait été beaucoup d'imbrication. Par rapport à trois hameçons (trois lignes).
Aussi. Les crochets sont composables, donc si vous avez besoin de partager une logique d'état composée de plusieurs crochets, vous pouvez créer un nouveau crochet qui utilise d'autres crochets et une logique supplémentaire et n'utiliser qu'un seul nouveau crochet.

Des choses comme le partage facile de la logique d'état entre les widgets étaient une chose qui me manquait lorsque j'essayais de flutter à l'automne 2019.

Il pourrait bien sûr y avoir beaucoup d'autres solutions possibles. Peut-être que c'est déjà résolu et que je ne l'ai tout simplement pas trouvé dans la doc.
Mais si ce n'est pas le cas, beaucoup de choses pourraient être faites pour accélérer considérablement le développement si une chose comme des crochets ou une autre solution pour les mêmes problèmes était disponible en tant que citoyen de première classe.

Je ne suggère certainement pas d'utiliser l'approche du constructeur, comme le mentionne le PO, qui pose toutes sortes de problèmes. Ce que je suggérerais, c'est d'utiliser simplement initState/dispose. Je ne comprends pas vraiment pourquoi c'est un problème.

Je suis curieux de savoir ce que les gens pensent du code dans https://github.com/flutter/flutter/issues/51752#issuecomment -664787791. Je ne pense pas que ce soit mieux que initState/dispose, mais si les gens aiment les crochets, est-ce qu'ils aiment ça aussi ? Les crochets sont-ils meilleurs? Pire?

Les crochets useAnimationController , je n'ai plus à penser à initState et à disposer. Cela dégage la responsabilité du développeur. Je n'ai pas à m'inquiéter de savoir si j'ai éliminé chaque contrôleur d'animation que j'ai créé.

initState et dispose conviennent pour une seule chose, mais imaginez devoir garder une trace de plusieurs types d'états disparates. Les crochets se composent en fonction de l'unité logique d'abstraction au lieu de les répartir dans le cycle de vie de la classe.

Je pense que ce que vous demandez est l'équivalent de demander pourquoi avoir des fonctions quand on peut s'occuper manuellement des effets à chaque fois. Je suis d'accord que ce n'est pas exactement la même chose, mais c'est globalement similaire. Il semble que vous n'ayez jamais utilisé de crochets auparavant, donc les problèmes ne vous semblent pas trop apparents, donc je vous encourage à faire un projet de petite ou moyenne taille en utilisant des crochets, avec le package flutter_hooks peut-être, et voir comment il se sent. Je dis cela avec tout le respect, en tant qu'utilisateur de Flutter, j'ai rencontré ces problèmes auxquels les crochets fournissent des solutions, comme d'autres. Je ne sais pas comment vous convaincre que ces problèmes existent vraiment pour nous, faites-nous savoir s'il existe un meilleur moyen.

J'ajouterai quelques réflexions du point de vue de React.
Excusez-moi s'ils ne sont pas pertinents, mais je voulais expliquer brièvement ce que nous pensons de Hooks.

Les crochets "cachent" définitivement des choses. Ou, selon la façon dont vous le regardez, encapsulez-les. En particulier, ils encapsulent l'état et les effets locaux (je pense que nos "effets" sont les mêmes choses que les "jetables"). L'"implicite" réside dans le fait qu'ils attachent automatiquement la durée de vie au composant à l'intérieur duquel ils sont appelés.

Cette implicite n'est pas inhérente au modèle. Vous pouvez imaginer qu'un argument soit explicitement enfilé dans tous les appels - du composant lui-même aux Hooks personnalisés, jusqu'à chaque Hook primitif. Mais dans la pratique, nous avons trouvé que c'était bruyant et pas vraiment utile. Nous avons donc créé l'état global implicite du composant en cours d'exécution. Ceci est similaire à la façon dont throw dans une VM recherche vers le haut le bloc catch le plus proche au lieu de vous passer autour de errorHandlerFrame dans le code.

D'accord, ce sont donc les fonctions avec un état caché implicite à l'intérieur, cela semble mauvais ? Mais dans React, les composants le sont aussi en général. C'est tout l'intérêt des composants. Ce sont des fonctions auxquelles est associée une durée de vie (qui correspond à une position dans l'arborescence de l'interface utilisateur). La raison pour laquelle les composants eux-mêmes ne sont pas une arme à feu en ce qui concerne l'état est que vous ne les appelez pas simplement à partir d'un code aléatoire. Vous les appelez à partir d'autres composants. Leur durée de vie a donc du sens car vous restez dans le contexte du code de l'interface utilisateur.

Cependant, tous les problèmes ne sont pas liés à des composants. Les composants combinent deux capacités : état+effets et une durée de vie liée à la position de l'arbre. Mais nous avons constaté que la première capacité est utile en soi. Tout comme les fonctions sont utiles en général car elles vous permettent d'encapsuler du code, il nous manquait une primitive qui nous permettrait d'encapsuler (et de réutiliser) les bundles état+effets sans nécessairement créer un nouveau nœud dans l'arborescence. C'est ce que sont les crochets. Composants = Hooks + UI renvoyée.

Comme je l'ai mentionné, une fonction arbitraire cachant un état contextuel est effrayante. C'est pourquoi nous appliquons une convention via un linter. Les Hooks ont une "couleur" — si vous utilisez un Hook, votre fonction est aussi un Hook. Et le linter impose que seuls les composants ou autres Hooks peuvent utiliser des Hooks. Cela supprime le problème des fonctions arbitraires masquant l'état contextuel de l'interface utilisateur, car elles ne sont désormais plus implicites que les composants eux-mêmes.

Conceptuellement, nous ne considérons pas les appels Hook comme des appels de fonction simples. Comme useState() est plus use State() si nous avions la syntaxe. Ce serait une fonction linguistique. Vous pouvez modéliser quelque chose comme des crochets avec effets algébriques dans des langages qui ont un suivi des effets. Donc, dans ce sens, il s'agirait de fonctions régulières, mais le fait qu'elles "utilisent" l'État ferait partie de leur signature de type. Ensuite, vous pouvez considérer React lui-même comme un "gestionnaire" pour cet effet. Quoi qu'il en soit, c'est très théorique mais je voulais souligner l'art antérieur en termes de modèle de programmation.

Concrètement, il y a quelques choses ici. Tout d'abord, il convient de noter que les Hooks ne sont pas une API "supplémentaire" pour React. Ils sont l' API React pour écrire des composants à ce stade. Je pense que je serais d'accord pour dire qu'en tant que fonctionnalité supplémentaire, ils ne seraient pas très convaincants. Donc, je ne sais pas s'ils ont vraiment du sens pour Flutter, qui a un paradigme global sans doute différent.

Quant à ce qu'ils permettent, je pense que la caractéristique clé est la possibilité d'encapsuler l'état + la logique efficace, puis de les enchaîner comme vous le feriez avec une composition de fonction normale. Parce que les primitives sont conçues pour composer, vous pouvez prendre une sortie Hook comme useState() , la passer comme entrée à un useGesture(state) , puis la passer comme entrée à plusieurs useSpring(gesture) appels qui vous donnent des valeurs échelonnées, et ainsi de suite. Chacune de ces pièces ignore complètement les autres et peut être écrite par des personnes différentes, mais elles se composent bien ensemble car l'état et les effets sont encapsulés et « attachés » au Composant englobant. Voici une petite démo de quelque chose comme ça, et un article où je récapitule brièvement ce que sont les Hooks.

Je tiens à souligner qu'il ne s'agit pas de réduire le passe-partout mais de la possibilité de composer dynamiquement des pipelines de logique encapsulée avec état. Notez qu'il est entièrement réactif, c'est-à-dire qu'il ne s'exécute pas une seule fois, mais qu'il réagit à tous les changements de propriétés au fil du temps. Une façon de les considérer est qu'ils sont comme des plugins dans un pipeline de signaux audio. Bien que je reçoive totalement la méfiance à propos des "fonctions qui ont des souvenirs" dans la pratique, nous n'avons pas trouvé que cela soit un problème car elles sont complètement isolées. En fait, cet isolement est leur principale caractéristique. Il s'effondrerait sinon. Ainsi, toute codépendance doit être exprimée explicitement en retournant et en transmettant des valeurs à l'élément suivant de la chaîne. Et le fait que n'importe quel Hook personnalisé puisse ajouter ou supprimer un état ou des effets sans casser (ou même affecter) ses consommateurs est une autre caractéristique importante du point de vue des bibliothèques tierces.

Je ne sais pas si cela a été utile du tout, mais j'espère que cela jette un peu de perspective sur le modèle de programmation.
Au plaisir de répondre à d'autres questions.

Je ne suggère certainement pas d'utiliser l'approche du constructeur, comme le mentionne le PO, qui pose toutes sortes de problèmes. Ce que je suggérerais, c'est d'utiliser simplement initState/dispose. Je ne comprends pas vraiment pourquoi c'est un problème.

Je suis curieux de savoir ce que les gens pensent du code dans #51752 (commentaire) . Je ne pense pas que ce soit mieux que initState/dispose, mais si les gens aiment les crochets, est-ce qu'ils aiment ça aussi ? Les crochets sont-ils meilleurs? Pire?

Le mot-clé late améliore les choses, mais il souffre toujours de quelques problèmes :

De tels Property peuvent être utiles pour les états qui sont autonomes ou qui ne dépendent pas de paramètres qui peuvent changer au fil du temps. Mais il peut devenir difficile à utiliser dans une situation différente.
Plus précisément, il manque la partie "mise à jour".

Par exemple, avec StreamBuilder le flux écouté peut changer dans le temps. Mais il n'y a pas de solution simple pour implémenter une telle chose ici, car l'objet n'est initialisé qu'une seule fois.

De même, les crochets ont un équivalent au Key de Widget - ce qui peut provoquer la destruction et la recréation d'un état lorsque cette clé change.

Un exemple de cela est useMemo , qui est un crochet qui met en cache une instance d'objet.
Combiné avec des clés, nous pouvons utiliser useMemo pour avoir une récupération de données implicite.
Par exemple, notre widget peut recevoir un ID de message - que nous utilisons ensuite pour récupérer les détails du message. Mais cet ID de message peut changer au fil du temps, nous devrons donc peut-être récupérer les détails.

Avec useMemo , cela peut ressembler à :

String messageId;

Widget build(context) {
  final Future<Message> message = useMemo(() => fetchMessage(messageId), [messageId]);

}

Dans cette situation, même si la méthode de génération est appelée à nouveau 10 fois, tant que messageId ne change pas, la récupération de données n'est pas exécutée à nouveau.
Mais lorsque le messageId change, un nouveau Future est créé.


Il convient de noter que je ne pense pas que flutter_hooks dans son état actuel soit raffiné pour Dart. Mon implémentation est plus un POC qu'une architecture à part entière.
Mais je pense que nous avons un problème avec la réutilisation du code des StatefulWidgets.

Je ne me souvenais pas où, mais je me souviens avoir suggéré que les crochets dans le monde idéal seraient un générateur de fonctions personnalisé, à côté de async* & sync* , ce qui peut être similaire à ce que Dan suggère avec use State plutôt que useState

@gaearon

Je tiens à souligner qu'il ne s'agit pas de réduire le passe-partout mais de la possibilité de composer dynamiquement des pipelines de logique encapsulée avec état.

Ce n'est pas le problème dont il est question ici. Je recommande de déposer un bogue séparé pour parler de l'incapacité de faire ce que vous décrivez. (Cela ressemble à un problème très différent et honnêtement plus convaincant que celui décrit ici.) Ce bogue concerne spécifiquement le fait qu'une partie de la logique est trop verbeuse.

Non, il a raison, c'est ma formulation qui peut prêter à confusion.
Comme je l'ai mentionné précédemment, il ne s'agit pas du nombre de lignes de code, mais des lignes de code elles-mêmes.

Il s'agit de factoriser l'état.

Ce bogue est extrêmement clair sur le problème étant "La réutilisation de la logique d'état est trop verbeuse/difficile" et sur le fait qu'il y a trop de code dans un état lorsque vous avez une propriété qui a besoin d'avoir du code pour la déclarer, dans initState, dans dispose et dans debugFillProperties. Si le problème qui vous intéresse est différent, je vous recommande de déposer un nouveau bogue décrivant ce problème.

Je recommande vraiment, vraiment fortement d'oublier les crochets (ou toute solution) jusqu'à ce que vous compreniez parfaitement le problème que vous souhaitez résoudre. Ce n'est qu'en ayant une compréhension claire du problème que vous pourrez articuler un argument convaincant en faveur d'une nouvelle fonctionnalité, car nous devons évaluer les fonctionnalités par rapport aux problèmes qu'elles résolvent.

Je pense que vous comprenez mal ce que j'ai dit dans ce numéro alors.

Le problème n'est en aucun cas un passe-partout, mais la réutilisabilité.

Le passe-partout est la conséquence d'un problème de réutilisabilité, pas la cause

Ce que ce problème décrit est :

Nous pouvons vouloir réutiliser/composer la logique d'état. Mais les options disponibles sont soit des mixins, soit des constructeurs, soit de ne pas les réutiliser - qui ont toutes leurs propres problèmes.

Les problèmes des options existantes peuvent être liés au passe-partout, mais le problème que nous essayons de résoudre ne l'est pas.
Bien que la réduction du passe-partout des constructeurs soit une voie (c'est ce que font les crochets), il peut y en avoir une autre.

Par exemple, quelque chose que je voulais suggérer pendant un moment était d'ajouter des méthodes telles que :

context.onDidChangeDependencies(() {

});
context.onDispose(() {

});

Mais ceux-ci ont leurs propres problèmes et ne résolvent pas complètement le problème, donc je ne l'ai pas fait.

@rrousselGit , n'hésitez pas à modifier l'énoncé du problème d'origine en haut ici pour mieux refléter le problème. N'hésitez pas non plus à créer un document de conception : https://flutter.dev/docs/resources/design-docs que nous pouvons parcourir ensemble (encore une fois, comme le suggère @Hixie , en nous concentrant pour l'instant sur l'exposition la plus précise possible de l'énoncé du problème ). J'aimerais que vous vous sentiez aussi autonome que n'importe quel autre ingénieur Flutter - vous faites partie de l'équipe, alors répétons ensemble !

J'ai réexaminé le problème plusieurs fois. En toute honnêteté, je ne comprends pas d'où vient le malentendu, donc je ne sais pas quoi améliorer.
Le commentaire original mentionne à plusieurs reprises le désir de réutilisabilité/factorisation. Les mentions sur le passe-partout ne sont pas "Flutter est verbeux" mais "Certaines logiques ne sont pas réutilisables"

Je ne pense pas que la suggestion du document de conception soit juste. Il faut beaucoup de temps pour rédiger un tel document, et je le fais pendant mon temps libre.
Je suis personnellement satisfait des crochets. Je ne suis pas l'auteur de ces questions dans mon intérêt, mais pour sensibiliser à un problème qui affecte un nombre important de personnes.

Il y a quelques semaines, j'ai été embauché pour discuter de l'architecture d'une application Flutter existante. Leur était probablement exactement ce qui est mentionné ici:

  • Ils ont une logique qui doit être réutilisée dans plusieurs widgets (gestion des états de chargement / marquage des "messages" comme lus lorsque certains widgets deviennent visibles / ...)
  • Ils ont essayé d'utiliser des mixins, ce qui a causé des défauts d'architecture majeurs.
  • Ils ont également essayé de gérer manuellement la "création/mise à jour/élimination" en réécrivant cette logique à plusieurs endroits, mais cela a causé des bogues.
    À certains endroits, ils ont oublié de fermer les abonnements. Dans d'autres, ils n'ont pas géré le scénario où leur instance stream change
  • marquer les "messages" comme lus lorsque certains widgets deviennent visibles

C'est un cas intéressant car il est similaire aux problèmes que j'ai rencontrés dans l'une de mes propres applications, alors j'ai regardé comment j'y avais implémenté le code et je ne vois vraiment pas beaucoup de problèmes décrits par ce bogue, c'est pourquoi j'ai du mal à comprendre le problème. Voici le code en question :

https://github.com/jocosocial/rainbowmonkey/blob/master/lib/src/views/forums.dart

Avez-vous des exemples d'applications réelles que je pourrais étudier pour voir le problème en action ?

(BTW, en général, je recommanderais fortement de ne pas utiliser du tout les flux. Je pense qu'ils aggravent généralement les choses.)

(BTW, en général, je recommanderais fortement de ne pas utiliser du tout les flux. Je pense qu'ils aggravent généralement les choses.)

(Je suis tout à fait d'accord. Mais la communauté a actuellement la réaction inverse. Peut-être que l'extraction de ChangeNotifier/Listenable/ValueNotifier de Flutter dans un package officiel aiderait)

Avez-vous des exemples d'applications réelles que je pourrais étudier pour voir le problème en action ?

Malheureusement non. Je ne peux que partager l'expérience que j'ai eue en aidant les autres. Je n'ai pas d'application sous la main.

C'est un cas intéressant car il est similaire aux problèmes que j'ai rencontrés dans l'une de mes propres applications, alors j'ai regardé comment j'y avais implémenté le code et je ne vois vraiment pas beaucoup de problèmes décrits par ce bogue, c'est pourquoi j'ai du mal à comprendre le problème. Voici le code en question :

Dans votre implémentation, la logique n'est liée à aucun cycle de vie et placée à l'intérieur de _build_, donc cela contourne en quelque sorte le problème.
Cela peut avoir du sens dans ce cas précis. Je ne sais pas si cet exemple était bon.

Un meilleur exemple peut être tirer pour rafraîchir.

Dans un pull-to-refresh typique, nous voudrons :

  • lors de la première génération, gérer les états de chargement/d'erreur
  • au rafraîchissement :

    • si l'écran était en état d'erreur, affichez à nouveau l'écran de chargement

    • si le rafraîchissement a été effectué pendant le chargement, annulez les requêtes HTTP en attente

    • si l'écran affichait des données :

    • continuer à afficher les données pendant le chargement du nouvel état

    • si l'actualisation échoue, continuez à afficher les données précédemment obtenues et affichez une barre de collation avec l'erreur

    • si l'utilisateur apparaît et revient à l'écran alors que l'actualisation est en attente, afficher l'écran de chargement

    • assurez-vous que le RefreshIndicator est visible pendant que l'actualisation est en attente

Et nous voudrons implémenter une telle fonctionnalité pour toutes les ressources et plusieurs écrans. De plus, certains écrans peuvent vouloir actualiser plusieurs ressources à la fois.

ChangeNotifier + provider + StatefulWidget aura pas mal de difficultés à factoriser cette logique.

Alors que mes dernières expériences (qui sont basées sur l'immuabilité et reposent sur flutter_hooks ) prennent en charge l'ensemble du spectre prêt à l'emploi :

final productsProvider = FutureProvider<List<Product>>.autoDispose((ref) async {
  final cancelToken = CancelToken();
  ref.onDispose(cancelToken.cancel);

  return await repository.fetchProducts(cancelToken: cancelToken);
});

// ...

Widget build(context) {
  // Listens to the Future created by productsProvider and handles all the refresh logic
  AsyncValue<List<Product>> products = useRefreshProvider(
    productsProvider,
    // TODO consider making a custom hook to encaplusate the snackbar logic
    onErrorAfterRefresh: (err, stack) => Scaffold.of(context).showSnackBar(...),
  );

  return RefreshIndicator(
    onRefresh: () => context.refresh(productsProvider),
    child: products.when(
      loading: () {
        return const SingleChildScrollView(
          physics: AlwaysScrollableScrollPhysics(),
          child: CircularProgressIndicator(),
        );
      },
      error: (err, stack) {
        return SingleChildScrollView(
          physics: const AlwaysScrollableScrollPhysics(),
          child: Text('Oops, something unexpected happened\n$err'),
        );
      },
      data: (products) {
        return ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            return ProductItem(products[index]);
          },
        );
      },
    ),
  );
}

Cette logique est entièrement autonome. Il peut être réutilisé avec n'importe quelle ressource à l'intérieur de n'importe quel écran.

Et si un écran veut actualiser plusieurs ressources à la fois, nous pouvons faire :

AsyncValue<First> first = useRefreshProvider(
  firstProvider,
  onErrorAfterRefresh: ...
);
AsyncValue<Second> second = useRefreshProvider(
  secondProvider,
  onErrorAfterRefresh: ...
);

return RefreshIndicator(
  onRefresh: () {
     return Future.wait([context.refesh(firstProvider), context.refresh(secondProvider)]);
  }
  ...
)

Je recommanderais de mettre toute cette logique dans l'état de l'application, en dehors des widgets, et de faire en sorte que l'état de l'application reflète l'état actuel de l'application. Pull to refresh n'a pas besoin d'état dans le widget, il suffit de dire à l'état ambiant qu'un rafraîchissement est en attente, puis d'attendre que son futur se termine.

Il n'est pas de la responsabilité de l'état ambiant de déterminer comment rendre une erreur vs chargement vs données

Avoir cette logique à l'état ambiant ne supprime pas toutes les logiques de l'interface utilisateur
L'interface utilisateur doit encore déterminer s'il faut afficher l'erreur en plein écran ou dans un snack-bar
Il faut encore forcer l'actualisation des erreurs lors du rechargement de la page

Et c'est moins réutilisable.
Si la logique de rendu est entièrement définie dans les widgets plutôt que dans l'état ambiant, alors cela fonctionnera avec _any_ Futureet peut même être inclus directement dans Flutter.

Je ne comprends pas vraiment ce que vous préconisez dans votre dernier commentaire. Ce que je veux dire, c'est que vous n'avez pas besoin de modifier le framework pour faire quelque chose d'aussi simple que le code de l'indicateur d'actualisation ci-dessus, comme le montre le code que j'ai cité plus tôt.

Si nous avons beaucoup de ces types d'interactions, pas seulement pour les indicateurs d'actualisation, mais pour les animations et autres, il est préférable de les encapsuler là où elles sont le plus nécessaires plutôt que de les mettre dans l'état de l'application, car l'état de l'application n'a pas besoin de connaître les spécificités de chaque interaction dans l'application si elle n'est pas nécessaire à plusieurs endroits dans l'application.

Je ne pense pas que nous soyons d'accord sur la complexité de la fonctionnalité et sa réutilisabilité.
Avez-vous un exemple qui montre qu'une telle fonctionnalité est facile ?

J'ai lié à la source d'une application que j'ai écrite ci-dessus. Ce n'est certainement pas un code parfait, et j'ai l'intention d'en réécrire des morceaux pour la prochaine version, mais je n'ai pas rencontré les problèmes que vous décrivez dans ce numéro.

Mais vous êtes l'un des leaders technologiques de Flutter.
Même face à un problème, vous auriez suffisamment de compétences pour trouver immédiatement une solution.

Pourtant, de l'autre côté, un nombre important de personnes ne comprennent pas ce qui ne va pas avec le code suivant :

FutureBuilder<User>(
  future: fetchUser(),
  builder: ...,
)

Ce fait est prouvé par la popularité d'un Q/AI réalisé sur StackOverflow .

Le problème n'est pas qu'il soit impossible d'abstraire la logique d'état de manière réutilisable et robuste (sinon cela ne sert à rien de faire ce problème).
Le problème est qu'il faut à la fois du temps et de l'expérience pour le faire.

En fournissant une solution officielle, cela réduit la probabilité qu'une application ne soit pas maintenable, ce qui augmente la productivité globale et l'expérience du développeur.
Tout le monde ne pourrait pas proposer votre suggestion de propriété . Si une telle chose était construite à l'intérieur de Flutter, elle serait documentée, aurait de la visibilité et, en fin de compte, aiderait des personnes qui n'y auraient jamais pensé au départ.

Le problème est que cela dépend vraiment de ce qu'est votre application, à quoi ressemble votre état, etc. Si la question ici est simplement "comment gérez-vous l'état de l'application", alors la réponse n'est pas du tout comme des crochets, c'est beaucoup de documentation qui parle de différentes façons de le faire et recommande différentes techniques pour différentes situations... en gros, cet ensemble de documents : https://flutter.dev/docs/development/data-and-backend/state-mgmt

Il existe un état éphémère et un état d'application, mais il semble également exister un autre cas d'utilisation : un état qui ne concerne qu'un seul type de widget mais que vous souhaitez néanmoins partager entre ce type de widget.

Par exemple, un ScrollController peut invoquer un certain type d'animation, mais il n'est pas nécessairement approprié de le mettre dans l'état global de l'application, car ce ne sont pas des données qui doivent être utilisées dans toute l'application. Cependant, plusieurs ScrollController peuvent avoir la même logique et vous souhaitez partager cette logique de cycle de vie entre chacun d'eux. L'état est toujours pour seulement ScrollController s, donc pas l'état global de l'application, mais le copier-coller de la logique est sujet à erreur.

De plus, vous souhaiterez peut-être packager cette logique pour la rendre plus composable pour vos futurs projets, mais aussi pour d'autres. Si vous regardez le site useHooks , vous verrez de nombreux éléments de logique qui compartimentent certaines actions courantes. Si vous utilisez useAuth vous l'écrivez une fois et vous n'avez jamais à vous soucier de savoir si vous avez manqué un appel initState ou dispose , ou si la fonction async a un then et catch . La fonction n'est écrite qu'une seule fois, donc la marge d'erreur disparaît fondamentalement. Par conséquent, ce type de solution est non seulement plus composable pour plusieurs parties de la même application et entre plusieurs applications, mais il est également plus sûr pour le programmeur final.

Je n'ai aucune objection à ce que les gens utilisent des crochets. Autant que je sache, rien ne l'empêche. (Si quelque chose empêche cela, veuillez signaler un bogue à ce sujet.)

Ce bogue ne concerne pas les hooks, il s'agit de "La réutilisation de la logique d'état est trop détaillée/difficile", et j'ai toujours du mal à comprendre pourquoi cela nécessite des modifications de Flutter. Il y a eu de nombreux exemples (y compris les hooks) montrant comment il est possible d'éviter la verbosité en structurant son application d'une manière ou d'une autre, et il existe déjà beaucoup de documentation à ce sujet.

Je vois, alors vous demandez pourquoi, s'il existe quelque chose comme un package de crochets qui a déjà été construit sans modification de Flutter, il doit y avoir une solution de première partie pour les crochets? Je suppose que @rrousselGit peut mieux répondre à cette question, mais la réponse implique probablement un meilleur support, un support plus intégré et plus de personnes les utilisant.

Je peux convenir avec vous qu'en plus de cela, je ne comprends pas non plus pourquoi des changements fondamentaux doivent être apportés à Flutter pour prendre en charge les crochets, car apparemment le package flutter_hooks existe déjà.

J'ai toujours du mal à comprendre pourquoi cela nécessite des modifications de Flutter.

Dire que ce problème est résolu parce que la communauté a créé un package, c'est comme dire que Dart n'a pas besoin de classes de données + types d'union parce que j'ai créé Freezed .
Freezed peut être très apprécié par la communauté comme solution à ces deux problèmes, mais nous pouvons encore faire mieux.

L'équipe Flutter a beaucoup plus d'influence que la communauté n'en aura jamais. Vous avez à la fois la possibilité de modifier l'intégralité de la pile ; des personnes expertes dans chaque domaine ; et un salaire pour parrainer le travail nécessaire.

Ce problème a besoin de ça.
N'oubliez pas : l'un des objectifs de l'équipe React est que les hooks fassent partie du langage, un peu comme avec JSX.

Même sans support linguistique, nous avons toujours besoin de travailler dans l'analyseur ; jeu de fléchettes ; Flutter/devtools; et de nombreux crochets pour simplifier toutes les différentes choses que fait Flutter (comme pour les animations implicites, les formulaires, etc.).

C'est un bon argument, j'en conviens, même si la philosophie générale de Flutter est d'avoir un petit noyau. Pour cette raison, nous ajoutons de plus en plus de nouvelles fonctionnalités sous forme de packages, même lorsqu'elles proviennent de Google, de personnages et d' animations cf. Cela nous donne une plus grande flexibilité pour apprendre et changer au fil du temps. Nous ferions de même pour cet espace, à moins qu'il n'y ait une raison technique impérieuse pour laquelle un package était insuffisant (et avec les méthodes d'extension, c'est encore moins probable que jamais).

Mettre les choses au cœur de Flutter est délicat. L'un des défis est, comme vous le savez bien par expérience directe, que l'état est un domaine qui évolue à mesure que nous en apprenons tous davantage sur ce qui fonctionne bien dans une architecture d'interface utilisateur réactive. Il y a deux ans, si nous avions été obligés de choisir un gagnant, nous aurions peut-être choisi BLoC, mais bien sûr, votre forfait fournisseur a pris le relais et est maintenant notre recommandation par défaut.

Je pourrais facilement concevoir des contributeurs employés par Google prenant en charge flutter_hooks ou un package de crochets similaire qui a du succès (bien que nous ayons beaucoup d'autres travaux qui se disputent notre attention , évidemment). En particulier, nous devrions Si vous cherchez que nous vous prenions le relais, c'est évidemment une autre question.

Argument intéressant, @timsneath. La communauté Rust fait également quelque chose de similaire, car une fois introduit dans le noyau ou la bibliothèque standard d'un langage ou d'un framework, il est très difficile de le retirer. Dans le cas de Rust, c'est impossible car ils veulent maintenir la compatibilité descendante pour toujours. Par conséquent, ils attendent que les colis soient arrivés et se soient affrontés jusqu'à ce que seuls quelques gagnants émergent, puis ils intègrent cela dans la langue.

Cela pourrait être un cas similaire avec Flutter. Il pourrait y avoir quelque chose de mieux que les hooks plus tard, tout comme React a dû passer des classes aux hooks mais devait toujours maintenir les classes, et les gens devaient migrer. Il pourrait alors être préférable d'avoir des solutions de gestion d'état concurrentes avant d'être ajoutées au noyau. Et peut-être que nous, la communauté, devrions innover au-dessus des crochets ou essayer de trouver des solutions encore meilleures.

Je comprends cette préoccupation, mais il ne s'agit pas d'une solution de gestion de l'État.

Cette fonctionnalité est plus proche de Inheritedwidget & StatefulWidget. C'est une primitive de bas niveau, qui peut être aussi basse qu'une caractéristique de langage.

Les crochets peuvent être indépendants du framework, mais ce n'est que par chance.
Comme je l'ai mentionné précédemment, un autre chemin vers ce problème peut être :

context.onDispose(() {

});

Et des écouteurs d'événements similaires.
Mais cela est impossible à mettre en œuvre hors du cadre.

Je ne sais pas ce que l'équipe proposerait.
Mais nous ne pouvons pas exclure la possibilité qu'une telle solution doive être directement à côté de Element

Les extensions sont-elles utiles ?

(Peut-être devrions-nous en parler dans un autre problème, cependant. C'est un peu hors sujet ici. Je préférerais vraiment que nous ayons un problème par problème que les gens voient, afin que nous puissions discuter des solutions au bon endroit. Ce n'est pas clairement comment context.onDispose aiderait à la verbosité.)

Je soupçonne fortement qu'il y a de très bonnes propositions linguistiques que nous pourrions faire à ce sujet.

Je pense qu'il serait utile de parler d'eux plus spécifiquement que de la façon dont ils pourraient activer un idiome de gestion d'état spécifique. Nous pourrions alors envisager plus sérieusement ce qu'ils permettraient et les compromis qu'ils pourraient entraîner.

En particulier, nous pourrions examiner comment et s'ils pourraient fonctionner à la fois dans les environnements d'exécution VM et JS

On ne sait pas comment context.onDispose aiderait à la verbosité.)

Comme je l'ai déjà mentionné, ce problème concerne davantage la réutilisation du code que la verbosité. Mais si nous pouvons réutiliser plus de code, cela devrait implicitement réduire la verbosité.

La façon dont context.onDispose est liée à ce problème est, avec la syntaxe actuelle que nous avons :

AnimationController controller;

<strong i="11">@override</strong>
void initState() {
  controller = AnimationController(...);
}

<strong i="12">@override</strong>
void dispose() {
  controller.dispose();
}

Le problème est:

  • ceci est étroitement lié à la définition de classe, donc ne peut pas être réutilisé
  • à mesure que le widget grandit, la relation entre l'initialisation et la disposition devient plus difficile à lire car il y a des centaines de lignes de code au milieu.

Avec un context.onDispose , on pourrait faire :

<strong i="21">@override</strong>
void initState() {
  controller = AnimationController(...);
  context.onDispose(controller.dispose);
}

La partie intéressante est :

  • celle-ci n'est plus étroitement liée à la définition de la classe, elle peut donc être extraite dans une fonction.
    On pourrait théoriquement avoir une logique semi-complexe :
    ``` fléchette
    AnimationController someReusableLogic (contexte BuildContext) {
    contrôleur final =AnimationController(...);
    controller.onDispose(controller.dispose);
    contrôleur.forward();
    auditeur vide() {}
    contrôleur.addListener(auditeur);
    context.onDispose(() => controller.removeListener(listener));
    }
    ...

@passer outre
void initState() {
contrôleur = someReusableLogic(context);
}
```

  • toute la logique est regroupée. Même si le widget atteint une longueur de 300, la logique de controller est toujours facilement lisible.

Le problème avec cette approche est :

  • context.myLifecycle(() {...}) n'est pas rechargeable à chaud
  • il n'est pas clair comment faire lire les propriétés de someReusableLogic partir du StatefulWidget sans coupler étroitement la fonction à la définition du widget.
    Par exemple, le AnimationController de Duration peut être passé en paramètre du widget. Nous devons donc gérer le scénario où la durée change.
  • on ne sait pas comment implémenter une fonction qui renvoie un objet qui peut changer dans le temps, sans avoir à recourir à un ValueNotifier et à traiter avec des auditeurs

    • Ceci est particulièrement important pour les états calculés.


Je vais réfléchir à une proposition linguistique. J'ai quelques idées, mais rien qui mérite d'être évoqué pour le moment.

Comme je l'ai mentionné précédemment, ce problème concerne davantage la réutilisation du code que la verbosité

D'accord. Pouvez-vous s'il vous plaît déposer un nouveau bogue qui en parle spécifiquement ? Ce bogue est littéralement appelé "La réutilisation de la logique d'état est trop verbeuse/difficile". Si la verbosité n'est pas le problème, alors _ce_ n'est pas le problème.

Avec un context.onDispose , on pourrait faire :

<strong i="11">@override</strong>
void initState() {
  controller = AnimationController(...);
  context.onDispose(controller.dispose);
}

Je ne sais pas pourquoi context est pertinent dans ce domaine (et onDispose viole nos conventions de nommage). Si vous voulez juste un moyen d'enregistrer des choses à exécuter pendant la destruction, vous pouvez le faire facilement aujourd'hui :

mixin StateHelper<T extends StatefulWidget> on State<T> {
  List<VoidCallback> _disposeQueue;

  void queueDispose(VoidCallback callback) {
    _disposeQueue ??= <VoidCallback>[];
    _disposeQueue.add(callback);
  }

  <strong i="17">@override</strong>
  void dispose() {
    if (_disposeQueue != null) {
      for (VoidCallback callback in _disposeQueue)
        callback();
    }
    super.dispose();
  }
}

Appelez-le comme ceci :

class _MyHomePageState extends State<MyHomePage> with StateHelper<MyHomePage> {
  TextEditingController controller;

  <strong i="21">@override</strong>
  void initState() {
    super.initState();
    controller = TextEditingController(text: 'button');
    queueDispose(controller.dispose);
  }

  ...
AnimationController someReusableLogic(BuildContext context) {
  final controller = AnimationController(...);
  controller.onDispose(controller.dispose);
  controller.forward();
  void listener() {}
  controller.addListener(listener);
  context.onDispose(() => controller.removeListener(listener));
}
...

<strong i="25">@override</strong>
void initState() {
  controller = someReusableLogic(context);
}

Vous pouvez faire cela aussi:

AnimationController someReusableLogic<T extends StatefulWidget>(StateHelper<T> state) {
  final controller = AnimationController(...);
  state.queueDispose(controller.dispose);
  controller.forward();
  void listener() {}
  controller.addListener(listener);
  state.queueDispose(() => controller.removeListener(listener));
  return controller;
}
...

<strong i="6">@override</strong>
void initState() {
  controller = someReusableLogic(this);
}

Le problème avec cette approche est :

  • context.myLifecycle(() {...}) n'est pas rechargeable à chaud

Dans ce contexte, cela ne semble pas avoir d'importance puisque c'est uniquement pour les choses appelées dans initState? Est-ce que j'ai raté quelque chose ?

  • il n'est pas clair comment faire lire les propriétés de someReusableLogic partir du StatefulWidget sans coupler étroitement la fonction à la définition du widget.
    Par exemple, le AnimationController de Duration peut être passé en paramètre du widget. Nous devons donc gérer le scénario où la durée change.

Il est assez simple d'ajouter une file d'attente didChangeWidget tout comme la file d'attente de disposition :

mixin StateHelper<T extends StatefulWidget> on State<T> {
  List<VoidCallback> _disposeQueue;
  List<VoidCallback> _didUpdateWidgetQueue;

  void queueDispose(VoidCallback callback) {
    _disposeQueue ??= <VoidCallback>[];
    _disposeQueue.add(callback);
  }

  void queueDidUpdateWidget(VoidCallback callback) {
    _didUpdateWidgetQueue ??= <VoidCallback>[];
    _didUpdateWidgetQueue.add(callback);
  }

  <strong i="24">@override</strong>
  void didUpdateWidget(T oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_didUpdateWidgetQueue != null) {
      for (VoidCallback callback in _didUpdateWidgetQueue)
        callback();
    }
  }

  <strong i="25">@override</strong>
  void dispose() {
    if (_disposeQueue != null) {
      for (VoidCallback callback in _disposeQueue)
        callback();
    }
    super.dispose();
  }
}

AnimationController conditionalAnimator(StateHelper state, ValueGetter<bool> isAnimating, VoidCallback listener) {
  final controller = AnimationController(vsync: state as TickerProvider, duration: const Duration(seconds: 1));
  state.queueDispose(controller.dispose);
  controller.addListener(listener);
  state.queueDispose(() => controller.removeListener(listener));
  if (isAnimating())
    controller.repeat();
  state.queueDidUpdateWidget(() {
    if (isAnimating()) {
      controller.repeat();
    } else {
      controller.stop();
    }
  });
  return controller;
}

Utilisé comme ceci :

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  <strong i="6">@override</strong>
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(animating: false),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.animating}) : super(key: key);

  final bool animating;

  <strong i="7">@override</strong>
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with StateHelper<MyHomePage>, SingleTickerProviderStateMixin {
  AnimationController controller;

  <strong i="8">@override</strong>
  void initState() {
    super.initState();
    controller = conditionalAnimator(this, () => widget.animating, () { print(controller.value); });
  }

  <strong i="9">@override</strong>
  Widget build(BuildContext context) {
    return Center(
      child: FadeTransition(
        opacity: controller,
        child: Text('Hello', style: TextStyle(fontSize: 100.0, color: Colors.white)),
      ),
    );
  }
}
  • on ne sait pas comment implémenter une fonction qui renvoie un objet qui peut changer dans le temps, sans avoir à recourir à un ValueNotifier et à traiter avec des auditeurs

    • Ceci est particulièrement important pour les états calculés.

Vous ne savez pas ce que cela signifie ici, qu'est-ce qui ne va pas avec ValueNotifier et, disons, un ValueListenableBuilder ?

Comme je l'ai mentionné précédemment, ce problème concerne davantage la réutilisation du code que la verbosité

D'accord. Pouvez-vous s'il vous plaît déposer un nouveau bogue qui en parle spécifiquement ? Ce bogue est littéralement appelé "La réutilisation de la logique d'état est trop verbeuse/difficile". Si la verbosité n'est pas le problème, alors ce n'est pas le problème.

Je commence à être assez mal à l'aise avec cette discussion. J'ai déjà répondu à ce point avant :
Le sujet de ce numéro est la réutilisabilité, et la verbosité est discutée en conséquence d'un problème de réutilisabilité ; pas comme sujet principal.

Il n'y a qu'une seule puce dans le commentaire supérieur mentionnant la verbosité, et c'est avec StreamBuilder, ciblant principalement les 2 niveaux d'indentations.

Je ne sais pas pourquoi le contexte est pertinent dans ce [...]. Si vous voulez juste un moyen d'enregistrer des choses à exécuter pendant la destruction, vous pouvez le faire facilement aujourd'hui :

Lorsque j'ai évoqué context.onDispose , j'ai mentionné explicitement que je ne pense pas que ce soit une bonne solution.
Je l'ai expliqué parce que vous avez demandé comment cela est lié à la discussion.

Quant à savoir pourquoi context au lieu de StateHelper , c'est parce que c'est plus flexible (comme travailler avec StatelessWidget)

context.myLifecycle(() {...}) n'est pas rechargeable à chaud

Dans ce contexte, cela ne semble pas avoir d'importance puisque c'est uniquement pour les choses appelées dans initState? Est-ce que j'ai raté quelque chose ?

Nous pouvons changer :

initState() {
  context.myLifecycle(() => print('hello'));
}

dans:

initState() {
  context.myLifecycle(() => print('world'));
}

Cela n'appliquera pas les modifications au rappel myLifecycle .

Mais si on utilisait :

myLifecycle() {
  super.myLifecycle();
  print('hello');
}

alors le rechargement à chaud fonctionnerait.

Vous ne savez pas ce que cela signifie ici, qu'est-ce qui ne va pas avec ValueNotifier et, disons, un ValueListenableBuilder ?

Cette syntaxe a été conçue pour éviter d'avoir à utiliser des générateurs, nous sommes donc revenus au problème d'origine.

De plus, si nous voulons vraiment rendre notre fonction composable, au lieu de votre suggestion ValueGetter + queueDidUpdateWidget , les fonctions devront prendre un ValueNotifier comme paramètre :

AnimationController conditionalAnimator(StateHelper state, ValueListenable<bool> isAnimating, VoidCallback listener) {
...
}

car nous pouvons vouloir obtenir isAnimating d'un endroit autre que didUpdateWidget selon le widget qui utilise cette fonction.
À un endroit, il peut s'agir de didUpdateWidget ; dans un autre, il peut s'agir de didChangeDependencies ; et à un autre endroit encore, il peut être à l'intérieur du rappel d'un stream.listen .

Mais ensuite, nous avons besoin d'un moyen de convertir facilement ces scénarios en ValueNotifier et de faire en sorte que notre fonction écoute un tel notificateur.
Nous rendons donc notre vie beaucoup plus difficile.
Il est plus fiable et plus facile d'utiliser un ConditionalAnimatorBuilder que ce modèle, je pense.

Quant à savoir pourquoi context au lieu de StateHelper , c'est parce que c'est plus flexible (comme travailler avec StatelessWidget)

StatelessWidget est destiné aux widgets sans état. L'essentiel est qu'ils ne créeraient pas d'état, ne disposeraient pas de choses, ne réagiraient pas sur didUpdateWidget, etc.

Re le rechargement à chaud, oui. C'est pourquoi nous utilisons des méthodes plutôt que de mettre des fermetures dans initState.

Je suis désolé de répéter cela, et je comprends que cela doit être frustrant, mais je ne comprends toujours pas quel est le problème que nous essayons de résoudre ici. Je pensais que c'était de la verbosité, selon le résumé du bogue d'origine et une grande partie de la description d'origine, mais je comprends que ce n'est pas ça. Alors quel est le problème? Il semble qu'il y ait ici de nombreux désirs qui s'excluent mutuellement, répartis dans les nombreux commentaires de ce bogue :

  • Déclarer comment disposer quelque chose doit être fait au même endroit qui l'alloue...
  • ... et l'endroit qui l'alloue n'a besoin de s'exécuter qu'une seule fois puisqu'il l'alloue...
  • ... et il doit fonctionner avec un rechargement à chaud (qui par définition ne réexécute pas le code qui ne s'exécute qu'une seule fois)...
  • ... et il doit être capable de créer un état qui fonctionne avec des widgets sans état (qui par définition n'ont pas d'état)...
  • ... et il doit permettre l'accrochage à des choses comme didUpdateWidget et didChangeDependencies...

Cette danse itérative dans laquelle nous sommes impliqués ici n'est pas un moyen productif de faire avancer les choses. Comme j'ai déjà essayé de le dire, le meilleur moyen d'obtenir quelque chose ici est de décrire le problème auquel vous êtes confronté d'une manière que nous puissions comprendre, avec tous les besoins décrits en un seul endroit et expliqués avec des cas d'utilisation. Je recommande de ne pas lister les solutions, surtout pas les solutions dont vous savez qu'elles ne satisfont pas vos besoins. Assurez-vous simplement que le besoin qui rend ces solutions inappropriées est répertorié dans la description.

Pour être honnête, fondamentalement, il me semble que vous demandez une conception de cadre entièrement différente. C'est parfaitement bien, mais ce n'est pas Flutter. Si nous devions faire un framework différent, ce serait, eh bien, un framework différent, et nous avons encore beaucoup de travail à faire sur _ce_ framework. En fait, une grande partie de ce que vous décrivez est très similaire à la conception de Jetpack Compose. Je ne suis pas un grand fan de cette conception car elle nécessite la magie du compilateur, donc le débogage de ce qui se passe est vraiment difficile, mais c'est peut-être plus votre affaire?

Il semble qu'il y ait ici de nombreux désirs qui s'excluent mutuellement, répartis dans les nombreux commentaires de ce bogue :

Ils ne s'excluent pas mutuellement. Les crochets font chacun de ceux-ci. Je n'entrerai pas dans les détails car nous ne voulons pas nous concentrer sur les solutions, mais ils cochent toutes les cases.

Comme j'ai déjà essayé de le dire, le meilleur moyen d'obtenir quelque chose ici est de décrire le problème auquel vous êtes confronté d'une manière que nous puissions comprendre, avec tous les besoins décrits en un seul endroit et expliqués avec des cas d'utilisation.

Je ne comprends toujours pas comment ce premier commentaire ne parvient pas à le faire.
Ce qui n'est pas clair pour les autres n'est pas clair pour moi.

En fait, une grande partie de ce que vous décrivez est très similaire à la conception de Jetpack Compose. Je ne suis pas un grand fan de cette conception car elle nécessite la magie du compilateur, donc le débogage de ce qui se passe est vraiment difficile, mais c'est peut-être plus votre affaire?

Je ne le connais pas, mais avec une recherche rapide, je dirais _oui_.

Ils ne s'excluent pas mutuellement.

Tous les points que j'ai énumérés ci-dessus font-ils partie du problème que nous essayons de résoudre ici ?

mais ils cochent toutes les cases

Pouvez-vous lister les cases?

Je ne comprends toujours pas comment ce premier commentaire ne parvient pas à le faire.

Par exemple, l'OP dit explicitement que le problème concerne les StatefulWidgets, mais l'un des commentaires récents sur ce problème a déclaré qu'une suggestion particulière n'était pas bonne car elle ne fonctionnait pas avec StatelessWidgets.

Dans l'OP tu dis :

Il est difficile de réutiliser la logique State . Soit nous nous retrouvons avec une méthode build complexe et profondément imbriquée, soit nous devons copier-coller la logique sur plusieurs widgets.

Donc, à partir de là, je suppose que les exigences incluent:

  • La solution ne doit pas être profondément imbriquée.
  • La solution ne doit pas nécessiter beaucoup de code similaire dans les endroits qui essaient d'ajouter un état.

Le premier point (à propos de l'imbrication) semble bien. Certainement pas essayer de suggérer que nous devrions faire des choses qui sont profondément imbriquées. (Cela dit, nous pouvons être en désaccord sur ce qui est profondément imbriqué ; cela n'est pas défini ici. D'autres commentaires impliquent plus tard que les générateurs provoquent du code profondément imbriqué, mais d'après mon expérience, les générateurs sont plutôt bons, comme le montre le code que j'ai cité plus tôt.)

Le deuxième point semble être une exigence de ne pas avoir de verbosité. Mais vous avez expliqué à plusieurs reprises qu'il ne s'agissait pas de verbosité.

La déclaration suivante que l'OP fait pour décrire un problème est :

Réutiliser une logique State sur plusieurs StatefulWidget est très difficile, dès lors que cette logique repose sur plusieurs cycles de vie.

Honnêtement, je ne sais pas vraiment ce que cela signifie. Pour moi, « difficile » signifie généralement que quelque chose implique une logique complexe difficile à comprendre, mais l'attribution, l'élimination et la réaction aux événements du cycle de vie sont très simples. La déclaration suivante qui pose un problème (ici je saute l'exemple qui est explicitement décrit comme "pas complexe" et donc vraisemblablement pas une description du problème) est :

Le problème commence lorsque nous voulons étendre cette approche.

Cela m'a suggéré que par "très difficile" vous vouliez dire "très verbeux" et que la difficulté venait du fait qu'il y avait beaucoup d'occurrences de code similaire, puisque la seule différence entre l'exemple "pas complexe" que vous donnez et le "très difficile" " Le résultat de la mise à l'échelle de l'exemple est littéralement que le même code se produit plusieurs fois (c'est-à-dire la verbosité, le code passe-partout).

Ceci est en outre soutenu par la déclaration suivante qui décrit un problème :

Copier-coller cette logique partout "fonctionne", mais crée une faiblesse dans notre code :

  • il peut être facile d'oublier de réécrire l'une des étapes (comme oublier d'appeler dispose )

Donc probablement c'est très difficile parce que la verbosité rend facile de faire une erreur lors du copier-coller du code ? Mais encore une fois, lorsque j'ai essayé de résoudre ce problème, que je qualifierais de "verbosité", vous avez dit que le problème n'était pas la verbosité.

  • ça rajoute beaucoup de bruit dans le code

Encore une fois, cela ressemble à de la verbosité / passe-partout pour moi, mais encore une fois, vous avez expliqué que ce n'était pas cela.

Le reste du PO décrit simplement des solutions que vous n'aimez pas, donc il ne décrit probablement pas le problème.

Cela explique-t-il comment l'OP ne parvient pas à expliquer le problème ? Tout dans le PO qui décrit réellement un problème semble décrire la verbosité, mais chaque fois que je suggère que c'est le problème, vous dites que ce n'est pas le cas et qu'il y a un autre problème.

Je pense que le malentendu se résume au mot sens.
Par exemple:

ça rajoute beaucoup de bruit dans le code

Encore une fois, cela ressemble à de la verbosité / passe-partout pour moi, mais encore une fois, vous avez expliqué que ce n'était pas cela.

Ce point ne concerne pas le nombre de controller.dispose() , mais la valeur que ces lignes de code apportent au lecteur.
Cette ligne doit toujours être là et est toujours la même. A ce titre, sa valeur pour le lecteur est quasi nulle.

Ce qui compte, ce n'est pas la présence de cette ligne, mais son absence.

Le problème est que plus nous avons de tels controller.dispose() , plus nous avons de chances de rater un problème réel dans notre méthode de disposition.
Si nous avons 1 contrôleur et 0 disposer, c'est facile à attraper
Si nous avons 100 contrôleurs et 99 en disposer, trouver celui qui manque est difficile.

Ensuite nous avons:

Donc probablement c'est très difficile parce que la verbosité rend facile de faire une erreur lors du copier-coller du code ? Mais encore une fois, lorsque j'ai essayé de résoudre ce problème, que je qualifierais de "verbosité", vous avez dit que le problème n'était pas la verbosité.

Comme je l'ai mentionné au point précédent, toutes les lignes de codes ne sont pas égales.

Si on compare :

+ T state;

<strong i="24">@override</strong>
void initState() {
  super.initState();
+  state = widget.valueNotifier.value;
+  widget.valueNotifier.addListener(_listener);
}

+ void _listener() => seState(() => state = widget.valueNotifier.value);

void dispose() {
+ widget.valueNotifier.removeListener(_listener);
  super.dispose();
}

vs:

+ ValueListenableBuilder<T>(
+   valueListenable: widget.valueNotifier,  
+   builder: (context, value, child) {

+    },
+ );

alors ces deux extraits ont le même nombre de lignes et font la même chose.
Mais ValueListenableBuilder est préférable.

La raison en est que ce n'est pas le nombre de lignes qui compte, mais ce que sont ces lignes.

Le premier extrait a :

  • 1 déclaration de propriété
  • 1 déclaration de méthode
  • 1 mission
  • 2 appels de méthode
  • qui sont tous répartis sur 2 cycles de vie différents. 3 si nous incluons build

Le deuxième extrait a :

  • 1 instanciation de classe
  • 1 fonction anonyme
  • pas de cycle de vie. 1 si nous incluons build

Ce qui rend le ValueListenableBuilder _simpler_.

Il y a aussi ce que ces lignes ne disent pas :
ValueListenableBuilder gère l'évolution de valueListenable fil du temps.
Même dans le scénario où widget.valueNotifier ne change pas avec le temps au moment où nous parlons, cela ne fait pas de mal.
Un jour, cette déclaration peut changer. Dans ce cas, ValueListenableBuilder gère gracieusement le nouveau comportement, alors que, avec le premier extrait, nous avons maintenant un bogue.

Ainsi, non seulement ValueListenableBuilder est plus simple, mais il est également plus résistant aux modifications du code - pour exactement le même nombre de lignes.


Sur ce, je pense que nous pouvons tous les deux convenir que ValueListenableBuilder est préférable.
La question est alors : « Pourquoi ne pas avoir un équivalent à ValueListenableBuilder pour chaque logique d'état réutilisable ? »

Par exemple, au lieu de :

final controller = TextEditingController(text: 'hello world');
...
controller.dispose();

nous aurions:

TextEditingControllerBuilder(
  initialText: 'hello world',
  builder: (context, controller) {

  },
);

avec l'avantage supplémentaire que les changements en initialText peuvent être rechargés à chaud.

Cet exemple peut être un peu trivial, mais nous pourrions utiliser ce principe pour des logiques d'état réutilisables légèrement plus avancées (comme votre ModeratorBuilder ).

C'est "bien" dans les petits extraits. Mais cela pose quelques problèmes car nous voulons étendre l'approche :

  • Les constructeurs reviennent au problème du "trop ​​de bruit".

Par exemple, j'ai vu certaines personnes gérer leur modèle de cette façon :

class User {
  final ValueNotifier<String> name;
  final ValueNotifier<int> age;
  final ValueNotifier<Gender> gender;
}

Mais alors, un widget peut vouloir écouter à la fois name , age et gender en même temps.
Ce qui veut dire qu'il faudrait faire :

return ValueListenableBuilder<String>(
  valueListenable: user.name,
  builder: (context, userName, _) {
    return ValueListenableBuilder<int>(
      valueListenable: user.age,
      builder: (context, userAge, _) {
        return ValueListenableBuilder<Gender>(
          valueListenable: user.gender,
          builder: (context, userGender, _) {
            return Text('$userGender. $userName ($userAge)');
          },
        );
      },
    );
  },
);

Ce n'est évidemment pas idéal. Nous avons supprimé la pollution à l'intérieur de initState / dispose pour polluer notre méthode build .

(ignorons Listenable.merge pour l'exemple. Cela n'a pas d'importance ici ; c'est plus une question de composition)

Si nous avons beaucoup utilisé Builders, il est facile de nous voir dans ce scénario exact - et sans équivalent à Listenable.merge (pas que j'aime ce constructeur, pour commencer 😛 )

  • Écrire un constructeur personnalisé est fastidieux

    Il n'y a pas de solution simple pour créer un Builder. Aucun outil de refactoring ne nous aidera ici - nous ne pouvons pas simplement "extraire en tant que Builder".
    De plus, ce n'est pas forcément intuitif. Faire un Builder personnalisé n'est pas la première chose à laquelle les gens penseront - d'autant plus que beaucoup seront contre le passe-partout (bien que je ne le sois pas).

    Les gens sont plus susceptibles de créer une solution de gestion d'état personnalisée et de se retrouver potentiellement avec un mauvais code.

  • Manipuler un arbre de Builders est fastidieux

    Supposons que nous voulions supprimer un ValueListenableBuilder dans notre exemple précédent ou en ajouter un nouveau, ce n'est pas facile.
    Nous pouvons passer quelques minutes à compter () et {} pour comprendre pourquoi notre code ne se compile pas.


Les hooks sont là pour résoudre les problèmes de Builder que nous venons de mentionner.

Si nous refactorisons l'exemple précédent en hooks, nous aurions :

final userName = useValueListenable(user.name);
final useAge = useValueListenable(user.age);
final useGender = useValueListenable(user.gender);

return Text('$userGender. $userName ($userAge)');

Il est identique au comportement précédent, mais le code a maintenant une indentation linéaire.
Ce qui signifie:

  • le code nettement plus lisible
  • c'est plus facile à éditer. Nous n'avons pas besoin d'avoir peur (){}; pour ajouter une nouvelle ligne.

C'est l'un des principaux provider on aime. Il a supprimé beaucoup d'imbrication en introduisant MultiProvider .

De même, contrairement à l'approche initState / dispose , nous bénéficions du rechargement à chaud.
Si nous ajoutions un nouveau useValueListenable , le changement serait appliqué immédiatement.

Et bien sûr, nous avons toujours la possibilité d'extraire des primitives réutilisables :

String useUserLabel(User user) {
  final userName = useValueListenable(user.name);
  final useAge = useValueListenable(user.age);
  final useGender = useValueListenable(user.gender);

  return '$userGender. $userName ($userAge)';
}

Widget build(context) {
  final label = useUserLabel(user);
  return Text(label);
}

et un tel changement peut être automatisé avec extract as function , ce qui fonctionnerait dans la plupart des scénarios.


Est-ce que ça répond à votre question?

Sûr. Le problème avec quelque chose comme ça, c'est qu'il n'a tout simplement pas assez d'informations pour réellement faire la bonne chose. Par exemple:

Widget build(context) {
  if (random.nextBool())
    final title = useLabel(title);
  final label = useLabel(name);
  return Text(label);
}

... finirait par être buggé de manière vraiment déroutante.

Vous pouvez contourner cela avec la magie du compilateur (c'est ainsi que Compose le fait) mais pour Flutter qui viole certaines de nos décisions de conception fondamentales. Vous pouvez contourner ce problème avec des clés, mais les performances en souffrent considérablement (puisque la recherche de variables finit par impliquer des recherches de carte, des hachages, etc.), ce qui pour Flutter viole certains de nos objectifs de conception fondamentaux.

La solution de propriété que j'ai suggérée plus tôt, ou quelque chose qui en dérive, semble éviter la magie du compilateur tout en atteignant les objectifs que vous avez décrits d'avoir tout le code au même endroit. Je ne comprends pas vraiment pourquoi cela ne fonctionnerait pas pour cela. (Évidemment, il serait étendu pour également se connecter à didChangeDependencies et ainsi de suite pour être une solution complète.) (Nous ne mettrons pas cela dans le cadre de base car cela violerait nos exigences de performances.)

C'est précisément à cause des bugs qui peuvent survenir, comme vous le dites, que les hooks ne doivent pas être appelés de manière conditionnelle. Voir le document Rules of Hooks de ReactJS pour plus de détails. L'essentiel est que, puisque dans leur mise en œuvre, ils sont suivis par ordre d'appel, leur utilisation conditionnelle rompra cet ordre d'appel et ne permettra donc pas leur suivi correctement. Pour utiliser correctement le hook, vous les appelez au niveau supérieur dans build sans aucune logique conditionnelle. Dans la version JS, vous récupérez

const [title, setTitle] = useLabel("title");

L'équivalent Dart peut être similaire, il est seulement plus long car il n'a pas de déballage comme le fait JS :

var titleHook = useLabel("title");
String title = titleHook.property;
Function setTitle = titleHook.setter;

Si vous voulez une logique conditionnelle, vous pouvez alors décider d'utiliser title dans la méthode de construction _après les avoir appelés au niveau supérieur_, car maintenant l'ordre d'appel est toujours conservé. Bon nombre de ces problèmes que vous soulevez ont été expliqués dans le document sur les crochets que j'ai lié ci-dessus.

Sûr. Et vous pouvez le faire dans un package. Je dis juste que ce genre d'exigence violerait notre philosophie de conception, c'est pourquoi nous n'ajouterions pas cela à Flutter le cadre. (Plus précisément, nous optimisons la lisibilité et le débogage ; avoir un code qui a l'air de fonctionner mais, à cause d'une condition (qui peut ne pas être évidente dans le code) ne fonctionne parfois pas, n'est pas quelque chose que nous voulons encourager ou activer dans le cadre de base.)

Le comportement de débogage/conditionnel n'est pas un problème. C'est pourquoi un plugin d'analyse est important. Un tel plugin :

  • avertir si une fonction utilise un hook sans être nommé useMyFunction
  • avertir si un crochet est utilisé de manière conditionnelle
  • avertir si un hook est utilisé dans une boucle/rappel.

Cela couvre toutes les erreurs potentielles. React a prouvé que c'est une chose faisable.

Il nous reste alors les avantages :

  • code plus lisible (comme indiqué précédemment)
  • meilleur rechargement à chaud
  • code plus réutilisable/composable
  • plus flexible - nous pouvons facilement créer des états calculés.

Concernant les états calculés, les hooks sont assez puissants pour mettre en cache l'instance d'un objet. Cela peut être utilisé pour reconstruire un widget uniquement lorsque son paramètre change.

Par exemple, on peut avoir :

class Example extends HookWidget {
  final int userId;

  Widget build(context) {
    // Calls fetchUser whenever userId changes
    // It is the equivalent to both initState and didUpdateWidget
    final future = useMemo1(() => fetchUser(userId), userId);

    final snapshot = useFuture(future);
    if (!snapshot.hasData)
      return Text('loading');
    return Text(snapshot.data.name);
  }  
}

Un tel crochet useMemo permet d'optimiser facilement les performances et de gérer à la fois init + update de manière déclarative, ce qui évite également les bogues.

C'est quelque chose que la proposition Property / context.onDispose manque.
Ils sont difficiles à utiliser pour les états déclaratifs sans coupler étroitement la logique à un cycle de vie ou complexifier le code avec ValueNotifier .

En savoir plus sur les raisons pour lesquelles la proposition ValueGetter n'est pas pratique :

Nous pouvons vouloir refactoriser :

final int userId;

Widget build(context) {
  final future = useMemo1(() => fetchUser(userId), userId);

dans:

Widget build(context) {
  final userId = Model.of(context).userId;
  final future = useMemo1(() => fetchUser(userId), userId);

Avec les hooks, ce changement fonctionne parfaitement, car useMemo n'est lié à aucun cycle de vie.

Mais avec Property + ValueGetter , nous devrions changer l'implémentation du Property pour que cela fonctionne - ce qui n'est pas souhaité car le code Property peut être réutilisé à plusieurs endroits. Nous avons donc à nouveau perdu la réutilisabilité.

FWIW, cet extrait est équivalent à :

class Example extends StatefulWidget {
  final int userId;
  <strong i="45">@override</strong>
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  Future<User> future;

  <strong i="46">@override</strong>
  void initState() {
    super.initState();
    future = fetchUser(widget.userId);
  }

  <strong i="47">@override</strong>
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.userId != widget.userId) {
      future = fetchUser(widget.userId);
    }
  }

  <strong i="48">@override</strong>
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: future,
      builder: (context, snapshot) {
        if (!snapshot.hasData)
          return Text('loading');
        return Text(snapshot.data.name);
      },
    );
  }
}

Je suppose que nous devrons trouver une solution qui résout les mêmes problèmes que ceux mentionnés par

Peut-être qu'une prochaine étape consiste à créer une solution unique à Flutter qui est la version des crochets de ce framework, compte tenu des contraintes de Flutter, tout comme Vue a créé sa version compte tenu des contraintes de Vue. J'utilise régulièrement les hooks de React et je dirais que le simple fait d'avoir un plugin d'analyse peut parfois ne pas suffire, il devrait probablement être plus intégré dans le langage.

En tout cas, je ne pense pas que nous parviendrons jamais à un consensus. On dirait que nous ne sommes pas d'accord même sur ce qui est lisible

Pour rappel, je partage ceci uniquement parce que je sais que la communauté a des problèmes avec ce problème. Personnellement, cela ne me dérange pas que Flutter ne fasse rien à ce sujet (bien que je trouve ce genre de triste), tant que nous avons :

  • un système de plugin d'analyseur approprié
  • la possibilité d'utiliser des packages à l'intérieur du dartpad

Si vous souhaitez poursuivre le plugin hooks, que j'encourage fortement, mais que vous rencontrez des problèmes, je vous recommande de signaler les problèmes pour ces problèmes et de déposer des PR pour résoudre ces problèmes. Nous sommes plus qu'heureux de travailler avec vous là-dessus.

Voici une nouvelle version de l'idée précédente de Property . Il gère didUpdateWidget et l'élimination (et peut facilement être configuré pour gérer d'autres éléments tels que didChangeDependencies) ; il prend en charge le rechargement à chaud (vous pouvez modifier le code qui enregistre la propriété et le rechargement à chaud, et il fera la bonne chose); il est de type sûr sans avoir besoin de types explicites (s'appuie sur l'inférence) ; il a tout au même endroit, à l'exception de la déclaration de propriété et de l'utilisation, et les performances devraient être raisonnablement bonnes (mais pas aussi bonnes que les manières plus détaillées de faire les choses).

Propriété/Gestionnaire de propriétés :

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

typedef InitStateCallback<T> = T Function(T oldValue);
typedef DidUpdateWidgetCallback<T, W extends StatefulWidget> = T Function(W oldWidget);

class Property<T, W extends StatefulWidget> {
  Property({
    T value,
    this.initState,
    this.didUpdateWidget,
    this.dispose,
  }) : _value = value;

  T get value {
    assert(_value != null);
    return _value;
  }
  T _value;

  final InitStateCallback<T> initState;
  void _initState(Property<T, W> oldProperty) {
    if (initState != null)
      _value = initState(oldProperty?.value);
    assert(_value != null);
  }

  final DidUpdateWidgetCallback<T, W> didUpdateWidget;
  void _didUpdateWidget(StatefulWidget oldWidget) {
    if (didUpdateWidget != null) {
      final T newValue = didUpdateWidget(oldWidget);
      if (newValue != null)
        _value = newValue;
    }
  }

  final ValueSetter<T> dispose;
  void _dispose() {
    if (dispose != null)
      dispose(value);
  }
}

mixin PropertyManager<W extends StatefulWidget> on State<W> {
  final Set<Property<Object, W>> _properties = <Property<Object, W>>{};
  bool _ready = false;

  Property<T, W> register<T>(Property<T, W> oldProperty, Property<T, W> property) {
    assert(_ready);
    if (oldProperty != null) {
      assert(_properties.contains(oldProperty));
      _properties.remove(oldProperty);
    }
    assert(property._value == null);
    property._initState(oldProperty);
    _properties.add(property);
    return property;
  }

  <strong i="9">@override</strong>
  void initState() {
    super.initState();
    _ready = true;
    initProperties();
  }

  <strong i="10">@override</strong>
  void reassemble() {
    super.reassemble();
    initProperties();
  }

  <strong i="11">@protected</strong>
  <strong i="12">@mustCallSuper</strong>
  void initProperties() { }

  <strong i="13">@override</strong>
  void didUpdateWidget(W oldWidget) {
    super.didUpdateWidget(oldWidget);
    for (Property<Object, W> property in _properties)
      property._didUpdateWidget(oldWidget);
  }

  <strong i="14">@override</strong>
  void dispose() {
    _ready = false;
    for (Property<Object, W> property in _properties)
      property._dispose();
    super.dispose();
  }
}

Voici comment vous l'utiliseriez :

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'properties.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  <strong i="18">@override</strong>
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Example(userId: 1),
    );
  }
}

class User {
  User(this.name);
  final String name;
}

Future<User> fetchUser(int userId) async {
  await Future.delayed(const Duration(seconds: 2));
  return User('user$userId');
}

class Example extends StatefulWidget {
  Example({ Key key, this.userId }): super(key: key);

  final int userId;

  <strong i="19">@override</strong>
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> with PropertyManager {
  Property future;

  <strong i="20">@override</strong>
  void initProperties() {
    super.initProperties();
    future = register(future, Property(
      initState: (_) => fetchUser(widget.userId),
      didUpdateWidget: (oldWidget) {
        if (oldWidget.userId != widget.userId)
          return fetchUser(widget.userId);
      }
    ));
  }

  <strong i="21">@override</strong>
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: future.value,
      builder: (context, snapshot) {
        if (!snapshot.hasData) return Text('loading');
        return Text(snapshot.data.name);
      },
    );
  }
}

Pour plus de commodité, vous pouvez créer des sous-classes de propriété préparées pour des choses comme AnimationControllers et ainsi de suite,

Vous pouvez probablement en créer une version qui fonctionnerait également dans les méthodes State.build ...

Je partage certains des doutes que @Hixie apporte à la table. D'un autre côté, je vois les avantages évidents des Hooks et il semble qu'un certain nombre de développeurs l'apprécient.
Mon problème avec l'approche de package proposée par
Si les packages commencent à implémenter des choses qui devraient être la capacité de réponse du framework, nous obtiendrons beaucoup de dialectes Flutter différents, ce qui rend difficile l'apprentissage de nouvelles bases de code. Donc, pour moi, je commencerais probablement à utiliser des crochets dès qu'il ferait partie de Flutter.
C'est un peu comme mon point de vue actuel sur le paquet gelé. J'aime la fonctionnalité, mais à moins que les unions et les classes de données ne fassent partie de Dart, je ne veux pas les inclure dans ma base de code car cela rendrait plus difficile la lecture de mon code.

@escamoteur Pour que je comprenne bien, proposez-vous de changer fondamentalement le fonctionnement des widgets ? Ou suggérez-vous qu'il devrait y avoir de nouvelles capacités spécifiques ? Étant donné que des choses comme les crochets et la proposition de propriété ci-dessus sont possibles sans aucune modification du cadre de base, je ne sais pas ce que vous voudriez réellement changer.

C'est orthogonal par rapport à la conversation sur tout changement proposé lui-même, mais je pense que ce que j'ai entendu de @escamoteur , @rrousselGit et d'autres ici et ailleurs, c'est qu'être _dans_ le cadre est perçu comme un moyen important d'établir la légitimité d'un particulier approcher. Corrigez-moi si vous n'êtes pas d'accord.

Je comprends cette ligne de pensée - puisqu'il y a beaucoup de choses qui découlent du cadre (par exemple, DartPad ne prend pas en charge les packages tiers aujourd'hui, certains clients se méfient du nombre de packages dont ils dépendent après avoir été gravés avec NPM, il se sent plus «officiel», il est garanti d'avancer avec des changements comme null-safe).

Mais l'inclusion a aussi des coûts importants : en particulier, elle sclérose une approche et une API. C'est pourquoi nous tenons tous les deux la barre très haute à ce que nous ajoutons, en particulier lorsqu'il n'y a pas d'accord unanime (cf.

Je me demande si nous devons documenter notre philosophie du paquetage d'abord, mais encore une fois, _où_ cela va est distinct d'une discussion sur _ce_ que nous pourrions vouloir changer pour améliorer la réutilisation de la logique d'état.

Notre politique de package est documentée ici : https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#deciding -where-to-put-code

Je comprends parfaitement l'approche du paquet d'abord et conviens que c'est une chose importante.
Mais je pense aussi que certains problèmes doivent être résolus dans le noyau, pas par des packages.

C'est pourquoi je ne prétends pas que provider devrait être fusionné dans Flutter, mais je pense également que ce problème décrit un problème que Flutter devrait résoudre nativement (pas nécessairement avec des crochets bien sûr).

Avec Provider, Flutter fournit une primitive intégrée pour résoudre ce genre de problème : InheritedWidgets.
Le fournisseur n'ajoute qu'une couche avisée sur le dessus pour le rendre "" plus agréable "".

Les crochets sont différents. Ils _are_ le primitif. Ils constituent une solution de bas niveau sans opinion à un problème spécifique : réutiliser la logique dans plusieurs états.
Ils ne sont pas le produit final, mais quelque chose que les gens sont censés utiliser pour créer des packages personnalisés (comme je l'ai fait avec hooks_riverpod )

Ce serait utile pour moi (en termes de compréhension des désirs ici, et des besoins auxquels les crochets répondent, etc.) si quelqu'un pouvait fournir un examen détaillé de la façon dont l'approche de la propriété que j'ai griffonnée ci-dessus se compare aux crochets. (Mon objectif avec l'idée de propriété est de superposer l'opinion au-dessus du cadre pour résoudre le problème de la réutilisation de la logique dans plusieurs états.)

Je pense que la proposition de propriété ne parvient pas à résoudre un objectif clé de ce problème : la logique d'état ne devrait pas se soucier d'où viennent les paramètres et dans quelle situation ils sont mis à jour.

Cette proposition augmente la lisibilité dans une certaine mesure en regroupant toute la logique en un seul endroit ; mais cela ne résout pas le problème de réutilisabilité

Plus précisément, on ne peut pas extraire :

Property(
  initState: (_) => fetchUser(widget.userId),
  didUpdateWidget: (oldWidget) {
    if (oldWidget.userId != widget.userId)
      return fetchUser(widget.userId);
  }
)

sur _ExampleState et le réutiliser dans un autre widget, car la logique est liée à Example et initState + didUpdateWidget

A quoi ça ressemblerait avec des crochets ?

Je suis d'accord avec @timsneath après avoir vu quelque chose de similaire dans la communauté Rust. Il est très difficile d'extraire quelque chose du noyau une fois qu'il est sorti. Le modèle BLoC a été spécifié avant l'arrivée du fournisseur, mais maintenant le fournisseur est la version recommandée. Peut-être que flutter_hooks peut être la version « bénie » de la même manière. Je dis cela parce qu'à l'avenir, il pourrait y avoir des améliorations par rapport aux crochets que les gens proposent. React, ayant eu des crochets maintenant, ne peut pas vraiment les changer ou s'en sortir. Ils doivent les prendre en charge, tout comme ils le font pour les composants de classe, essentiellement pour toujours, puisqu'ils sont dans le noyau. Par conséquent, je suis d'accord avec la philosophie du paquet.

Le problème semble être que l'adoption sera faible et que les gens utiliseront ce qui leur convient. Cela peut être résolu comme je le dis en recommandant aux gens d'utiliser flutter_hooks. Cela pourrait également ne pas être un gros problème si nous examinons de manière analogue le nombre de solutions de gestion d'état, même si de nombreuses personnes utilisent le fournisseur. J'ai également rencontré des problèmes et des « pièges » dans d'autres frameworks qui devraient être résolus afin de créer une solution supérieure à la logique de cycle de vie composable et réutilisable.

A quoi ça ressemblerait avec des crochets ?

Sans utiliser les crochets primitifs fournis par React/flutter_hooks, nous pourrions avoir :

class FetchUser extends Hook<AsyncSnapshot<User>> {
  const FetchUser(this.userId);
  final int userId;

  <strong i="8">@override</strong>
  _FetchUserState createState() => _FetchUserState();
}

class _FetchUserState extends HookState<AsyncSnapshot<User>, FetchUser> {
  Future<User> userFuture;

  <strong i="9">@override</strong>
  void initHook() {
    userFuture = fetchUser(hook.userId);
  }  

  void didUpdateHook(FetchUser oldHook) {
    if (oldHook.userId != hook.userId)
      userFuture = fetchUser(hook.userId);
  }


  <strong i="10">@override</strong>
  User build() {
    return useFuture(userFuture);
  }
}

Ensuite utilisé :

class Example extends HookWidget {
  const Example({Key key, this.userId}) : super(key: key);

  final int userId;

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    AsyncSnapshot<User> user = use(FetchUser(userId));

    if (!user.hasData)
      return CircularProgressIndicator();
    return Text(user.data.name);
  }
}

Dans cette situation, la logique est complètement indépendante de Example et des cycles de vie d'un StatefulWidget .
Nous pourrions donc le réutiliser dans un autre widget qui gère ses userId différemment. Peut-être que cet autre widget sera un StatefulWidget qui gère ses userId interne. Peut-être qu'il obtiendra le userId d'un InheritedWidget à la place.

Cette syntaxe devrait rendre évident que les hooks sont comme des objets State indépendants avec leurs propres cycles de vie.

En remarque, un inconvénient de l'approche package-first est le suivant : les auteurs de packages sont moins susceptibles de publier des packages en s'appuyant sur des hooks pour résoudre des problèmes.

Par exemple, un problème courant auquel les utilisateurs de fournisseur sont confrontés est qu'ils souhaitent supprimer automatiquement l'état d'un fournisseur lorsqu'il n'est plus utilisé.
Le problème est que les utilisateurs de Provider sont également très friands de la syntaxe context.watch / context.select , par opposition à la syntaxe verbeuse Consumer(builder: ...) / Selector(builder:...) .
Mais nous ne pouvons pas avoir à la fois cette belle syntaxe _et_ résoudre le problème mentionné précédemment sans crochets (ou https://github.com/flutter/flutter/pull/33213, qui a été rejeté).

Le problème est:
Le fournisseur ne peut pas dépendre de flutter_hooks pour résoudre ce problème.
En raison de la popularité du fournisseur, il serait déraisonnable de dépendre des crochets.

Donc au final j'ai opté pour :

  • Fournisseur de forking (sous le nom de code de Riverpod )
  • perdre volontairement la recommandation "Flutter favorite"/Google en conséquence
  • résoudre ce problème (et d'autres)
  • ajouter une dépendance aux crochets pour offrir une syntaxe que les personnes qui aiment context.watch aimeraient.

Je suis assez satisfait de ce que j'ai proposé, car je pense que cela apporte une amélioration significative par rapport à Provider (cela rend InheritedWidgets sûr pour la compilation).
Mais le chemin pour y arriver m'a laissé un mauvais arrière-goût.

Pour autant que je sache, il y a essentiellement trois différences entre la version hooks et la version Property :

  • La version Hooks est beaucoup plus de code de support
  • La version Property est beaucoup plus de code passe-partout
  • La version Hooks a le problème dans les méthodes de construction où si vous appelez les hooks dans le mauvais ordre, les choses tournent mal et il n'y a vraiment aucun moyen de le voir immédiatement à partir du code.

Le code passe-partout est-il vraiment si important ? Je veux dire, vous pouvez facilement réutiliser la propriété maintenant, le code est au même endroit. Donc c'est vraiment _seulement_ un argument de verbosité maintenant.

Je pense qu'une bonne solution ne devrait pas dépendre du fait que d'autres packages le sachent. Peu importe que ce soit dans le cadre ou non. Les personnes qui ne l'utilisent pas ne devraient pas être un problème. Si les personnes qui ne l'utilisent pas est un problème, cela, à mon humble avis, est un drapeau rouge pour l'API.

Je veux dire, vous pouvez facilement réutiliser la propriété maintenant, le code est au même endroit.

Le code étant à un endroit ne signifie pas qu'il est réutilisable.
Cela vous dérangerait-il de créer un widget secondaire qui réutilise le code actuellement situé dans _ExampleState dans un autre widget ?
Avec une torsion : ce nouveau widget devrait gérer son userID en interne à l'intérieur de son état, de sorte que nous ayons :

class _WhateverState extends State with PropertyManager {
  // may change over time after some setState calls
  int userId;
}

Si les personnes qui ne l'utilisent pas est un problème, cela, à mon humble avis, est un drapeau rouge pour l'API.

Les gens qui n'utilisent pas quelque chose parce que ce n'est pas officiel ne signifie pas que l'API est mauvaise.

Il est tout à fait légitime de ne pas vouloir ajouter de dépendances supplémentaires car c'est un travail supplémentaire à maintenir (en raison du versioning, de la licence, de la dépréciation et d'autres choses).
D'après mes souvenirs, Flutter a l'obligation d'avoir le moins de dépendances possible.

Même avec le fournisseur lui-même, qui est largement accepté et presque officiel maintenant, j'ai vu des gens dire "Je préfère utiliser les InheritedWidgets intégrés pour éviter d'ajouter une dépendance".

Cela vous dérangerait-il de créer un widget secondaire qui réutilise le code actuellement situé dans _ExampleState dans un autre widget ?

Le code en question consiste à obtenir un userId à partir d'un widget et à le transmettre à une méthode fetchUser. Le code de gestion de l'ID utilisateur changeant localement dans le même objet serait différent. Cela semble être bien ? Je ne sais pas vraiment quel problème vous essayez de résoudre ici.

Pour mémoire, je n'utiliserais pas Property pour faire ce que vous décrivez, cela ressemblerait simplement à :

class Example extends StatefulWidget {
  Example({ Key key }): super(key: key);

  <strong i="10">@override</strong>
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> with PropertyManager {
  int _userId;
  Future<User> _future;

  void _setUserId(int newId) {
    if (newId == _userId)
      return;
    setState(() {
      _future = fetchUser(_userId);
    });
  }

  // ...code that uses _setUserId...

  <strong i="11">@override</strong>
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: _future.value,
      builder: (context, snapshot) {
        if (!snapshot.hasData) return Text('loading');
        return Text(snapshot.data.name);
      },
    );
  }
}

Les gens qui n'utilisent pas quelque chose parce que ce n'est pas officiel ne signifie pas que l'API est mauvaise.

D'accord.

Le fait que les gens n'utilisent pas quelque chose en soi étant mauvais est ce qui signifie que l'API est mauvaise. Lorsque vous dites "Les auteurs de packages sont moins susceptibles de publier des packages en s'appuyant sur des hooks pour résoudre des problèmes", cela indique que les hooks dépendent des autres personnes qui les utilisent pour vous être utiles. Une bonne API, à mon humble avis, ne devient pas mauvaise si personne d'autre ne l'adopte ; il devrait tenir le coup même si personne d'autre ne le sait. Par exemple, l'exemple Property ci-dessus ne dépend pas des autres packages l'utilisant pour lui-même être utile.

Même avec le fournisseur lui-même, qui est largement accepté et presque officiel maintenant, j'ai vu des gens dire "Je préfère utiliser les InheritedWidgets intégrés pour éviter d'ajouter une dépendance".

Quel est le problème avec les personnes qui préfèrent utiliser InheritedWidget ? Je ne veux pas imposer une solution aux gens. Ils devraient utiliser ce qu'ils veulent utiliser. Vous décrivez littéralement un non-problème. La solution pour les personnes préférant utiliser InheritedWidget est de se retirer et de les laisser utiliser InheritedWidget.

. Une bonne API, à mon humble avis, ne devient pas mauvaise si personne d'autre ne l'adopte ; il devrait tenir le coup même si personne d'autre ne le sait. Par exemple, l'exemple de propriété ci-dessus ne dépend pas des autres packages qui l'utilisent pour lui-même être utile.

Il y a un malentendu.

Le problème n'est pas que les gens n'utilisent pas de crochets en général.
Il s'agit de l'impossibilité pour le fournisseur d'utiliser des crochets pour résoudre les problèmes, car les crochets ne sont pas officiels alors que le fournisseur l'est.


Le code de gestion de l'ID utilisateur changeant localement dans le même objet serait différent. Cela semble être bien ? Je ne sais pas vraiment quel problème vous essayez de résoudre ici.

Pour mémoire, je n'utiliserais pas Property pour faire ce que vous décrivez, cela ressemblerait simplement à :

Cela ne répond pas à la question. J'ai demandé cela spécifiquement pour comparer la réutilisation du code entre les crochets et la propriété.

Avec des crochets, on pourrait réutiliser FetchUser :

class _WhateverState extends State with PropertyManager {
  // may change over time after some setState calls
  int userId;

  Widget build(context) {
    final user = use(FetchUser(userId));
  }
}

Avec des crochets, on pourrait réutiliser FetchUser :

Je ne comprends pas pourquoi c'est souhaitable. FetchUser n'a pas de code intéressant, c'est juste un adaptateur de Hooks vers la fonction fetchUser . Pourquoi ne pas appeler directement fetchUser ? Le code que vous réutilisez n'est pas du code intéressant.

Il s'agit de l'impossibilité pour le fournisseur d'utiliser des crochets pour résoudre les problèmes, car les crochets ne sont pas officiels alors que le fournisseur l'est.

À mon humble avis, une bonne solution au problème de réutilisation du code n'aurait pas du tout besoin d'être adoptée par le fournisseur. Ce seraient des concepts entièrement orthogonaux. C'est quelque chose dont parle le guide de style Flutter sous la rubrique "éviter de compliquer".

Je ne comprends pas pourquoi c'est souhaitable. FetchUser n'a pas de code intéressant, c'est juste un adaptateur de Hooks vers la fonction fetchUser. Pourquoi ne pas simplement appeler fetchUser directement ? Le code que vous réutilisez n'est pas du code intéressant.

Ce n'est pas grave. Nous essayons de démontrer la réutilisabilité du code. fetchUser peut être n'importe quoi, y compris ChangeNotifier.addListener par exemple.

Nous pourrions avoir une implémentation alternative qui ne dépend pas de fetchUser , et simplement fournir une API pour faire une récupération de données implicite :

int userId;

Widget build(context) {
  AsyncSnapshot<User> user = use(ImplicitFetcher<User>(userId, fetch: () => fetchUser(userId));
}

À mon humble avis, une bonne solution au problème de réutilisation du code n'aurait pas du tout besoin d'être adoptée par le fournisseur. Ce seraient des concepts entièrement orthogonaux. C'est quelque chose dont parle le guide de style Flutter sous la rubrique "éviter de compliquer".

C'est pourquoi j'ai mentionné que les crochets sont une primitive

Comme métaphore :
package:animations dépend de Animation . Mais ce n'est pas un problème, car il s'agit d'un noyau primitif.
Ce serait une autre affaire si à la place package:animations utilisait un fork de Animation maintenu par la communauté

@escamoteur Pour que je comprenne bien, proposez-vous de changer fondamentalement le fonctionnement des widgets ? Ou suggérez-vous qu'il devrait y avoir de nouvelles capacités spécifiques ? Étant donné que des choses comme les crochets et la proposition de propriété ci-dessus sont possibles sans aucune modification du cadre de base, je ne sais pas ce que vous voudriez réellement changer.

@Hixie non, ce que je
Je partage beaucoup vos inquiétudes mais d'un autre côté un Widget avec des crochets a l'air vraiment élégant.
Cela n'interdirait pas de faire les choses comme avant.

Cela n'interdirait pas de faire les choses comme avant.

Je pense que ce sera le cas, je ne pense pas que ce sera une bonne idée pour l'équipe Flutter de dire "hey, nous recommandons maintenant les crochets flutter mais vous pouvez toujours faire les choses comme avant", les gens seront confus à ce sujet. De plus, si l'équipe Flutter recommande des hooks à l'avenir, elle devra également arrêter de publier le code flutter réel à titre d'exemple.

Les gens suivent toujours la "manière officielle" de faire les choses et je pense qu'il ne devrait pas y avoir deux manières officielles d'utiliser Flutter.

Ce n'est pas grave. Nous essayons de démontrer la réutilisabilité du code. fetchUser peut être n'importe quoi, y compris ChangeNotifier.addListener par exemple.

Sûr. C'est à ça que servent les fonctions : faire abstraction du code. Mais nous avons déjà des fonctions. Le code Property ci-dessus et le code _setUserId ci-dessus montrent que vous pouvez rassembler tout le code qui appelle ces fonctions au même endroit sans avoir besoin d'une aide particulière de la part du framework. Pourquoi avons-nous besoin de Hooks pour envelopper les appels à ces fonctions ?

À mon humble avis, une bonne solution au problème de réutilisation du code n'aurait pas du tout besoin d'être adoptée par le fournisseur. Ce seraient des concepts entièrement orthogonaux. C'est quelque chose dont parle le guide de style Flutter sous la rubrique "éviter de compliquer".

C'est pourquoi j'ai mentionné que les crochets sont une primitive

Ils sont une commodité, ils ne sont pas un primitif. S'il s'agissait d'un primitif, il serait beaucoup plus facile de répondre à la question « quel est le problème ». Vous diriez "voici une chose que je veux faire et je ne peux pas le faire".

Comme métaphore :
package:animations dépend de Animation . Mais ce n'est pas un problème, car il s'agit d'un noyau primitif.
Ce serait une autre affaire si à la place package:animations utilisait un fork de Animation maintenu par la communauté

La hiérarchie des classes Animation fait quelque chose de fondamental : elle introduit des tickers et un moyen de les contrôler et de s'y abonner. Sans la hiérarchie des classes Animation, vous devez inventer quelque chose comme la hiérarchie des classes Animation pour faire des animations. (Idéalement quelque chose de mieux. Ce n'est pas notre meilleur travail.) Hooks n'introduit pas de nouvelle fonctionnalité fondamentale. Il fournit simplement un moyen d'écrire le même code différemment. Il se peut que ce code soit plus simple ou factorisé différemment qu'il ne le serait autrement, mais ce n'est pas une primitive. Vous n'avez pas besoin d'un framework de type Hooks pour écrire du code qui fait la même chose que le code utilisant des Hooks.


Fondamentalement, je ne pense pas que le problème décrit dans ce problème soit quelque chose que le framework doit résoudre. Différentes personnes auront des besoins très différents sur la façon d'y faire face. Il existe de nombreuses façons de le corriger, nous en avons déjà discuté plusieurs dans ce bogue ; certains des moyens sont très simples et peuvent être écrits en quelques minutes, ce n'est donc pas un problème si difficile à résoudre qu'il nous apporte de la valeur pour posséder et maintenir la solution. Chacune des propositions a des forces et des faiblesses ; les faiblesses sont dans chaque cas des choses qui empêcheraient quelqu'un de les utiliser. Il n'est même pas vraiment clair que tout le monde soit d'accord pour dire que le problème doit être résolu.

Les crochets _sont_ des primitives
Voici un fil de Dan : https://twitter.com/dan_abramov/status/1093698629708251136 expliquant cela. Certaines formulations diffèrent, mais la logique s'applique principalement à Flutter en raison de la similitude entre les composants de la classe React et les Flutter StatefulWidgets

Plus précisément, vous pouvez considérer les flutter_hooks comme des mixins d'état dynamiques.

S'il s'agissait d'un primitif, il serait beaucoup plus facile de répondre à la question « quel est le problème ». Vous diriez "voici une chose que je veux faire et je ne peux pas le faire".

C'est dans l'OP :

Il est difficile de réutiliser la logique d'état. Soit nous nous retrouvons avec une méthode de construction complexe et profondément imbriquée, soit nous devons copier-coller la logique sur plusieurs widgets.
Il n'est ni possible de réutiliser une telle logique à travers des mixins ni des fonctions.

Il se peut que ce code soit plus simple ou factorisé différemment qu'il ne le serait autrement, mais ce n'est pas une primitive. Vous n'avez pas besoin d'un framework de type Hooks pour écrire du code qui fait la même chose que le code utilisant des Hooks.

Vous n'avez pas besoin de classes pour écrire un programme. Mais les classes vous permettent de structurer votre code et de le factoriser de manière significative.
Et les classes sont des primitives.

Même chose avec les mixins, qui sont aussi des primitifs

Les crochets, c'est la même chose.

Pourquoi avons-nous besoin de Hooks pour envelopper les appels à ces fonctions ?

Car quand nous devons appeler cette logique non pas à _un_ endroit mais à _deux_ endroits.

Il n'est ni possible de réutiliser une telle logique à travers des mixins ni des fonctions.

Merci de me donner un exemple concret où c'est le cas. Jusqu'à présent, tous les exemples que nous avons étudiés ont été simples sans crochets.

Jusqu'à présent dans ce fil, je n'ai vu aucune autre solution que les crochets @rrousselGit qui résolvent et facilitent la réutilisation et la composition de la logique d'état.

Certes, je n'ai pas fait beaucoup de fléchettes et de flottement ces derniers temps, il se peut donc que je manque des éléments dans les exemples de code de solution de propriété ci-dessus, mais existe-t-il des solutions? Quelles sont aujourd'hui les options qui ne nécessitent pas de copier-coller au lieu d'être réutilisées ?
Quelles sont les réponses à la question @rrousselGit :

Cela vous dérangerait-il de créer un widget secondaire qui réutilise le code actuellement situé dans _ExampleState dans un autre widget ?
Avec une torsion : ce nouveau widget devrait gérer son userID en interne à l'intérieur de son état

S'il n'est pas possible de réutiliser une logique d'état aussi simple avec la solution de propriété ci-dessus, quelles sont les autres options ?
La réponse est-elle simplement qu'il ne devrait pas être facilement réutilisable en flutter ? Ce qui est tout à fait bien mais un peu triste à mon humble avis.

BTW, SwiftUI le fait-il d'une manière nouvelle/autre inspirante ? Ou leur manque-t-il également la même réutilisabilité de la logique d'état ? Je n'ai pas du tout utilisé swiftui moi-même. C'est peut-être trop différent ?

Tous les constructeurs, en gros. Les constructeurs sont le seul moyen de réutiliser l'état pour le moment.
Les crochets rendent les constructeurs plus lisibles et plus faciles à créer


Voici une collection de crochets personnalisés que moi ou certains clients ont réalisés le mois dernier pour différents projets :

  • useQuery – qui est l'équivalent du crochet ImplicitFetcher j'ai donné précédemment mais qui fait une requête GraphQL à la place.
  • useOnResume qui donne un rappel pour effectuer une action personnalisée sur AppLifecycleState.resumed sans avoir à
    se donner la peine de faire un WidgetsBindingObserver
  • useDebouncedListener qui écoute un objet écoutable (généralement TextField ou ScrollController), mais avec un anti-rebond sur l'auditeur
  • useAppLinkService qui permet aux widgets d'exécuter une certaine logique sur un événement personnalisé similaire à AppLifecycleState.resumed mais avec des règles métier
  • useShrinkUIForKeyboard pour une gestion fluide de l'apparence du clavier. Il renvoie un booléen qui indique si l'interface utilisateur doit s'adapter ou non au remplissage inférieur (qui est basé sur l'écoute d'un focusNode)
  • useFilter , qui combine useDebouncedListener et useState (un hook primitif qui déclare une seule propriété) pour exposer un filtre pour une barre de recherche.
  • useImplicitlyAnimated<Int/Double/Color/...> – équivalent à TweenAnimationBuilder comme crochet

Les applications utilisent également de nombreux crochets de bas niveau pour différentes logiques.

Par exemple, au lieu de :

Whatever whatever;

initState() {
  whatever = doSomething(widget.id);
}

didUpdateWidget(oldWidget) {
  if (oldWidget.id != widget.id)
    whatever = doSomething(widget.id);
}

Ils font:

Widget build(context) {
  final whatever = useUnaryEvent<Whatever, int>(widget.id, (int id) => doSomething(id));
}

Cela évite les doublons entre initState / didUpdateWidget / didChangeDependencies .

Ils utilisent également beaucoup de useProvider , de Riverpod qui devrait autrement être un StreamBuilder / ValueListenableBuilder


La partie importante est que les widgets utilisent rarement "un seul crochet".
Par exemple, un widget peut faire

class ChatScreen extends HookWidget {
  const ChatScreen({Key key}) : super(key: key);

  <strong i="13">@override</strong>
  Widget build(BuildContext context) {
    final filter = useFilter(debounceDuration: const Duration(seconds: 2));
    final userId = useProvider(authProvider).userId;
    final chatId = useProvider(selectedChatProvider);
    final chat = useQuery(ChatQuery(userId: userId, chatId: chatId, filter: filter.value));

    return Column(
      children: [
        Searchbar(onChanged: (value) => filter.value = value),
        Expanded(
          child: ChatList(chat: chat),
        ),
      ],
    );
  }
}

C'est concis et très lisible (en supposant que vous ayez une connaissance de base de l'API bien sûr).
Toute la logique peut être lue de haut en bas - il n'y a pas de saut entre les méthodes pour comprendre le code.
Et tous les hooks utilisés ici sont réutilisés à plusieurs endroits dans la base de code

Si nous devions faire exactement la même chose sans crochets, nous aurions :

class ChatScreen extends StatefulWidget {
  const ChatScreen({Key key}) : super(key: key);

  <strong i="20">@override</strong>
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  String filter;
  Timer timer;

  <strong i="21">@override</strong>
  void dispose() {
    timer?.cancel();
    super.dispose();
  }

  <strong i="22">@override</strong>
  Widget build(BuildContext context) {
    return Consumer<Auth>(
      provider: authProvider,
      builder: (context, auth, child) {
        return Consumer<int>(
          provider: selectedChatProvider,
          builder: (context, chatId, child) {
            return GraphQLBuilder<Chat>(
              query: ChatQuery(
                userId: auth.userId,
                chatId: chatId,
                filter: filter.value,
              ),
              builder: (context, chat, child) {
                return Column(
                  children: [
                    Searchbar(
                      onChanged: (value) {
                        timer?.cancel();
                        timer = Timer(const Duration(seconds: 2), () {
                          filter = value;
                        });
                      },
                    ),
                    Expanded(
                      child: ChatList(chat: chat),
                    ),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }
}

C'est nettement moins lisible.

  • Nous avons 10 niveaux d'indentation – 12 si nous faisons un FilterBuilder pour réutiliser la logique du filtre
  • La logique de filtrage n'est pas réutilisable telle quelle.

    • nous pouvons oublier d'annuler le timer par erreur

  • la moitié de la méthode build n'est pas utile pour le lecteur. The Builders nous distrait de ce qui compte
  • J'ai perdu 5 bonnes minutes à essayer de comprendre pourquoi le code ne se compile pas à cause d'une parenthèse manquante

En tant qu'utilisateur de flutter_hooks moi-même, je donnerai mon opinion. Avant d'utiliser des crochets, j'étais satisfait de Flutter. Je ne voyais pas la nécessité de quelque chose comme ça. Après avoir lu à ce sujet et regardé une vidéo youtube à ce sujet, je n'étais toujours pas convaincu, ça avait l'air cool, mais j'avais besoin de pratique ou d'exemples pour vraiment le motiver. Mais ensuite j'ai remarqué quelque chose. J'évitais à tout prix les widgets avec état, il y avait juste beaucoup de passe-partout impliqué et je sautais dans la classe pour essayer de trouver des choses. À cause de cela, j'avais déplacé la plupart de mon état éphémère dans une solution de gestion d'état avec le reste de l'état de l'application, et j'avais simplement utilisé des widgets sans état. Cependant, cela oblige la logique métier à dépendre rapidement de Flutter en raison de la dépendance à l'obtention du Navigator , ou du BuildContext pour accéder à InheritedWidget s / Providers plus haut dans l'arbre. Je ne dis pas que c'était une bonne approche de gestion de l'État, je sais que ce n'était pas le cas. Mais j'ai fait tout ce que je pouvais pour ne pas avoir à me soucier de la gestion de l'état dans l'interface utilisateur.

Après avoir utilisé des hooks pendant un petit moment, je me suis retrouvé beaucoup plus productif, beaucoup plus heureux d'utiliser Flutter, mettant l'état éphémère au bon endroit (avec l'interface utilisateur) plutôt qu'avec l'état de l'application.

Pour moi, c'est comme un ramasse-miettes pour les états/contrôleurs éphémères. Je n'ai pas à me souvenir de disposer de tous les abonnements dans l'interface utilisateur, même si je suis toujours très conscient du fait que c'est ce que flutter_hooks fait pour moi. Cela rend également beaucoup plus facile la maintenance et la refactorisation de mon code. Parlant de l'écriture d'environ 10 applications au cours de la dernière année pour mes recherches universitaires et mon plaisir.

Comme d'autres, je ne sais pas exactement quelle devrait être la principale motivation pour l'inclure dans le SDK Flutter lui-même. Cependant, voici deux réflexions à ce sujet.

  1. De temps en temps, je créerai un crochet pour faciliter l'utilisation d'un package contenant des contrôleurs qui doivent être initialisés / supprimés. (Par exemple golden_layout , ou zefyr ). Je pense que les autres utilisateurs utilisant flutter_hooks bénéficieraient d'un tel package. Cependant, je n'arrive pas à justifier la publication d'un package contenant littéralement 1 à 3 fonctions. L'alternative serait de créer un package évier de cuisine qui contient beaucoup de crochets pour les différents packages que j'utilise, je peux alors simplement utiliser une dépendance git, mais toute personne utilisant ces autres packages + flutter_hooks devrait dépendre de mon git dans afin d'en bénéficier (qui est moins détectable et contient probablement des dépendances sur des packages dont ils ne se soucient pas), ou sur un package qui contient 3 fonctions ou je publie un package garden-sink sur pub.dev. Toutes les idées semblent ridicules, et pas très découvrables. Les autres utilisateurs de flutter_hooks pourraient facilement copier et coller ces fonctions dans leur code ou essayer de comprendre la logique eux-mêmes, mais cela manque totalement l'intérêt de partager du code / des packages. Les fonctions seraient bien mieux placées dans les packages d'origine, et non dans un "package d'extension". Si flutter_hooks faisait partie du framework, ou même juste un package utilisé ou exporté depuis le framework comme characters , alors les auteurs du package d'origine accepteraient beaucoup plus probablement une pull request pour un hook simple fonctions, et nous n'aurons pas un gâchis de 1 à 3 packages de fonctions.
    Si flutter_hooks n'est pas adopté par Flutter, je prévois un tas de 1 à 3 packages de fonctions encombrant les résultats de la recherche pub.dev. Le fait que ces packages soient vraiment petits me rend vraiment d'accord avec @rrousselGit qu'il s'agit d'une primitive. Si les 1228 étoiles sur le référentiel flutter_hooks n'indiquent pas que cela résout les problèmes mentionnés par @rrousselGit, je ne sais pas ce que c'est.

  2. Je regardais une vidéo youtube sur la contribution au dépôt Flutter depuis que je voulais voir ce que je pouvais faire pour aider. Pendant que je regardais, la personne qui a créé la vidéo a ajouté assez facilement dans la nouvelle propriété mais a presque oublié de s'occuper de la mise dispose jour de didUpdateWidget et debugFillProperties . Voir à nouveau toutes les complexités d'un widget avec état et à quel point il est facile de rater quelque chose m'a redonné confiance en eux et m'a rendu moins enthousiaste à l'idée de contribuer au référentiel principal de Flutter. Sans dire que cela m'a complètement dissuadé, je suis toujours intéressé à contribuer, mais j'ai l'impression que je créerais un code passe-partout difficile à maintenir et à réviser. Il ne s'agit pas de la complexité d'écrire le code, mais de la complexité de lire le code et de vérifier que vous avez correctement éliminé et pris en charge l'état éphémère.

Désolé pour la longue réponse, cependant, j'ai examiné ce problème de temps en temps et je suis quelque peu déconcerté par la réponse de l'équipe Flutter. Il semble que vous n'ayez pas pris le temps d'essayer une application dans les deux sens et de voir la différence par vous-même. Je comprends la volonté de ne pas entretenir une dépendance supplémentaire ou de trop l'intégrer dans le framework. Cependant, la partie centrale du framework flutter_hook n'est constituée que de 500 lignes de code assez bien documentées. Encore une fois, désolé si cela est tangent à la conversation et j'espère que je n'offense personne pour avoir donné mes 2 cents et avoir parlé. Je n'ai pas parlé plus tôt parce que je sentais que @rrousselGit faisait de très bons points et était clair.

Désolé pour la longue réponse, cependant, j'ai examiné ce problème de temps en temps et je suis quelque peu déconcerté par la réponse de l'équipe Flutter. Il semble que vous n'ayez pas pris le temps d'essayer une application dans les deux sens et de voir la différence par vous-même.

Pour être juste, il s'agit d'un fil incroyablement long et le fondateur du framework a activement contribué plusieurs fois par jour, avec plusieurs solutions, a demandé des commentaires à leur sujet et s'est engagé avec eux ainsi qu'a travaillé pour comprendre ce qui est demandé. Honnêtement, j'ai du mal à penser à un exemple plus clair d'un mainteneur utile.

J'aurais aimé que ce soit un peu plus de patience avec ce problème - je ne comprends plus les crochets après avoir lu ce fil, à part qu'ils sont une autre façon de lier la durée de vie des articles jetables à un État. Je ne préfère pas cette approche du point de vue stylistique, et je pense qu'il y a quelque chose de fondamentalement défectueux si la position est « prenez simplement le temps d'écrire une toute nouvelle application dans le paradigme, alors vous comprendrez pourquoi il doit être inséré dans le cadre ! ' - comme l'ingénieur React l'a noté dans ce fil, ce ne serait vraiment pas conseillé pour Flutter, et les avantages décrits dans ce fil sont faibles par rapport au coût du type de recâblage, ce qui signifie que vous avez besoin d'une toute nouvelle base de code pour voir les avantages.

Honnêtement, j'ai du mal à penser à un exemple plus clair d'un mainteneur utile.

D'accord. Je remercie Hixie d'avoir pris le temps de participer à cette discussion.

je ne comprends plus les crochets après avoir lu ce fil

Pour être juste, ce problème essaie explicitement d'éviter de parler spécifiquement des crochets.
Il s'agit plus d'essayer d'expliquer le problème que la solution

Avez-vous l'impression qu'il ne parvient pas à le faire?

Je peux sentir les deux côtés ( @rrousselGit et @Hixie) ici et je voulais laisser des commentaires d'un (mon) côté / perspective d'utilisation du framework Flutter.

L'approche flutter_hooks réduit considérablement le passe-partout (juste à partir des exemples montrés ici car nous pouvons réutiliser de telles configurations d'état) et réduit la complexité en n'ayant pas à penser activement à l'initialisation / la suppression des ressources. De manière générale, il améliore et soutient bien le flux / la vitesse de développement... même s'il ne s'intègre pas aussi bien dans le "noyau" de Flutter lui-même (subjectivement).

Comme au moins > 95% du code que j'écris, la méthode de construction n'est que déclarative, pas de variables locales ni d'appels en dehors du sous-arbre de widgets renvoyé, toute la partie logique est à l'intérieur de ces fonctions d'état pour initialiser, affecter et disposer des ressources et ajouter auditeurs (dans mon cas, les réactions MobX) et ce genre de choses logiques. Comme c'est aussi l'approche pour la plupart dans Flutter lui-même, cela semble très natif. Cela vous donne également au développeur la possibilité d'être toujours explicite et ouvert sur ce que vous faites - cela m'oblige à toujours convertir de tels widgets en StatefulWidget et à écrire un code similaire dans initState / dispose, mais il aboutit toujours à écrire exactement ce que vous avez l'intention de faire directement dans le Widget utilisé. Pour moi personnellement, comme @Hixie l'a déjà mentionné, cela ne me dérange en aucune façon d'écrire ce genre de code passe-partout et me permet en tant que développeur de décider comment le gérer correctement au lieu de compter sur quelque chose comme flutter_hooks de le faire pour moi et de ne pas comprendre pourquoi quelque chose pourrait se comporter comme il le fait. L'extraction de widgets par petits morceaux garantit également que ce type de passe-partout est parfaitement adapté au cas d'utilisation pour lequel il est utilisé. Avec flutter_hooks j'aurais encore besoin de réfléchir au type d'états qui valent la peine d'être écrits pour être un crochet et donc réutilisés - différentes saveurs pourraient entraîner divers crochets à usage "unique" ou aucun crochet du tout puisque je pourrais ne pas réutiliser les configurations trop souvent mais a tendance à en écrire des plus personnalisées.

Ne vous méprenez pas, l'approche de ces crochets semble très agréable et utile, mais pour moi, cela ressemble à un concept très fondamental qui change le concept de base de la façon de gérer cela. C'est très bien en tant que package lui-même de donner aux développeurs la possibilité d'utiliser ce type d'approche s'ils ne sont pas satisfaits de la façon de le faire "nativement", mais l'intégrer au framework Flutter lui-même serait, au moins, propre / unifié, entraîne soit la réécriture de grandes parties de Flutter pour utiliser ce concept (beaucoup de travail), soit l'utilisation pour des éléments futurs / sélectionnés (ce qui peut être déroutant d'avoir des approches aussi mixtes).

S'il était intégré dans le framework Flutter lui-même et pris en charge / activement utilisé, je sauterais évidemment dedans. Puisque je comprends et même aime l'approche actuelle et vois les actions (possibles) nécessaires pour l'implémenter nativement, je peux comprendre l'hésitation et/ou pourquoi cela ne devrait pas être fait et plutôt le garder comme un package.

Corrigez-moi si je me trompe, mais ce fil traite des problèmes de réutilisation de la logique d'état dans plusieurs widgets de manière lisible et composable. Pas de crochets spécifiquement. Je crois que ce fil a été ouvert en raison du souhait d'avoir une discussion autour du problème avec une approche ouverte de ce que devrait être la solution.

Les crochets sont cependant mentionnés car ils sont une solution et je pense que @rrousselGit les a utilisés ici pour essayer d'expliquer le problème/problème qu'ils résolvent (puisqu'ils sont une solution) afin qu'une autre solution peut-être plus native pour flutter puisse être trouvée avec et présenté. Jusqu'à présent, à ma connaissance, il n'y a pas eu d'autres solutions présentées dans ce fil bien que cela résolve les problèmes de réutilisabilité?

Cela dit, je ne sais pas où va le fil pour le moment.
Je pense que le problème existe vraiment. Ou en débattons-nous ?
Si nous sommes tous d'accord sur le fait qu'il est difficile de réutiliser la logique d'état de manière composable dans plusieurs widgets avec le noyau de flutter aujourd'hui, quelles solutions existe-t-il qui pourraient être une solution de base ? puisque les constructeurs sont vraiment (pour citer)

nettement moins lisible

La solution de propriété ne semble pas être si facilement réutilisable ou est-ce une mauvaise conclusion que j'ai tirée (?)

créer un widget secondaire qui réutilise le code actuellement situé dans _ExampleState dans un autre widget ?
Avec une torsion : ce nouveau widget devrait gérer son userID en interne à l'intérieur de son état

Je serais prêt à aider avec un document de conception comme @timsneath l'a suggéré. Je pense que c'est probablement un meilleur format pour expliquer le problème avec quelques exemples d'études de cas, ainsi que mentionner les différentes solutions et explorer si nous pouvons trouver une solution qui correspond au flutter et où elle en est. Je suis d'accord que la discussion dans la question se perd un peu.

Je suis assez sceptique quant à l'idée de faire un document de conception pour le moment.
Il est évident que pour l'instant, @Hixie est contre la résolution directe de ce problème dans Flutter.

Pour moi, il semble que nous ne soyons pas d'accord sur l'importance du problème et le rôle de Google dans la résolution de ce problème.
Si les deux parties ne sont pas d'accord sur ce point, je ne vois pas comment nous pouvons avoir une discussion productive sur la façon de résoudre ce problème – quelle que soit la solution.

Ce fil de discussion a été une lecture très intéressante et je suis heureux de voir que l'échange de points de vue est resté civil. Je suis cependant un peu surpris de l'impasse actuelle.

En ce qui concerne les crochets, mon point de vue est que si Flutter n'a pas nécessairement besoin de la solution de crochets spécifique présentée par @rrousselGit , il ne le dit pas non plus. Flutter a besoin d'une solution qui offre des avantages similaires à ceux des crochets, pour toutes les raisons mentionnées par Remi et d'autres partisans. @emanuel-lundman a résumé les arguments bien ci-dessus et je suis d'accord avec son point de vue.

En l'absence de toute autre proposition viable offrant les mêmes capacités et étant donné que les crochets ont fait leurs preuves dans React, et qu'il existe une solution existante sur laquelle il pourrait être basé pour Flutter, je ne pense pas que ce serait un mauvais choix de le faire. Je ne pense pas que le concept de crochets, en tant que primitive qui est également inclus dans le SDK Flutter (ou même inférieur), enlèverait quoi que ce soit à Flutter. À mon avis, cela ne ferait que l'enrichir et faciliter encore plus le développement d'applications faciles à maintenir et agréables avec Flutter.

Bien que l'argument selon lequel les crochets soient disponibles sous forme de package pour ceux qui souhaitent en tirer parti, soit un point valable, je pense qu'il n'est pas optimal pour une primitive comme les crochets. Voici pourquoi.

Très souvent, même lors de la création de packages réutilisables en interne, nous débattons si le package doit être «pur», dans le sens où il ne peut dépendre que du SDK Dart+Flutter, ou si nous autorisons d'autres packages et, dans l'affirmative, lesquels ceux. Même le fournisseur est absent pour les packages «purs», mais souvent autorisé pour les packages de niveau supérieur. Pour une application, il y a toujours le même débat, quels packages sont OK et lesquels ne le sont pas. Le fournisseur est vert, mais quelque chose comme Hooks est toujours un point d'interrogation en tant que package.

Si une solution de type crochet faisait partie du SDK, ce serait un choix évident d'utiliser les capacités qu'il offre. Bien que je veuille utiliser Hooks et l'autoriser déjà en tant que package, je suis également préoccupé par le fait qu'il crée un style de code Flutter et introduit des concepts qui pourraient ne pas être familiers aux développeurs Flutter qui ne l'utilisent pas. Cela ressemble un peu à une fourche sur la route si nous empruntons cette voie sans support dans le SDK. Pour les petits projets personnels, c'est un choix facile d'utiliser des crochets. Je recommande de l'essayer avec Riverpod.

(Notre conservatisme de paquet vient, je suppose, d'avoir été brûlé par des paquets et un désordre de dépendance sur d'autres gestionnaires de paquets dans le passé, probablement pas unique.)

Je ne dis pas que les crochets seraient le seul moyen de résoudre le problème actuel, même si c'est la seule solution démontrée qui fonctionne jusqu'à présent. Il pourrait certainement être intéressant et une approche valable pour étudier les options à un niveau plus générique avant de s'engager dans une solution. Pour que cela se produise, il faut reconnaître que Flutter SDK _présente actuellement une faille en ce qui concerne la logique d'état facilement réutilisable_, ce qui, malgré des explications élaborées, ne semble actuellement pas être.

Pour moi, il y a deux raisons principales de ne pas simplement mettre les Hooks dans le framework de base. La première est que l'API contient des pièges dangereux. Principalement, si vous finissez par appeler les crochets dans le mauvais ordre, les choses vont se casser. Cela me semble être un problème fatal. Je comprends qu'avec de la discipline et en suivant la documentation, vous pouvez l'éviter, mais à mon humble avis, une bonne solution à ce problème de réutilisation de code n'aurait pas ce défaut.

La seconde est qu'il ne devrait vraiment y avoir aucune raison pour que les gens ne puissent pas simplement utiliser des Hooks (ou une autre bibliothèque) pour résoudre ce problème. Maintenant, avec les Hooks en particulier, cela ne fonctionne pas, comme les gens l'ont dit, parce que l'écriture du hook est suffisamment lourde pour que les gens souhaitent que des bibliothèques non liées prennent en charge les hooks. Mais je pense qu'une bonne solution à ce problème n'aurait pas besoin de cela. Une bonne solution serait autonome et n'aurait pas besoin que toutes les autres bibliothèques soient au courant.

Nous avons récemment ajouté RestorableProperties au framework. Il serait intéressant de voir s'ils pourraient être exploités ici d'une manière ou d'une autre...

J'accepte @Hixie sur l'API ayant des problèmes cachés qui nécessitent un analyseur ou un linter pour résoudre. Je pense que nous, comme quiconque veut participer, devrions examiner diverses solutions, peut-être via la suggestion du document de conception, ou autrement, sur le problème de la gestion du cycle de vie réutilisable. Idéalement, il serait plus spécifique à Flutter et tirerait parti des API Flutter tout en résolvant les problèmes de l'API hook. Je pense que la version Vue est un bon modèle pour commencer, comme je l'ai mentionné précédemment, car elle ne repose pas sur l'ordre de raccrochage. Est-ce que quelqu'un d'autre est intéressé à enquêter avec moi?

@Hixie mais vous êtes d'accord avec le problème, alors qu'il n'y a pas de bon moyen de réutiliser la logique d'état de manière composable entre les widgets ? C'est pourquoi vous avez commencé à penser à tirer parti de ResuableProperties d'une manière ou d'une autre ?

Pour moi, il y a deux raisons principales de ne pas simplement mettre les Hooks dans le framework de base. La première est que l'API contient des pièges dangereux. Principalement, si vous finissez par appeler les crochets dans le mauvais ordre, les choses vont se casser. Cela me semble être un problème fatal. Je comprends qu'avec de la discipline et en suivant la documentation, vous pouvez l'éviter, mais à mon humble avis, une bonne solution à ce problème de réutilisation de code n'aurait pas ce défaut.

Après avoir travaillé avec des crochets et travaillé avec d'autres personnes qui utilisent des crochets, ce n'est vraiment pas un si gros problème à mon humble avis. Et pas du tout par rapport à tous les gros gains (gros gains en vitesse de développement, réutilisabilité, composabilité et code facilement lisible) qu'ils apportent.
Un hook est un hook, comme une classe est une classe, pas seulement une fonction, et vous ne pouvez pas l'utiliser de manière conditionnelle. Vous apprenez aussi vite. Et votre éditeur peut également vous aider à résoudre ce problème.

La seconde est qu'il ne devrait vraiment y avoir aucune raison pour que les gens ne puissent pas simplement utiliser des Hooks (ou une autre bibliothèque) pour résoudre ce problème. Maintenant, avec les Hooks en particulier, cela ne fonctionne pas, comme les gens l'ont dit, parce que l'écriture du hook est suffisamment lourde pour que les gens souhaitent que des bibliothèques non liées prennent en charge les hooks. Mais je pense qu'une bonne solution à ce problème n'aurait pas besoin de cela. Une bonne solution serait autonome et n'aurait pas besoin que toutes les autres bibliothèques soient au courant.

Les crochets d'écriture ne sont pas encombrants.
C'est toujours plus facile que les solutions disponibles maintenant à mon humble avis (pour reprendre cette expression 😉).
Peut-être que j'interprète mal ce que vous écrivez. Mais je pense que personne n'a dit ça ?
Je le lis comme si les gens appréciaient vraiment tous les gains apportés par la solution du crochet et souhaitaient pouvoir l'utiliser partout. Pour récolter tous les bénéfices. Étant donné qu'un crochet est réutilisable, ce serait formidable si les développeurs tiers pouvaient se sentir en confiance pour coder et expédier leurs propres crochets sans obliger tout le monde à écrire leurs propres wrappers. Profitez de la réutilisabilité de la logique d'état.
Je pense que @rrousselGit et @gaearon ont déjà expliqué la chose primitive. Donc je n'entrerai pas là-dedans.
Peut-être que je ne comprends pas cette déclaration parce que je ne vois pas que c'est un bon résumé de ce que les gens ont écrit dans ce fil. Je suis désolé.

J'espère qu'il y a une voie à suivre. Mais je pense qu'il est temps d'au moins convenir qu'il s'agit d'un problème et d'aller de l'avant avec des solutions alternatives qui sont meilleures, car les crochets ne semblent même pas être sur la table.
Ou décidez simplement de ne pas résoudre le problème dans Flutter Core.

Qui décide de la marche à suivre ?
Quelle est la prochaine étape ?

Cela me semble être un problème fatal. Je comprends qu'avec de la discipline et en suivant la documentation, vous pouvez l'éviter, mais à mon humble avis, une bonne solution à ce problème de réutilisation de code n'aurait pas ce défaut.

Dans React, nous résolvons ce problème avec un linter - une analyse statique. D'après notre expérience, cette faille n'a pas été importante, même dans une grande base de code. Il y a d'autres problèmes que nous pourrions considérer comme des défauts, mais je voulais juste souligner que même si le recours à un ordre d'appel persistant est ce que les gens pensent intuitivement être un problème, l'équilibre finit par être assez différent dans la pratique.

La vraie raison pour laquelle j'écris ce commentaire est que Flutter utilise un langage compilé. "Linting" n'est pas facultatif. Ainsi, s'il y a un alignement entre le langage hôte et le framework d'interface utilisateur, il est certainement possible d'appliquer ce problème "conditionnel" qui ne se pose jamais de manière statique. Mais cela ne fonctionne que lorsque le cadre de l'interface utilisateur peut motiver des changements de langue (par exemple, Compose + Kotlin).

@Hixie mais vous êtes d'accord avec le problème, alors qu'il n'y a pas de bon moyen de réutiliser la logique d'état de manière composable entre les widgets ? C'est pourquoi vous avez commencé à penser à tirer parti de ResuableProperties d'une manière ou d'une autre ?

C'est certainement quelque chose que les gens ont soulevé. Ce n'est pas quelque chose avec lequel j'ai une expérience viscérale. Ce n'est pas quelque chose que j'ai ressenti comme un problème lors de l'écriture de mes propres applications avec Flutter. Cela ne veut pas dire que ce n'est pas un vrai problème pour certaines personnes, cependant.

Étant donné qu'un crochet est réutilisable, ce serait formidable si les développeurs tiers pouvaient se sentir en confiance pour coder et expédier leurs propres crochets sans obliger tout le monde à écrire leurs propres wrappers

Mon point est qu'une bonne solution ici ne nécessiterait pas que quiconque écrive des wrappers.

Quelle est la prochaine étape ?

Il y a plusieurs étapes suivantes, par exemple :

  • S'il y a des problèmes spécifiques avec Flutter dont nous n'avons pas parlé ici, classez les problèmes et décrivez les problèmes.
  • Si vous avez une bonne idée de la façon de résoudre le problème de ce problème de manière meilleure que les Hooks, créez un package qui le fait.
  • S'il y a des choses qui peuvent être faites pour améliorer les Hooks, faites-le.
  • S'il y a des problèmes avec Flutter qui empêchent les Hooks d'atteindre leur plein potentiel, classez-les en tant que nouveaux problèmes.
    etc.

Ce fil de discussion a été une lecture très intéressante et je suis heureux de voir que l'échange de points de vue est resté civil.

Je détesterais voir à quoi ressemble un fil incivil alors. Il y a tellement peu d'empathie dans ce fil qu'il a été difficile à lire et à suivre en marge

Mon point est qu'une bonne solution ici ne nécessiterait pas que quiconque écrive des wrappers.

Vous n'êtes pas obligé d'écrire des wrappers cependant. Mais vous voudrez peut-être profiter des avantages et de la réutilisabilité de votre propre code auquel vous vous êtes habitué. Vous pouvez certainement toujours utiliser les bibliothèques telles quelles. Si vous écrivez un truc d'emballage de crochet (si possible), ce n'est probablement pas parce que vous pensez que c'est un fardeau mais que c'est mieux que l'alternative.

C'est en fait une bonne raison et une raison mentionnée pour laquelle une solution au problème dans ce fil serait excellente dans le noyau. Une solution de logique d'état composable réutilisable dans le noyau signifierait que les gens n'auraient pas à écrire des wrappers puisqu'une telle logique réutilisable pourrait être expédiée en toute sécurité dans tous les packages sans ajouter de dépendances.

Une solution de logique d'état composable réutilisable dans le noyau signifierait que les gens n'auraient pas à écrire des wrappers puisqu'une telle logique réutilisable pourrait être expédiée en toute sécurité dans tous les packages sans ajouter de dépendances.

Le point que j'essaie de faire est qu'à mon humble avis, une bonne solution ne nécessiterait pas que _personne_ écrive cette logique. Il n'y aurait tout simplement pas de logique redondante à réutiliser. Par exemple, en regardant l'exemple "fetchUser" de plus tôt, personne n'aurait à écrire un crochet, ou son équivalent, pour appeler la fonction "fetchUser", vous appelleriez simplement la fonction "fetchUser" directement. De même, "fetchUser" n'aurait pas besoin de savoir quoi que ce soit sur les crochets (ou tout ce que nous utilisons) et les crochets (ou tout ce que nous utilisons) n'auraient pas besoin de savoir quoi que ce soit sur "fetchUser". Tout en gardant la logique que vous écrivez triviale, comme c'est le cas avec les crochets.

Les restrictions actuelles sont causées par le fait que les hooks sont un patch au-dessus des limitations de langue.

Dans certains langages, les hooks sont une construction de langage, telle que :

state count = 0;

return RaisedButton(
  onPressed: () => count++,
  child: Text('clicked $count times'),
)

Il s'agirait d'une variante des fonctions async /sync , qui peuvent conserver un certain état entre les appels.

Il ne nécessite plus d'utilisation non conditionnelle puisque, dans le cadre du langage, nous pouvons différencier chaque variable par son numéro de ligne plutôt que par son type.

J'ajouterais que les limitations des hooks sont similaires aux limitations --track-widget-creation.

Ce drapeau rompt la canonisation du constructeur const pour les widgets. Mais ce n'est pas un problème car les widgets sont déclaratifs.

En ce sens, les crochets sont les mêmes. Les limitations n'ont pas vraiment d'importance, car elles sont manipulées de manière déclarative.
Nous n'obtiendrons pas un crochet bien précis sans lire les autres.

Peut-être que l'exemple de fetchuser n'est pas l'idéal.
Mais useStream, useAnimstion ou useStreamCintroller rendent l'arbre des widgets beaucoup plus propre et vous évitent d'oublier de disposer ou de vous occuper de dudChangeDependencues.
Par conséquent, la méthode actuelle a ses pièges dans lesquels vous pouvez vous faire prendre. Je suppose donc que le problème potentiel avec la séquence d'appels n'est pas plus important que ceux-là.
Je ne sais pas si je commencerais à écrire mes propres crochets, mais avoir une collection d'éléments souvent nécessaires prêts à être utilisés dans le cadre serait bien.
Ce serait juste une autre façon de les traiter.

@Hixie , vraiment désolé de ne pas pouvoir saisir ce que vous essayez de décrire, je blâme qu'il soit tard dans la soirée ici mais c'est probablement juste moi 😳.. Mais dans la bonne solution que vous décrivez, où seraient les valeurs de l'état, la logique métier d'état et la logique d'événement de durée de vie que la solution au problème enveloppe/encapsule pour être facilement composable et partagée entre les widgets résident ? Pourriez-vous expliquer un peu ce qu'est une bonne solution et comment, selon vous, elle fonctionnerait idéalement ?

J'interviens un peu ici, vu qu'il y a des mentions de civilité dans cette discussion. Personnellement, je n'ai pas l'impression que quelqu'un ici soit incivil.

Cela dit, je pense qu'il vaut la peine de noter que c'est un sujet auquel les gens se soucient profondément, de tous côtés.

  • @rrousselGit répond aux questions des débutants sur la gestion des états sur StackOverflow et sur le package:provider issue tracker depuis des années maintenant. Je ne suis que le dernier de ces lances à incendie, et je n'ai que du respect pour la diligence et la patience de Rémi.
  • @Hixie et d'autres membres de l'équipe Flutter se soucient profondément de l'API de Flutter, de sa stabilité, de sa surface, de sa maintenabilité et de sa lisibilité. C'est grâce à cela que l'expérience de développeur de Flutter est là où elle est aujourd'hui.
  • Les développeurs Flutter se soucient profondément de la gestion des états, car c'est ce qu'ils font une partie importante de leur temps de développement.

Il est clair que toutes les parties dans cette discussion ont une bonne raison de défendre ce qu'elles font. Il est également compréhensible qu'il faille du temps pour faire passer les messages.

Donc, je serais heureux si la discussion se poursuivait, que ce soit ici ou sous une autre forme. Si je peux aider de quelque manière que ce soit, comme avec un doc formel, faites-le moi savoir.

Si, d'un autre côté, les gens pensent que la discussion ici devient incontrôlable, alors faisons une pause et voyons s'il y a une meilleure façon de communiquer.

(Séparément, je tiens à remercier @gaearon pour avoir rejoint cette discussion. L'expérience de React à cet égard est inestimable.)

@emanuel-lundman

Mais dans la bonne solution que vous décrivez, où résideraient les valeurs d'état, la logique métier d'état et la logique d'événement de durée de vie que la solution au problème enveloppe/encapsule pour être facilement composables et partagées entre les widgets ? Pourriez-vous expliquer un peu ce qu'est une bonne solution et comment, selon vous, elle fonctionnerait idéalement ?

Malheureusement, je ne peux pas développer car je ne sais pas. :-)

@escamoteur

Peut-être que l'exemple de fetchuser n'est pas l'idéal.
Mais useStream, useAnimstion ou useStreamCintroller rendent l'arbre des widgets beaucoup plus propre et vous évitent d'oublier de disposer ou de vous occuper de dudChangeDependencues.

L'une des difficultés de ce problème a été un "déplacement des poteaux de but" où un problème est décrit, puis lorsqu'il est analysé, le problème est rejeté comme n'étant pas le vrai problème et un nouveau problème est décrit, et ainsi de suite. Ce qui pourrait être vraiment utile serait de proposer des exemples canoniques, par exemple une application de démonstration Flutter qui a un exemple réel du problème, qui n'est pas trop simplifié pour des raisons d'exposition. Ensuite, nous pourrions réimplémenter cela en utilisant des Hooks et d'autres propositions, et les évaluer vraiment les uns par rapport aux autres en termes concrets. (Je le ferais moi-même, sauf que je ne comprends pas vraiment exactement quel est le problème, il est donc probablement préférable que quelqu'un qui défend les crochets le fasse.)

Ce qui pourrait être vraiment utile serait de proposer des exemples canoniques, par exemple une application de démonstration Flutter qui a un exemple réel du problème, qui n'est pas trop simplifié pour des raisons d'exposition

Que pensez-vous de l'exemple que j'ai donné ici? https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

Il s'agit d'un extrait de code du monde réel.

Je pense que ce serait un bon début. Pouvons-nous le mettre dans un état où il s'exécute en tant qu'application autonome, avec une version qui n'utilise pas de hooks et une version qui le fait ?

Désolé, je voulais dire en tant qu'extrait de code, pas en tant qu'application.

Je pense que l'un des problèmes avec l'idée de "l'application de démonstration Flutter" est que les exemples faits dans ce fil sont très réels.
Ils ne sont pas simplistes.
Le principal cas d'utilisation des crochets consiste à factoriser des micro-états, tels que l'anti-rebond, les gestionnaires d'événements, les abonnements ou les effets secondaires implicites, qui sont combinés pour obtenir des logiques plus utiles.

J'ai quelques exemples sur Riverpod, comme https://marvel.riverpod.dev/#/ où le code source est ici : https://github.com/rrousselGit/river_pod/tree/master/examples/marvel/lib
Mais cela ne va pas être très différent de ce qui a été mentionné jusqu'à présent.

@Hixie

J'ai vraiment du mal à comprendre pourquoi c'est un problème. J'ai écrit beaucoup d'applications Flutter, mais cela ne semble vraiment pas être un problème? Même dans le pire des cas, il faut quatre lignes pour déclarer une propriété, l'initialiser, la supprimer et la rapporter aux données de débogage (et c'est généralement moins, car vous pouvez généralement la déclarer sur la même ligne que vous l'initialisez, les applications en général n'avez pas à vous soucier d'ajouter un état aux propriétés de débogage, et beaucoup de ces objets n'ont pas d'état qui doit être supprimé).

Je suis dans le meme bateau.
J'avoue que je ne comprends pas vraiment les problèmes décrits ici non plus. Je ne comprends même pas à quelle "logique d'état" les gens se réfèrent, qui doit être réutilisable.

J'ai de nombreux widgets de formulaire avec état, certains contenant des dizaines de champs de formulaire, et je dois gérer moi-même les contrôleurs de texte et les nœuds de focus. Je les crée et les dispose dans les méthodes de cycle de vie du widget sans état. Bien que ce soit assez fastidieux, je n'ai aucun widget qui utilise le même nombre de contrôleurs/focusNodes ou pour le même cas d'utilisation. La seule chose commune entre eux est le concept général d'être avec état et d'être une forme. Ce n'est pas parce que c'est un modèle que le code est répété.
Je veux dire, dans de nombreuses parties de mon code, je dois parcourir des tableaux, je n'appellerais pas faire "for(var chose dans les choses)" tout au long de mon application en répétant le code.

J'aime la puissance du StatefulWidget qui vient de la simplicité de son API de cycle de vie. Cela me permet d'écrire des StatefulWidgets qui font une chose et le font indépendamment du reste de l'application. L'"état" de mes widgets est toujours privé pour eux-mêmes, donc la réutilisation de mes widgets n'est pas un problème, pas plus que la réutilisation du code.

J'ai quelques problèmes avec les exemples présentés ici, qui correspondent quelque peu à vos points :

  • créer plusieurs widgets avec état avec exactement la même "logique d'état" semble tout simplement faux et va à l'encontre de l'idée d'avoir des widgets autonomes. Mais encore une fois, je suis confus quant à ce que les gens entendent par "logique d'état" commune.
  • les crochets ne semblent pas faire quelque chose que je ne peux pas déjà faire en utilisant des concepts de programmation simples et de base (tels que des fonctions)
  • les problèmes semblent être liés ou causés par un certain style de programmation, un style qui semble favoriser « l'état global réutilisable ».
  • faire abstraction de quelques lignes de code sent "l'optimisation de code prématurée" et ajoute de la complexité afin de résoudre un problème qui n'a rien à voir avec le framework et tout à voir avec la façon dont les gens l'utilisent.

C'est nettement moins lisible.

  • Nous avons 10 niveaux d'indentation – 12 si nous faisons un FilterBuilder pour réutiliser la logique du filtre
  • La logique de filtrage n'est pas réutilisable telle quelle.

    • nous pouvons oublier d'annuler le timer par erreur
  • la moitié de la méthode build n'est pas utile pour le lecteur. The Builders nous distrait de ce qui compte
  • J'ai perdu 5 bonnes minutes à essayer de comprendre pourquoi le code ne se compile pas à cause d'une parenthèse manquante

Votre exemple montre davantage à quel point le fournisseur est détaillé et pourquoi abuser de InheritedWidgets pour tout est une mauvaise chose, plutôt qu'un problème réel avec l'API de cycle de vie StatelessWidget et State de Flutter.

@rrousselGit Désolé si je n'ai pas été clair. La suggestion que je faisais ci-dessus était spécifiquement de créer des applications Vanilla Flutter (à l'aide de StatefulWidget, etc.) qui soient réalistes et montrent les problèmes que vous décrivez, afin que nous puissions ensuite faire des propositions basées sur une vraie application complète. Les exemples concrets dont nous avons discuté ici, tels que l'exemple "fetchUser", ont toujours abouti à une discussion du type "bien, vous pourriez gérer ce cas comme celui-ci et ce serait simple et n'aurait pas besoin de crochets" suivi par "eh bien, c'est trop simplifié, dans le monde réel, vous avez besoin de Hooks". Donc, mon point est, créons un exemple du monde réel qui a vraiment besoin de Hooks, qui n'est pas trop simplifié, qui montre la difficulté à réutiliser le code, afin que nous puissions voir s'il est possible d'éviter ces problèmes sans utiliser de nouveau code , ou si nous avons besoin d'un nouveau code, et dans ce dernier cas, s'il doit avoir la forme de Hooks ou si nous pouvons trouver une solution encore meilleure.

Je ne comprends même pas à quelle "logique d'état" les gens se réfèrent, qui doit être réutilisable.

Assez juste
Un parallèle à la logique d'état serait la logique de l'interface utilisateur et ce que les widgets apportent à la table.

Nous pourrions techniquement supprimer la couche Widget. Dans cette situation, il ne resterait que les RenderObjects.

Par exemple, nous pourrions avoir un compteur minimaliste :

var counter = 0;

final counterLabel = RenderParagraph(
  TextSpan(text: '$counter'),
  textDirection: TextDirection.ltr,
);

final button = RenderPointerListener(
  onPointerUp: (_) {
    counter++;
    counterLabel.text = TextSpan(text: '$counter');
  },
  child: counterLabel,
);

Ce n'est pas forcément complexe. Mais c'est sujet aux erreurs. Nous avons un doublon sur le rendu counterLabel

Avec les widgets, on a :

class _CounterState exends State {
  int counter = 0;

  Widget build(context ) {
    return GestureDetector(
      onTap: () => setState(() => counter++);
      child: Text('$counter'),
    );
  }
}

La seule chose que cela a fait est de factoriser la logique Text , en la rendant déclarative.
C'est un changement minimaliste. Mais sur une grande base de code, c'est une simplification significative .

Les crochets font exactement la même chose.
Mais au lieu de Text , vous obtenez des hooks personnalisés pour les logiques d'état. Ce qui inclut les écouteurs, anti-rebond, faire des requêtes HTTP, ...


Votre exemple montre davantage à quel point le fournisseur est détaillé et pourquoi abuser de InheritedWidgets pour tout est une mauvaise chose, plutôt qu'un problème réel avec l'API de cycle de vie StatelessWidget et State de Flutter.

Ceci n'est pas lié au fournisseur (ce code n'utilise pas le fournisseur après tout).
Si quoi que ce soit, le fournisseur l'a mieux car il a context.watch au lieu de Consumer .

La solution standard serait de remplacer Consumer par ValueListenableBuilder – ce qui conduit exactement au même problème.

Je suis d'accord @Hixie , je pense que nous avons besoin de deux comparaisons côte à côte pour juger de l'efficacité de Flutter par rapport aux crochets. Cela aiderait également à convaincre les autres si les crochets sont meilleurs ou non, ou peut-être qu'une autre solution est encore meilleure si l'application vanille est construite avec cette troisième solution. Ce concept d'application vanille existe depuis un certain temps, avec des choses comme TodoMVC montrant la différence entre divers frameworks front-end, donc ce n'est pas nécessairement nouveau. Je peux vous aider à créer ces exemples d'applications.

@satvikpendem
Je serais prêt à aider.
Je pense que l'exemple d'application dans le flutter_hooks présente probablement plusieurs crochets différents et ce qu'ils facilitent / les problèmes qu'ils résolvent, et serait un bon point de départ.

Je pense également que nous pourrions également utiliser quelques-uns des exemples et des approches qui sont présentés dans ce numéro.

Mise à jour : le code est ici, https://github.com/TimWhiting/local_widget_state_approaches
Je ne suis pas sûr du nom approprié du référentiel, alors ne supposez pas que c'est le problème que nous essayons de résoudre. J'ai fait l'application de compteur de base dans stateful et hooks. Je n'ai pas beaucoup de temps plus tard ce soir, mais je vais essayer d'ajouter d'autres cas d'utilisation qui illustrent mieux ce qui pourrait être le problème. Toute personne souhaitant contribuer, veuillez demander l'accès.

Les exemples concrets dont nous avons discuté ici, tels que l'exemple "fetchUser", ont toujours abouti à une discussion du type "bien, vous pourriez gérer ce cas comme celui-ci et ce serait simple et n'aurait pas besoin de crochets" suivi par "eh bien, c'est trop simplifié, dans le monde réel, vous avez besoin de Hooks".

Je ne suis pas d'accord. Je ne pense pas avoir vu un tel "vous pourriez gérer ce cas comme celui-ci" et j'ai convenu que le code résultant était meilleur que la variante du crochet.

Tout ce que je voulais dire, c'est que même si nous pouvons faire les choses différemment, le code résultant est sujet aux erreurs et/ou moins lisible.
Cela s'applique également à fetchUser

Les crochets font exactement la même chose.
Mais au lieu de Text , vous obtenez des crochets personnalisés pour les logiques d'état. Ce qui inclut les écouteurs, anti-rebond, faire des requêtes HTTP, ...

Non, je ne comprends toujours pas ce que cette logique d'état commune est censée être. Je veux dire que j'ai de nombreux widgets qui lisent à partir d'une base de données dans leurs méthodes "initState/didUpdateDependency", mais je ne trouve pas deux widgets qui font exactement la même requête, donc leur "logique" n'est pas la même.

En utilisant l'exemple de la création d'une requête HTTP. En supposant que j'ai un "makeHTTPRequest(url, paramters)" quelque part dans ma classe de service que certains de mes widgets doivent utiliser, pourquoi utiliserais-je un crochet au lieu d'appeler la méthode directement chaque fois que j'en ai besoin ? En quoi l'utilisation de hooks est-elle différente des appels de méthode simples dans ce cas ?

Les auditeurs. Je n'ai pas de widgets qui écoutent les mêmes choses. Chacun de mes widgets est responsable de s'abonner à tout ce dont il a besoin et de s'assurer qu'il se désabonne. Les crochets peuvent être du sucre syntaxique pour la plupart des choses, mais comme mes widgets n'écouteraient pas la même combinaison d'objets, les crochets devraient être "paramétrés" d'une manière ou d'une autre. Encore une fois, en quoi les crochets sont-ils différents d'une ancienne fonction simple ?


Ceci n'est pas lié au fournisseur (ce code n'utilise pas le fournisseur après tout).
Si quoi que ce soit, le fournisseur l'a mieux car il a context.watch au lieu de Consumer .

Hein? Votre contre-exemple de ce que votre HookWidget "ChatScreen" est censé résoudre, était le suivant :

<strong i="19">@override</strong>
  Widget build(BuildContext context) {
    return Consumer<Auth>(
      provider: authProvider,
      builder: (context, auth, child) {
        return Consumer<int>(
          provider: selectedChatProvider,
          builder: (context, chatId, child) {

En quoi est-ce sans rapport avec le fournisseur ? Je suis confus. Je ne suis pas un expert en Provider, mais cela ressemble vraiment à du code utilisant Provider.

Je voudrais insister sur le fait que cette question ne concerne pas les états complexes.
Il s'agit de petits incréments qui peuvent être appliqués à l'ensemble de la base de code.

Si nous ne sommes pas d'accord avec la valeur des exemples donnés ici, une application n'apportera rien à la conversation - car il n'y a rien que nous puissions faire avec les crochets que nous ne pouvons faire avec StatefulWidget.

Ma recommandation serait plutôt de comparer des micro-extraits côte à côte comme ImplicitFetcher , et de déterminer _objectivement_ quel code est le meilleur en utilisant des métriques mesurables, et de le faire pour une grande variété de petits extraits.


En quoi est-ce sans rapport avec le fournisseur ? Je suis confus. Je ne suis pas un expert en Provider, mais cela ressemble vraiment à du code utilisant Provider.

Ce code ne provient pas de Provider mais d'un projet différent qui n'utilise pas InheritedWidgets.
Le Consumer du fournisseur n'a pas provider paramètre

Et comme je l'ai mentionné, vous pouvez remplacer Consumer -> ValueListenableBuilder/StreamBuilder/BlocBuilder/Observer/...

dans leurs méthodes "initState/didUpdateDependency", mais je ne trouve pas deux widgets qui font exactement la même requête, donc leur "logique" n'est pas la même.

La logique d'état que nous voulons réutiliser n'est pas "faire une requête" mais "faire quelque chose quand x change". Le "faire quelque chose" peut changer, mais le "quand x change" est commun

Exemple concret :
Nous pouvons souhaiter qu'un widget fasse une requête HTTP chaque fois que l'ID qu'il reçoit change.
Nous souhaitons également annuler les demandes en attente à l'aide package:async CancelableOperation de

Maintenant, nous avons deux widgets qui veulent faire exactement la même chose, mais avec une requête HTTP différente.
Au final, on a :

CancelableOperation<User> pendingUserRequest;

initState() {
  pendingUserRequest = fetchUser(widget.userId);
}

didUpdateWidget(oldWidget) {
  if (widget.userId != oldWidget.userId) {
      pendingUserRequest.cancel();
      pendingUserRequest = fetchUser(widget.userId);
  }
}

dispose() {
  pendingUserRequest.cancel();
}

VS:

CancelableOperation<Message> pendingMessageRequest;

initState() {
  pendingMessageRequest = fetchMessage(widget.messageId);
}

didUpdateWidget(oldWidget) {
  if (widget.userId != oldWidget.messageId) {
      pendingMessageRequest.cancel();
      pendingMessageRequest = fetchMessage(widget.messageId);
      message = pendingMessageRequest.value;
  }
}

dispose() {
  pendingMessageRequest.cancel();
}

La seule différence est que nous avons changé fetchUser par fetchMessage . La logique est sinon à 100% la même. Mais nous ne pouvons pas le réutiliser, ce qui est source d'erreurs.

Avec les crochets, nous pourrions factoriser cela en un crochet useUnaryCancelableOperation .

Ce qui signifie qu'avec les deux mêmes widgets, cela ferait plutôt :

Widget build(context) {
  Future<User> user = useUnaryCancelableOperation(userId, fetchUser);
}

VS

Widget build(context) {
  Future<Message> message = useUnaryCancelableOperation(messageId, fetchMessage);
}

Dans ce scénario, toute la logique liée à la création de la demande et à son annulation est partagée. Il ne nous reste qu'une différence significative, qui est de fetchUser vs fetchMessage .
Nous pourrions même créer un package à partir de ce useUnaryCancelableOperation , et maintenant tout le monde peut le réutiliser dans son application.

Si nous ne sommes pas d'accord avec la valeur des exemples donnés ici, une application n'apportera rien à la conversation - car il n'y a rien que nous puissions faire avec les crochets que nous ne pouvons faire avec StatefulWidget.

Si c'est vraiment le cas, je suppose que nous devrions fermer ce bogue, car nous avons déjà discuté des exemples donnés ici et ils n'ont pas été convaincants. J'aimerais vraiment mieux comprendre la situation, cependant, et d'après les commentaires précédents de ce bogue, les avantages se situent au niveau de l'application, d'où ma suggestion d'étudier des exemples d'applications.

La seule différence est que nous avons changé fetchUser avec fetchMessage . La logique est sinon à 100% la même. Mais nous ne pouvons pas le réutiliser, ce qui est source d'erreurs.

Qu'est-ce qui est sujet aux erreurs et qu'y a-t-il à réutiliser ? Implémenter une toute nouvelle couche d'abstraction et de hiérarchie de classes juste pour que nous n'ayons pas à implémenter trois méthodes dans une classe et c'est bien excessif.

Encore une fois, ce n'est pas parce que quelque chose est un modèle commun que vous devez créer une nouvelle fonctionnalité pour cela. En outre, si vous souhaitez réduire le code répétitif dans ce cas, vous pouvez simplement étendre la classe StatefulWidget* et remplacer les méthodes initstate/didUpdateWidget par les bits communs.

Avec les crochets, nous pourrions factoriser cela en un crochet useUnaryCancelableOperation .

Ce qui signifie qu'avec les deux mêmes widgets, cela ferait plutôt :

Widget build(context) {
  Future<User> user = useUnaryCancelableOperation(userId, fetchUser);
}

VS

Widget build(context) {
  Future<Message> message = useUnaryCancelableOperation(messageId, fetchMessage);
}

Dans ce scénario, toute la logique liée à la création de la demande et à son annulation est partagée. Il ne nous reste qu'une différence significative, qui est de fetchUser vs fetchMessage .
Nous pourrions même créer un package à partir de ce useUnaryCancelableOperation , et maintenant tout le monde peut le réutiliser dans son application.

Je suis désolé mais c'est un grand non catégorique de ma part. Mis à part le fait qu'il n'économise qu'une quantité mineure de redondance de code, "factoriser" un code qui appartient conceptuellement aux méthodes de cycle de vie "initState" et "update", dans la méthode de construction est un grand non.

Je m'attends à ce que mes méthodes de construction ne construisent que la mise en page et rien d'autre. La configuration et la suppression des dépendances n'appartiennent certainement pas à la méthode de construction, et je suis assez heureux d'avoir à réécrire explicitement le même type de code pour rendre explicite pour mon futur moi et pour les autres ce que fait mon widget. Et ne collons pas tout dans la méthode de construction.

Si c'est vraiment le cas, je suppose que nous devrions fermer ce bogue

@Hixie S'il vous plaît, ne le faites pas. Les gens se soucient de cette question. Je vous ai parlé de la même chose sur reddit, mais dans le contexte de SwiftUI : https://github.com/szotp/SwiftUI_vs_Flutter

Il ne s'agit pas de crochets, il s'agit d'améliorer en quelque sorte les widgets avec état. Les gens détestent tout simplement écrire du passe-partout. Pour les développeurs qui écrivent du code SwiftUI, qui sont habitués à RAII et à copier la sémantique des vues, la gestion manuelle des éléments jetables semble juste à côté.

J'encourage donc l'équipe Flutter à au moins voir cela comme un problème et à réfléchir à des solutions / améliorations alternatives.

Je m'attends à ce que mes méthodes de construction ne construisent que la mise en page et rien d'autre. La configuration et la suppression des dépendances n'appartiennent certainement pas à la méthode de construction,
C'est un point important. Les méthodes de construction doivent être pures. J'aurais quand même souhaité que nous puissions avoir les avantages sans les difficultés

Je ne comprends vraiment pas la poussée pour plus d'exemples ici. C'est clair sur son visage.

Le problème résolu par les crochets est simple et évident, il garde le code SEC. Les avantages de ceci sont les plus évidents, moins de code == moins de bogues, la maintenance est plus facile, moins d'endroits pour cacher les bogues, un nombre de lignes inférieur augmente la lisibilité, les programmeurs juniors sont mieux isolés, etc.

Si vous parlez d'un cas d'utilisation réel, il s'agit d'une application dans laquelle vous configurez et démontez 12 contrôleurs d'animation dans 12 vues différentes, à chaque fois, laissant la porte ouverte à l'absence d'un appel dispose () ou à l'annulation de certains autre méthode de cycle de vie. puis appliquez cela à des dizaines d'autres instances avec état, et vous regardez facilement des centaines ou des milliers de lignes de code inutiles.

Flutter est plein de ces cas où nous devons constamment nous répéter, mettre en place et démonter l'état de petits objets, qui créent toutes sortes d'opportunités pour des bogues qui n'ont pas besoin d'exister, mais qui existent, car il n'y a actuellement aucune approche élégante pour partageant cette logique de configuration/démontage/synchronisation par cœur.

Vous pouvez voir ce problème dans littéralement _n'importe quel_ état qui a une phase de configuration et de démontage, ou qui a un crochet de cycle de vie qui doit toujours être lié.

Moi-même, je trouve que l'utilisation de widgets est la meilleure approche, j'utilise rarement AnimatorController, par exemple, car la configuration / le démontage est si ennuyeux, verbeux et sujet aux bugs, au lieu d'utiliser TweenAnimationBuilder partout où je le peux. Mais cette approche a ses limites car vous obtenez un nombre plus élevé d'objets avec état dans une vue donnée, forçant l'imbrication et la verbosité là où aucun ne devrait être requis.

@szotp Je n'ai pas... Je préférerais de

Les applications

@esDotDev Nous avons discuté de cas comme celui-ci dans ce bogue jusqu'à présent, mais chaque fois que nous en avons, des solutions autres que les Hooks sont rejetées car elles ne résolvent pas un problème qui n'était pas inclus dans l'exemple traité par la solution. Par conséquent, de simples descriptions des problèmes ne semblent pas suffisantes pour en saisir toute l'étendue. Par exemple, le cas des "12 contrôleurs d'animation" pourrait peut-être être résolu par un ensemble de contrôleurs d'animation et les fonctionnalités fonctionnelles de Dart. TweenAnimationBuilder pourrait être une autre solution. Aucun de ceux-ci n'implique Hooks. Mais je suis sûr que si je suggère cela, quelqu'un signalera quelque chose que j'ai manqué et dira "ça ne marche pas parce que..." et soulèvera ce problème (nouveau, dans le contexte de l'exemple). D'où la nécessité d'une application de base qui, nous en convenons tous, couvre toute la propagation du problème.

Si quelqu'un souhaite faire avancer les choses, je pense vraiment que la meilleure façon de le faire est ce que j'ai décrit ci-dessus (https://github.com/flutter/flutter/issues/51752#issuecomment-670249755 et https://github.com /flutter/flutter/issues/51752#issuecomment-670232842). Cela nous donnera un point de départ dont nous pouvons tous convenir qu'il représente l'étendue du problème que nous essayons de résoudre; Ensuite, nous pouvons concevoir des solutions qui répondent à ces problèmes de manière à répondre à tous les désirs (par exemple , le besoin de réutilisation de @rrousselGit , le besoin de

Je pense que nous pourrions tous assez facilement être d'accord sur le problème :
_Il n'existe aucun moyen élégant de partager les tâches de configuration/démontage associées à des éléments tels que les flux, les contrôleurs d'animation, etc. Cela entraîne une verbosité inutile, des ouvertures pour les bogues et une lisibilité réduite._

Est-ce que quelqu'un n'est pas d'accord avec ça? Ne pouvons-nous pas commencer par là et aller de l'avant à la recherche d'une solution ? Nous devons convenir qu'il s'agit d'abord d'un problème central, ce que nous ne pensons toujours pas.

Au moment où j'écris cela, j'ai réalisé que correspond exactement au nom du problème, qui est ouvert et laisse place à la discussion :
" La réutilisation de la logique d'état est soit trop verbeuse, soit trop difficile "

Pour moi, c'est un problème très évident, et nous devrions rapidement dépasser le stade du débat et réfléchir à ce qui fonctionnerait, sinon à quoi. Nous avons besoin de micro-états réutilisables... Je suis sûr que nous pouvons trouver une solution. Cela nettoierait vraiment beaucoup de vues Flutter à la fin de la journée et les rendrait plus robustes.

@Hixie S'il vous plaît, ne le faites pas. Les gens se soucient de cette question. Je vous ai parlé de la même chose sur reddit, mais dans le contexte de SwiftUI : https://github.com/szotp/SwiftUI_vs_Flutter

Votre exemple SwiftUI peut être répliqué dans dart en quelques lignes de code, en étendant simplement la classe StatefulWidget.

J'ai des StatefulWidgets qui ne s'abonnent à aucun notificateur et/ou ne font aucun appel externe, et en fait la plupart d'entre eux sont comme ça. J'ai environ 100 widgets personnalisés (bien que pas tous avec état), et peut-être 15 d'entre eux ont une sorte de "logique d'état commune" comme décrit par les exemples ici.

À long terme, écrire quelques lignes de code (alias passe-partout) est un petit compromis afin d'éviter une surcharge inutile. Et encore une fois, ce problème d'avoir à implémenter les méthodes initState/didUpdate semble bien exagéré. Lorsque je crée un widget qui utilise l'un des modèles décrits ici, je passe peut-être les 5 à 10 premières minutes à "implémenter" les méthodes de cycle de vie, puis quelques jours à écrire et à peaufiner le widget lui-même sans jamais toucher auxdites méthodes de cycle de vie. Le temps que je passe à écrire le soi-disant code de configuration/démontage passe-partout est minuscule par rapport au code de mon application.

Comme je l'ai dit, le fait que les StatefulWidgets fassent si peu d'hypothèses sur leur utilisation est ce qui les rend si puissants et efficaces.

L'ajout d'un nouveau type de widget à Flutter qui sous-classe StatefulWidget (ou non) pour ce cas d'utilisation particulier serait bien, mais ne l'intégrons pas dans le StatefulWidget lui-même. J'ai beaucoup de widgets qui n'ont pas besoin de la surcharge qui viendrait avec un système de "crochets" ou des micro-états.

@esDotDev Je suis d'accord pour dire que c'est un problème auquel certaines personnes sont confrontées ; J'ai même proposé quelques solutions plus tôt dans ce numéro (rechercher mes différentes versions d'une classe Property , pourrait être enterré maintenant puisque GitHub n'aime pas afficher tous les commentaires). La difficulté est que ces propositions ont été rejetées car elles ne résolvaient pas des problèmes spécifiques (par exemple, dans un cas, ne géraient pas le rechargement à chaud, dans un autre, ne géraient pas didUpdateWidget). Alors j'ai fait d'autres propositions, mais celles-ci ont à nouveau été rejetées parce qu'elles ne traitaient pas d'autre chose (je ne sais plus quoi). C'est pourquoi il est important d'avoir une sorte de base de référence qui représente le problème _entier_ afin que nous puissions trouver des solutions à ce problème.

L'objectif n'a jamais changé. Les critiques formulées sont que les solutions proposées manquent de flexibilité. Aucun d'entre eux ne continue à fonctionner en dehors de l'extrait pour lequel ils ont été implémentés.

C'est pourquoi le titre de ce numéro mentionne « Difficile » : Parce qu'il n'y a actuellement aucune flexibilité dans la manière dont nous résolvons actuellement les problèmes.


Une autre façon de le voir est :

Ce problème soutient essentiellement que nous devons implémenter une couche Widget, pour la logique d'état.
Les solutions suggérées sont "Mais vous pouvez le faire avec RenderObjects".

_À long terme, écrire quelques lignes de code (alias passe-partout) est un petit compromis afin d'éviter des frais généraux inutiles._

Couplez lentes avec cette déclaration:

  1. Ce ne sont pas vraiment quelques lignes, si vous prenez des crochets, des interlignes @overides , etc. dans acct, vous pouvez regarder plus de 10 à 15 lignes pour un simple contrôleur d'animation. Ce n'est pas trivial dans mon esprit... comme bien au-delà du trivial. 3 lignes pour faire ça me dérange (dans Unity c'est Thing.DOTween() ). 15 est ridicule.
  1. Il ne s'agit pas de taper, même si c'est pénible. Il s'agit de la bêtise d'avoir une classe de 50 lignes, où 30 lignes sont du passe-partout par cœur. Son obscurcissement. Il s'agit du fait que si vous _n'écrivez pas_ le passe-partout, il n'y a pas d'avertissement ou quoi que ce soit, vous venez d'ajouter un bogue.
  2. Je ne vois pas de frais généraux qui méritent d'être discutés avec quelque chose comme Hooks. Nous parlons d'un tableau d'objets, avec une poignée de fxns sur chacun. Dans Dart, ce qui est très rapide. Ceci est un imo hareng rouge.

@esDotDev

Pour moi, c'est un problème très évident, et nous devrions rapidement dépasser le stade du débat et réfléchir à ce qui fonctionnerait, sinon à quoi.

Extension des widgets. Comme la façon dont ValueNotifier étend le ChangeNotifier pour simplifier un modèle d'utilisation commun, tout le monde peut écrire ses propres variantes de StatelessWidgets pour ses cas d'utilisation spécifiques.

Oui, je suis d'accord, c'est une approche efficace, mais elle laisse à désirer. Si j'ai 1 animateur, je peux simplement utiliser un TweenAnimationBuilder. Cool, c'est toujours comme 5 lignes de code, mais peu importe. ça marche... pas trop mal. Mais si j'en ai 2 ou 3 ? Maintenant, je suis dans l'enfer de la nidification, si j'ai un cpl d'autres objets avec état pour une raison quelconque, eh bien, tout cela devient un peu un gâchis d'indentation, ou je crée un widget très spécifique qui encapsule une collection aléatoire de configuration, de mise à jour et de démontage logique.

Extension des widgets. Comme la façon dont ValueNotifier étend le ChangeNotifier pour simplifier un modèle d'utilisation commun, tout le monde peut écrire ses propres variantes de StatelessWidgets pour ses cas d'utilisation spécifiques.

Vous ne pouvez étendre qu'une seule classe de base à la fois. Cela ne s'échelonne pas

Les mixins sont la prochaine tentative logique. Mais comme le mentionne le PO, ils ne s'adaptent pas non plus.

@esDotDev

ou je crée un widget très spécifique qui encapsule une collection aléatoire de logique de configuration, de mise à jour et de démontage.

Un type de widget qui doit configurer 3 à 4 types de contrôleurs d'animation ressemble à un cas d'utilisation très spécifique et la prise en charge d'une collection aléatoire de logique de configuration/démontage ne devrait certainement pas faire partie d'un cadre. En fait, c'est pourquoi les méthodes initState/didUpdateWidget sont exposées en premier lieu, afin que vous puissiez faire votre collection aléatoire de configuration à votre guise.

Ma méthode initState la plus longue est de 5 lignes de code, mes widgets ne souffrent pas d'imbrication excessive, nous avons donc des besoins et des cas d'utilisation différents. Ou un style de développement différent.

@esDotDev

3. Je ne vois aucune surcharge qui vaille la peine d'être discutée avec quelque chose comme Hooks. Nous parlons d'un tableau d'objets, avec une poignée de fxns sur chacun. Dans Dart, ce qui est très rapide. Ceci est un imo hareng rouge.

Si la solution proposée ressemble au package flutter_hooks, c'est totalement faux. Oui, conceptuellement, c'est un tableau contenant des fonctions, mais l'implémentation est loin d'être triviale ou efficace.

Je veux dire, je me trompe peut-être, mais il semble que le HookElement vérifie s'il doit se reconstruire dans sa propre méthode de construction ?!
Vérifier également si les crochets doivent être initialisés, réinitialisés ou supprimés sur chaque construction de widget semble être une surcharge importante. Je ne me sens pas bien, alors j'espère que je me trompe.

Serait-il logique de prendre l'un des exemples d'architecture de @brianegan comme application de base pour une comparaison ?

Si je peux intervenir ici, je ne sais pas si cela a déjà été dit. Mais dans React, nous ne pensons pas vraiment au cycle de vie avec des crochets, et cela peut sembler effrayant si vous êtes habitué à créer des composants/widgets, mais voici pourquoi le cycle de vie n'a pas vraiment d'importance.

La plupart du temps, lorsque vous créez des composants / widgets avec un état ou des actions à entreprendre en fonction des accessoires, vous voulez que quelque chose se produise lorsque cet état / ces accessoires changent (par exemple, comme je l'ai vu mentionné dans ce fil, vous voudrez re -récupérer les détails d'un utilisateur lorsque la prop userId a changé). Il est généralement beaucoup plus naturel de considérer cela comme un "effet" du changement d'ID utilisateur, plutôt que comme quelque chose qui se produit lorsque tous les accessoires du widget changent.

Même chose pour le nettoyage, il est généralement beaucoup plus naturel de penser « je dois nettoyer cet état/écouteur/contrôleur lorsque cet accessoire/état change » plutôt que « je dois me rappeler de nettoyer X lorsque tous les accessoires/l'état changent ou lorsque tout le composant est détruit".

Je n'ai pas écrit Flutter depuis un moment, donc je n'essaie pas de donner l'impression que je connais le climat actuel ou les limites que cette approche aurait sur l'état d'esprit actuel de Flutter, je suis ouvert aux opinions divergentes. Je pense juste que beaucoup de gens qui ne sont pas familiers avec les crochets React ont la même confusion que j'avais quand je leur ai été présenté parce que mon état d'esprit était tellement ancré dans le paradigme du cycle de vie.

@escamoteur @Rudiksz @Hixie il y a eu un projet GitHub créé par @TimWhiting auquel j'ai été invité où nous commençons à créer ces exemples. Chaque personne/groupe peut créer comment il résoudrait un problème prédéfini. Ce ne sont pas des applications complètes, plutôt des pages, mais nous pouvons également ajouter des applications, si elles servent à montrer des exemples plus complexes. Ensuite, nous pouvons discuter des problèmes et créer une meilleure API. @TimWhiting peut inviter toute personne intéressée, je suppose.

https://github.com/TimWhiting/local_widget_state_approaches

Jetpack compose est également similaire aux crochets, qui a été comparé à réagir ici .

@satvikpendem @TimWhiting C'est super ! Merci.

@esDotDev
cas d'utilisation très spécifique et prenant en charge la collecte aléatoire de la logique de configuration/démontage ne devraient certainement pas faire partie d'un cadre.

C'est le clou qui accroche les coups sur la tête. Chaque type d'objet est responsable de sa propre configuration et de son démontage. Les animateurs savent se créer, se mettre à jour et se détruire, tout comme les streams, etc. Hooks résout spécifiquement ce problème de « collections aléatoires » d'échafaudages d'état dispersés dans votre vue. Le permet à la majeure partie du code de la vue de se concentrer sur la logique métier et le formatage de la mise en page, ce qui est une victoire. Cela ne vous oblige pas à créer des widgets personnalisés, juste pour masquer un passe-partout générique qui est le même dans chaque projet.

Ma méthode initState la plus longue est de 5 lignes de code, mes widgets ne souffrent pas d'imbrication excessive, nous avons donc des besoins et des cas d'utilisation différents. Ou un style de développement différent.

Le mien aussi. Mais c'est initState() + dispose() + didUpdateDependancies(), et manquer l'un des 2 derniers peut provoquer des bogues.

Je pense que l'exemple canonique serait quelque chose comme : écrivez une vue qui utilise 1 contrôleur de flux et 2 contrôleurs d'animation.

Pour autant que je sache, vous avez 3 options :

  1. Ajoutez environ 30 lignes de passe-partout à votre classe et quelques mixins. Ce qui est non seulement verbeux, mais assez difficile à suivre au départ.
  2. Utilisez 2 TweenAnimationBuilders et un StreamBuilder, pour environ 15 niveaux d'indentation, avant même d'accéder à votre code de vue, et vous avez encore beaucoup de passe-partout pour le Stream.
  3. Ajoutez comme 6 lignes de code non indenté en haut de build(), pour obtenir vos 3 sous-objets avec état, et définissez n'importe quel code d'initialisation/destruction personnalisé

Il existe peut-être une 4ème option qui est un SingleStreamBuilderDoubleAnimationWidget, mais ce n'est qu'un truc de travail pour les développeurs et assez ennuyeux en général.

A noter également que la charge cognitive de 3 est nettement inférieure à celle des 2 autres pour un nouveau développeur. La plupart des nouveaux développeurs ne savent même pas que TweenAnimationBuilder existe, et apprendre simplement le concept de SingleTickerProvider est une tâche en soi. Dire simplement « Donnez-moi un animateur s'il vous plaît » est une approche plus simple et plus robuste.

Je vais essayer de coder quelque chose aujourd'hui.

2. Utilisez 2 TweenAnimationBuilders et un StreamBuilder, pour environ 15 niveaux d'indentation, avant même d'accéder à votre code de vue, et vous avez encore beaucoup de passe-partout pour le Stream.

Droit. Montrez-nous un exemple de code du monde réel qui utilise 15 niveaux d'indentation.

Comment remplacer 30 lignes de code par 6 lignes + des centaines de lignes dans une bibliothèque réduit-il la charge cognitive ? Ouais, je peux juste ignorer la "magie" de la bibliothèque, mais pas ses règles. Par exemple, le package hooks vous dit en termes clairs que les hooks ne doivent être utilisés que dans les méthodes de construction. Vous avez maintenant une contrainte supplémentaire à gérer.

J'ai probablement moins de 200 lignes de code qui impliquent des focusnodes, des textcontrollers, des singletickerproviders ou les différentes méthodes de cycle de vie de mes statefulwidgets, dans un projet avec 15k lignes de code. De quelle surcharge cognitive parlez-vous ?

@Rudiksz s'il vous plaît arrêtez d'être passif agressif.
Nous pouvons être en désaccord sans nous battre.


Les contraintes de crochets sont le cadet de mes soucis.

Nous ne parlons pas spécifiquement des crochets, mais du problème.
S'il le faut, nous pouvons trouver une autre solution.

Ce qui compte, c'est le problème que nous voulons résoudre.

De plus, les widgets ne peuvent être utilisés qu'à l'intérieur de la construction et inconditionnellement (ou sinon nous modifions la profondeur de l'arborescence, ce qui est interdit)

C'est identique aux contraintes des crochets, mais je ne pense pas que les gens s'en plaignent.

De plus, les widgets ne peuvent être utilisés qu'à l'intérieur de la construction et inconditionnellement (ou sinon nous modifions la profondeur de l'arborescence, ce qui est interdit)

C'est identique aux contraintes des crochets, mais je ne pense pas que les gens s'en plaignent.

Non, ce n'est pas identique. Le problème qui est présenté ici semble être lié au code qui _prépare_ les widgets _avant_ d'être (re)construit. Préparation de l'état, des dépendances, des flux, des contrôleurs, etc. et gestion des modifications dans l'arborescence. Aucun de ceux-ci ne doit être dans la méthode de construction, même s'il est caché derrière un seul appel de fonction.
Le point d'entrée de cette logique ne doit jamais se trouver dans la méthode de génération.

M'obliger à mettre une logique d'initialisation de quelque nature que ce soit dans la méthode de construction n'est nulle part la même chose que de "m'obliger" à composer une arborescence de widgets dans la méthode de construction. La raison principale de la méthode de construction est de prendre un état existant (ensemble de variables) et de produire un arbre de widgets qui est ensuite peint.

À l'inverse, je serais également contre le fait de me forcer à ajouter du code qui construit des widgets à l'intérieur des méthodes initState/didUpdateWidget.

Dans l'état actuel des choses, les méthodes de cycle de vie statefulwidget ont un rôle très clair et permettent de séparer très facilement et directement le code avec des préoccupations totalement différentes.

Conceptuellement, je commence à comprendre les problèmes qui sont décrits ici, mais je n'arrive toujours pas à les voir comme un problème réel. Peut-être que des exemples réels (qui ne sont pas l'application de compteur) peuvent m'aider à changer d'avis.

En remarque, Riverpod , ma dernière expérience, a des idées très accrocheuses, sans les contraintes.

Par exemple, il résout le :

Consumer(
  provider: provider,
  builder: (context, value) {
    return Consumer(
      provider: provider2,
      builder: (context, value2) {
        return Text('$value $value2');
      },
    );
  },
)

en ayant:

Consumer(
  builder (context, watch) {
    final value = watch(provider);
    final value2 = watch(provider2);
  },
)

watch peut être appelé conditionnellement :

Consumer(
  builder: (context, watch) {
    final value = watch(provider);
    if (something) {
      final value2 = watch(provider2);
    }
  },
)

Nous pourrions même nous débarrasser entièrement de Consumer en ayant une classe de base StatelessWidget / StatefulWidget :

class Example extends ConsumerStatelessWidget {
  <strong i="21">@override</strong>
  Widget build(ConsumerBuildContext context) {
    final value = context.watch(provider);
    final value2 = context.watch(provider2);
  }
}

Le problème principal est que cela est spécifique à un type d'objet, et cela fonctionne en s'appuyant sur le fait que l'instance d'objet a un hashCode cohérent à travers les reconstructions.

On est donc encore loin de la souplesse des crochets

@rrousselGit Je pense que sans étendre les classes StatelessWidget / StatefulWidget et créer quelque chose comme ConsumerStatelessWidget, il est possible d'avoir quelque chose comme context.watch en utilisant des méthodes d'extension sur BuildContext class et demander au fournisseur de fournir la fonction de surveillance avec InheritedWidgets.

C'est un autre sujet. Mais tl; dr, nous ne pouvons pas compter sur InheritedWidgets comme solution à ce problème : https://github.com/flutter/flutter/issues/30062

Pour résoudre ce problème, l'utilisation de InheritedWidgets nous bloquerait à cause de https://github.com/flutter/flutter/issues/12992 et https://github.com/flutter/flutter/pull/33213

Conceptuellement, je commence à comprendre les problèmes qui sont décrits ici, mais je n'arrive toujours pas à les voir comme un problème réel.

En comparant Flutter à SwiftUI, pour moi, il est évident qu'il y a un problème réel, ou plutôt - les choses ne sont pas aussi bonnes qu'elles pourraient l'être.

C'est peut-être difficile à voir, car Flutter et d'autres ont travaillé dur : nous avons des wrappers pour chaque cas spécifique : AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity, etc. StatefulWidget fonctionne très bien pour implémenter ces petits utilitaires réutilisables, mais c'est tout simplement trop bas niveau pour les composants spécifiques à un domaine non réutilisables, où vous pouvez avoir une multitude de contrôleurs de texte, d'animations ou tout ce que la logique métier appelle. La solution habituelle consiste soit à mordre la balle et à écrire tout ce passe-partout, soit à créer un arbre soigneusement construit de fournisseurs et d'auditeurs. Aucune des deux approches n'est satisfaisante.

C'est comme le dit @rrousselGit , autrefois (UIKit), nous étions obligés de gérer manuellement nos UIViews (l'équivalent de RenderObjects), et n'oubliez pas de copier les valeurs du modèle vers la vue et inversement, supprimer les vues inutilisées, recycler, etc. Ce n'était pas sorcier, et beaucoup de gens n'ont pas vu ce vieux problème, mais je pense que tout le monde ici conviendra que Flutter a clairement amélioré la situation.
Avec l'état d'avancement, le problème est très similaire : c'est un travail ennuyeux et sujet aux erreurs qui pourrait être automatisé.

Et, au fait, je ne pense pas que les crochets résolvent cela du tout. C'est juste que les crochets sont la seule approche possible sans changer les éléments internes du flottement.

StatefulWidget fonctionne très bien pour implémenter ces petits utilitaires réutilisables, mais c'est tout simplement un niveau trop bas pour les composants spécifiques à un domaine non réutilisables, où vous pouvez avoir une multitude de contrôleurs de texte, d'animations ou tout ce que la logique métier appelle.

Je suis confus lorsque vous dites que pour créer vos composants spécifiques à un domaine non réutilisables, vous avez besoin d'un widget de haut niveau. Habituellement, c'est exactement le contraire.

AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity sont tous des widgets qui implémentent un certain cas d'utilisation. Lorsque j'ai besoin d'un widget qui a une logique si spécifique que je ne peux utiliser aucun de ces widgets de niveau supérieur , c'est à ce moment-là que je descends à une API de niveau inférieur afin que je puisse écrire mon propre cas d'utilisation spécifique. Le soi-disant passe-partout implémente la façon dont mon widget unique gère sa combinaison unique de flux, d'appels réseau, de contrôleurs et ainsi de suite.

Plaidoyer pour les crochets, un comportement similaire à un crochet ou même simplement une "automatisation", c'est comme dire que nous avons besoin d'un widget de bas niveau qui peut gérer une logique de

Avec l'état d'avancement, le problème est très similaire : c'est un travail ennuyeux et sujet aux erreurs qui pourrait être automatisé.

De nouveau. Vous souhaitez automatiser __"des composants spécifiques à un domaine non réutilisables, où vous pouvez avoir une multitude de contrôleurs de texte, d'animations ou toute autre logique métier"__?!

C'est comme le dit @rrousselGit , autrefois (UIKit), nous étions obligés de gérer manuellement nos UIViews (l'équivalent de RenderObjects), et n'oubliez pas de copier les valeurs du modèle vers la vue et inversement, supprimer les vues inutilisées, recycler, etc. Ce n'était pas sorcier, et beaucoup de gens n'ont pas vu ce vieux problème, mais je pense que tout le monde ici conviendra que Flutter a clairement amélioré la situation.

J'ai développé iOS et Android il y a 6 à 7 ans (à l'époque où Android est passé à leur conception matérielle) et je ne me souviens pas que ces vues de gestion et de recyclage soient un problème et Flutter ne semble ni meilleur ni pire. Je ne peux pas parler de l'actualité, j'ai arrêté quand Swift et Kotlin ont été lancés.

Le passe-partout que je suis obligé d'écrire dans mes StatefulWidgets représente environ 1% de ma base de code. Est-ce sujet aux erreurs ? Chaque ligne de code est une source potentielle de bugs, c'est sûr. Est-ce encombrant ? 200 lignes de code sur 15000 ? Je ne pense vraiment pas, mais ce n'est que mon avis. Les contrôleurs de texte/animation de Flutter, les focusnodes ont tous des problèmes qui peuvent être améliorés, mais être verbeux n'en est pas un.

Je suis vraiment curieux de voir ce que les gens développent dont ils ont besoin de tant de passe-partout.

Écouter certains des commentaires ici ressemble à la gestion de 5 lignes de code au lieu de 1 est comme 5 fois plus difficile. Ce n'est pas.

Ne seriez-vous pas d'accord pour dire qu'au lieu de configurer initState et disposer pour chaque AnimationController, par exemple, cela peut être plus sujet aux erreurs que de le faire une seule fois et de réutiliser cette logique ? Même principe que l'utilisation des fonctions, la réutilisabilité. Je suis d'accord qu'il est problématique de mettre des crochets dans la fonction de construction, il existe certainement un meilleur moyen.

On a vraiment l'impression que la différence entre ceux qui voient et ne voient pas le problème ici est que les premiers ont déjà utilisé des constructions de type hook, comme dans React, Swift et Kotlin, et les seconds ne l'ont pas fait, comme travailler en Java pur. ou Android. Je pense que la seule façon d'être convaincu, d'après mon expérience, est d'essayer des crochets et de voir si vous pouvez revenir à la méthode standard. Souvent, beaucoup de gens ne peuvent pas, encore une fois, d'après mon expérience. Vous le savez quand vous l'utilisez.

À cette fin, j'encourage les personnes sceptiques à utiliser flutter_hooks pour un petit projet et à voir comment cela se passe, puis à le refaire de la manière par défaut. Il ne suffit pas que nous créions simplement des versions de l'application pour que l'on puisse lire comme dans la suggestion de

Il ne suffit pas que nous créions simplement des versions de l'application pour que l'on puisse lire comme dans la suggestion de

J'ai perdu des jours à essayer le fournisseur, encore plus de jours à essayer bloc, je n'ai trouvé ni l'un ni l'autre une bonne solution. Si cela fonctionne pour vous, tant mieux.

Pour que je puisse même essayer votre solution proposée à un problème que vous rencontrez, vous devez démontrer ses avantages. J'ai regardé des exemples avec des crochets flottants et j'ai regardé sa mise en œuvre. Tout simplement pas.

Quel que soit le code de réduction standard ajouté au framework, j'espère que les Stateful/StatelessWidgets ne seront pas modifiés. Je ne peux pas ajouter grand-chose à cette conversation.

On recommence, dans un monde hypothétique où l'on peut changer de Dart, sans parler d'hameçons.

Le problème débattu est :

Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}

Ce code n'est pas lisible.

Nous pourrions résoudre le problème de lisibilité en introduisant un nouveau mot-clé qui change la syntaxe en :

Widget build(context) {
  final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final value2 = keyword StreamBuilder(stream: someStream);
  final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

  return Text('$value $value2 $value3');
}

Ce code est nettement plus lisible, n'est pas lié aux hooks et ne souffre pas de ses limitations.
Le gain de lisibilité ne tient pas beaucoup au nombre de lignes, mais au formatage et aux indentations.


Mais qu'en est-il lorsque le Builder n'est pas le widget racine ?

Exemple:

Widget build(context) {
  return Scaffold(
    body: StreamBuilder(
      stream: stream,
      builder: (context, value) {
        return Consumer<Value2>(
          builder: (context, value2, child) {
            return Text('${value.data} $value2');
          },
        );
      },
    ),
  );
}

Nous pourrions avoir:

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword StreamBuilder(stream: stream);
      final value2 = keyword Consumer<Value2>();
      return Text('${value.data} $value2');
    }
  );
}

Mais quel est le rapport avec la question de la réutilisabilité ?

La raison pour laquelle cela est lié est que les constructeurs sont techniquement un moyen de réutiliser la logique d'état. Mais leur problème est qu'ils rendent le code peu lisible si nous prévoyons d'utiliser _beaucoup_ de constructeurs, comme dans ce commentaire https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

Avec cette nouvelle syntaxe, nous avons corrigé le problème de lisibilité. En tant que tel, nous pouvons extraire plus de choses dans Builders.

Ainsi, par exemple, le useFilter mentionné dans ce commentaire pourrait être :

FilterBuilder(
  debounceDuration: const Duration(seconds: 2),
  builder: (context, filter) {
    return TextField(onChange: (value) => filter.value = value);
  }
)

Que nous pouvons ensuite utiliser avec le nouveau mot clé :

class ChatScreen extends HookWidget {
  const ChatScreen({Key key}) : super(key: key);

  <strong i="15">@override</strong>
  Widget build(BuildContext context) {
    final filter = keyword FilterBuilder(debounceDuration: const Duration(seconds: 2));
    final userId = keyword Consumer(authProvider).userId;
    final chatId = keyword Consumer(selectedChatProvider);
    final chat = keyword QueryBuilder(ChatQuery(userId: userId, chatId: chatId, filter: filter.value));

    return Column(
      children: [
        Searchbar(onChanged: (value) => filter.value = value),
        Expanded(
          child: ChatList(chat: chat),
        ),
      ],
    );
  }
}

Qu'en est-il de "l'extraction en tant que fonction" dont nous avons parlé avec les crochets, pour créer des crochets/Builders personnalisés ?

On pourrait faire la même chose avec un tel mot-clé, en extrayant une combinaison de Builders dans une fonction :

Builder<Chat> ChatBuilder()  {
  final filter = keyword FilterBuilder(debounceDuration: const Duration(seconds: 2));
  final userId = keyword Consumer(authProvider).userId;
  final chatId = keyword Consumer(selectedChatProvider);
  final chat = keyword QueryBuilder(ChatQuery(userId: userId, chatId: chatId, filter: filter.value));

  return Builder(chat);
}

class ChatScreen extends HookWidget {
  const ChatScreen({Key key}) : super(key: key);

  <strong i="21">@override</strong>
  Widget build(BuildContext context) {
    final chat = keyword ChatBuilder();

    return Column(
      children: [
        Searchbar(onChanged: (value) => filter.value = value),
        Expanded(
          child: ChatList(chat: chat),
        ),
      ],
    );
  }
}

De toute évidence, on n'a pas beaucoup réfléchi à toutes les implications d'une telle syntaxe. Mais c'est l'idée de base.


Les crochets sont cette fonctionnalité.
Les limitations des crochets existent car ils sont implémentés comme un package plutôt que comme une fonctionnalité de langage.

Et le mot-clé est use , de sorte que keyword StreamBuilder devient use StreamBuilder , qui est finalement implémenté comme useStream

Ce code est nettement plus lisible

Je pense que c'est une question d'opinion. Je suis d'accord que certaines personnes pensent que les versions que vous décrivez comme plus lisibles sont meilleures ; personnellement, je préfère les versions beaucoup plus explicites sans magie. Mais je n'ai aucune objection à rendre possible le second style.

Cela dit, la prochaine étape consiste à travailler sur l' application de

Pour ce que ça vaut, https://github.com/flutter/flutter/issues/51752#issuecomment -670959424 est assez similaire à l'inspiration pour Hooks in React. Le modèle Builder semble identique au modèle Render Props qui prévalait auparavant dans React (mais conduisait à des arbres tout aussi profonds). Plus tard, @trueadm a suggéré du sucre de syntaxe pour les accessoires de rendu, et plus tard, cela a conduit à des crochets (pour supprimer la surcharge d'exécution inutile).

`Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}`

Si la lisibilité et l'indentation sont le problème, cela peut être réécrit comme

  <strong i="8">@override</strong>
  Widget build(context) {
    return ValueListenableBuilder<String>(
      valueListenable: someValueListenable,
      builder: (context, value, _) => buildStreamBuilder(value),
    );
  }

  StreamBuilder<int> buildStreamBuilder(String value) => StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) => buildTweenAnimationBuilder(value, value2),
      );

  Widget buildTweenAnimationBuilder(String value, AsyncSnapshot<int> value2) =>
      TweenAnimationBuilder<double>(
        duration: Duration(milliseconds: 500),
        tween: Tween(),
        builder: (context, value3, _) => Text('$value $value2 $value3'),
      );

Si les fonctions ne sont pas votre truc, ou si vous avez besoin de réutilisation, extrayez-les en tant que widgets

class NewWidget extends StatelessWidget {
  var someValueListenable;

  var someStream;

  <strong i="12">@override</strong>
  Widget build(context) {
    return ValueListenableBuilder<String>(
      valueListenable: someValueListenable,
      builder: (context, value, _) {
        return MyStreamedWidget(value, someStream);
      },
    );
  }
}

class MyStreamedWidget extends StatelessWidget {
  const MyStreamedWidget(
    this.value,
    this.someStream, {
    Key key,
  }) : super(key: key);

  final String value;
  final Stream someStream;

  <strong i="13">@override</strong>
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: someStream,
      builder: (context, value2) => MyAnimatedWidget(value, value2),
    );
  }
}

class MyAnimatedWidget extends StatelessWidget {
  final String value;
  final AsyncSnapshot<int> value2;

  const MyAnimatedWidget(
    this.value,
    this.value2, {
    Key key,
  }) : super(key: key);

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      tween: Tween(),
      builder: (context, value3, _) {
        return Text('$value $value2 $value3');
      },
    );
  }
}

Il n'y a rien dans votre exemple qui justifie un nouveau mot clé ou une nouvelle fonctionnalité.

Je sais ce que vous allez dire. La variable 'value' doit être transmise à tous les widgets/appels de fonction, mais c'est juste le résultat de la façon dont vous architectz votre application. Je décompose mon code en utilisant à la fois des méthodes "build" et des widgets personnalisés, selon le cas d'utilisation, et je n'ai jamais à passer la même variable à une chaîne de trois appels.

Le code réutilisable est réutilisable lorsqu'il repose le moins possible sur des effets secondaires externes (tels que InheritedWidgets ou état (semi) global).

@Rudiksz Je ne pense pas que vous

Il est clair qu'il y a beaucoup de gens qui voient cela comme un point douloureux fondamental, et le simple fait de faire des allers-retours est inutile. Vous n'allez pas, à travers divers arguments, convaincre les gens qu'ils ne veulent pas ce qu'ils veulent ou changer d'avis ici. Tout le monde dans cette discussion a clairement des centaines ou des milliers d'heures dans Flutter à son actif, et on ne s'attend pas à ce que nous soyons tous d'accord sur les choses.

Je pense que c'est une question d'opinion. Je suis d'accord que certaines personnes pensent que les versions que vous décrivez comme plus lisibles sont meilleures ; personnellement, je préfère les versions beaucoup plus explicites sans magie. Mais je n'ai aucune objection à rendre possible le second style.

Si c'est une question d'opinion, je suppose que c'est assez déséquilibré dans un sens.

  1. Les deux ont de la magie. Je ne sais pas nécessairement ce que font ces constructeurs en interne. La version non magique écrit le passe-partout réel à l'intérieur de ces constructeurs. L'utilisation d'un mixin SingleAnimationTIckerProvider est également magique pour 95% des développeurs de Flutter.
  2. L'un masque les noms de variables très importants qui seront utilisés plus tard dans l'arborescence, à savoir value1 et value2 , l'autre les place devant et au centre en haut de la construction. Il s'agit d'une victoire claire en matière d'analyse / maintenance.
  3. L'un a 6 niveaux d'indentation avant même que l'arborescence des widgets ne commence, l'autre a 0
  4. L'un est de 5 lignes verticales, l'autre est de 15
  5. L'un montre le contenu réel en évidence (Text ()) l'autre le cache, imbriqué, tout en bas dans l'arborescence. Une autre victoire d'analyse claire.

Dans un sens général, je pouvais voir que c'était peut-être une question de goût. Mais Flutter a spécifiquement un problème avec le nombre de lignes, l'indentation et le rapport signal:bruit en général. Bien que j'adore la possibilité de former de manière déclarative un arbre dans le code Dart, cela peut conduire à un code extrêmement illisible/verbeux, en particulier lorsque vous vous appuyez sur plusieurs couches de constructeurs encapsulés. Donc, dans le contexte de Flutter lui-même, où nous menons continuellement cette bataille, ce type d'optimisation est une fonctionnalité qui tue, car elle nous fournit un excellent outil pour lutter contre ce problème quelque peu omniprésent de verbosité générale.

TL; DR - Tout ce qui réduit considérablement l'indentation et le nombre de lignes dans Flutter est doublement précieux, en raison du nombre de lignes généralement élevé et de l'indentation inhérente à Flutter.

@Rudiksz Je ne pense pas que vous

Sauf s'il s'agit d'un changement dans le framework de base, cela m'affecte, non ?

Il est clair qu'il y a beaucoup de gens qui voient cela comme un point douloureux fondamental, et le simple fait de faire des allers-retours est inutile. Vous n'allez pas, à travers divers arguments, convaincre les gens qu'ils ne veulent pas ce qu'ils veulent ou changer d'avis ici. Tout le monde dans cette discussion a clairement des centaines ou des milliers d'heures dans Flutter à son actif, et on ne s'attend pas à ce que nous soyons tous d'accord sur les choses.

D'accord, ce cheval a déjà été battu à mort un nombre incalculable de fois, je ne tomberai donc pas dans le piège de répondre à d'autres commentaires.

Les constructeurs sont techniquement un moyen de réutiliser la logique d'état. Mais leur problème est qu'ils rendent le code peu lisible.

Ceci l'énonce parfaitement. Pour y penser en termes Flutter, nous avons besoin de constructeurs monolignes.

Des générateurs qui ne nécessitent pas des dizaines d'onglets et de lignes, mais qui permettent tout de même d'intégrer un passe-partout personnalisé dans le cycle de vie du widget.

Le mantra "tout est un widget" n'est pas spécifiquement une bonne chose ici. Le code pertinent dans un générateur n'est généralement que ses accessoires de configuration et la chose avec état qu'il renvoie dont le fxn de build a besoin. Chaque saut de ligne et chaque tabulation sont fondamentalement inutiles.

Sauf s'il s'agit d'un changement dans le framework de base, cela m'affecte, non ?

@Rudiksz Je ne pense pas que quiconque propose de changer les widgets Stateful. Vous pouvez toujours les utiliser dans leur forme actuelle si vous le souhaitez. Quelle que soit la solution que nous pourrions trouver, nous utiliserions soit des widgets Stateful sans modification, soit un autre type de widget entièrement. Nous ne disons pas que les widgets Stateful sont mauvais, nous aimerions juste un autre type de widget qui permette un état de widget plus composable. Considérez-le comme un widget Stateful qui, au lieu d'un objet d'état associé, contient plusieurs objets d'état et une fonction de construction séparée mais ayant accès à ces objets d'état. De cette façon, vous pouvez réutiliser des bits d'état commun (avec la logique d'état associée à cet état) avec leurs initState et dispose déjà implémentés pour vous. État essentiellement plus modulaire, qui peut être composé de différentes manières dans différentes situations. Encore une fois, ce n'est pas une proposition concrète, mais peut-être une autre façon de l'envisager. Peut-être que cela pourrait se transformer en une solution qui ressemble plus à flutter , mais je ne sais pas.

Cela dit, la prochaine étape consiste à travailler sur l' application de

C'est délicat car ce problème est intrinsèquement celui d'une mort par mille coupures. Cela ne fait qu'ajouter du poids et diminue la lisibilité dans la base de code. Son impact est pire dans les petits widgets, où la classe entière est inférieure à 100 lignes, et la moitié est consacrée à la gestion d'un contrôleur d'animation. Donc, je ne sais pas ce que le fait de voir 30 de ces exemples montrera, ce n'est pas le cas.

C'est vraiment la différence entre ceci :

<strong i="10">@override</strong>
  Widget build(BuildContext context) {
    final controller = get AnimationController(vsync: this, duration: widget.duration);
    //do stuff
  }

Et ça:

  AnimationController _controller;

  <strong i="14">@override</strong>
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.duration);
  }

  <strong i="15">@override</strong>
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.duration != oldWidget.duration) {
      _controller.duration = widget.duration;
    }
  }

  <strong i="16">@override</strong>
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  <strong i="17">@override</strong>
  Widget build(BuildContext context) {
    //Do Stuff
  }

Il n'y a tout simplement pas de meilleure façon d'illustrer que cela. Vous pouvez étendre ce cas d'utilisation à n'importe quel objet de type contrôleur. AnimatorController, FocusController et TextEditingController sont probablement les plus courants et les plus ennuyeux à gérer au quotidien. Maintenant, imaginez 50 ou 100 de ces saupoudrés partout dans ma base de code.

  • Vous avez environ 1 000 à 2 000 lignes qui pourraient disparaître.
  • Vous avez probablement des dizaines de bugs et de RTE (à différents stades de développement) qui n'ont jamais eu besoin d'exister car un override ou un autre manquait.
  • Vous avez des widgets qui, lorsqu'ils sont regardés avec des yeux froids, sont beaucoup plus difficiles à saisir en un coup d'œil. J'ai besoin de lire chacune de ces dérogations, je ne peux pas simplement supposer qu'elles sont passe-partout.

Et vous pouvez bien sûr étendre cela aux contrôleurs personnalisés. L'ensemble du concept d'utilisation de contrôleurs est moins attrayant dans Flutter car vous savez que vous devrez les amorcer, les gérer et les détruire comme ceci, ce qui est ennuyeux et sujet aux bugs. Cela vous permet d'éviter de créer le vôtre et de créer à la place des StatefulWidgets/Builders personnalisés. Ce serait bien si les objets de type contrôleur étaient simplement plus faciles à utiliser et plus robustes, car les constructeurs ont des problèmes de lisibilité (ou du moins, sont beaucoup plus détaillés et chargés d'espaces blancs).

C'est délicat

Oui, la conception d'API est délicate. Bienvenue dans ma vie.

Donc, je ne sais pas ce que le fait de voir 30 de ces exemples montrera, ce n'est pas le cas.

Ce ne sont pas 30 exemples qui vont aider, c'est 1 exemple qui est suffisamment élaboré pour qu'il ne puisse pas être simplifié d'une manière que vous diriez alors "bien, bien sûr, pour cet exemple qui fonctionne, mais ce n'est pas le cas pour un exemple _réel_".

Ce ne sont pas 30 exemples qui vont aider, c'est 1 exemple qui est suffisamment élaboré pour qu'il ne puisse pas être simplifié d'une manière telle que vous diriez alors "bien, bien sûr, pour cet exemple qui fonctionne, mais ce n'est pas le cas pour un exemple réel".

Je l'ai déjà dit plusieurs fois, mais cette façon de juger les crochets est injuste.
Les crochets ne visent pas à rendre possible quelque chose qui était auparavant impossible. Il s'agit de fournir une API cohérente pour résoudre ce genre de problème.

Demander une application qui montre quelque chose qui ne peut pas être simplifié différemment, c'est juger un poisson par sa capacité à grimper aux arbres.

Nous n'essayons pas seulement de juger les crochets, nous essayons d'évaluer différentes solutions pour voir s'il y en a qui répondent aux besoins de tout le monde.

(Je suis curieux de savoir de quelle manière vous évalueriez différentes propositions ici, si ce n'est en écrivant des applications dans chacune des propositions et en les comparant. Quelle mesure d'évaluation proposeriez-vous à la place ?)

Une bonne façon de juger de la solution à ce problème n'est pas une application (car chaque utilisation individuelle de l'API sera rejetée comme l'étaient les exemples ici).

Ce sur quoi nous devrions juger la solution proposée est :

  • Le code résultant est-il objectivement meilleur que la syntaxe par défaut ?

    • Évite-t-il les erreurs ?

    • Est-ce plus lisible ?

    • Est-ce plus facile d'écrire ?

  • Dans quelle mesure le code produit est-il réutilisable ?
  • Combien de problèmes peuvent être résolus avec cette API ?

    • Perdons-nous des bénéfices pour certains problèmes spécifiques ?

Lorsqu'elle est évaluée sur cette grille, la proposition Property / addDispose peut avoir un bon « le code résultant est-il meilleur ? » score, mais mal évalué par rapport à la réutilisabilité et à la flexibilité.

Je ne sais pas comment répondre à ces questions sans voir chaque proposition réellement utilisée.

Pourquoi?

Je n'ai pas eu besoin de faire des applications en utilisant Property pour savoir que cette proposition aura du mal à produire du code vraiment réutilisable et à résoudre de nombreux problèmes.

Nous pouvons prendre n'importe quel *Builder existant et essayer de les réimplémenter en utilisant la solution proposée.
Nous pouvons également essayer de réimplémenter les hooks listés dans ce fil, ou certains hooks créés dans la communauté React (il existe de nombreuses compilations de hooks disponibles en ligne).

Je n'ai pas eu besoin de faire des applications en utilisant Property pour savoir que cette proposition aura du mal à produire du code vraiment réutilisable et à résoudre de nombreux problèmes.

Malheureusement, je ne partage pas votre instinct ici (comme en témoigne le fait que je pensais que Property (https://github.com/flutter/flutter/issues/51752#issuecomment-667737471) fonctionnait très bien jusqu'à ce que vous disiez qu'il devait gérer des valeurs à la fois à partir d'un widget et d'un état local avec la même API, ce dont je ne savais pas qu'il s'agissait d'une contrainte jusqu'à ce que vous l'appeliez). Si je fournis une version de Property qui résout également ce problème, est-ce que ça va définitivement aller, ou y aura-t-il un nouveau problème qu'il ne couvrira pas ? Sans objectif, nous convenons tous que c'est l'objectif, je ne sais pas pourquoi nous concevons des solutions.

Nous pouvons prendre n'importe quel *Builder existant et essayer de les réimplémenter en utilisant la solution proposée.

Clairement pas _n'importe_. Par exemple, vous en avez donné un dans le PO, et lorsque j'ai fait ma première proposition de propriété (https://github.com/flutter/flutter/issues/51752#issuecomment-664787791) vous avez signalé des problèmes qui n'étaient pas illustrés par le constructeur d'origine.

Nous pouvons également essayer de réimplémenter les hooks listés dans ce fil, ou certains hooks créés dans la communauté React (il existe de nombreuses compilations de hooks disponibles en ligne).

Je me fiche de savoir par où nous commençons. Si vous avez un exemple particulièrement bon qui, selon vous, illustre le problème et qu'il utilise des crochets, alors super, ajoutons-le au référentiel de

Ce ne sont pas 30 exemples qui vont aider, c'est 1 exemple qui est suffisamment élaboré pour qu'il ne puisse pas être simplifié d'une manière que vous diriez alors "bien, bien sûr, pour cet exemple qui fonctionne, mais ce n'est pas le cas pour un exemple _réel_".

Il n'y aura jamais rien de plus élaboré que le simple désir d'utiliser un AnimatorController (ou tout autre composant avec état réutilisable auquel vous pouvez penser) sans un constructeur illisible ou un tas de passe-partout de cycle de vie sujet aux bogues.

Aucune solution n'a été proposée pour répondre aux avantages de lisibilité et de robustesse demandés, de manière générale.

J'insiste sur le fait que _n'importe quel_ constructeur fera l'affaire, car ce problème pourrait être renommé en "Nous avons besoin de sucre de syntaxe pour les constructeurs" et conduire à la même discussion.

Tous les autres arguments avancés (tels que la création et la suppression d'un AnimationController ) sont basés sur le fait qu'ils pourraient également être extraits dans un générateur :

Widget build(context) {
  return AnimationControllerBuilder(
    duration: Duration(seconds: 2),
    builder: (context, animationController) {

    }
  );
}

Au final, je pense que l'exemple parfait est d'essayer de réimplémenter StreamBuilder dans son intégralité et de le tester dans différents scénarios :

  • le flux provient du Widget
  • // à partir d'un InheritedWidget
  • de l'Etat local

et testez chaque cas individuel par rapport à « le flux peut changer au fil du temps », donc :

  • didUpdateWidget avec un nouveau flux
  • le InheritedWidget mis à jour
  • nous avons appelé setState

Ceci est actuellement insoluble avec Property ou onDispose

@esDotDev Pouvez-vous énumérer « les avantages de lisibilité et de robustesse demandés » ? Si quelqu'un fait une proposition qui gère AnimationController avec ces avantages en termes de lisibilité et de robustesse, alors nous avons terminé ?

@rrousselGit Je ne

Mais si quelqu'un devait créer une solution qui fait tout ce que fait StreamBuilder, mais sans l'indentation, serait-ce ça ? Seriez-vous heureux ?

Très probablement oui

Il faudrait bien sûr comparer cette solution à d'autres solutions. Mais cela atteindrait le niveau acceptable.

@esDotDev Pouvez-vous énumérer « les avantages de lisibilité et de robustesse demandés » ?

Robustesse dans la mesure où il peut entièrement encapsuler le passe-partout autour des dépendances et du cycle de vie. c'est à dire. Je ne devrais pas avoir à dire à fetchUser à chaque fois qu'il devrait probablement se reconstruire lorsque l'identifiant change, il sait le faire en interne. Ne devrait pas avoir à dire à une animation de se supprimer chaque fois que son widget parent est supprimé, etc. (je ne comprends pas parfaitement si la propriété peut faire cela). Cela empêche les développeurs de commettre des erreurs pour les tâches par cœur, dans toute la base de code (l'un des principaux avantages de l'utilisation de Builders actuellement imo)

La lisibilité étant que nous pouvons obtenir la chose avec état avec une seule ligne de code non indenté, et la variable de la chose est hissée et clairement visible.

@esDotDev Si quelqu'un fait une proposition qui gère AnimationController avec ces avantages de lisibilité et de robustesse, alors nous avons terminé ici ?

Si vous voulez dire spécifiquement AnimationController no. Si vous voulez dire n'importe quel objet de type AnimationController/FocusController/TextEditingController, alors oui.

Avoir une API de type fonction qui renvoie une valeur qui a une durée de vie définie qui n'est pas claire est

Je pense que c'est un malentendu essentiel. La durée de vie d'un crochet est claire, car ce sont par définition des sous-états. Ils existent _toujours_ pour la durée de vie de l'Etat qui les "utilise". Cela ne pourrait pas être plus clair en fait. La syntaxe est peut-être étrange et inconnue, mais elle ne manque certainement pas de clarté.

Semblable à la façon dont la durée de vie d'un TweenAnimationBuilder() est également claire. Il s'en ira quand son parent s'en ira. Comme un widget enfant, ce sont des états enfants. Ce sont des "composants" d'état entièrement indépendants, nous pouvons les assembler et les réutiliser facilement et nous ne gérons explicitement pas leur durée de vie car nous voulons qu'ils soient naturellement liés à l'état dans lequel ils sont déclarés, une fonctionnalité et non un bogue.

@esDotDev

etc

Peux-tu être plus précis? (C'est pourquoi j'ai suggéré de créer une application de démonstration qui couvrait toutes les bases. Je continue de penser que c'est la meilleure façon de le faire.) ressources allouées lorsque l'élément hôte est supprimé ?

Objet de type TextEditingController

Peux-tu être plus précis? TextEditingController est-il plus élaboré qu'AnimationController d'une manière ou d'une autre ?


@rrousselGit

Mais si quelqu'un devait créer une solution qui fait tout ce que fait StreamBuilder, mais sans l'indentation, serait-ce ça ? Seriez-vous heureux ?

Très probablement oui

Voici une solution qui fait tout ce que fait StreamBuilder, sans aucune indentation :

Widget build(context) {
  var result = Text('result:');
  var builder1 = (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Row(children: [result, Text(snapshot.data)]);
  };
  result = StreamBuilder(stream: _stream1, builder: builder1);
  var builder2 = (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Column(children: [result, Text(snapshot.data)]);
  };
  result = StreamBuilder(stream: _stream2, builder: builder2);
}

Je suppose que cela viole une autre contrainte, cependant. C'est pourquoi je préférerais avoir quelque chose dont nous puissions tous convenir, c'est une description complète du problème avant d'essayer de le résoudre.

Ce sont simplement les mêmes contraintes que les constructeurs ont @Hixie, personne ne demande plus que cela. Un générateur peut être lié à widget.whatever, un générateur peut gérer entièrement tout état interne requis dans le contexte de l'arborescence des widgets. C'est tout ce qu'un crochet peut faire, et tout ce que n'importe qui demande un micro-état ou peu importe comment vous voulez l'appeler.

Peux-tu être plus précis? TextEditingController est-il plus élaboré qu'AnimationController d'une manière ou d'une autre ?

Non, mais cela peut faire différentes choses dans init/dispose, ou il se liera à différentes propriétés, et je voudrais encapsuler ce passe-partout spécifique.

@esDotDev Vous voulez donc la même chose qu'un constructeur, mais sans l'indentation et sur une seule ligne (sans le rappel du constructeur lui-même, vraisemblablement) ? L'exemple que je viens de publier (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483) fait cela avec les constructeurs aujourd'hui, donc il y a probablement des contraintes supplémentaires au-delà de cela?

(FWIW, je ne pense pas que les constructeurs, ou quelque chose comme les constructeurs mais sur une seule ligne, soient une bonne solution, car ils nécessitent que chaque type de données ait son propre constructeur créé pour cela; il n'y a pas de bon moyen d'en créer un à la volée .)

(FWIW, je ne pense pas que les constructeurs, ou quelque chose comme les constructeurs mais sur une seule ligne, soient une bonne solution, car ils nécessitent que chaque type de données ait son propre constructeur créé pour cela; il n'y a pas de bon moyen d'en créer un à la volée .)

Je ne comprends pas ce que cela signifie. Pourriez-vous le reformuler ? ??

Vous devez créer un AnimationBuilder pour les animations et un StreamBuilder pour les flux et ainsi de suite. Au lieu de n'avoir qu'un seul Builder et de dire "voici comment en obtenir un nouveau, voici comment le disposer, voici comment extraire les données", etc. lorsque vous créez votre StatefulWidget.

Je suppose que cela viole une autre contrainte, cependant. C'est pourquoi je préférerais avoir quelque chose dont nous puissions tous convenir, c'est une description complète du problème avant d'essayer de le résoudre.

Je pense que cela viole assez manifestement toute demande de code lisible, ce qui est en fin de compte le but ici, sinon nous utiliserions tous un million de constructeurs spécifiquement typés, les imbriquerions pour toujours et l'appellerions un jour.

Je pense que ce qui est demandé est quelque chose comme (supportez-moi, je n'utilise pas beaucoup Streams):

Widget build(context) {
   var snapshot1 = get AsyncSnapshot<int>(stream1);
   var snapshot2 = get AsyncSnapshot<int>(stream2);
   return Column(children: [Text(snapshot1.data), Text(snapshot2.data)]);
}

C'est tout le code. Il n'y aurait rien de plus, comme le Stream est créé pour nous, le stream est disposé pour nous, nous ne pouvons pas nous tirer une balle dans le pied ET le code est beaucoup plus lisible.

Vous devez créer un AnimationBuilder pour les animations et un StreamBuilder pour les flux et ainsi de suite.

Je ne vois pas cela comme un problème. Nous avons déjà RestorableInt vs RestorableString vs RestorableDouble

Et les génériques peuvent résoudre cela :

GenericBuilder<Stream<int>>(
  create: (ref) {
    var controller = StreamController<int>();
    ref.onDispose(controller.close);
    return controller.stream;
  }
  builder: (context, Stream<int> stream) {

  }
)

De même, Flutter ou Dart pourraient inclure une interface Disposable si cela pose vraiment un problème.

@esDotDev

Je pense que ce qui est demandé est quelque chose comme:

Cela violerait certaines des contraintes assez raisonnables que d'autres ont énumérées (par exemple @Rudiksz), à savoir garantir qu'aucun code d'initialisation ne se produit lors de l'appel à la méthode de construction.

@rrousselGit

Je ne vois pas cela comme un problème. Nous avons déjà RestorableInt vs RestorableString vs RestorableDouble

Et nous avons AnimationBuilder et StreamBuilder et ainsi de suite, oui. Dans les deux cas c'est dommage.

GénériqueBuilder

C'est similaire à ce que j'ai proposé pour Property, mais si j'ai bien compris vos préoccupations, vous avez pensé que c'était trop verbeux.

Plus tôt, vous avez dit que si quelqu'un créait une solution qui faisait tout ce que fait StreamBuilder, mais sans l'indentation, vous seriez probablement heureux. Vous n'avez pas commenté ma tentative de le faire (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483). Êtes-vous satisfait de cette solution?

@esDotDev

Je pense que ce qui est demandé est quelque chose comme:

Cela violerait certaines des contraintes assez raisonnables que d'autres ont énumérées (par exemple @Rudiksz), à savoir garantir qu'aucun code d'initialisation ne se produit lors de l'appel à la méthode de construction.

Il n'est pas important que ce code soit dans build. La partie importante est que

  1. Je ne suis pas obligé de mettre mon arbre en retrait ou d'ajouter une tonne de lignes supplémentaires.
  2. Le code de cycle de vie spécifique à cette chose est encapsulé.

Ce serait incroyable :

AsyncSnapshot<int> snapshot1 = createLifecycleState(widget.stream1);
AsyncSnapshot<int> snapshot2 = createLifecycleState(widget.stream2);
AniamtorController animator = createLifecycleState(duration: Duration(seconds: 1), (a)=>a.forward());

Widget build(context) {
   return Opacity(opacity: animator.value, child: Column(children: [Text(snapshot1.data), Text(snapshot2.data)]));
}

Ou, pas aussi succinct, mais toujours beaucoup plus lisible que d'utiliser des constructeurs, et moins détaillé et sujet aux erreurs que de le faire directement :

AsyncSnapshot<int> stream1;
AsyncSnapshot<int> stream2;
<strong i="18">@override</strong> 
void initState(){
    snapshot1 = createLifecycleState(widget.stream1);
    snapshot2 = createLifecycleState(widget.stream2);
   super.initState();
}

Widget build(context) {
   return Column(children: [Text(snapshot1.data), Text(snapshot2.data)]);
}

Je ne comprends pas pourquoi nous revenons sans cesse à la verbosité.
J'ai explicitement dit à plusieurs reprises que ce n'était pas le problème et que le problème était la réutilisabilité contre la lisibilité contre la flexibilité.

J'ai même fait une grille pour évaluer la solution https://github.com/flutter/flutter/issues/51752#issuecomment -671000137 et un cas de test https://github.com/flutter/flutter/issues/51752#issuecomment - 671002248


Plus tôt, vous avez dit que si quelqu'un créait une solution qui faisait tout ce que fait StreamBuilder, mais sans l'indentation, vous seriez probablement heureux. Vous n'avez pas commenté ma tentative de le faire (#51752 (commentaire)). Êtes-vous satisfait de cette solution?

Cela atteint le niveau minimum acceptable de flexibilité.

Son évaluation selon https://github.com/flutter/flutter/issues/51752#issuecomment -671000137 donne :

  • Le code résultant est-il objectivement meilleur que la syntaxe par défaut ?

    • Évite-t-il les erreurs ?

      _La syntaxe par défaut (StreamBuilder sans piratage) est moins sujette aux erreurs. Cette solution n'évite pas les erreurs, elle crée des_ ❌

    • Est-ce plus lisible ?

      _Ce n'est clairement pas plus lisible_ ❌

    • Est-ce plus facile d'écrire ?

      _Ce n'est pas facile d'écrire_ ❌

  • Dans quelle mesure le code produit est-il réutilisable ?
    _StreamBuilder n'est pas lié au Widget/State/life-cycles, donc ce pass_ ✅
  • Combien de problèmes peuvent être résolus avec cette API ?
    _Nous pouvons faire des constructeurs personnalisés et utiliser ce modèle. Donc ce pass_. ??
  • Perdons-nous des bénéfices pour certains problèmes spécifiques ?
    _Non, la syntaxe est relativement cohérente_. ??
  1. Cette fonctionnalité IMO pourrait s'étendre à tous les widgets du constructeur, y compris LayoutBuilder par exemple.
  2. Il doit y avoir un moyen de désactiver l'écoute, afin que vous puissiez créer des contrôleurs 10x et les transmettre aux feuilles pour la reconstruction, ou flutter doit savoir quelle partie de l'arbre a utilisé la valeur obtenue par le constructeur.
  3. L'utiliser ne devrait pas être beaucoup plus verbeux que les crochets.
  4. Le compilateur doit être étendu pour gérer cela correctement.
  5. Des aides au débogage sont nécessaires. Disons que vous mettez des points d'arrêt dans l'un de vos widgets qui utilise cela. Lorsque vous atteignez un point d'arrêt dans une méthode de construction, car l'un des générateurs s'est déclenché, l'IDE pouvait afficher des informations supplémentaires pour chaque générateur utilisé :
Widget build(context) {
   // this builder is not highlighted, but you can hover over it to see how often it rebuilds, how heavy were those rebuilds, and when was the last rebuild
   var snapshot1 = keyword StreamBuilder(stream1);
   // this builder will be highlighted because it triggered the rebuild
   var constraints = keyword LayoutBuilder(); 

   // <-- I had a breakpoint here and parent constraint changed, breakpoints got reached.
   return Column(children: [Text(snapshot1.data), Text(snapshot2.data)]);
}

Aussi, @Hixie

Cela violerait certaines des contraintes assez raisonnables que d'autres ont énumérées (par exemple @Rudiksz), à savoir garantir qu'aucun code d'initialisation ne se produit lors de l'appel à la méthode de construction.

Nous le faisons déjà implicitement en utilisant *Builders. Nous avons juste besoin d'un sucre de syntaxe pour les désindenter. C'est un peu comme async/wait et futures, je pense.

@esDotDev Ce que vous décrivez ressemble beaucoup à ce que j'ai proposé précédemment avec Property (voir par exemple https://github.com/flutter/flutter/issues/51752#issuecomment-664787791 ou https://github.com/flutter/flutter/ issues/51752#issuecomment-667737471). Y a-t-il quelque chose qui empêche ce type de solution d'être créé en tant que package ? C'est-à-dire, quel changement le framework de base aurait-il besoin d'y apporter pour vous permettre d'utiliser ce type de fonctionnalité ?

@rrousselGit Comme pour Shawn, je vous demanderais alors la même chose. Si la seule différence entre la fonctionnalité actuelle de StreamBuilder et celle qui satisferait vos besoins est une syntaxe différente, qu'exigez-vous de la syntaxe de base pour vous permettre d'utiliser une telle fonctionnalité ? Ne serait-il pas suffisant de créer la syntaxe que vous préférez et de l'utiliser ?

Le problème que j'ai avec votre grille est que si je devais l'appliquer aux solutions jusqu'à présent, j'obtiendrais ceci, ce qui, je suppose, est très différent de ce que vous obtiendriez :

StatefulWidget

  • Le code résultant est-il objectivement meilleur que la syntaxe par défaut ?

    • Évite-t-il les erreurs ?

      _C'est la même que la syntaxe par défaut, qui n'est pas particulièrement sujette aux erreurs._ 🔷

    • Est-ce plus lisible ?

      _C'est pareil, donc c'est tout aussi lisible, ce qui est raisonnablement lisible._ 🔷

    • Est-ce plus facile d'écrire ?

      _C'est la même chose, donc c'est tout aussi facile à écrire, ce qui est raisonnablement facile._ 🔷

  • Dans quelle mesure le code produit est-il réutilisable ?
    _Il y a très peu de code à réutiliser._ ✅
  • Combien de problèmes peuvent être résolus avec cette API ?
    _C'est la ligne de base._ 🔷
  • Perdons-nous des bénéfices pour certains problèmes spécifiques ?
    _Ça n'a pas l'air de ça._ ✅

Variations sur la propriété

  • Le code résultant est-il objectivement meilleur que la syntaxe par défaut ?

    • Évite-t-il les erreurs ?

      _Il déplace le code à un endroit différent, mais ne réduit pas particulièrement le nombre d'erreurs._ 🔷

    • Est-ce plus lisible ?

      _Il met le code d'initialisation et le code de nettoyage et d'autres codes de cycle de vie au même endroit, donc c'est moins clair._ ❌

    • Est-ce plus facile d'écrire ?

      _Il mélange le code d'initialisation et le code de nettoyage et d'autres codes de cycle de vie, il est donc plus difficile à écrire._ ❌

  • Dans quelle mesure le code produit est-il réutilisable ?
    _Exactement aussi réutilisable que StatefulWidget, juste à des endroits différents._ ✅
  • Combien de problèmes peuvent être résolus avec cette API ?
    _C'est du sucre syntaxique pour StatefulWidget, donc pas de différence._ 🔷
  • Perdons-nous des bénéfices pour certains problèmes spécifiques ?
    _Les performances et l'utilisation de la mémoire en souffriraient légèrement._ ❌

Variations sur les constructeurs

  • Le code résultant est-il objectivement meilleur que la syntaxe par défaut ?

    • Évite-t-il les erreurs ?

      _Il s'agit essentiellement de la solution StatefulWidget mais factorisée ; les erreurs devraient être à peu près équivalentes._ 🔷

    • Est-ce plus lisible ?

      _Les méthodes de construction sont plus complexes, le reste de la logique se déplace vers un widget différent, donc à peu près le même._ 🔷

    • Est-ce plus facile d'écrire ?

      _Plus difficile à écrire la première fois (création du widget constructeur), légèrement plus facile par la suite, donc à peu près la même chose._ 🔷

  • Dans quelle mesure le code produit est-il réutilisable ?
    _Exactement aussi réutilisable que StatefulWidget, juste à des endroits différents._ ✅
  • Combien de problèmes peuvent être résolus avec cette API ?
    _C'est du sucre syntaxique pour StatefulWidget, donc la plupart du temps aucune différence. Dans certains aspects, c'est en fait mieux, par exemple, cela réduit la quantité de code à exécuter lors de la gestion d'un changement de dépendance._ ✅
  • Perdons-nous des bénéfices pour certains problèmes spécifiques ?
    _Ça n'a pas l'air de ça._ ✅

Solutions de type crochet

  • Le code résultant est-il objectivement meilleur que la syntaxe par défaut ?

    • Évite-t-il les erreurs ?

      _Encourage les mauvais modèles (par exemple la construction dans la méthode de construction), risque de générer des bogues en cas d'utilisation accidentelle avec des conditions._ ❌

    • Est-ce plus lisible ?

      _Augmente le nombre de concepts qui doivent être connus pour comprendre une méthode de construction._ ❌

    • Est-ce plus facile d'écrire ?

      _Le développeur doit apprendre à écrire des crochets, ce qui est un nouveau concept, donc plus difficile._ ❌

  • Dans quelle mesure le code produit est-il réutilisable ?
    _Exactement aussi réutilisable que StatefulWidget, juste à des endroits différents._ ✅
  • Combien de problèmes peuvent être résolus avec cette API ?
    _C'est du sucre syntaxique pour StatefulWidget, donc pas de différence._ 🔷
  • Perdons-nous des bénéfices pour certains problèmes spécifiques ?
    _Les performances et l'utilisation de la mémoire souffrent._ ❌

Je ne comprends pas pourquoi nous revenons sans cesse à la verbosité.
J'ai explicitement dit à plusieurs reprises que ce n'était pas le problème et que le problème était la réutilisabilité contre la lisibilité contre la flexibilité.

Mes excuses, je ne me suis pas souvenu de qui c'était qui a dit que la propriété était trop bavarde. Vous avez raison, votre préoccupation était simplement qu'il y avait un nouveau cas d'utilisation qui n'avait pas été répertorié auparavant et qu'il n'a pas géré, bien que je pense qu'il serait trivial d'étendre Property pour gérer ce cas d'utilisation également (j'ai pas essayé, il semble préférable d'attendre d'avoir une application de démonstration claire afin de pouvoir résoudre les problèmes une fois pour toutes plutôt que d'avoir à répéter à plusieurs reprises au fur et à mesure que les exigences sont ajustées).

@szotp

  1. Cette fonctionnalité IMO pourrait s'étendre à tous les widgets du constructeur, y compris LayoutBuilder par exemple.

LayoutBuilder est un widget très différent de la plupart des constructeurs, FWIW. Aucune des propositions qui ont été discutées jusqu'à présent ne fonctionnerait pour des problèmes de type LayoutBuilder, et aucune des exigences décrites avant votre commentaire n'inclut LayoutBuilder. Si nous devions également utiliser cette nouvelle fonctionnalité pour gérer LayoutBuilder, il est important de le savoir ; Je recommande de travailler avec @TimWhiting pour vous assurer que l'exemple d'application sur lequel nous allons baser les propositions inclut des générateurs de mise en page à titre d'exemple.

  1. Il doit y avoir un moyen de désactiver l'écoute, afin que vous puissiez créer des contrôleurs 10x et les transmettre aux feuilles pour la reconstruction, ou flutter doit savoir quelle partie de l'arbre a utilisé la valeur obtenue par le constructeur.

Je ne sais pas exactement ce que cela signifie. Pour autant que je sache, vous pouvez le faire aujourd'hui avec des auditeurs et des constructeurs (par exemple, j'utilise ValueListenableBuilder dans l'application que j'ai citée plus tôt pour faire exactement cela).

Cela violerait certaines des contraintes assez raisonnables que d'autres ont énumérées (par exemple @Rudiksz), à savoir garantir qu'aucun code d'initialisation ne se produit lors de l'appel à la méthode de construction.

Nous le faisons déjà implicitement en utilisant *Builders.

Je ne pense pas que ce soit exact. Cela dépend du générateur, mais certains travaillent très dur pour séparer initState, didChangeDependencies, didUpdateWidget et la logique de génération, afin que seule la quantité minimale de code ait besoin d'exécuter chaque génération en fonction de ce qui a changé. Par exemple, ValueListenableBuilder enregistre uniquement les écouteurs lors de sa création initiale, son générateur peut être réexécuté sans que le parent ou l'initState du générateur ne soit réexécuté. Ce n'est pas quelque chose que Hooks peut faire.

@esDotDev Ce que vous décrivez ressemble beaucoup à ce que j'ai proposé plus tôt avec Property (voir par exemple #51752 (commentaire) ou #51752 (commentaire) ).

Si je comprends bien, nous pourrions créer UserProperty qui gère automatiquement le DidDependancyChange pour UserId, ou AnimationProperty , ou toute autre propriété dont nous avons besoin pour gérer l'init/update/dispose pour ce type ? Ensuite, cela me semble sympa. Les cas d'utilisation les plus courants pourraient être rapidement créés.

La seule chose qui me décourage, c'est le futur constructeur ici. Mais je pense que c'est juste dû à l'exemple que vous avez choisi ?

Par exemple, pourrais-je créer ceci ?

class _ExampleState extends State<Example> with PropertyManager {
  AnimationProperty animProp1;
  AnimationProperty animProp2;

  <strong i="15">@override</strong>
  void initProperties() {
    super.initProperties();
    anim1= register(anim1, AnimationProperty (
      AnimationController(duration: Duration(seconds: 1)),
      initState: (animator) => animator.forward()
      // Not dealing with updates or dispose here, cause AnimationProperty handles it
    ));
   anim2 = register(anim2, AnimationProperty(
       AnimationController(duration: Duration(seconds: 2))..forward(),
   ));
  }

  <strong i="16">@override</strong>
  Widget build(BuildContext context) {
    return Column(children: [
       FadeTransition(opacity: anim1, child: ...),
       FadeTransition(opacity: anim2, child: ...),
   ])
  }
}

Si c'est le cas, c'est totalement LGTM ! En termes d'ajout au framework, il s'agit de savoir si cela doit être promu à une approche syntaxique de première classe (ce qui signifie qu'il devient une pratique générale dans un an environ), ou s'il existe en tant que plugin qu'un pourcentage à un chiffre de développeurs utilisation.

Il s'agit de savoir si vous souhaitez pouvoir mettre à jour les exemples détaillés et (légèrement ?) sujets aux erreurs avec une syntaxe meilleure et plus concise. Devoir synchroniser manuellement les propriétés et disposer manuellement les choses () entraîne des bogues et une charge cognitive.

Imo, ce serait bien si un développeur pouvait utiliser des animateurs, avec didUpdate et disposer et debugFillProperties appropriés et l'ensemble des travaux, sans jamais avoir à y réfléchir à deux fois (exactement comme nous le faisons lorsque nous utilisons TweenAnimationBuilder maintenant, qui est la principale raison pour laquelle nous recommandons tous nos développeurs l'utilisent par défaut plutôt que de gérer manuellement les animateurs).

Si c'est le cas, c'est totalement LGTM ! En termes d'ajout au framework, il s'agit de savoir si cela doit être promu à une approche syntaxique de première classe (ce qui signifie qu'il devient une pratique générale dans un an environ), ou s'il existe en tant que plugin qu'un pourcentage à un chiffre de développeurs utilisation.

Étant donné à quel point Property est trivial, ma recommandation à quelqu'un qui aime ce style serait de créer le leur (peut-être en commençant par mon code si cela peut aider) et de l'utiliser directement dans leur application comme bon leur semble, en l'ajustant pour répondre à leurs besoins. Il pourrait être transformé en un package si beaucoup de gens l'aiment aussi, mais encore une fois pour quelque chose d'aussi trivial, je ne sais pas à quel point c'est bénéfique par rapport à simplement le copier dans son code et l'ajuster au besoin.

La seule chose qui me décourage, c'est le futur constructeur ici. Mais je pense que c'est juste dû à l'exemple que vous avez choisi ?

J'essayais de répondre à un exemple donné par @rrousselGit . En principe, il peut être adapté pour fonctionner pour n'importe quoi.

Par exemple, pourrais-je créer ceci ?

Vous voudriez déplacer le constructeur AnimationController dans une fermeture qui serait appelée, plutôt que de l'appeler à chaque fois, puisque initProperties est appelé pendant le rechargement à chaud pour obtenir les nouvelles fermetures mais généralement vous ne voulez pas créer un nouveau contrôleur pendant le rechargement à chaud (par exemple, cela réinitialiserait les animations). Mais oui, à part ça, ça a l'air bien. Vous pouvez même créer un AnimationControllerProperty qui prend les arguments du constructeur AnimationController et fait la bonne chose avec eux (par exemple, mettre à jour la durée lors du rechargement à chaud si elle change).

Imo, ce serait bien si un développeur pouvait utiliser des animateurs, avec didUpdate et disposer et debugFillProperties appropriés et l'ensemble des travaux, sans jamais avoir à y réfléchir à deux fois (exactement comme nous le faisons lorsque nous utilisons TweenAnimationBuilder maintenant, qui est la principale raison pour laquelle nous recommandons tous nos développeurs l'utilisent par défaut plutôt que de gérer manuellement les animateurs).

Ce qui m'inquiète que les développeurs n'y pensent pas, c'est que si vous ne pensez pas au moment où les choses sont allouées et éliminées, vous êtes plus susceptible d'allouer beaucoup de choses dont vous n'avez pas toujours besoin, ou d'exécuter une logique qui n'en a pas besoin. pas besoin de s'exécuter ou de faire d'autres choses qui conduisent à un code moins efficace. C'est l'une des raisons pour lesquelles je serais réticent à en faire un style recommandé par défaut.

Vous pouvez même créer un AnimationControllerProperty qui prend les arguments du constructeur AnimationController et fait la bonne chose avec eux (par exemple, mettre à jour la durée lors du rechargement à chaud si elle change).

Merci @Hixie, c'est vraiment cool et je pense que cela résout assez bien le problème.

Je ne suggère pas que les développeurs ne devraient jamais penser à ces choses, mais je pense que le cas d'utilisation à 99% de ces choses est presque toujours lié au StatefulWidget dans lequel ils sont utilisés, et faire autre chose que cela vous amène déjà dans le domaine des développeurs intermédiaires.

Encore une fois, je ne vois pas en quoi cela est fondamentalement différent de recommander TweenAnimationBuilder sur AnimatorController brut. C'est essentiellement l'idée que SI vous voulez que l'état soit entièrement contenu/géré dans cet autre état (et c'est généralement ce que vous voulez), alors faites-le de cette façon, c'est plus simple et plus robuste.

À ce stade, nous devons organiser un appel et en discuter avec les différentes parties intéressées.
Parce que cette discussion n'avance pas, car nous répondons encore et encore à la même question.

Je ne comprends pas comment, après une si longue discussion, avec autant d'arguments avancés, nous pouvons toujours affirmer que les Builders n'évitent pas les erreurs par rapport à StatefulWidget, ou que les hooks ne sont pas plus réutilisables que les StatefulWidgets bruts.

C'est particulièrement frustrant d'argumenter étant donné que tous les principaux frameworks déclaratifs (React, Vue, Swift UI, Jetpack Compose) ont une manière ou une autre de résoudre ce problème de manière native.
Il semble que seul Flutter refuse de considérer ce problème.

@esDotDev La principale raison à mon humble avis d'utiliser un AnimationBuilder ou TweenAnimationBuilder ou ValueListenableBuilder est qu'ils ne reconstruisent que lorsque la valeur change, sans reconstruire le reste de leur widget hôte . C'est une question de performance. Il ne s'agit pas vraiment de verbosité ou de réutilisation de code. Je veux dire, c'est bien de les utiliser pour ces raisons aussi, si vous les trouvez utiles pour ces raisons, mais ce n'est pas le cas d'utilisation principal, du moins pour moi. C'est aussi quelque chose que Property (ou Hooks) ne vous donne pas. Avec ceux-ci, vous finissez par reconstruire le widget _entier_ lorsque quelque chose change, ce qui n'est pas bon pour les performances.

@rrousselGit

Il semble que seul Flutter refuse de considérer ce problème.

J'ai passé littéralement des heures de mon temps ce week-end, sans parler de nombreuses heures du temps de Google avant cela, à considérer ce problème, à décrire des solutions possibles et à essayer de définir précisément ce que nous essayons de résoudre. S'il vous plaît, ne confondez pas le manque de compréhension sur ce qui est un problème avec le refus de le considérer. Surtout quand j'ai déjà décrit la meilleure chose à faire pour faire avancer les choses (créer une application de démonstration qui a toute la logique d'état qui est "trop ​​verbeuse ou trop difficile", pour citer le titre du problème, à réutiliser), qui d'autres sur ce bogue ont entrepris une tâche, et à laquelle vous avez refusé de participer.

The main reason IMHO to use an AnimationBuilder or TweenAnimationBuilder or ValueListenableBuilder is that they rebuild only when the value changes, without rebuilding the rest of their host widget. It's a performance thing.

Intéressant. Pour nous, nous n'avons vraiment jamais mesuré ou observé une amélioration des performances en sauvegardant de petites reconstructions comme celle-ci. Il est beaucoup plus important sur une grande base de code d'application de garder le code succinct, lisible et exempt de toute erreur de routine qui peut se produire lorsque vous supprimez des centaines de fichiers de classe toutes les deux semaines.

D'après notre expérience, le coût de repeindre les pixels, qui semble arriver à l'arbre complet à moins que vous ne soyez déterminé à définir vos RepaintBoundaries, est un facteur beaucoup plus important dans les performances du monde réel que les coûts de mise en page partielle des widgets. Surtout lorsque vous entrez dans la plage des moniteurs 4k.

Mais c'est un bon exemple de cas où les constructeurs ont un sens pour ce genre de chose. Si je veux créer un sous-contexte, alors un constructeur a du sens et est un bon moyen d'y arriver.

Souvent, nous ne le faisons pas, et dans ce cas, Builder ne fait qu'ajouter du fouillis, mais nous l'acceptons, car l'alternative est juste un autre type de fouillis, et au moins avec Builder, les choses sont plus ou moins garanties sans bogue. Dans les cas où la vue entière est reconstruite, ou où il n'y a pas nécessairement de reconstruction de vue du tout (TextEditingController, FocusController), l'utilisation d'un générateur n'a pas de sens, et dans tous les cas, faire rouler le passe-partout à la main n'est fondamentalement pas SEC.

C'est certainement très spécifique à la situation, comme le sont souvent les problèmes de performances. Je pense qu'il est logique que les gens utilisent quelque chose comme Hooks ou Property s'ils aiment ce style. C'est possible aujourd'hui et ne semble pas avoir besoin de quoi que ce soit de plus du framework (et comme le montre Property, cela ne nécessite vraiment pas beaucoup de code).

Non, mais c'est un peu comme demander à la communauté de construire TweenAnimationBuilder et ValueListenableBuilder et ne pas leur fournir un StatefulWidget sur lequel s'appuyer.

Ce n'est pas ce que vous demandez, mais l'un des principaux avantages de ce type d'architecture est qu'elle se prête naturellement à de minuscules composants pouvant être facilement partagés. Si vous mettez une petite pièce fondamentale en place...

StatefulWidget est un _lot_ de code, comparé à Property, et il n'est pas trivial (contrairement à Property, qui est principalement du code de colle). Cela dit, si la propriété est quelque chose qui a du sens à réutiliser largement (par opposition à la création de versions sur mesure pour chaque application ou équipe en fonction de ses besoins précis), alors j'encouragerais quelqu'un qui préconise son utilisation à créer un package et à le télécharger sur pub . Il en va de même, en effet, pour les Hooks. Si c'est quelque chose que la communauté aime, il sera très utilisé, tout comme le fournisseur. Il n'est pas clair pourquoi nous aurions besoin de mettre quelque chose comme ça dans le cadre lui-même.

Je suppose que c'est intrinsèquement extensible. Le fournisseur n'est pas, c'est juste un outil simple. C'est quelque chose qui est fait pour être étendu, tout comme StatefulWidget, mais pour StatefulComponents. Le fait qu'il soit relativement anodin ne doit-il pas nécessairement lui être reproché ?

Une note sur "ceux qui préfèrent ce style". Si vous pouvez enregistrer 3 remplacements et 15 à 30 lignes, ce sera juste un gain de lisibilité dans la plupart des cas. Objectivement parlant imo. Il élimine également objectivement 2 classes entières d'erreurs (oubli de disposer des choses, oubli de mettre à jour les deps).

Merci beaucoup pour la discussion géniale, impatient de voir où cela va, je vais certainement le laisser ici :)

Je suis désolé de dire que ce fil me désillusionne de revenir dans le flottement, ce qui était le plan lors de la fin d'un autre projet sur lequel je travaille. Je ressens aussi de la frustration à cause de

C'est particulièrement frustrant d'argumenter étant donné que tous les principaux frameworks déclaratifs (React, Vue, Swift UI, Jetpack Compose) ont une manière ou une autre de résoudre ce problème de manière native.

Je suis d'accord avec @rrousselGit en ce
De plus, nous ne pouvons pas écrire une application en flutter si nous recherchons une solution, car nous avons besoin des solutions pour écrire une application. Depuis que les personnes flottant dans cette conversation ont au moins été assez claires, elles n'aiment pas les crochets. Et Flutter n'a tout simplement pas d'autre solution au problème tel que décrit dans l'OP. Comment devrait-il même être écrit.

J'ai l' impression (du moins pour moi) que ce n'est pas pris au sérieux, je suis désolé We understand the problem and want to solve it . Comme d'autres cadres déclaratifs semblent l'avoir fait.
Sur un autre mais un peu la même note des choses comme ça :

Est-ce plus facile d'écrire ?
Le développeur doit apprendre à écrire des crochets, ce qui est un nouveau concept, donc plus difficile

Me rend triste. Pourquoi améliorer ou changer quoi que ce soit ? vous pouvez toujours faire valoir cet argument, quoi qu'il arrive. Même si les nouvelles solutions sont bien plus faciles et agréables une fois apprises. Vous pouvez remplacer les crochets dans cette instruction par beaucoup de choses. Ma mère aurait pu utiliser ce genre de phrase sur les micro-ondes il y a 30 ans. Cela fonctionne par exemple de la même manière si vous remplacez "hooks" dans la phrase par "flutter" ou "dart".

Est-ce plus facile d'écrire ?
C'est la même chose, donc c'est tout aussi facile à écrire, ce qui est raisonnablement facile

Je ne pense pas que @rrousselGit signifie avec is it easier to write? (une question à réponse booléenne) c'est que si c'est la même chose, la réponse n'est pas false / undefined .

Je ne vois pas comment nous pourrons jamais arriver quelque part puisque nous ne sommes même pas d'accord qu'il y a un problème, seulement que beaucoup de gens trouvent cela un problème. Par exemple:

C'est certainement quelque chose que les gens ont soulevé. Ce n'est pas quelque chose avec lequel j'ai une expérience viscérale. Ce n'est pas quelque chose que j'ai ressenti comme un problème lors de l'écriture de mes propres applications avec Flutter. Cela ne veut pas dire que ce n'est pas un vrai problème pour certaines personnes, cependant.

Et même si beaucoup ont fourni de nombreux arguments à plusieurs reprises, pourquoi une solution au PO doit être dans le noyau.
Par exemple, il doit être dans le noyau pour que les tiers puissent l'utiliser aussi facilement et naturellement qu'ils utilisent et créent des widgets aujourd'hui. Et une multitude d'autres raisons. Le mantra semble être, il suffit de le mettre dans un emballage. Mais il existe déjà des packages comme celui des hooks. Si c'est ce qui a été décidé, pourquoi ne pas simplement fermer le fil.

J'espère vraiment que vous accepterez son offre avec @rrousselGit et organiserez un appel, peut-être qu'il sera plus facile d'avoir une discussion en temps réel à ce sujet au lieu d'écrire des choses dans les deux sens tout le temps. Si des personnes des autres frameworks ont résolu le problème décrit dans le PO, si l'une d'entre elles est vraiment gentille, elles pourraient peut-être également participer à l'appel pendant un certain temps et partager leurs 5 cents sur les choses qui se présentent. On pourrait toujours demander.

Quoi qu'il en soit, je me désabonne maintenant car je suis un peu triste de suivre ce fil en direct car je ne le vois pas aller nulle part. Mais j'espère que ce fil parviendra à convenir qu'il y a un problème afin qu'il puisse se concentrer sur les solutions possibles au PO. Puisqu'il semble un peu futile de proposer des solutions si vous ne comprenez pas le problème auquel les gens sont confrontés, comme @Hixie est probablement d'accord, je veux dire, puisque les personnes ayant des problèmes vous diront pourquoi la solution ne fonctionne pas par la suite.

Je vous souhaite vraiment bonne chance pour terminer ce fil, soit en disant simplement que le flottement ne devrait pas résoudre ce problème dans le noyau malgré le fait que les gens le souhaitent. Ou en trouvant une solution. ??

LayoutBuilder est un widget très différent de la plupart des constructeurs, FWIW. Aucune des propositions qui ont été discutées jusqu'à présent ne fonctionnerait pour des problèmes de type LayoutBuilder, et aucune des exigences décrites avant votre commentaire n'inclut LayoutBuilder. Si nous devions également utiliser cette nouvelle fonctionnalité pour gérer LayoutBuilder, il est important de le savoir ; Je recommande de travailler avec @TimWhiting pour vous assurer que l'exemple d'application sur lequel nous allons baser les propositions inclut des générateurs de mise en page à titre d'exemple.

@Hixie Oui, nous avons définitivement besoin d'échantillons. Je vais préparer quelque chose (mais je pense toujours que des modifications du compilateur sont nécessaires, donc l'échantillon peut être incomplet). L'idée générale est - un sucre de syntaxe qui aplatit les constructeurs et ne se soucie pas de la façon dont le constructeur est implémenté.

Pourtant, j'ai l'impression que personne dans l'équipe Flutter n'a examiné de plus près SwiftUI, je pense que nos préoccupations seraient faciles à comprendre autrement. Il est important pour l'avenir du framework que les personnes migrant à partir d'autres plates-formes aient une conduite aussi fluide que possible. Une bonne compréhension des autres plates-formes est donc nécessaire et une connaissance des avantages et des inconvénients. Et voir si certains des inconvénients de Flutter pourraient être corrigés. Évidemment, Flutter a pris beaucoup de bonnes idées de React et je pourrais faire la même chose avec des frameworks plus récents.

@emanuel-lundman

J'ai l' impression (du moins pour moi) que ce n'est pas pris au sérieux, je suis désolé We understand the problem and want to solve it . Comme d'autres cadres déclaratifs semblent l'avoir fait.

Je suis tout à fait d'accord que je ne comprends pas le problème. C'est pourquoi je continue de m'engager sur cette question, en essayant de la comprendre. C'est pourquoi j'ai suggéré de créer une application de démonstration qui résume le problème. Que ce soit quelque chose que nous décidons finalement de résoudre en changeant fondamentalement le framework, ou en ajoutant une petite fonctionnalité au framework, ou par un package, ou pas du tout, dépend vraiment de la nature du problème.

@szotp

Pourtant, j'ai l'impression que personne dans l'équipe Flutter n'a examiné de plus près SwiftUI, je pense que nos préoccupations seraient faciles à comprendre autrement.

J'ai étudié Swift UI. Il est certainement moins détaillé d'écrire du code Swift UI que du code Flutter, mais le coût de lisibilité est très élevé à mon humble avis. Il y a beaucoup de « magie » (au sens de logique qui fonctionne d'une manière qui n'est pas évidente dans le code de la consommation). Je peux totalement croire que c'est un style que certaines personnes préfèrent, mais je crois aussi que l'une des forces de Flutter est qu'il a très peu de magie. Cela signifie que vous écrivez parfois plus de code, mais cela signifie également que le débogage de ce code est _beaucoup_ plus facile.

Je pense qu'il y a de la place pour beaucoup de styles de cadres. Style MVC, style React, magique super laconique, sans magie mais verbeux... L'un des avantages de l'architecture de Flutter est que l'aspect portabilité est entièrement séparé du framework lui-même, il est donc possible de tirer parti de tous nos outils -- support multiplateforme, rechargement à chaud, etc -- mais créez un tout nouveau framework. (Il existe déjà d'autres frameworks Flutter, par exemple flutter_sprites.) De même, le framework lui-même est conçu en couches afin que, par exemple, vous puissiez réutiliser toute notre logique RenderObject mais avec un remplacement pour la couche Widgets, donc si Les widgets c'est trop, quelqu'un pourrait créer un framework alternatif qui le remplace. Et bien sûr, il y a le système d'emballage pour que des fonctionnalités puissent être ajoutées sans rien perdre du code du framework existant.

Quoi qu'il en soit, le point de ma digression ici est simplement que ce n'est pas tout ou rien. Même si sur le long terme on ne finit pas par adopter une solution qui vous rend heureux, cela ne veut pas dire que vous ne pouvez pas continuer à bénéficier des parties de l'offre qui vous plaisent.


J'exhorte les personnes intéressées par ce problème à travailler avec @TimWhiting pour créer une application qui montre pourquoi vous voudriez réutiliser le code et à quoi cela ressemble aujourd'hui quand vous ne pouvez pas (https://github.com/TimWhiting/local_widget_state_approaches). Cela nous aidera directement à créer des propositions sur la façon de résoudre ce problème d'une manière qui réponde aux besoins de _toutes_ les personnes qui commentent ici (y compris celles qui aiment les Hooks et celles qui n'aiment pas les Hooks).

Il ne peut pas être si difficile de comprendre pourquoi "_un sucre de syntaxe qui aplatit les constructeurs et ne se soucie pas de la façon dont le constructeur est implémenté._" est souhaité par les développeurs comme une fonctionnalité de première classe. Nous avons souligné à maintes reprises les problèmes liés aux approches alternatives.

En bref, les constructeurs résolvent le problème de la réutilisation, mais sont difficiles à lire et à composer. Le « problème » est simplement que nous aimerions une fonctionnalité de type constructeur beaucoup plus facile à lire.

Aucune application ne peut le montrer plus clairement, si vous n'êtes pas fondamentalement d'accord pour dire que 3 constructeurs imbriqués sont difficiles à lire, ou que les constructeurs en général ne servent pas vraiment à réutiliser du code. Plus important d'entendre simplement que beaucoup d'entre nous aiment vraiment réduire l'imbrication et n'aiment vraiment pas dupliquer le code dans toute notre application, et nous sommes donc coincés entre 2 options non idéales.

J'ai passé littéralement des heures de mon temps ce week-end, sans parler des nombreuses heures de Google avant cela, à considérer ce problème, à décrire des solutions possibles et à essayer de définir précisément ce que nous essayons de résoudre.

je suis reconnaissant pour ça

S'il vous plaît ne confondez pas le manque de compréhension sur ce qui est un problème avec le refus de le considérer

Je vais bien avec un manque de compréhension, mais la situation actuelle semble désespérée.
Nous débattons encore des points qui ont été soulevés au tout début de la discussion.

De mon point de vue, j'ai l'impression d'avoir passé des heures à rédiger des commentaires détaillés présentant les différents problèmes et à répondre aux questions, mais mes commentaires ont été rejetés et la même question a été posée à nouveau.

Par exemple, le manque de lisibilité de la syntaxe actuelle est au centre de la discussion.
J'ai fait plusieurs analyses du problème de lisibilité pour étayer ceci:

Ces analyses ont un nombre important de et d'autres peuples semblent d'accord

Et pourtant d'après votre récente réponse, il n'y a pas de problème de lisibilité : https://github.com/flutter/flutter/issues/51752#issuecomment -671009593

Vous avez également suggéré :

Widget build(context) {
  var result = Text('result:');
  var builder1 = (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Row(children: [result, Text(snapshot.data)]);
  };
  result = StreamBuilder(stream: _stream1, builder: builder1);
  var builder2 = (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Column(children: [result, Text(snapshot.data)]);
  };
  result = StreamBuilder(stream: _stream2, builder: builder2);
}

sachant que ce n'est pas lisible

De ces deux commentaires, nous pouvons conclure que :

  • nous ne sommes pas d'accord pour dire qu'il y a un problème de lisibilité
  • on ne sait toujours pas si la lisibilité fait partie du champ d'application de ce problème ou non

C'est décourageant à entendre, étant donné que le seul but des crochets est d'améliorer la syntaxe des constructeurs - qui sont au sommet de la réutilisabilité mais ont une mauvaise lisibilité/écriture
Si nous ne sommes pas d'accord sur un fait aussi fondamental, je ne sais pas ce que nous pouvons faire.

@Hixie merci, cela aide beaucoup à comprendre votre point de vue. Je suis d'accord qu'ils sont peut-être allés trop loin avec la magie du code, mais je suis sûr qu'il y a au moins peu de choses qu'ils ont bien compris.

Et j'aime beaucoup l'architecture en couches de Flutter. J'aimerais aussi continuer à utiliser des widgets. Alors peut-être que la réponse est d'améliorer simplement l'extensibilité de Dart & Flutter, ce qui serait pour moi :

Rendre la génération de code plus transparente - il peut être possible d'implémenter la magie SwiftUI dans Dart, mais la configuration habituelle requise est tout simplement trop volumineuse et trop lente.

Si l'utilisation de la génération de code était aussi simple que d'importer un package et d'ajouter des annotations, alors les personnes qui ont le problème discuté le feraient et arrêteraient de se plaindre. Le reste continuerait à utiliser directement les bons vieux StatefulWidgets.

EDIT : Je pense que flutter generate était un pas dans la bonne direction, dommage qu'il ait été supprimé.

Je pense que ce serait une question très intéressante à poser dans le prochain sondage des développeurs Flutter.

Ce serait un bon début. Divisez ce problème en différentes parties/questions et voyez s'il s'agit d'un problème réel que les développeurs de Flutter souhaitent résoudre.

Une fois que c'est clair, cette conversation sera plus fluide et enrichissante

De mon point de vue, j'ai l'impression d'avoir passé des heures à rédiger des commentaires détaillés présentant les différents problèmes et à répondre aux questions, mais mes commentaires ont été rejetés et la même question a été posée à nouveau.

Si je pose les mêmes questions, c'est parce que je ne comprends pas les réponses.

Par exemple, pour revenir à votre commentaire précédent (https://github.com/flutter/flutter/issues/51752#issuecomment-670959424) :

Le problème débattu est :

Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}

Ce code n'est pas lisible.

Je ne vois vraiment pas ce qui n'est pas lisible à ce sujet. Il explique exactement ce qui se passe. Il y a quatre widgets, trois des widgets ont des méthodes de construction, un a juste une chaîne. Personnellement, je n'omettrais pas les types, je pense que cela rend la lecture plus difficile car je ne peux pas dire quelles sont toutes les variables, mais ce n'est pas un gros problème.

Pourquoi est-ce illisible ?

Pour être clair, vous le trouvez clairement illisible, je n'essaie pas de dire que vous vous trompez. Je ne comprends tout simplement pas pourquoi.

Nous pourrions résoudre le problème de lisibilité en introduisant un nouveau mot-clé qui change la syntaxe en :

Widget build(context) {
  final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final value2 = keyword StreamBuilder(stream: someStream);
  final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

  return Text('$value $value2 $value3');
}

Ce code est nettement plus lisible, n'est pas lié aux hooks et ne souffre pas de ses limitations.

C'est certainement moins bavard. Je ne suis pas sûr que ce soit plus lisible, du moins pour moi. Il y a plus de concepts (maintenant nous avons à la fois des widgets et cette fonctionnalité "mot-clé"); plus de concepts signifie plus de charge cognitive. (C'est aussi potentiellement moins efficace, selon le degré d'indépendance de ces objets ; par exemple, si l'animation change toujours plus souvent que la valeur listenable et stream, maintenant nous reconstruisons le ValueListenableBuilder et le StreamBuilder même si normalement ils ne seraient pas déclenchés ; aussi la logique d'initialisation doit maintenant être entrée et ignorée à chaque build.)

Vous avez dit que la verbosité n'est pas le problème, donc ce n'est pas parce que c'est plus lacon que c'est plus lisible, je suppose (bien que je sois confus à ce sujet aussi puisque vous avez mis "trop ​​verbeux" dans le titre du problème et dans la description originale du problème). Vous avez mentionné vouloir moins d'indentation, mais vous avez décrit la version utilisant des générateurs sans indentation comme illisible également, donc ce n'est probablement pas l'indentation dans l'original qui est le problème.

Vous dites que les constructeurs sont le summum de la réutilisabilité et que vous voulez juste une syntaxe alternative, mais les propositions que vous avez suggérées ne ressemblent en rien aux constructeurs (ils ne créent pas de widgets ou d'éléments), ce n'est donc pas spécifiquement l'aspect constructeur que vous sont en train de chercher.

Vous avez une solution que vous aimez (Hooks), qui pour autant que je sache fonctionne très bien, mais vous voulez que quelque chose change dans le framework afin que les gens utilisent Hooks ? Ce que je ne comprends pas non plus, car si les gens n'aiment pas assez les Hooks pour l'utiliser comme un package, ce n'est probablement pas non plus une bonne solution pour le framework (en général, on s'oriente davantage vers l'utilisation de packages, voire de fonctionnalités l'équipe Flutter crée, pour ce que ça vaut).

Je comprends qu'il y ait un désir pour une réutilisation plus facile du code. Je ne sais juste pas ce que cela signifie.

Comment la lisibilité suivante se compare-t-elle aux deux versions ci-dessus ?

Widget build(context) {
  return
    ValueListenableBuilder(valueListenable: someValueListenable, builder: (context, value, _) =>
    StreamBuilder(stream: someStream, builder: (context, value2) =>
    TweenAnimationBuilder(tween: Tween(...), builder: (context, value3) =>
    Text('$value $value2 $value3'),
  )));
}

@szotp S'il y a trop de frictions autour de notre solution actuelle de codegen, n'hésitez pas à déposer un bogue demandant des améliorations.

@jamesblasco Je ne pense pas qu'il y ait le moindre doute qu'il y a un vrai problème ici que les gens veulent résoudre. La question pour moi est exactement ce qu'est ce problème, afin que nous puissions concevoir une solution.

Je pourrais répondre aux préoccupations concernant les failles des hooks ou le souhait d'être inclus dans le code, mais je ne pense pas que ce soit ce sur quoi nous devrions nous concentrer en ce moment.

Nous devons d'abord nous mettre d'accord sur le problème. Sinon, je ne vois pas comment on pourrait s'entendre sur d'autres sujets.

Je ne vois vraiment pas ce qui n'est pas lisible à ce sujet. Il explique exactement ce qui se passe. Il y a quatre widgets, trois des widgets ont des méthodes de construction, un a juste une chaîne. Personnellement, je n'omettrais pas les types, je pense que cela rend la lecture plus difficile car je ne peux pas dire quelles sont toutes les variables, mais ce n'est pas un gros problème.

Je pense qu'une grande partie du problème ici est que la façon dont vous codez est radicalement différente de la façon dont la plupart des gens codent.

Par exemple, Flutter et l'exemple d'application que vous avez donné tous les deux :

  • ne pas utiliser dartfmt
  • utiliser always_specify_types

Avec juste ces deux points, je serais surpris si cela représentait plus de 1% de la communauté.

En tant que tel, ce que vous évaluez comme lisible est probablement très différent de ce que la plupart des gens pensent être lisible.

Je ne vois vraiment pas ce qui n'est pas lisible à ce sujet. Il explique exactement ce qui se passe. Il y a quatre widgets, trois des widgets ont des méthodes de construction, un a juste une chaîne. Personnellement, je n'omettrais pas les types, je pense que cela rend la lecture plus difficile car je ne peux pas dire quelles sont toutes les variables, mais ce n'est pas un gros problème.

Pourquoi est-ce illisible ?

Ma recommandation serait d'analyser où votre œil regarde lorsque vous recherchez une chose spécifique, et combien d'étapes il faut pour y arriver.

Faisons une expérience :
Je vais vous donner deux arbres de widgets. L'un utilisant une syntaxe linéaire, l'autre avec une syntaxe imbriquée.
Je vais également vous donner des éléments spécifiques que vous devez rechercher dans cet extrait.

Est-il plus facile de trouver la réponse à l'utilisation de la syntaxe linéaire ou de la syntaxe imbriquée ?

Questions:

  • Quel est le widget non constructeur renvoyé par cette méthode de construction ?
  • Qui crée la variable bar ?
  • Combien de constructeurs avons-nous ?

Utilisation de constructeurs :

 Construction du widget (contexte) {
 return ValueListenableBuilder(
 valueListenable : someValueListenable,
 constructeur : (contexte, foo, _) {
 retourner StreamBuilder(
 stream: someStream,
 constructeur : (contexte, baz) {
 retourner TweenAnimationBuilder(
 interpolation : interpolation(...),
 constructeur : (contexte, barre) {
 return Conteneur();
 },
 );
 },
 );
 },
 );
 }

En utilisant une syntaxe linéaire :

 Construction du widget (contexte) {
 final foo = mot-clé ValueListenableBuilder(valueListenable: someValueListenable);
 barre finale = mot-clé StreamBuilder(stream: someStream);
 baz final = mot-clé TweenAnimationBuilder(tween : Tween(...));

retourner Image(); }


Dans mon cas, j'ai du mal à parcourir le code imbriqué pour trouver la réponse.
Par contre, trouver la réponse avec l'arbre linéaire est instantané

Vous avez mentionné vouloir moins d'indentation, mais vous avez décrit la version utilisant des générateurs sans indentation comme illisible également, donc vraisemblablement, ce n'est pas l'indentation dans l'original qui est le problème.

Le StreamBuilder divisé en plusieurs variables était-il une suggestion sérieuse ?
D'après ce que j'ai compris, c'était une suggestion sarcastique pour argumenter. N'était-ce pas ? Pensez-vous vraiment que ce modèle conduirait à un code plus lisible, même sur de gros widgets ?

Ignorant le fait que l'exemple ne fonctionne pas, cela ne me dérange pas de le décomposer pour expliquer pourquoi il n'est pas lisible. Cela aurait-il de la valeur ?

``` fléchette
Construction du widget (contexte) {
revenir
ValueListenableBuilder(valueListenable: someValueListenable, builder: (context, value, _) =>
StreamBuilder(stream : unFlux, constructeur : (contexte, valeur2) =>
TweenAnimationBuilder(tween : Tween(...), générateur : (contexte, valeur3) =>
Texte('$valeur $valeur2 $valeur3'),
)));
}

Ça a l'air mieux.
Mais cela suppose que les gens n'utilisent pas dartfmt

Avec dartfmt, on a :

Widget build(context) {
  return ValueListenableBuilder(
      valueListenable: someValueListenable,
      builder: (context, value, _) => StreamBuilder(
          stream: someStream,
          builder: (context, value2) => TweenAnimationBuilder(
                tween: Tween(),
                builder: (context, value3) => Text('$value $value2 $value3'),
              )));
}

qui n'est presque pas différent du code d'origine.

Vous dites que les constructeurs sont le summum de la réutilisabilité et que vous voulez juste une syntaxe alternative, mais les propositions que vous avez suggérées ne ressemblent en rien aux constructeurs (ils ne créent pas de widgets ou d'éléments), ce n'est donc pas spécifiquement l'aspect constructeur que vous sont en train de chercher.

C'est un détail de mise en oeuvre.
Il n'y a pas de raison particulière d'avoir un élément ou non.
En fait, il peut être intéressant d'avoir un Element, afin que nous puissions inclure LayoutBuilder et potentiellement GestureDetector .

Je pense que c'est une faible priorité. Mais dans la communauté React, parmi les différentes bibliothèques de hooks, j'ai vu :

  • useIsHovered - renvoie un booléen qui indique si le widget est survolé
  • useSize - (devrait probablement être useContraints dans Flutter) qui donne la taille de l'interface utilisateur associée.

(C'est aussi potentiellement moins efficace, selon le degré d'indépendance de ces objets ; par exemple, si l'animation change toujours plus souvent que la valeur listenable et stream, maintenant nous reconstruisons le ValueListenableBuilder et le StreamBuilder même si normalement ils ne seraient pas déclenchés ; aussi la logique d'initialisation doit maintenant être entrée et ignorée à chaque build.)

Cela dépend de la façon dont la solution est résolue.

Si nous optons pour un correctif de langue, ce problème ne serait pas du tout un problème.

On pourrait faire ça :

Widget build(context) {
  final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final value2 = keyword StreamBuilder(stream: someStream);
  final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

  return Text('$value $value2 $value3');
}

"compile" en :

Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}

Si nous utilisons des hooks, alors flutter_hooks est livré avec un widget HookBuilder , afin que nous puissions toujours diviser les choses quand nous en avons besoin.
De même, il faudrait des repères appropriés pour déterminer s'il s'agit vraiment d'un problème, en particulier dans l'exemple présenté ici.

Avec des crochets, nous ne reconstruisons qu'un seul élément.
Avec les constructeurs, la reconstruction est répartie sur plusieurs éléments. Cela ajoute aussi des frais généraux, même s'ils sont petits.

Il n'est pas impossible qu'il soit plus rapide de réévaluer tous les crochets. Il semble que ce soit la conclusion de l'équipe React à laquelle ils sont parvenus lors de la conception des crochets.
Cela peut ne pas s'appliquer à Flutter cependant.

Pourquoi est-ce illisible ?

En raison de l'imbrication, l'imbrication rend plus difficile le balayage rapide et la connaissance des parties que vous pouvez ignorer et de celles qui sont essentielles pour comprendre ce qui se passe. Le code est de nature un peu "séquentielle", mais l'imbrication le cache. L'imbrication rend également difficile le travail avec elle - imaginez que vous vouliez réorganiser deux choses - ou injecter une nouvelle chose entre deux - triviale dans un code vraiment séquentiel, mais difficile lorsque vous devez travailler avec l'imbrication.

Ceci est très similaire à async/wait sugar vs travailler avec raw Future API, concept basé sur la continuation dame en dessous (et même les arguments pour et contre sont très similaires) - oui Future API peut être utilisé directement et ne cache rien, mais la lisibilité et la maintenabilité n'est certainement pas bon - async/await est un gagnant là-bas.

Ma recommandation serait d'analyser où votre œil regarde lorsque vous recherchez une chose spécifique, et combien d'étapes il faut pour y arriver.

Je programme depuis 25 ans maintenant dans plus de 10 langages différents et c'est de loin la pire des façons d'évaluer ce qui rend un code lisible. La lisibilité du code source est délicate, mais il s'agit davantage de savoir à quel point il exprime les concepts et la logique de programmation, plutôt que « où mes yeux regardent » ou le nombre de lignes de code qu'il utilise.

Ou plutôt, il me semble que vous vous concentrez trop sur la lisibilité et moins sur la maintenabilité .

Vos exemples sont moins lisibles, car l'__intention__ du code est moins évidente et différentes préoccupations cachées au même endroit le rendent plus difficile à maintenir.


final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);

Quelle serait même la valeur ? Un widget ? Une variable chaîne ? Je veux dire qu'il est utilisé à l'intérieur d'un
return Text('$value $value2 $value3');

Fondamentalement, ce que vous voulez, c'est qu'en référençant la variable A dans la méthode de construction du widget B, cela devrait amener B à reconstruire chaque fois que la valeur de A change ? C'est littéralement ce que fait mobx, et il le fait exactement avec la bonne quantité de magie/passe-partout.


final value2 = keyword StreamBuilder(stream: someStream);

Qu'est-ce que ce retour? Un widget ? Un courant? Une valeur de chaîne ?

Encore une fois, cela ressemble à une valeur de chaîne. Vous voulez donc pouvoir simplement référencer un flux dans une méthode de construction, provoquer la reconstruction de ce widget chaque fois que le flux émet une valeur et avoir accès à la valeur émise et créer/mettre à jour/éliminer le flux chaque fois que le widget est créé/mis à jour /détruit? En une seule ligne de code ? À l'intérieur de la méthode de construction ?

Oui, avec mobx, vous pouvez faire en sorte que vos méthodes de construction ressemblent exactement à votre exemple "plus lisible" (sauf que vous référencez des observables). Vous devez toujours écrire le code réel qui fait tout le travail, comme vous le faites avec les hooks. Le code réel fait environ 10 lignes et il est réutilisable dans n'importe quel widget.


final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

Une classe appelée "TweenAnimationBuilder" renvoyant une chaîne ?! Je ne vais même pas près de pourquoi c'est une idée terrible.

Il n'y a pas de différence de retrait/lisibilité entre :

Future<double> future;

AsyncSnapshot<double> value = keyword FutureBuilder<double>(future: future);

et:

Future<double> future;

double value = await future;

Les deux font exactement la même chose : écouter un objet et dévoiler sa valeur.

Je ne vois vraiment pas ce qui n'est pas lisible à ce sujet. Il explique exactement ce qui se passe. Il y a quatre widgets, trois des widgets ont des méthodes de construction, un a juste une chaîne. Personnellement, je n'omettrais pas les types, je pense que cela rend la lecture plus difficile car je ne peux pas dire quelles sont toutes les variables, mais ce n'est pas un gros problème.

Le même argument pourrait être appliqué aux chaînes Promise/Future.

foo().then(x =>
  bar(x).then(y =>
    baz(y).then(z =>
      qux(z)
    )
  )
)

vs

let x = await foo();
let y = await bar(x);
let z = await baz(y);
await qux(z);

On pourrait dire que la première façon d'écrire indique clairement que des promesses sont créées sous le capot et comment exactement la chaîne est formée. Je me demande si vous souscrivez à cela, ou si vous considérez que les promesses sont fondamentalement différentes des constructeurs en ce sens qu'elles méritent une syntaxe.

Une classe appelée "TweenAnimationBuilder" renvoyant une chaîne ?! Je ne vais même pas près de pourquoi c'est une idée terrible.

Vous pouvez faire le même argument à propos des Promesses/Futures et dire que await masque le fait qu'il renvoie une Promesse.

Je dois noter que l'idée de "déballer" les choses via la syntaxe n'est pas nouvelle. Oui, dans les langages grand public, il est venu via async/wait, mais, par exemple, F# a des expressions de calcul , similaires à la notation do dans certains langages FP hardcore. Là, il a beaucoup plus de puissance et est généralisé pour fonctionner avec toutes les enveloppes qui satisfont à certaines lois. Je ne suggère pas d'ajouter des Monads à Dart, mais je pense qu'il vaut la peine de souligner qu'il existe certainement un précédent pour la syntaxe de type sécurisé pour "déballer" des choses qui ne correspondent pas nécessairement à des appels asynchrones.

En prenant du recul, je pense qu'une chose avec laquelle beaucoup de gens ici ont du mal (moi y compris) est cette question sur la lisibilité. Comme @rrousselGit l' a mentionné, il y a eu beaucoup d'exemples tout au long de ce fil de problèmes de lisibilité avec l'approche actuelle basée sur Builder . Pour beaucoup d'entre nous, il semble évident que ceci :

Widget build(context) {
  return ValueListenableBuilder<String>(
    valueListenable: someValueListenable,
    builder: (context, value, _) {
      return StreamBuilder<int>(
        stream: someStream,
        builder: (context, value2) {
          return TweenAnimationBuilder<double>(
            tween: Tween(...),
            builder: (context, value3) {
              return Text('$value $value2 $value3');
            },
          );
        },
      );
    },
  );
}

est nettement moins lisible que ceci :

Widget build(context) {
  final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);
  final value2 = keyword StreamBuilder(stream: someStream);
  final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

  return Text('$value $value2 $value3');
}

Mais ce n'est clairement pas évident, puisque @Hixie et @Rudiksz ne sont pas convaincus (ou s'opposent activement) à l'idée que la seconde soit plus lisible que la première.

Voici donc ma ventilation (pour le petit montant que cela vaut) sur les raisons pour lesquelles le deuxième bloc de code est plus lisible que le premier :

1. Le premier bloc de code est nettement plus indenté que le second

D'après mon expérience, l'indentation équivaut généralement à l'asynchronicité, à la ramification ou aux rappels, qui nécessitent tous une charge cognitive plus importante pour réfléchir qu'un code linéaire non indenté. Le premier bloc de code a plusieurs couches d'indentation, et en tant que tel, il me faut un temps non négligeable pour comprendre ce qui se passe ici et ce qui est finalement rendu (un seul Text ). Peut-être que d'autres personnes sont meilleures à travailler à travers cette indentation dans leur esprit.


Dans le deuxième bloc de code, il n'y a pas d'indentation qui atténue le problème.

2. Le premier bloc de code nécessite plus de syntaxe pour exprimer son intention

Dans le premier bloc de code, il y a trois instructions return , trois instructions de générateur, trois en-têtes lambda, trois contextes et enfin trois valeurs. En fin de compte, ce qui nous intéresse, ce sont ces trois valeurs - le reste est passe-partout pour nous y amener. En fait, je trouve que c'est la partie la plus difficile de ce bloc de code. Il se passe tellement de choses, et les pièces qui me tiennent vraiment à cœur (les valeurs renvoyées par les constructeurs) sont si petites que je passe la majeure partie de mon énergie mentale à grocker le passe-partout au lieu de me concentrer sur les pièces dont j'ai réellement besoin ( encore une fois, les valeurs).


Dans le deuxième bloc de code, il y a une énorme réduction du passe-partout afin que je puisse me concentrer sur la partie qui m'intéresse - encore une fois, les valeurs.

3. Le premier bloc de code cache la partie la plus importante de la méthode build dans la partie la plus profonde de l'imbrication

Je reconnais que toutes les parties de cette méthode build sont importantes, mais j'ai constaté que lorsque je lis ce style de code d'interface utilisateur déclaratif, la chose que je recherche généralement est tout ce qui est affiché à l'utilisateur, qui dans ce cas est le widget Text intégré dans le constructeur imbriqué le plus profond. Plutôt que d'être au premier plan, ce widget Text est enterré dans plusieurs couches d'indentation, de syntaxe et d'intention. Si vous jetez un Column ou un Row dans l'une de ces couches, cela devient encore plus profondément imbriqué, et à ce stade, vous n'avez même pas l'avantage de simplement tracer la section la plus en retrait .


Dans le deuxième bloc de code, le Widget pouvant être rendu réel est en bas de la fonction, ce qui est immédiatement apparent. De plus, j'ai constaté que lorsque vous avez quelque chose comme la syntaxe OP proposée, vous pouvez compter sur le visuel Widget toujours en bas de la fonction, ce qui rend le code beaucoup plus prévisible et facile à lire.

Concernant l'imbrication, il y a une différence entre l' imbrication exprimant un _arbre_ et l'imbrication exprimant une _séquence_ .

Dans le cas d'une imbrication normale de View -> Text , l'imbrication est importante car elle représente les relations parent-enfant à l'écran. Pour des fonctionnalités telles que Context (je ne sais pas si Flutter l'a), cela représente l'étendue des contextes. Ainsi, l'imbrication elle-même a une signification sémantique importante dans ces cas et ne peut être ignorée. Vous ne pouvez pas simplement échanger les places des parents et des enfants et vous attendre à ce que le résultat soit le même.

Cependant, avec l'imbrication de Builders (alias "Render Props" dans React), ou l'imbrication de Promises, l'imbrication est censée communiquer une séquence de transformations/augmentations . L'arbre lui-même n'est pas aussi important — par exemple, lors de l'imbrication de ABuilder -> BBuilder -> CBuilder indépendants, leurs relations parent-enfant ne transmettent pas de signification supplémentaire.

Tant que les trois valeurs sont disponibles dans le champ d'application ci-dessous, leur structure arborescente n'est pas vraiment pertinente. Il est conceptuellement « plat », et l'imbrication n'est qu'un artefact de la syntaxe. Bien sûr, ils peuvent utiliser les valeurs de l'autre (auquel cas leur ordre compte), mais c'est aussi le cas avec les appels de fonction séquentiels, et cela peut être fait sans aucune imbrication.

C'est pourquoi async/await est convaincant. Il supprime les informations supplémentaires (relation parent-enfant des promesses) qui décrivent un mécanisme de bas niveau et vous permet à la place de vous concentrer sur l'intention de haut niveau (décrire une séquence).

Un arbre est une structure plus flexible qu'une liste. Mais quand chaque parent n'a qu'un enfant, cela devient un arbre pathologique - essentiellement, une liste. Async/Await et Hooks reconnaissent que nous gaspillons de la syntaxe sur quelque chose qui ne transmet pas d'informations et les suppriment.

C'est en fait intéressant parce que j'ai dit plus tôt "ce n'est pas une question de passe-partout" et maintenant il semble que je me contredis. Je pense qu'il y a deux choses ici.

En soi, les constructeurs (ou au moins les accessoires de rendu dans React) sont la solution (AFAIK) au problème de la "logique réutilisable". C'est juste qu'ils ne sont pas très ergonomiques si on en utilise beaucoup. Vous êtes naturellement découragé par l'indentation d'en utiliser plus de 4 ou 5 dans le même composant. Chaque niveau suivant est un coup de lisibilité.

Donc, la partie qui me semble non résolue est de réduire le coût de l'imbrication pour le lecteur. Et cet argument est précisément le même argument que pour async / await .

Ce n'est pas aussi lisible pour les raisons suivantes :

  • L'utilisation excessive d'espaces blancs ne s'adapte pas bien. Nous n'avons qu'un nombre limité de lignes sur notre moniteur, et forcer le défilement réduit la lisibilité et augmente la charge cognitive. Imaginez que nous ayons déjà un arbre de widgets de 60 lignes et que vous venez de m'en imposer 15 supplémentaires pour les constructeurs, ce qui n'est pas idéal.
  • Cela gaspille de l'espace Hz, ce que nous sommes limités, ce qui conduit à un emballage supplémentaire, ce qui gaspille encore plus d'espace de ligne.
  • Il pousse le nœud de la feuille, alias le contenu, plus loin du côté gauche et dans l'arbre où il est plus difficile à repérer d'un coup d'œil
  • Il est nettement plus difficile d'identifier d'un coup d'œil les « acteurs clés » ou les « non passe-partout ». Je dois "trouver les choses importantes" avant même que mon raisonnement puisse commencer.

Une autre façon de voir cela est de simplement mettre en évidence le code non standard, et s'il est regroupé pour que votre œil puisse facilement se régaler, ou dispersé partout pour que votre œil ait à balayer:

Avec la mise en évidence, il est très facile de raisonner. Sans cela, je dois lire toute la verbosité avant de pouvoir déterminer qui utilise quoi et où :
image

Maintenant, comparez à cela, la mise en évidence est fondamentalement redondante, car il n'y a nulle part ailleurs où aller pour mon œil :
image

Il convient de noter qu'il existe probablement un désaccord entre la lisibilité et la grokabilité. @Hixie passe probablement son temps dans des fichiers de classe monolithiques, où il doit constamment lire et comprendre des arbres massifs, alors que votre développeur d'applications typique consiste beaucoup plus à créer des centaines de classes plus petites, et lorsque vous gérez de nombreuses petites classes, la capacité de grok est la clé . Ce n'est pas tant que le code est lisible lorsque vous ralentissez et que vous le lisez, mais je peux dire ce que cela fait en un coup d'œil, afin que je puisse intervenir et modifier ou réparer quelque chose.

Pour référence, l'équivalent de Context dans React est InheritedWidgets/Provider

La seule différence entre eux est que, dans React before hooks, nous avons dû utiliser le modèle Builder pour consommer un Context/Inheritedwidget

Alors que Flutter a un moyen de lier la reconstruction avec un simple appel de fonction.
Il n'y a donc pas besoin de crochets pour aplatir les arbres en utilisant InheritedWidgets - ce qui contourne le problème des constructeurs

C'est probablement l'une des raisons pour lesquelles la discussion est plus difficile, car nous avons moins souvent besoin de Builders.

Mais il convient de mentionner que l'introduction d'une solution de type crochet résoudrait à la fois https://github.com/flutter/flutter/issues/30062
et https://github.com/flutter/flutter/issues/12992

Il semble également que @Hixie soit plus habitué à lire des arbres profondément imbriqués, car Flutter est essentiellement gauche à droite , avec une imbrication profonde, un peu comme le HTML avec lequel je suppose que @Hixie a eu de l'expérience, ayant créé la spécification HTML5. Cela signifie que le point le plus à droite du bloc de code est l'endroit où résident la logique principale et la valeur de retour.

Cependant, la plupart des développeurs ne sont pas, ou viennent de langages de haut en bas , où la logique est, encore une fois, lue de haut en bas, plutôt que dans des arbres imbriqués ; il se trouve au point le plus bas du bloc de code. Par conséquent, ce qui lui est lisible ne l'est pas nécessairement avec de nombreux autres développeurs, ce qui explique potentiellement pourquoi vous voyez ici la dichotomie des opinions sur la lisibilité.

Une autre façon de voir les choses est de savoir combien de code mon cerveau a-t-il besoin pour rédiger visuellement. Pour moi, cela représente avec précision le "travail de grognement" que mon cerveau doit faire avant de pouvoir analyser ce qui est renvoyé de l'arbre :
image

En termes simples, la version builder a une empreinte verticale 4 fois plus grande tout en n'ajoutant littéralement aucune information ou contexte supplémentaire, ET emballe le code de manière beaucoup plus clairsemée/éparpillée. Dans mon esprit, c'est un cas ouvert et fermé, il est objectivement moins lisible pour cette seule raison, et cela ne tient même pas compte de la charge cognitive supplémentaire autour de l'indentation et de l'alignement des accolades que nous avons tous traités en flottant.

Considérez mon œil comme un processeur affamé, lequel est le plus optimisé pour le traitement ? :)

Dans le cas d'une imbrication de View -> Text normale et autre, l'imbrication est importante car elle représente _les relations parent-enfant_ à l'écran. Pour des fonctionnalités telles que Context (je ne sais pas si Flutter l'a), cela représente l'étendue des contextes. Ainsi, l'imbrication elle-même a une signification sémantique importante dans ces cas et ne peut être ignorée. Vous ne pouvez pas simplement échanger les places des parents et des enfants et vous attendre à ce que le résultat soit le même.

Tout à fait d'accord et je l'ai mentionné plus tôt. Sémantiquement, cela n'a aucun sens de créer des couches de contexte supplémentaires dans une arborescence d'affichage visuel, car j'utilise des contrôleurs non visuels supplémentaires qui ont un état. En utilisant 5 animateurs, votre widget a maintenant 5 couches de profondeur ? Juste à ce niveau élevé, l'approche actuelle sent un peu.

Il y a deux problèmes qui me sautent aux yeux ici.

  1. Je soupçonne qu'il y a un certain désaccord sur la difficulté/explicite que cela devrait être lors de l'utilisation d'une ressource coûteuse. La philosophie de Flutter est qu'ils devraient être plus difficiles/explicites pour que le développeur réfléchisse sérieusement à quand et comment les utiliser. Les flux, les animations, les générateurs de mise en page, etc. représentent des coûts non négligeables qui pourraient être utilisés de manière inefficace s'ils sont trop faciles.

  2. La construction est synchronisée, mais les choses les plus intéressantes que vous traitez en tant que développeur d'applications sont asynchrones. Bien sûr, nous ne pouvons pas rendre build async. Nous avons créé ces commodités comme Stream/Animation/FutureBuilder, mais elles ne fonctionnent pas toujours assez bien pour les besoins d'un développeur. C'est probablement révélateur que nous n'utilisons pas beaucoup Stream ou FutureBuilder dans le framework.

Je ne pense pas que la solution soit de dire aux développeurs de toujours écrire des objets de rendu personnalisés lorsqu'ils travaillent avec des opérations asynchrones bien sûr. Mais dans les exemples que je vois dans ce bogue, il y a des mélanges de travail asynchrone et de synchronisation que nous ne pouvons pas simplement attendre. Build doit produire quelque chose à chaque appel.

fwiw, l'équipe React a abordé la réutilisation du problème de lisibilité comme motivation 1:
Motivation des crochets : il est difficile de réutiliser la logique avec état entre les composants
React n'offre pas de moyen d'"attacher" un comportement réutilisable à un composant ... vous connaissez peut-être les modèles... qui tentent de résoudre ce problème. Mais ces modèles vous obligent à restructurer vos composants lorsque vous les utilisez, ce qui peut être lourd et rendre le code plus difficile à suivre .

Ceci est très similaire à la façon dont Flutter ne nous offre actuellement aucun moyen de « composer un état » de manière native. Cela fait également écho à ce qui se passe lorsque nous sommes des constructeurs, ce qui modifie notre arborescence de mise en page et la rend plus lourde à utiliser, et "plus difficile à suivre", a déclaré l'arborescence.

@dnfield si build doit être appelé à chaque fois, nous pouvons peut-être faire en sorte que les crochets ne soient pas dans la méthode build afin que la construction soit toujours synchronisée, c'est-à-dire les mettre dans la classe où initState et dispose sont. Y a-t-il des problèmes à le faire, de la part de ceux qui écrivent des crochets ?

Vous pouvez faire le même argument à propos des Promesses/Futures et dire que await masque le fait qu'il renvoie une Promesse.

Non, vous ne le faites pas. Attendre n'est littéralement que du sucre syntaxique pour une seule fonctionnalité. Que vous utilisiez le verbeux Futures ou la syntaxe déclarative, l'__intent__ du code est le même.

Les exigences ici sont de déplacer le code source qui traite de problèmes entièrement différents sous le même parapluie, de cacher toutes sortes de comportements différents derrière un seul mot-clé et de prétendre que cela réduit d'une manière ou d'une autre la charge cognitive.

C'est entièrement faux, car maintenant, chaque fois que j'utilise ce mot-clé, je dois me demander si le résultat effectuera une opération asynchrone, déclenchera des reconstructions inutiles, initialisera des objets de longue durée, effectuera des appels réseau, lira des fichiers à partir du disque ou renverra simplement une valeur statique . Toutes ces situations sont très différentes et je devrais être familier avec la saveur du crochet que j'utilise.

Je comprends de la discussion que la plupart des développeurs ici n'aiment pas se soucier de ce genre de détails et veulent un développement facile, en pouvant simplement utiliser ces "hooks" sans avoir à se soucier des détails de l'implémentation.
Utiliser ce soi-disant « crochets » bon gré mal gré sans comprendre leur pleine implication conduira à un code inefficace et mauvais, et amènera les gens à se tirer une balle dans le pied - par conséquent, cela ne résout même pas le problème de « protéger les développeurs débutants ».

Si vos cas d'utilisation sont simples, alors oui, vous pouvez utiliser des crochets bon gré mal gré. Vous pouvez utiliser et imbriquer les constructeurs à votre guise, mais comme votre application devient complexe et que vous avez du mal à réutiliser le code, je pense qu'il est justifié de devoir faire plus attention à votre propre code et à votre architecture. Si je construisais une application pour potentiellement des millions d'utilisateurs, j'hésiterais beaucoup à utiliser la "magie" qui me fait abstraction de détails importants. À l'heure actuelle, je trouve que l'API de Flutter est juste pour être simple pour des cas d'utilisation très simples, et toujours flexible pour permettre à quiconque d'implémenter tout type de logique complexe de manière très efficace.

@Rudiksz Encore

Et de toute façon, les gens peuvent toujours écrire du code efficace une fois qu'ils voient que plusieurs hooks bloquent d'une manière ou d'une autre leur application ; ils le verront lorsqu'ils établiront leur profil ou même exécuteront simplement l'application, comme vous le feriez avec le style actuel.

@Rudiksz Encore

Oh mon Dieu, ce même argument s'applique également aux personnes qui se plaignent de problèmes avec le cadre. Personne ne les oblige à ne pas utiliser le package hooks.

Je vais être très direct ici.
Ce problème ne concerne vraiment pas les hooks, statefulwidget et qui utilise quoi, mais plutôt le retour à des décennies de bonnes pratiques afin que certaines personnes puissent écrire 5 lignes de code en moins.

Votre argument ne fonctionne pas vraiment. La raison pour laquelle ce problème a été créé est que le package flutter_hooks ne fait pas tout ce qui est possible avec quelque chose dans le framework, alors que le modèle actuel l'est, car il est déjà nativement dans le framework. L'argument est de déplacer nativement les fonctionnalités de flutter_hooks dans le framework. Votre argument postule que tout ce que je peux faire avec le modèle actuel, je peux aussi le faire avec le package hooks, ce qui est faux, semble-t-il d'autres dans cette discussion. S'ils étaient vrais, alors cela fonctionnerait, ce qui signifierait également que les crochets étaient nativement dans le framework, et donc, encore une fois, puisque les crochets et les non-hooks seraient équivalents, vous pouvez utiliser le modèle actuel aussi bien que les crochets basés modèle, ce que je disais.

Je ne sais pas d'où viennent vos meilleures pratiques, car je sais que garder le code facilement lisible est une bonne pratique, et qu'une imbrication excessive est un anti-modèle. De quelles bonnes pratiques parlez-vous exactement ?

fwiw, l'équipe React a abordé la réutilisation du problème de lisibilité comme motivation 1:
Motivation des crochets : il est difficile de réutiliser la logique avec état entre les composants
React n'offre pas de moyen d'"attacher" un comportement réutilisable à un composant ... vous connaissez peut-être les modèles... qui tentent de résoudre ce problème. Mais ces modèles vous obligent à restructurer vos composants lorsque vous les utilisez, ce qui peut être lourd et rendre le code plus difficile à suivre .

J'entends tout le monde s'extasier sur le fait que Flutter est tellement plus incroyable que React. C'est peut-être parce qu'il ne fait pas tout comme React ? Vous ne pouvez pas jouer dans les deux sens, vous ne pouvez pas dire que Flutter a des kilomètres d'avance sur React et aussi lui demander de faire tout exactement comme React le fait.

Quelle que soit la solution que Flutter décide d'utiliser pour un problème donné, elle doit reposer sur ses propres mérites. Je ne connais pas React, mais apparemment, il me manque une technologie vraiment incroyable. :/

Je pense que personne ne prétend que Flutter devrait tout faire comme React.

Mais le fait est que la couche de widgets de Flutter est fortement inspirée de React. C'est indiqué dans la documentation officielle.
Et par conséquent, les widgets ont à la fois les mêmes avantages et les mêmes problèmes que les composants React.

Cela signifie également que React a plus d'expérience que Flutter pour traiter ces problèmes.
Il les a affrontés plus longtemps et les comprend mieux.

Il ne devrait donc pas être surprenant que les solutions aux problèmes Flutter soient similaires aux solutions aux problèmes React.

L'API utilisateur de @Rudiksz Flutter est très similaire au modèle basé sur les classes de React, même si l'API interne peut être différente (je ne sais pas si elles diffèrent, je ne rencontre pas vraiment beaucoup l'API interne). Je vous encourage à essayer React avec des crochets pour voir comment c'est, comme je l'ai dit plus tôt qu'il semble y avoir une dichotomie d'opinions basée presque exclusivement sur ceux qui ont et n'ont pas utilisé des constructions de type crochet dans d'autres frameworks.

Compte tenu de leur similitude, il n'est pas surprenant que les solutions aux problèmes se ressemblent, comme indiqué ci-dessus.

S'il vous plaît, faisons de notre mieux pour ne pas nous battre les uns avec les autres.

La seule chose à laquelle les combats nous mèneront, c'est de tuer cette discussion et de ne pas trouver de solution.

J'entends tout le monde s'extasier sur le fait que Flutter est tellement plus incroyable que React. C'est peut-être parce qu'il ne fait pas tout comme React ? Vous ne pouvez pas jouer dans les deux sens, vous ne pouvez pas dire que Flutter a des kilomètres d'avance sur React et aussi lui demander de faire tout exactement comme React le fait.

Souligner que l'équipe React avait des motivations similaires lorsqu'elle a proposé des crochets, valide les préoccupations que nous exprimons ici. Cela valide certainement qu'il existe un problème de réutilisation et de combinaison d'une logique à état commune dans ce type de cadre basé sur des composants, et valide également dans une certaine mesure la discussion sur la lisibilité, l'imbrication et le problème général de "l'encombrement" dans vos vues.

Je ne m'extasie sur rien, je n'ai même jamais travaillé dans React, et j'adore Flutter. Je peux juste facilement voir le problème ici.

@Rudiksz, nous ne pouvons pas être sûrs qu'il soit performant dans la pratique jusqu'à ce que nous le mettions en pratique. Ce n'est pas très facile de trancher maintenant.

@Hixie c'est un exemple d'un parcours qu'un utilisateur de flutter commun peut avoir pour implémenter un widget pour afficher le surnom de l'utilisateur à partir de l'ID utilisateur à la fois avec HookWidget et StatefulWidget .

__widget de crochet__

String useUserNickname(Id userid) {
  final name = useState("");
  useEffect(async () {
    name.value = "Loading...";
    name.value = await fetchNicknames()[userId;
  }, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__widget avec état__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";

   initState() {
     super.initState();
     fetchAndUpdate();
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      final result = await fetchNicknames()[widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

pour l'instant rien d'intéressant. les deux solutions sont assez acceptables, simples et performantes.
maintenant nous voulons utiliser UserNickname dans un ListView . comme vous pouvez le voir, fetchNicknames renvoie une carte de surnoms, pas seulement un surnom. donc l'appeler à chaque fois est redondant. quelques solutions que nous pouvons appliquer ici :

  • déplacez l'appel fetchNicknames() logique
  • à l'aide d'un gestionnaire de cache.

la première solution est acceptable mais a 2 problèmes.
1 - cela rend UserNickname inutile car ce n'est plus qu'un widget Texte et si vous voulez l'utiliser ailleurs, vous devez répéter ce que vous avez fait dans le widget parent (qui a le ListView ) . la logique pour afficher le surnom appartient au UserNickname mais nous devons le déplacer séparément.
2 - nous pouvons utiliser fetchNicknames() dans de nombreux autres sous-arbres et il est préférable de le mettre en cache pour toute l'application et pas seulement pour une partie de l'application.

alors imaginez que nous choisissions le gestionnaire de cache et que nous fournissions une classe CacheManager avec InheritedWidgets ou Provider .

après avoir ajouté la prise en charge de la mise en cache :

__widget de crochet__

String useUserNickname(Id userid) {
  final context = useContext();
  final cache = Provider.of<CacheManager>(context);
  final name = useState("");
  useEffect(async () {
    name.value = "Loading...";
    var cachedValue = cache.get("nicknames");
    if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        cache.set("nicknames", result );
        cachedValue = result ;
    }
    final result = cachedValue[widget.userId];
    name.value = result ;
  }, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__widget avec état__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";
  CacheManager cache;

   initState() {
     super.initState();
     fetchAndUpdate();
     this.cache = Provider.of<CacheManager>(context);
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      var cachedValue = this.cache.get("nicknames");
      if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        this.cache.set("nicknames", result );
        cachedValue = result ;
      }
      final result = cachedValue [widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

nous avons un serveur socket qui avertit les clients lorsque les surnoms changent.

__widget de crochet__

String useUserNickname(Id userid) {
  final context = useContext();
  final cache = Provider.of<CacheManager>(context);
  final notifications = Provider.of<ServiceNotifications>(context);
  final name = useState("");

  fetchData() async {
    name.value = "Loading...";
    var cachedValue = cache.get("nicknames");
    if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        cache.set("nicknames", result );
        cachedValue = result ;
    }
    final result = cachedValue[widget.userId];
    name.value = result ;
   }

  useEffect(() {
     final sub = notifications.on("nicknameChanges", fetchData);
     return () => sub.unsubscribe();
   }, [])

  useEffect(fetchData, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__widget avec état__

class UserNickname extends Widget {
  final userId;
  // ... createState() ...
}

class UserNicknameState extends State {

  String nickname= "";
  CacheManager cache;
  ServerNotification notifications;
  CancelableOperation sub;

   initState() {
     super.initState();
     fetchAndUpdate();
     this.cache = Provider.of<CacheManager>(context);
     this.notifications = Provider.of<ServerNotification>(context);
     this.sub = notifications.on("nicknameChanges", fetchAndUpdate);
   }

   dispose() {
      super.dispose();
      this.sub.unsubscribe();
   }

   fetchAndUpdate() async {
      setState(() { this.nickname = "Loading..." });
      var cachedValue = this.cache.get("nicknames");
      if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        this.cache.set("nicknames", result );
        cachedValue = result ;
      }
      final result = cachedValue [widget.userId];
      setState(() { this.nickname = result });
    }


 void didUpdateWidget(oldWidget) { 
     if (oldWidget.userId != widget.userId) {
        fetchAndUpdate();
     }
   }

  Widget build(BuildContext context) {
    return Text(this.nickname);
  }
}

jusqu'à présent, les deux mises en œuvre sont acceptables et bonnes. OMI, le passe-partout dans le statful ne pose aucun problème. le problème survient lorsque nous avons besoin d'un widget comme UserInfo qui a à la fois le pseudo et l'avatar de l'utilisateur. nous ne pouvons pas non plus utiliser le widget UserNickname car nous devons l'afficher dans une phrase comme "Bienvenue [nom d'utilisateur]".

__widget de crochet__

useFetchUserNickname(userId) // old code
useUserAvatar(userId) // implementation like `useFetchUserNickname`

class UserNickname extends HookWidget {
  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    final avatar = useUserAvatar(userId);
    return Row(
      children: [Image.network(avatar), Text(nickname)],
    );
  }
}

mais pour le __stateful widget__, nous ne pouvons pas simplement utiliser la logique que nous avons écrite. nous devons déplacer la logique dans une classe (comme un Property que vous avez suggéré) et nous devons toujours réécrire le widget glue avec la classe de propriété dans le nouveau widget.

si vous voyez les changements dans les 3 premiers exemples, nous n'avons pas du tout changé le widget lui-même car les seuls changements nécessaires étaient dans la logique d'état et le seul endroit qui a changé était tout, la logique d'état.
cela nous a donné une logique d'état propre (opiniâtre), composable et totalement réutilisable que nous pouvons utiliser n'importe où.

À mon humble avis, le seul problème est d'appeler useUserNickname est effrayant car une seule fonction peut faire autant.
mais dans mes années d'expérience dans la réaction et l'utilisation de flutter_hooks dans 2 applications qui sont en production rn (qui utilisent beaucoup de hooks) prouve que ne pas avoir une bonne gestion d'état (j'ai aussi essayé MobX et d'autres solutions de gestion d'état mais la colle dans le widget est toujours là) est beaucoup plus effrayant. Je n'ai pas besoin d'écrire des documents de 5 pages pour chaque écran d'une application frontale. L'application appelle trop le serveur ? tâche facile, je vais au crochet associé et je le modifie et l'ensemble de l'application est corrigé, car toute l'application utilise ce crochet. nous pouvons avoir des choses similaires dans les applications sans utiliser de crochets avec une bonne abstraction, mais ce que je dis, c'est que les crochets sont cette bonne abstraction.

Je suis presque sûr que @gaearon peut le dire mieux que moi. (s'il est d'accord avec moi ofc)

en voyant l'exemple ci-dessus, aucune des méthodes ci-dessus (widget avec état et hook) n'est plus performante que l'autre. mais le fait est que l'un d'eux encourage les gens à écrire le code performant.

il est également possible de mettre à jour uniquement le sous-arbre que nous devons mettre à jour comme StreamBuilder lorsqu'il y a trop de mises à jour (par exemple des animations) avec :

1 - créer simplement un nouveau widget qui est une option totalement viable pour HookWidget et StatefulWidget/StatelessWidget
2 - utiliser quelque chose de similaire à HookWidgetBuilder dans le package flutter_hooks car les données des widgets parents et enfants sont très étroitement couplées.

Note latérale : j'apprécie vraiment @Hixie et @rrousselGit pour avoir discuté de ce sujet et mis autant d'énergie dans ce numéro. J'attends avec impatience le résultat de ces discussions.

Je trouve quelque chose d'assez cool/élégant, je pense, basé sur le point de départ de @Hixie . Pas encore tout à fait prêt à partager, mais cela me permet de créer des exemples de code assez décents, qui, je pense, seront plus faciles à comparer apples:apples plutôt que des hooks qui semblent si étrangers.

Alors, imaginons que nous ayons un StatefulWidget avec cette signature :

class ExampleSimple extends StatefulWidget {
  final Duration duration1;
  final Duration duration2;
  final Duration duration3;

  const ExampleSimple({Key key, this.duration1, this.duration2, this.duration3}) : super(key: key);

  <strong i="9">@override</strong>
  _ExampleSimpleState createState() => _ExampleSimpleState();
}

Si nous devions implémenter l'état à l'aide de contrôleurs d'animation vanille, nous obtenons quelque chose comme :

class _ExampleSimpleVanillaState extends State<ExampleSimpleVanilla> with SingleTickerProviderStateMixin {
  AnimationController _anim1;
  AnimationController _anim2;
  AnimationController _anim3;

  <strong i="13">@override</strong>
  void initState() {
    _anim1 = AnimationController(duration: widget.duration1, vsync: this);
    _anim1.forward();
    _anim2 = AnimationController(duration: widget.duration2, vsync: this);
    _anim2.forward();
    _anim3 = AnimationController(duration: widget.duration3, vsync: this);
    _anim3.forward();
    super.initState();
  }

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: _anim2.value * 20, horizontal: _anim3.value * 30,),
      color: Colors.red.withOpacity(_anim1.value),
    );
  }

  <strong i="15">@override</strong>
  void didUpdateWidget(ExampleSimpleVanilla oldWidget) {
    if (oldWidget.duration1 != widget.duration1) {
      _anim1.duration = widget.duration1;
    }
    if (oldWidget.duration2 != widget.duration2) {
      _anim1.duration = widget.duration1;
    }
    if (oldWidget.duration3 != widget.duration3) {
      _anim1.duration = widget.duration1;
    }
    super.didUpdateWidget(oldWidget);
  }

  <strong i="16">@override</strong>
  void dispose() {
    _anim1.dispose();
    _anim2.dispose();
    _anim3.dispose();
    super.dispose();
  }
}

Si nous le créons à l'aide d'une StatefulProperty, nous obtenons quelque chose comme ceci :

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulAnimationProperty _anim1;
  StatefulAnimationProperty _anim2;
  StatefulAnimationProperty _anim3;

  <strong i="6">@override</strong>
  void initStatefulProperties({bool firstRun = false}) {
    _anim1 = initProperty(_anim1, StatefulAnimationProperty(duration: widget.duration1, playOnInit: true));
    _anim2 = initProperty(_anim2, StatefulAnimationProperty(duration: widget.duration2, playOnInit: true));
    _anim3 = initProperty(_anim3, StatefulAnimationProperty(duration: widget.duration3, playOnInit: true));
    super.initStatefulProperties(firstRun: firstRun);
  }

  <strong i="7">@override</strong>
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: _anim2.controller.value * 20, horizontal: _anim3.controller.value * 30,),
      color: Colors.red.withOpacity(_anim1.controller.value),
    );
  }
}

Quelques notes sur les différences ici :

  1. En haut, l'un fait 20 lignes, l'autre 45. L'un fait 1315 caractères et l'autre 825. Seules 3 lignes et 200 caractères comptent dans cette classe (ce qui se passe dans la construction), donc c'est déjà une amélioration massive du signal : rapport de
  2. Les options vanilles ont plusieurs points où des bogues peuvent être créés. Oubliez de disposer, ou oubliez de gérer didChange, ou faites une erreur dans didChange, et vous avez un bogue dans votre base de code. Cela empire lorsque plusieurs types de contrôleurs sont utilisés. Ensuite, vous avez des fonctions uniques qui détruisent des objets de tous types différents, qui ne seront pas nommés de manière agréable et séquentielle comme ceci. Cela devient compliqué et il est assez facile de faire des erreurs ou de manquer des entrées.
  3. L'option vanille ne fournit aucune méthode pour réutiliser des modèles ou une logique communs, comme playOnInit, je dois donc dupliquer cette logique ou créer une fonction personnalisée dans une classe très unique qui souhaite utiliser un animateur.
  4. Il n'est pas nécessaire de comprendre SingleTickerProviderMixin ici, qui est «magique» et a obscurci ce qu'est un Ticker pour moi pendant des mois (avec le recul, j'aurais dû lire le cours, mais chaque tutoriel dit simplement : ajoutez ce mixin magique). Ici, vous pouvez consulter directement le code source de StatefulAnimationProperty et voir comment les contrôleurs d'animation utilisent un fournisseur de ticker directement et en contexte.

Vous devez plutôt comprendre ce que fait StatefulPropertyManager, mais surtout, cela a été appris une fois et appliqué à des objets de tout type, SingleTickerProviderMixin est principalement spécifique à l'utilisation de contrôleurs d'animation, et chaque contrôleur peut avoir son propre mixage pour faciliter l'utilisation, ce qui devient compliqué. Le simple fait d'avoir des "StatefulObjects" discrets qui connaissent tout cela (comme le fait un constructeur !), est beaucoup plus propre et s'adapte mieux.

Le code de StatefulAnimationProperty ressemblerait à ceci :

class StatefulAnimationProperty extends BaseStatefulProperty<StatefulAnimationProperty> implements TickerProvider {
  final Duration duration;
  final TickerProvider vsync;
  final bool playOnInit;

  StatefulAnimationProperty({<strong i="7">@required</strong> this.duration, <strong i="8">@required</strong> this.vsync, this.playOnInit = false});

  AnimationController get controller => _controller;
  AnimationController _controller;

  Ticker _ticker;

  <strong i="9">@override</strong>
  Ticker createTicker(onTick) {
    _ticker ??= Ticker((elapsed)=>handleTick(elapsed, onTick));
    return _ticker;
  }

  handleTick(Duration elapsed, TickerCallback onTick) {
    managerWidget.buildWidget(); //TODO: This just calls setState() on the host widget. Is there some better way to do this?
    onTick(elapsed);
  }

  <strong i="10">@override</strong>
  void init(StatefulAnimationProperty old) {
    _controller = old?.controller ??
        AnimationController(
          duration: duration ?? Duration(seconds: 1),
          vsync: vsync ?? this,
        );
    if (playOnInit && old?.controller == null) {
      _controller.forward();
    }
    super.init(old);
  }

  <strong i="11">@override</strong>
  void update(StatefulAnimationProperty old) {
    if (duration != old.duration) {
      _controller.duration = duration;
    }
    if (vsync != old.vsync) {
      _controller.resync(vsync);
    }
    super.update(old);
  }

  <strong i="12">@override</strong>
  void dispose() {
    _controller?.dispose();
    _ticker?.dispose();
    super.dispose();
  }
}

Enfin, il convient de noter que la lisibilité peut être encore améliorée avec l'utilisation d'extensions, nous pourrions donc avoir quelque chose comme :

  void initStatefulProperties({bool firstRun = false}) {
    _anim1.init(duration: widget.duration1, playOnInit: true);
    _anim2.init(duration: widget.duration2, playOnInit: true);
    _anim3.init(duration: widget.duration3, playOnInit: true);
    super.initStatefulProperties(firstRun: firstRun);
  }

[Edit] Comme pour faire mon propre point, mon exemple vanille a un bug. J'ai oublié de transmettre la bonne durée à chaque animateur dans le didUpdateWidget. Combien de temps cela nous aurait-il fallu pour trouver ce bogue dans la nature, si personne ne l'avait remarqué lors de la revue de code ?? Quelqu'un ne l'a-t-il pas remarqué en lisant ? Le laisser en cause, c'est un exemple parfait de ce qui se passe dans le monde réel.

Voici une vue à vol d'oiseau, avec le passe-partout marqué en rouge :
image

Ce ne serait pas si mal s'il s'agissait d'un simple passe-partout, et le compilateur vous criait dessus s'il manquait. Mais tout est facultatif ! Et lorsqu'il est omis, crée des bogues. C'est donc en fait une très mauvaise pratique et pas du tout SEC. C'est là qu'interviennent les constructeurs, mais ils ne sont bons que pour des cas d'utilisation simplistes.

Ce que je pense est très intéressant à ce sujet, c'est comment une centaine de lignes et un simple mixin à State, rendent un tas de classes existantes redondantes. Il n'est pratiquement plus nécessaire maintenant d'utiliser le TickerProviderMixins par exemple. TweenAnimationBuilder n'a presque jamais besoin d'être utilisé, à moins que vous ne vouliez réellement créer un sous-contexte. De nombreux problèmes traditionnels tels que la gestion des contrôleurs de mise au point et des contrôleurs textInput sont considérablement allégés. L'utilisation de Streams devient beaucoup plus attrayante et moins fastidieuse. Dans l'ensemble de la base de code, l'utilisation de Builders pourrait être réduite en général, ce qui conduira à des arbres plus facilement accessibles.

Il est également _extrêmement_ facile de créer vos propres objets d'état personnalisés, comme l'exemple FetchUser répertorié précédemment, qui nécessite actuellement un générateur.

Je pense que ce serait une question très intéressante à poser dans le prochain sondage des développeurs Flutter.

Ce serait un bon début. Divisez ce problème en différentes parties/questions et voyez s'il s'agit d'un problème réel que les développeurs de Flutter souhaitent résoudre.

Une fois que c'est clair, cette conversation sera plus fluide et enrichissante

Les réactions Emoji sous chaque commentaire donnent une idée claire si la communauté considère cela comme un problème ou non. L'opinion des développeurs qui ont lu plus de 250 longs commentaires sur ce problème signifie beaucoup à mon humble avis.

@esDotDev C'est similaire à certaines des idées avec lesquelles j'ai joué, bien que j'aime votre idée d'avoir simplement la propriété elle-même comme fournisseur de téléscripteur, je n'y avais pas pensé. Une chose qui manque à votre implémentation et que nous devrions ajouter, je pense, est la gestion de TickerMode (qui est le point de TickerProviderStateMixin).

La principale chose avec laquelle je me bats est de savoir comment le faire de manière efficace. Par exemple, ValueListenableBuilder prend un argument enfant qui peut être utilisé pour améliorer les performances de manière mesurable. Je ne vois pas comment faire cela avec l'approche Propriété.

@Hixie
Je comprends que les pertes d'efficacité avec des approches comme celle-ci semblent inévitables. Mais j'aime l'état d'esprit de Flutter consistant à optimiser après avoir profilé votre code. De nombreuses applications bénéficieraient de la clarté et de la concision de l'approche Propriété. L'option de profiler votre code et de le refactoriser dans les constructeurs ou de séparer une partie du widget dans son propre widget est toujours là.

La documentation aurait juste besoin de refléter les meilleures pratiques et de clarifier les compromis.

La principale chose avec laquelle je me bats est de savoir comment le faire de manière efficace. Par exemple, ValueListenableBuilder prend un argument enfant qui peut être utilisé pour améliorer les performances de manière mesurable. Je ne vois pas comment faire cela avec l'approche Propriété.

Hm, je pense que tout l'intérêt des propriétés concerne les objets non visuels. Si quelque chose veut avoir un emplacement de contexte dans l'arborescence, alors cette chose devrait être un constructeur (en fait, ce sont les seules choses qui devraient maintenant être des constructeurs, je pense ?)

Nous aurions donc une StatefulValueListenableProperty que nous utilisons la plupart du temps lorsque nous voulons simplement lier la vue entière. Nous avons alors également un ValueListenableBuilder dans le cas où nous voulons reconstruire une sous-section de notre arbre.

Cela résout également le problème de l'imbrication, car l'utilisation d'un générateur comme nœud feuille n'est pas aussi perturbatrice pour la lisibilité, que l'imbrication de 2 ou 3 en haut de votre arborescence de widgets.

@TimWhiting Une grande partie de la philosophie de conception de Flutter consiste à guider les gens vers le bon choix. J'aimerais éviter d'encourager les gens à suivre un style dont ils devraient ensuite s'éloigner pour obtenir de meilleures performances. Il se peut qu'il n'y ait aucun moyen de répondre à tous les besoins à la fois, mais nous devons absolument essayer.

@Hixie
Et quelque chose comme ça pour les constructeurs ?

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulAnimationProperty _anim1;
  StatefulAnimationBuilderProperty _anim2;

  <strong i="7">@override</strong>
  void initStatefulProperties({bool firstRun = false}) {
    _anim1 = initProperty(_anim1, StatefulAnimationProperty(duration: widget.duration1, playOnInit: true));
    _anim2 = initProperty(_anim3, StatefulAnimationBuilderProperty(duration: widget.duration3, playOnInit: true));
    super.initStatefulProperties(firstRun: firstRun);
  }

  <strong i="8">@override</strong>
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red.withOpacity(_anim1.controller.value),
      child: _anim2(child: SomeChildWidget()),
    );
  }
}

Peux-tu élaborer? Je ne suis pas sûr de comprendre la proposition.

Je pense qu'il dit que le StatefulProperty pourrait fournir une méthode de construction facultative pour les propriétés qui ont un composant visuel :

return Column(
   children: [
      TopContent(),
      _valueProperty.build(SomeChildWidget()),
   ]
)

Ce qui est assez imo,

Oui, je ne sais pas si cela fonctionnerait, mais la méthode de construction prendrait un enfant comme un constructeur ordinaire, sauf que les autres propriétés du constructeur sont définies par la propriété.
Si vous avez besoin du contexte du générateur, la méthode build accepte un argument de générateur qui fournit le contexte.

Sous le capot, la méthode peut simplement créer un générateur normal avec les propriétés spécifiées, passer l'argument enfant au générateur normal et le renvoyer.

Supposons que vous ayez ce code :

Widget build(BuildContext context) {
  return ExpensiveParent(
    child: ValueListenableBuilder(
      valueListenable: foo,
      child: ExpensiveChild(),
      builder: (BuildContext context, value, Widget child) {
        return SomethingInTheMiddle(
          value: value,
          child: child,
        );
      }
    ),
  );
}

... comment convertiriez-vous cela?

@esDotDev J'aime votre idée de faire en sorte que la propriété elle-même soit le fournisseur de téléscripteur, je n'y avais pas pensé.

L'un des aspects les plus forts de cette approche de style crochet est que vous pouvez _complètement_ encapsuler la logique avec état, quelle qu'elle soit. Donc, dans ces cas, la liste complète pour AC est :

  1. Créer le CA
  2. Donnez-lui un ticker
  3. Gérer les changements de widget
  4. Gérer le nettoyage de l'ac et du ticker
  5. Reconstruire la vue sur la coche

Actuellement, les choses sont partagées avec les développeurs qui gèrent (ou ne gèrent pas) 1,3,4 manuellement et de manière répétitive, et le semi-magique SingleTickerProviderMixin s'occupe des 2 et 5 (avec nous en passant 'cela' comme vsync, ce qui m'a dérouté pendant des mois ! ). Et SingleTickerProviderMixin lui-même est clairement une tentative de solution à ce type de problème, sinon pourquoi ne pas aller jusqu'au bout et nous faire implémenter TickerProvider pour chaque classe, ce serait beaucoup plus clair.

Supposons que vous ayez ce code :

Widget build(BuildContext context) {
  return ExpensiveParent(
    child: ValueListenableBuilder(
      valueListenable: foo,
      child: ExpensiveChild(),
      builder: (BuildContext context, value, Widget child) {
        return SomethingInTheMiddle(
          value: value,
          child: child,
        );
      }
    ),
  );
}

... comment convertiriez-vous cela?

class _ExampleSimpleState extends State<ExampleSimple> with StatefulPropertyManager {
  StatefulValueListenableBuilder _fooBuilder;

  <strong i="10">@override</strong>
  void initStatefulProperties({bool firstRun = false}) {
    _fooBuilder = initProperty(StatefulValueListenableProperty(valueListenable: widget.foo)); 
    super.initStatefulProperties(firstRun: firstRun);
  }

  <strong i="11">@override</strong>
  Widget build(BuildContext context) {
    return ExpensiveParent(
      child: SomethingInTheMiddle(
        _fooBuilder.value,
        _fooBuilder.builder(childBuilder: () => ExpensiveChild()),
      ),
    );
  }
}

@Hixie
Merci pour l'exemple. J'ai fait de mon mieux. J'ai peut-être raté quelque chose.

Il est important de noter que le générateur met en cache l'enfant. La question serait quand faut-il réellement reconstruire l'enfant ? Je pense que c'était la question que tu essayais de soulever..

@Hixie avez-vous vu https://github.com/flutter/flutter/issues/51752#issuecomment -671104377
Je pense qu'il y a de très bons points.
Je construis aujourd'hui quelque chose avec beaucoup de ValueListenableBuilder et je peux seulement dire que ce n'est pas agréable à lire.

@Hixie
Merci pour l'exemple. J'ai fait de mon mieux. J'ai peut-être raté quelque chose.

Je ne pense pas que cela fonctionne car la propriété se lie à l'état dans lequel elle est définie, donc ExpensiveParent est toujours reconstruit ici. Ensuite, je pense que la mise en cache de l'enfant est également problématique, car dans l'exemple Builder, il ne saurait reconstruire l'enfant que lorsque l'état parent est généré, mais dans cette méthode, la propriété ne sait pas quand invalider son cache (mais peut-être que cela est soluble ?)

Mais tbh, c'est le cas d'utilisation parfait pour les constructeurs, lorsque vous souhaitez introduire un nouveau contexte. Je pense qu'il est assez élégant d'avoir juste le contexte de StatefulProperties (état pur) et StatefulWidgets (mélange d'état et de disposition).

Chaque fois que vous créez intentionnellement un sous-contexte, vous le ferez par définition plus loin dans votre arbre, ce qui permet de lutter contre l'un des principaux inconvénients pour les constructeurs (imbrication forcée sur l'ensemble de l'arbre)

@escamoteur (et @sahandevs qui a écrit ce commentaire) ouais

Cela dit, je pense que le travail que @esDotDev et @TimWhiting montrent ci-dessus est très intéressant et pourrait résoudre ces problèmes. Ce n'est pas aussi bref que Hooks, mais c'est plus fiable. Je pense qu'il serait parfaitement logique d'emballer quelque chose comme ça, cela pourrait même être un favori Flutter si cela fonctionne bien. Je ne suis pas sûr que cela ait du sens en tant que fonctionnalité de base du cadre, car l'amélioration n'est pas _que_ substantielle une fois que vous prenez en compte la complexité des propriétés de construction et l'impact sur les performances, et comment différentes personnes préféreraient différents styles. En fin de compte, il devrait être acceptable pour différentes personnes d'utiliser différents styles, mais nous ne voudrions pas que le framework de base ait plusieurs styles, c'est tout simplement trompeur pour les nouveaux développeurs.

Il y a aussi un argument à faire valoir que la bonne façon d'apprendre Flutter est d'abord de comprendre les widgets, puis d'apprendre les outils qui les abstrait (Hooks ou autre), plutôt que de sauter directement à la syntaxe abstraite. Sinon, il vous manque un élément clé du fonctionnement du système, ce qui est susceptible de vous égarer en termes d'écriture de code performant.

Je ne vois pas non plus un bon moyen d'obtenir la syntaxe aussi brève que celle proposée dans ce commentaire sans casser le rechargement à chaud (par exemple, si vous modifiez le nombre de crochets que vous utilisez, il n'est pas clair comment ils pourraient être conservés avec état lors d'un rechargement ).

Les crochets fonctionnent avec le rechargement à chaud sans problème.
Le premier hook avec un runtimeType non correspondant provoque la destruction de tous les hooks suivants.

Cela prend en charge l'ajout, la suppression et la réorganisation.

Je pense qu'il existe un argument selon lequel l'abstraction complète est préférable à l'abstraction partielle qui existe actuellement.

Si je veux comprendre comment Animator fonctionne dans le contexte d'une propriété, soit je l'ignore totalement, soit je me lance, et tout est là, autonome et cohérent.

Si je veux comprendre comment AnimatorController fonctionne dans le contexte d'un StatefulWidget, j'ai besoin (je suis obligé) de comprendre les crochets du cycle de vie de base, mais j'ai ensuite été épargné de comprendre comment fonctionne le mécanisme de tick sous-jacent. C'est le pire des deux mondes dans un certain sens. Pas assez de magie pour que cela "fonctionne", mais juste assez pour embrouiller les nouveaux utilisateurs et les forcer à se fier aveuglément à un mixin (ce qui en soi est un nouveau concept pour la plupart) et à une propriété vsync magique.

Je ne suis pas sûr des autres exemples dans la base de code, mais cela s'appliquerait à toute situation où des mixins d'aide ont été fournis pour StatefulWidget, mais il y a encore d'autres amorçages qui doivent toujours être effectués. Les développeurs apprendront le bootstrap (la partie ennuyeuse) et ignoreront le Mixin (le bit intéressant/complexe)

Cela dit, je pense que le travail que @esDotDev et @TimWhiting montrent ci-dessus est très intéressant et pourrait résoudre ces problèmes. Ce n'est pas aussi bref que Hooks, mais c'est plus fiable

Comment est-ce plus fiable ?

Nous ne pouvons toujours pas créer/mettre à jour des propriétés de manière conditionnelle ou en dehors de son cycle de vie, car nous pourrions entrer dans un mauvais état. Par exemple, l'appel conditionnel d'une propriété ne supprimera pas la propriété lorsque la condition est fausse.
Et toutes les propriétés sont toujours réévaluées à chaque reconstruction.

Mais cela provoque plusieurs problèmes, tels que forcer les utilisateurs à utiliser ! partout après NNBD ou permettre potentiellement aux utilisateurs d'accéder à une propriété avant sa mise à jour.

Par exemple, que se passe-t-il si quelqu'un lit une propriété dans didUpdateWidget ?

  • initProperties exécuté avant le cycle de vie ? Mais cela signifie que nous devrons peut-être mettre à jour les propriétés plusieurs fois par build.
  • initProperties t-il été exécuté après didUpdateWidget ? Ensuite, l'utilisation de propriétés dans didUpdateWidget peut conduire à un état obsolète

Donc au final, on a tous les problèmes de hooks mais :

  • nous ne pouvons pas utiliser les propriétés dans `StatelessWidget. Ainsi, la lisibilité de StreamBuilder/ValueListenableBuilder/... est toujours un problème – ce qui était la principale préoccupation.
  • il existe de nombreux cas limites
  • il est plus difficile de créer des propriétés personnalisées (on ne peut pas simplement extraire un tas de propriétés dans une fonction)
  • il est plus difficile d'optimiser les reconstructions

Au final, l'exemple donné n'est pas différent en comportement de :

class Example extends StatelessWidget {
  <strong i="29">@override</strong>
  Widget build(context) {
    final value1 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));
    final value2 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));
    final value3 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));

    return Container(
     margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30),
     color: Colors.red.withOpacity(value1),
      child: _anim2(child: SomeChildWidget()),
    );
  }
}

Mais cette syntaxe prend en charge beaucoup plus de choses, telles que :

Retours anticipés :

class Example extends StatelessWidget {
  <strong i="34">@override</strong>
  Widget build(context) {
    final value1 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));

    if (condition) {
      return Container();
    }

    final value2 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));

    ...
  }
}

qui éliminerait value2 lorsque condition passe à false

Extraction de groupes de constructeurs dans une fonction :

Widget build(context) {
  final foo = keyword FooBuilder();
  final bar = keyword BarBuilder();

  return Text('$foo $bar');
}

peut être changé en :

Builder<String> LabelBuilder() builder* {
  final foo = keyword FooBuilder();
  final bar = keyword BarBuilder();

  return '$foo $bar';
}

Widget build(context) {
  final label = keyword LabelBuilder();

  return Text(label);
}

Optimiser les reconstructions

Le paramètre child est toujours réalisable :

Widget build(context) {
  final value = keyword StreamBuilder();

  return Builder(
    builder: (context, child) {
      final value2 = keyword TweenAnimationBuilder();
      final value = keyword ValueListenableBuilder();

      return Whatever(child: child);
    },
    child: ExpensiveChild()
  );
}

Dans le cadre du langage, nous pourrions même avoir du sucre de syntaxe pour cela :

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword TweenAnimationBuilder();
      final value2 = keyword ValueListenableBuilder();

      return Text();
    },
  );
}

Bonus : en tant que fonction linguistique, les appels conditionnels sont pris en charge

Dans le cadre du langage, nous pouvons prendre en charge un tel scénario :

Widget build(context) {
  String label;

  if (condition) {
    label = keyword LabelBuilder();
  } else {
    label = keyword AnotherBuilder();
  }

  final value2 = keyword WhateverBuilder();

  return ...
}

Ce n'est pas très utile, mais supporté - car puisque la syntaxe est compilée, il est capable de différencier chaque utilisation de keyword en s'appuyant sur des métadonnées qui ne sont pas disponibles autrement.

Concernant la lisibilité des constructeurs, voici l'exemple précédent, mais réalisé avec des constructeurs. Il résout tous les besoins de fiabilité et d'utilisation du code, mais regardez ce qu'il a fait à mon pauvre arbre de widgets :'(

class _ExampleSimpleBuilderState extends State<ExampleSimpleBuilder> {
  <strong i="6">@override</strong>
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
        tween: Tween(begin: 0, end: 1),
        duration: widget.duration1,
        builder: (_, value1, __) {
          return TweenAnimationBuilder<double>(
              tween: Tween(begin: 0, end: 1),
              duration: widget.duration2,
              builder: (_, value2, __) {
                return TweenAnimationBuilder<double>(
                    tween: Tween(begin: 0, end: 1),
                    duration: widget.duration3,
                    builder: (_, value3, __) {
                      return Container(
                        margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30),
                        color: Colors.red.withOpacity(value1),
                      );
                    });
              });
        });
  }
}

Il est beaucoup plus difficile (à mes yeux du moins) de repérer le code qui compte. De plus, fwiw, j'ai dû recommencer environ 3 fois en écrivant ceci, car j'étais continuellement confus quant à savoir à quel support appartenait où, où mes demi-colonnes devraient aller, etc. Les constructeurs imbriqués ne sont tout simplement pas amusants à écrire ou à travailler à l'intérieur. Un mauvais point-virgule et dartfmt écrasent complètement le tout.

Comment est-ce plus fiable ?

C'est un exemple parfait de la raison pour laquelle ce _devrait_ être un plugin de base imo. La connaissance du domaine requise ici est _deep_. J'ai les connaissances en script pour implémenter un système de mise en cache simple comme celui-ci, je n'ai même pas les connaissances du domaine pour connaître tous les cas extrêmes qui peuvent se produire, ou les mauvais états dans lesquels nous pouvons entrer. A part Remi, je pense qu'il y a environ 4 développeurs dans le monde en dehors de l'équipe Flutter qui connaissent ce genre de choses ! (en exagérant évidemment).

La question de la prise en charge des widgets sans état est une bonne question. D'un côté, je comprends, les StatefulWidgets sont étrangement verbeux. Par contre, on parle bien ici de verbosité pure. Il n'y a pas de bogues qui peuvent arriver du fait de devoir définir 2 classes, il n'y a aucun moyen de tout gâcher, le compilateur ne vous laisse pas, il n'y a jamais rien d'intéressant que je veuille faire dans le StatelessWidget. Donc, je ne suis pas convaincu que ce soit un problème majeur... Ce serait CERTAINEMENT bien d'avoir, mais c'est le dernier 5% imo, pas quelque chose sur quoi rester coincé.

D'un autre côté... cette syntaxe de remi avec prise en charge des mots-clés est absolument magnifique et incroyablement flexible/puissante. Et s'il vous offre le support StatelessWidget gratuitement, alors c'est juste en plus 🔥

Soutenir StatelessWidget est un gros problème pour l'OMI. Facultatif, mais toujours très cool.

Bien que je convienne que ce n'est pas critique, les gens se battent déjà pour utiliser des fonctions au lieu de StatelessWidget.
Exiger des gens qu'ils utilisent un StatefulWidget pour utiliser Builders (car la plupart des Builders auraient probablement un équivalent Property) ne ferait qu'aggraver le conflit.

Non seulement cela, mais, dans un monde où nous pouvons créer des fonctions d'ordre supérieur dans dart (https://github.com/dart-lang/language/issues/418), nous pourrions nous débarrasser complètement des classes :

<strong i="9">@StatelessWidget</strong>
Widget Example(BuildContext context, {Key key, String param}) {
  final value = keyword StreamBuilder();

  return Text('$value');
}

ensuite utilisé comme :

Widget build(context) {
  // BuildContext and Key are automatically injected
  return Example(param: 'hello');
}

C'est quelque chose qui est pris en charge parfunctional_widget - qui est un générateur de code où vous écrivez une fonction et il génère une classe pour vous - qui prend également en charge HookWidget .

La différence étant que la prise en charge des fonctions d'ordre supérieur dans Dart supprimerait le besoin de génération de code pour prendre en charge une telle syntaxe.

Je devine ce que @Hixie voulait dire par plus fiable, c'est qu'il ne souffre pas de l'ordre des opérations / du problème conditionnel des crochets, car c'est très "peu fiable" d'un POV architectural (bien que je réalise que c'est une règle facile à apprendre et ne pas violer une fois appris).

Mais votre proposition avec le mot-clé non plus. Je pense que les arguments en faveur d'un nouveau mot clé sont assez solides :

  • Plus flexible et composable que de se greffer sur State
  • Syntaxe encore plus succincte
  • Fonctionne en Stateless, ce qui est une très bonne option à avoir

Ce que je n'aime pas à ce sujet, c'est que nous nous inquiétons du coût de la définition des propriétés sur un objet simple plusieurs fois/construction, mais que nous préconisons ensuite une solution qui créera essentiellement un million de niveaux de contexte et un tas de coûts de mise en page. Est-ce que j'ai mal compris ?

L'autre inconvénient est cette idée de magie. Mais si vous allez faire quelque chose de magique, un nouveau mot-clé est un moyen efficace de le faire, je pense, car il permet de mettre en évidence et d'appeler facilement la communauté, et d'expliquer ce que c'est et comment cela fonctionne. Ce serait essentiellement tout ce dont tout le monde parle pour la prochaine année dans Flutter et je suis sûr que nous verrions une explosion de plugins sympas qui en découleraient.

Je devine ce que @Hixie voulait dire par plus fiable, c'est qu'il ne souffre pas de l'ordre des opérations / du problème conditionnel des crochets, car c'est très "peu fiable" d'un POV architectural (bien que je réalise que c'est une règle facile à apprendre et ne pas violer une fois appris).

Mais les hooks ne souffrent pas non plus de ce problème, car ils sont analysables statiquement, et nous pouvons donc avoir une erreur de compilation lorsqu'ils sont mal utilisés.

Ceci est un non-problème

De même, si les erreurs personnalisées sont interdites, comme je l'ai mentionné précédemment, Property souffre exactement du même problème.
On ne peut pas raisonnablement écrire :

Property property;

<strong i="12">@override</strong>
void initProperties() {
  if (condition) {
    property = init(property, MyProperty());
  }
}

car le passage de condition de vrai à faux ne supprimera pas la propriété.

Nous ne pouvons pas vraiment l'appeler en boucle non plus. Cela n'a pas vraiment de sens, puisqu'il s'agit d'une mission unique. Quel est le cas d'utilisation de l'exécution de la propriété dans une boucle ?

Et le fait que nous puissions lire les propriétés dans n'importe quel ordre semble dangereux
Par exemple on pourrait écrire :

Property first;
Property second;

<strong i="20">@override</strong>
void initProperties() {
  // The state of first depends on second, but second is updated after first
  // So we could end up in a bad state, similar to how the build method of a Widget should depend
  // on the context.size
  first = init(property, MyProperty(second?.value));

  second = init(property, Whatever());
}
> class _ExampleSimpleBuilderState extends State<ExampleSimpleBuilder> {
>   <strong i="5">@override</strong>
>   Widget build(BuildContext context) {
>     return TweenAnimationBuilder<double>(
>         tween: Tween(begin: 0, end: 1),
>         duration: widget.duration1,
>         builder: (_, value1, __) {
>           return TweenAnimationBuilder<double>(
>               tween: Tween(begin: 0, end: 1),
>               duration: widget.duration2,
>               builder: (_, value2, __) {
>                 return TweenAnimationBuilder<double>(
>                     tween: Tween(begin: 0, end: 1),
>                     duration: widget.duration3,
>                     builder: (_, value3, __) {
>                       return Container(
>                         margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30),
>                         color: Colors.red.withOpacity(value1),
>                       );
>                     });
>               });
>         });
>   }
> }

C'est un exemple tellement étrange. Êtes-vous sûr qu'AnimatedContainer ne peut pas déjà le faire ?

Bien sûr. L'exemple ici est d'utiliser 3 animations dans un widget pour faire "X". Le X est volontairement simplifié dans l'exemple pour mettre en évidence la quantité de passe-partout.

Ne vous concentrez pas sur la façon dont je les utilise. Dans un exemple réel, le widget "core" aurait une centaine de lignes ou quelque chose, les propriétés animées ne seraient pas si simples, et nous aurions plusieurs gestionnaires et autres fonctions définis. Supposons que je fasse quelque chose qui n'est pas géré par l'un des widgets implicites (pas difficile car à part AnimatedContainer, ils sont extrêmement à usage unique).

Le fait est que lors de la construction de quelque chose comme ça, les constructeurs ne fonctionnent pas très bien, car ils vous collent dans un trou de lisibilité (et d'écriture) pour commencer, en tant que tels, ils sont bien adaptés aux cas d'utilisation simples, ils ne "composent" pas bien. Composer étant la composition de 2 ou plusieurs choses.

Ne vous concentrez pas sur la façon dont je les utilise. Dans un exemple réel,...

... et retour à la case départ. Pourquoi n'apportez-vous pas un exemple réel ?

Vous avez besoin d'un exemple concret d'utilisation d'animations complexes ?
https://github.com/gskinnerTeam/flutter_vignettes

Montrer une animation complexe arbitraire ne ferait que brouiller l'exemple. Il suffit de dire qu'il existe de nombreux cas d'utilisation pour utiliser plusieurs animateurs (ou tout autre objet avec état que vous pouvez imaginer) dans un widget

Bien sûr. L'exemple ici est d'utiliser 3 animations dans un widget pour faire "X". Le X est volontairement simplifié dans l'exemple pour mettre en évidence la quantité de passe-partout.

le widget "core" serait une centaine de lignes ou quelque chose

Dans un autre article, vous avez posté un exemple avec un passe-partout qui masque le "noyau", mais maintenant vous nous dites que le noyau serait composé de centaines de lignes ? Donc en réalité, le passe-partout serait minuscule par rapport au noyau ? Vous ne pouvez pas l'avoir dans les deux sens.
Vous changez constamment vos arguments.

Ne vous concentrez pas sur la façon dont je les utilise. Dans un exemple réel,...

... et retour à la case départ. Pourquoi n'apportez-vous pas un exemple réel ?

Probablement parce qu'il faut beaucoup de temps pour créer un exemple réel en jouant simplement avec diverses idées. L'intention est que le lecteur imagine comment il pourrait être utilisé dans une situation réelle, sans mentionner qu'il existe des moyens de le contourner. Bien sûr, on peut utiliser un conteneur animé, mais s'ils ne le pouvaient pas ? Et si c'était trop complexe à faire avec juste un conteneur animé.

Maintenant que les auteurs n'utilisent pas d'exemples vraiment réels, qui peuvent être montrés comme bons ou mauvais, je n'ai pas d'opinion là-dessus, je ne fais que commenter la tendance dans ce fil à apporter des améliorations à des problèmes qui ne résoudre complètement le problème posé. Cela semble être une source majeure de confusion entre les partisans et les opposants des crochets, car chacun semble parler dans une certaine mesure devant l'autre, donc je soutiens la proposition de Hixie de créer de vraies applications telles qu'un adversaire ne puisse pas dire qu'un "vrai" exemple a été pas montré et un promoteur ne peut pas dire qu'il faut simplement imaginer un scénario du monde réel.

Je pense avoir dit qu'il serait idiot d'avoir une classe de 100 lignes, dont la moitié est passe-partout. C'est exactement ce que je décris ici. Le noyau, aussi grand soit-il, ne doit pas être obscurci par un tas de bruit, ce qui est certainement le cas lors de l'utilisation de plusieurs constructeurs.

Et la raison en est la capacité de numérisation, la lisibilité et la maintenance sur une grande base de code. Ce n'est pas l'écriture des lignes, bien que l'écriture dans les constructeurs soit un perdant de productivité imo en raison de la tendance à entrer dans l'enfer des accolades.

Dans un autre article, vous avez posté un exemple avec un passe-partout qui masque le "noyau", mais maintenant vous nous dites que le noyau serait composé de centaines de lignes ? Donc en réalité, le passe-partout serait minuscule par rapport au noyau ? Vous ne pouvez pas l'avoir dans les deux sens.
Vous changez constamment vos arguments.

Encore une fois, ce problème n'est pas une question de passe-partout mais de lisibilité et de réutilisation.
Ce n'est pas grave si nous avons 100 lignes.
Ce qui compte, c'est à quel point ces lignes sont lisibles/maintenables/réutilisables.

Même s'il s'agissait d'un argument passe-partout, pourquoi devrais-je, en tant qu'utilisateur, tolérer un tel passe-partout dans tous les cas, étant donné une manière suffisamment équivalente d'exprimer la même chose ? La programmation consiste à créer des abstractions et à automatiser le travail, je ne vois pas vraiment l'intérêt de refaire la même chose encore et encore dans différentes classes et fichiers.

Vous avez besoin d'un exemple concret d'utilisation d'animations complexes ?
https://github.com/gskinnerTeam/flutter_vignettes

Vous ne pouvez sûrement pas vous attendre à ce que je creuse tout votre projet. Quel fichier dois-je regarder exactement ?

Montrer une animation complexe arbitraire ne ferait que brouiller l'exemple.

L'exact opposé. Afficher une animation complexe arbitraire qui ne peut être résolu par une solution existante serait l'exemple, et c'est ce que Hixie continue à demander, je crois.

Scannez simplement les gifs et commencez à imaginer comment vous pourriez créer certaines de ces choses. Ce référentiel est en fait 17 applications autonomes. Vous ne pouvez pas non plus vous attendre à ce que je vous écrive une animation arbitraire juste pour vous prouver que des animations complexes peuvent exister. Je les construis depuis 20 ans à partir de Flash, tous différents les uns des autres. Et de toute façon, ce n'est pas spécifique aux animations, ce ne sont que l'API la plus simple et la plus familière pour illustrer un point plus large.

Comme vous le savez, lorsque vous utilisez un animateur, il y a environ 6 choses que vous devez faire à chaque fois, mais il faut aussi des crochets de cycle de vie ?? Ok, maintenant étendez cela à TOUT ce qui a 6 étapes que vous devez faire à chaque fois... Et vous devez l'utiliser à 2 endroits. Ou vous devez en utiliser 3 à la fois. C'est tellement évidemment un problème à première vue, je ne sais pas ce que je peux ajouter d'autre pour l'expliquer.

La programmation consiste à créer des abstractions et à automatiser le travail, je ne vois pas vraiment l'intérêt de refaire la même chose encore et encore dans différentes classes et fichiers.

__Tous__? Donc la performance, la maintenabilité n'est pas pertinente ?

Il arrive un moment où "automatiser" un travail et "faire" un travail, c'est la même chose.

Les amis, c'est bien si vous n'avez pas le temps ou l'envie de créer de vrais exemples, mais s'il vous plaît, si vous n'êtes pas intéressé par la création d'exemples pour expliquer le problème, vous ne devriez pas non plus vous attendre à ce que les gens se sentent alors obligés de résoudre le problème ( ce qui représente beaucoup plus de travail que de créer des exemples pour montrer le problème). Personne ici n'est obligé de faire quoi que ce soit pour qui que ce soit, c'est un projet open source où nous essayons tous de nous entraider.

@TimWhiting voudriez-vous mettre un fichier de licence dans votre référentiel https://github.com/TimWhiting/local_widget_state_approaches ? Certaines personnes sont incapables de contribuer sans une licence applicable (BSD, MIT ou similaire idéalement).

pourquoi devrais-je, l'utilisateur, tolérer un tel passe-partout dans tous les cas, étant donné une manière suffisamment équivalente d'exprimer la même chose ?

Oui, la maintenabilité et les performances comptent bien sûr. Je veux dire quand il y a une solution équivalente, nous devrions choisir celle qui a moins de passe-partout, est plus facile à lire, est plus réutilisable, et ainsi de suite. Cela ne veut pas dire que les crochets sont la réponse, car je n'ai pas mesuré leurs performances par exemple, mais ils sont plus faciles à maintenir d'après mon expérience. Je ne suis toujours pas sûr de votre argument sur la manière dont cela affecterait votre travail si une construction en forme de crochet était insérée dans le noyau Flutter.

Scannez simplement les gifs et commencez à imaginer comment vous pourriez créer certaines de ces choses.

J'ai scanné les gifs. Je n'utiliserais pas de widgets de constructeur.
La plupart des animations sont si complexes que si je savais que vous les avez implémentées à l'aide des constructeurs de niveau supérieur, je n'utiliserais probablement pas votre package.

Quoi qu'il en soit, cette discussion semble devenir incontrôlable avec des désaccords plus personnels. Nous devons nous concentrer sur la tâche principale à accomplir. Je ne sais pas comment les partisans du crochet peuvent montrer des exemples plus petits si les opposants, comme je l'ai dit plus tôt, trouveront des améliorations qui ne résolvent pas vraiment le problème posé. Je pense que nous devrions contribuer au référentiel de

Montrer une animation complexe arbitraire qui ne peut être résolue par aucune solution existante serait l'exemple, et c'est ce que Hixie continue de demander, je crois.

Montrer des exemples de quelque chose qui n'est pas possible aujourd'hui n'entre pas dans le cadre de ce problème.
Il s'agit d'améliorer la syntaxe de ce qui est déjà faisable, pas de débloquer certaines choses qui ne sont pas possibles aujourd'hui.

Toute demande de fournir quelque chose qui n'est pas possible aujourd'hui est hors sujet.

@TimWhiting voudriez-vous mettre un fichier de licence dans votre référentiel https://github.com/TimWhiting/local_widget_state_approaches ? Certaines personnes sont incapables de contribuer sans une licence applicable (BSD, MIT ou similaire idéalement).

Terminé. Désolé, je n'ai pas eu beaucoup de temps pour travailler sur les exemples, mais j'y reviendrai probablement cette semaine.

Montrer une animation complexe arbitraire qui ne peut être résolue par aucune solution existante serait l'exemple, et c'est ce que Hixie continue de demander, je crois.

Montrer des exemples de quelque chose qui n'est pas possible aujourd'hui n'entre pas dans le cadre de ce problème.
Il s'agit d'améliorer la syntaxe de ce qui est déjà faisable, pas de débloquer certaines choses qui ne sont pas possibles aujourd'hui.

Toute demande de fournir quelque chose qui n'est pas possible aujourd'hui est hors sujet.

Permettez-moi de reformuler ce que j'ai dit.

Montrer des cas d'utilisation complexes arbitraires difficiles à écrire et qui peuvent être améliorés de manière significative sans affecter les performances serait l'exemple, et c'est ce que Hixie continue de demander, je crois.

Je comprends la pression pour moins de passe-partout, plus de réutilisation, plus de magie. J'aime aussi avoir à écrire moins de code, et le langage/framework qui fait plus de travail est assez appétissant.
Jusqu'à présent, aucun des combos exemples/solutions présentés ici n'améliorerait considérablement le code. Autrement dit, si nous nous soucions plus que du nombre de lignes de codes que nous devons écrire.

Les amis, c'est bien si vous n'avez pas le temps ou l'envie de créer de vrais exemples, mais s'il vous plaît, si vous n'êtes pas intéressé par la création d'exemples pour expliquer le problème, vous ne devriez pas non plus vous attendre à ce que les gens se sentent alors obligés de résoudre le problème ( ce qui représente beaucoup plus de travail que de créer des exemples pour montrer le problème). Personne ici n'est obligé de faire quoi que ce soit pour qui que ce soit, c'est un projet open source où nous essayons tous de nous entraider.

@TimWhiting voudriez-vous mettre un fichier de licence dans votre référentiel https://github.com/TimWhiting/local_widget_state_approaches ? Certaines personnes sont incapables de contribuer sans une licence applicable (BSD, MIT ou similaire idéalement).

J'ai passé environ 6 heures à créer ces divers exemples et extraits de code. Mais je ne vois vraiment pas l'intérêt de fournir des exemples concrets d'animations complexes juste pour prouver qu'elles peuvent exister.

La demande consiste essentiellement à transformer cela en quelque chose qui ne peut pas être géré par AnimatedContainer :

Container(margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30), color: Colors.red.withOpacity(value1));

C'est tellement trivial au point d'être presque intentionnellement obtus à la question. Est-il si difficile d'imaginer que je pourrais avoir quelques boutons qui clignotent, des particules en mouvement, peut-être quelques champs de texte qui s'estompent lors de la mise à l'échelle ou des cartes qui se retournent ? Peut-être que je crée une barre de son avec 15 barres indépendantes, peut-être que je fais glisser un menu mais que j'ai aussi besoin de la possibilité de faire glisser des éléments individuels. Et ainsi de suite. Et c'est juste pour les animations. Il s'applique à tout cas d'utilisation qui est lourd dans le contexte d'un widget.

Je pense avoir fourni d'excellents exemples canoniques du problème avec les deux constructeurs et la réutilisation de l'état vanille :
https://github.com/flutter/flutter/issues/51752#issuecomment -671566814
https://github.com/flutter/flutter/issues/51752#issuecomment -671489384

Vous devez simplement imaginer bon nombre de ces instances (choisissez votre poison), réparties sur un projet de plus de 1000 fichiers de classe, et vous obtenez une image parfaite des problèmes de lisibilité et de maintenabilité que nous essayons d'éviter.

Les exemples d'images fournis par @esDotDev , montrant comment l'imbrication rend le code plus difficile à lire, ne seraient-ils pas suffisants pour vous, @Rudiksz ? Que leur manque-t-il ? Je suppose qu'il n'y a pas de mesures de performance là-bas, mais @rrousselGit est sûr qu'ils ne sont pas moins performants que les constructeurs.

@esDotDev Je pense que le but est d'avoir un exemple canonique singulier à partir duquel toutes les solutions de gestion du cycle de vie peuvent être comparées (pas seulement les crochets mais aussi d'autres à l'avenir). C'est le même principe que TodoMVC, vous n'indiqueriez pas nécessairement diverses autres implémentations dans React, Vue, Svelte, etc.

Cela a du sens pour moi, mais je ne comprends pas pourquoi il doit être plus grand qu'une seule page.

La gestion de plusieurs animations est l'exemple parfait de quelque chose qui est commun, nécessite un tas de passe-partout, est sujet aux erreurs et n'a pas de bonne solution pour le moment. Si cela n'est pas pertinent, si les gens disent qu'ils ne comprennent même pas à quel point les animations peuvent être complexes, alors clairement tout cas d'utilisation sera renversé pour le contexte du cas d'utilisation, et non pour le problème architectural que nous 'essayez d'illustrer.

Bien sûr, le référentiel de @TimWhiting n'a pas d'application complète, il contient des pages singulières comme exemples, comme vous le dites, si vous pouvez créer un exemple canonique d'animation pour ce référentiel à partir duquel d'autres pourraient implémenter leur solution, cela fonctionnerait.

Je ne pense pas non plus que nous ayons besoin d'une application énorme ou quoi que ce soit, mais il devrait y avoir une complexité suffisante similaire à celle de TodoMVC. Fondamentalement, cela doit être suffisant pour que vos adversaires ne puissent pas dire "bon je pourrais mieux faire ça de telle ou telle manière".

@Hixie La demande d'applications réelles pour comparer les approches est erronée.

Il y a deux défauts :

  • Nous ne sommes pas encore d'accord sur le problème, comme vous l'avez dit vous-même vous ne le comprenez pas
  • Nous ne pouvons pas mettre en œuvre des exemples dans des conditions réelles de production, car nous aurons des pièces manquantes.

Par exemple, nous ne pouvons pas écrire une application en utilisant :

final snapshot = keyword StreamBuilder();

car cela n'est pas mis en œuvre.

Nous ne pouvons pas non plus juger les performances, car il s'agit de comparer un POC à un code de production.

Nous ne pouvons pas non plus évaluer si quelque chose comme "les crochets ne peuvent pas être appelés conditionnellement" est sujet aux erreurs, car il n'y a pas d'intégration du compilateur pour signaler les erreurs en cas d'utilisation abusive.

Juger les performances des conceptions, évaluer la convivialité de l'API, mettre en œuvre des choses avant d'avoir des implémentations... tout cela fait partie de la conception de l'API. Bienvenue dans mon travail. :-) (Flutter Trivia : saviez-vous que les quelques milliers de premières lignes de RenderObject et RenderBox et al ont été implémentées avant de créer dart:ui ?)

Cela ne change rien au fait que vous demandez l'impossible.

Certaines des propositions faites ici font partie du langage ou de l'analyseur. Il est impossible pour la communauté de mettre cela en œuvre.

Je ne suis pas si sûr, d'autres frameworks et langages conçoivent des API tout le temps, je ne pense pas que ce soit trop différent ici, ou que Flutter ait des différences ou des difficultés écrasantes pour la conception d'API par rapport aux autres langages. Comme dans, ils le font sans avoir de support de compilateur ou d'analyseur, ce ne sont que des preuves de concept.

J'ai rassemblé un exemple de scénario d'animation « complexe » qui fait bon usage de 3 animations et qui est assez chargé de passe-partout et de cruft.

Il est important de noter que j'aurais pu faire n'importe quelle animation nécessitant un retour rapide à la position de départ (en éliminant tous les widgets implicites), ou une rotation sur l'axe z, ou une mise à l'échelle sur un seul axe, ou tout autre cas d'utilisation non couvert par les IW. Je craignais que ceux-ci ne soient pas pris au sérieux (bien que mes concepteurs me remettent ces trucs toute la journée), alors j'ai construit quelque chose de plus « monde réel ».

Voici donc un échafaudage simple, il comporte 3 panneaux qui coulissent pour s'ouvrir et se fermer. Il utilise 3 animateurs avec des états discrets. Dans ce cas, je n'ai pas vraiment besoin du contrôle total d'AnimatorController, TweenAnimationBuilder ferait l'affaire, mais l'imbrication résultante dans mon arbre serait très indésirable. Je ne peux pas imbriquer les TAB dans l'arborescence, car les panneaux dépendent des valeurs de chacun. AnimatedContainer n'est pas une option ici car chaque panneau doit glisser hors de l'écran, ils ne "s'écrasent" pas.
https://i.imgur.com/BW6M3uM.gif
image

class _SlidingPanelViewState extends State<SlidingPanelView> with TickerProviderStateMixin {
  AnimationController leftMenuAnim;
  AnimationController btmMenuAnim;
  AnimationController rightMenuAnim;

  <strong i="12">@override</strong>
  void initState() {
    // Here I have to pass vsync to AnimationController, so I have to include a SingleTickerProviderMixin and somewhat magically pass 'this' as vsync.
    leftMenuAnim = AnimationController(duration: widget.slideDuration, vsync: this);
    btmMenuAnim = AnimationController(duration: widget.slideDuration, vsync: this);
    rightMenuAnim = AnimationController(duration: widget.slideDuration, vsync: this);
    // Here I have to call forward 3 times, cause there's no way to automate this common setup behavior
    leftMenuAnim.forward();
    btmMenuAnim.forward();
    rightMenuAnim.forward();
    // Here I have to manually bind to build, cause there is encapsulate this common setup behavior
    leftMenuAnim.addListener(() => setState(() {}));
    btmMenuAnim.addListener(() => setState(() {}));
    rightMenuAnim.addListener(() => setState(() {}));
    super.initState();
  }

  // Following 2 fxn are a blind spot as far as compiler is concerned.
  // Things may, or may not be implemented correctly, no warnings, no errors.
  <strong i="13">@override</strong>
  void dispose() {
    btmMenuAnim.dispose();
    leftMenuAnim.dispose();
    rightMenuAnim.dispose();
    super.dispose();
  }

  <strong i="14">@override</strong>
  void didUpdateWidget(SlidingPanelView oldWidget) {
    if (leftMenuAnim.duration != widget.slideDuration) {
      leftMenuAnim.duration = widget.slideDuration;
      btmMenuAnim.duration = widget.slideDuration;
      rightMenuAnim.duration = widget.slideDuration;
    }
    super.didUpdateWidget(oldWidget);
  }

  // End error-prone blind spot without a single line of unique code
  // ~50 lines in we can start to see some unique code

  void _toggleMenu(AnimationController anim) {
    bool isOpen = anim.status == AnimationStatus.forward || anim.value == 1;
    isOpen ? anim.reverse() : anim.forward();
  }

  <strong i="15">@override</strong>
  Widget build(BuildContext context) {
    double leftPanelSize = 320;
    double leftPanelPos = -leftPanelSize * (1 - leftMenuAnim.value);
    double rightPanelSize = 230;
    double rightPanelPos = -rightPanelSize * (1 - rightMenuAnim.value);
    double bottomPanelSize = 80;
    double bottomPanelPos = -bottomPanelSize * (1 - btmMenuAnim.value);
    return Stack(
      children: [
        //Bg
        Container(color: Colors.white),
        //Content Panel
        Positioned(
          top: 0,
          left: leftPanelPos + leftPanelSize,
          bottom: bottomPanelPos + bottomPanelSize,
          right: rightPanelPos + rightPanelSize,
          child: ChannelInfoView(),
        ),
        //Left Panel
        Positioned(
          top: 0,
          left: leftPanelPos,
          bottom: bottomPanelPos + bottomPanelSize,
          width: leftPanelSize,
          child: ChannelMenu(),
        ),
        //Bottom Panel
        Positioned(
          left: 0,
          right: 0,
          bottom: bottomPanelPos,
          height: bottomPanelSize,
          child: NotificationsBar(),
        ),
        //Right Panel
        Positioned(
          top: 0,
          right: rightPanelPos,
          bottom: bottomPanelPos + bottomPanelSize,
          width: rightPanelSize,
          child: SettingsMenu(),
        ),
        // Buttons
        Row(
          children: [
            Button("left", ()=>_toggleMenu(leftMenuAnim)),
            Button("btm", ()=>_toggleMenu(btmMenuAnim)),
            Button("right", ()=>_toggleMenu(rightMenuAnim)),
          ],
        )
      ],
    );
  }
}

//Demo helpers
Widget Button(String lbl, VoidCallback action) => FlatButton(child: Text(lbl), onPressed: action, color: Colors.grey);
Widget ChannelInfoView() => _buildPanel(Colors.red);
Widget ChannelMenu() => _buildPanel(Colors.pink);
Widget SettingsMenu() => _buildPanel(Colors.blue);
Widget NotificationsBar() => _buildPanel(Colors.grey);
Widget _buildPanel(Color c) => Container(color: c, child: Container(color: Colors.white.withOpacity(.5)), padding: EdgeInsets.all(10));

Ainsi, sur les 100 lignes environ de ce corps, environ 40 % environ sont du pur passe-partout. 15 lignes en particulier, où quelque chose manque ou est mal tapé, pourrait causer des bogues difficiles à repérer.

Si nous utilisons quelque chose comme StatefulProperty, cela réduirait le passe-partout à environ 15% (économise environ 25 lignes). De manière critique, cela résoudrait complètement le problème des bogues sournois et de la logique métier en double, mais c'est encore un peu verbeux, d'autant plus que cela nécessite StatefulWidget, qui est un hit de 10 lignes dès le début.

Si nous utilisons quelque chose comme « mot-clé », nous réduisons les lignes passe-partout à essentiellement 0 %. Le cours pourrait se concentrer entièrement sur la logique métier (unique) et les éléments de l'arborescence visuelle. Nous rendons l'utilisation des StatefulWidgets beaucoup plus rare en général, la grande majorité des vues deviennent 10 ou 20% moins verbeuses et plus ciblées.

De plus, il convient de noter que le scénario du Panel ci-dessus est du monde réel, et dans le monde réel, cette approche n'est évidemment pas agréable, nous ne l'avons donc pas utilisée et vous ne la verriez pas dans la base de code. Nous n'utiliserions pas non plus de constructeurs imbriqués, car ceux-ci ont l'air grossiers, donc vous ne le verrez pas non plus.

Nous avons construit un widget SlidingPanel dédié, qui prend une propriété IsOpen, et s'ouvre et se ferme. C'est généralement la solution dans chacun de ces cas d'utilisation où vous avez besoin d'un comportement spécifique, vous déplacez la logique avec état dans un widget ultra-spécifique, et vous l'utilisez. Vous écrivez essentiellement votre propre Widget ImplicitlyAnimated.

Cela fonctionne généralement bien, mais c'est toujours du temps et des efforts, et littéralement la SEULE raison pour laquelle cela existe est parce que l'utilisation d'animations est si difficile (ce qui existe parce que la réutilisation des composants avec état en général est si difficile). Dans Unity ou AIR par exemple, je ne créerais pas une classe dédiée simplement pour déplacer un panneau sur un seul axe, ce serait juste une seule ligne de code à ouvrir ou à fermer, il n'y aurait rien à faire pour un widget dédié. Dans Flutter, nous devons créer un widget dédié, car c'est littéralement le seul moyen raisonnable d'encapsuler l'amorçage et le démontage d'AnimatorController (sauf si nous voulons imbriquer, imbriquer, imbriquer avec TAB)

Mon point principal ici est que ce genre de chose est pourquoi les exemples du monde réel sont si difficiles à trouver. En tant que développeurs, nous ne pouvons pas trop laisser ces choses exister dans nos bases de code, nous les contournons donc avec des solutions de contournement moins qu'idéales mais efficaces. Ensuite, lorsque vous regardez la base de code, vous voyez simplement ces solutions de contournement en vigueur et tout semble bien, mais ce n'est peut-être pas ce que l'équipe voulait faire, cela leur a peut-être pris 20% de plus pour y arriver, cela a peut-être été un total douleur à déboguer, rien de tout cela n'est évident en jetant un coup d'œil au code.

Pour être complet, voici le même cas d'utilisation, réalisé avec des constructeurs. Le nombre de lignes est fortement réduit, il n'y a aucune chance pour les bogues, pas besoin d'apprendre le concept étranger RE TickerProviderMixin... ) rend la logique métier beaucoup plus difficile à lire qu'elle ne devrait l'être.

``` fléchette
la classe _SlidingPanelViewState étend l'état{
bool isLeftMenuOpen = true;
bool isRightMenuOpen = true;
bool isBtmMenuOpen = true;

@passer outre
Construction du widget (contexte BuildContext) {
retourner TweenAnimationBuilder(
interpolation : interpolation(début : 0, fin : isLeftMenuOpen ? 1 : 0),
durée : widget.slideDuration,
constructeur : (_, leftAnimValue, __) {
retourner TweenAnimationBuilder(
interpolation : interpolation(début : 0, fin : isRightMenuOpen ? 1 : 0),
durée : widget.slideDuration,
constructeur : (_, rightAnimValue, __) {
retourner TweenAnimationBuilder(
interpolation : interpolation(début : 0, fin : isBtmMenuOpen ? 1 : 0),
durée : widget.slideDuration,
constructeur : (_, btmAnimValue, __) {
double gauchePanelSize = 320 ;
double leftPanelPos = -leftPanelSize * (1 - leftAnimValue);
double rightPanelSize = 230 ;
double rightPanelPos = -rightPanelSize * (1 - rightAnimValue);
double bottomPanelSize = 80;
double bottomPanelPos = -bottomPanelSize * (1 - btmAnimValue);
retourner la pile(
enfants: [
//Bg
Conteneur (couleur : Colors.white),
// Zone de contenu principale
Positionné(
haut : 0,
gauche : gauchePanelPos + leftPanelSize,
bottom : bottomPanelPos + bottomPanelSize,
à droite : rightPanelPos + rightPanelSize,
enfant : ChannelInfoView(),
),
//Panneau de gauche
Positionné(
haut : 0,
gauche : gauchePanelPos,
bottom : bottomPanelPos + bottomPanelSize,
largeur : leftPanelSize,
enfant : ChannelMenu(),
),
//Panneau du bas
Positionné(
à gauche : 0,
à droite : 0,
bas : bottomPanelPos,
hauteur: bottomPanelSize,
enfant : NotificationsBar(),
),
//Panneau de droite
Positionné(
haut : 0,
à droite : rightPanelPos,
bottom : bottomPanelPos + bottomPanelSize,
largeur : rightPanelSize,
enfant : ParamètresMenu(),
),
// Boutons
Ligne(
enfants: [
Button("gauche", () => setState(() => isLeftMenuOpen = !isLeftMenuOpen)),
Button("btm", () => setState(() => isBtmMenuOpen = !isBtmMenuOpen)),
Button("right", () => setState(() => isRightMenuOpen = !isRightMenuOpen)),
],
)
],
);
},
);
},
);
},
);
}
}

Ce dernier est intéressant... J'allais à l'origine suggérer que les constructeurs devraient être autour des widgets positionnés plutôt que de la pile (et je le suggérerais toujours pour les panneaux de gauche et de droite), mais j'ai réalisé que celui du bas affecte tous les trois, et j'ai réalisé que le constructeur ne vous donnant qu'un seul argument child n'est en fait pas suffisant, car vous voulez vraiment garder à la fois le Container et le Row constants à travers construit. Je suppose que vous pouvez simplement les créer au-dessus du premier constructeur.

Mon point principal ici est que ce genre de chose est pourquoi les exemples du monde réel sont si difficiles à trouver. En tant que développeurs, nous ne pouvons pas trop laisser ces choses exister dans nos bases de code, nous les contournons donc avec des solutions de contournement moins qu'idéales mais efficaces. Ensuite, lorsque vous regardez la base de code, vous voyez simplement ces solutions de contournement en vigueur et tout semble bien, mais ce n'est peut-être pas ce que l'équipe voulait faire, cela leur a peut-être pris 20% de plus pour y arriver, cela a peut-être été un total douleur à déboguer, rien de tout cela n'est évident en jetant un coup d'œil au code.

Pour mémoire, ce n'est pas un accident. C'est tout à fait par conception. Le fait que ces widgets soient leurs propres widgets améliore les performances. Nous voulions vraiment que ce soit la façon dont les gens utilisaient Flutter. C'est ce à quoi je faisais référence ci-dessus lorsque j'ai mentionné "une grande partie de la philosophie de conception de Flutter est de guider les gens vers le bon choix".

J'allais à l'origine suggérer que les constructeurs devraient être autour des widgets positionnés plutôt que de la pile (et je suggérerais toujours cela pour les panneaux de gauche et de droite)

En fait, je l'ai omis par souci de concision, mais normalement, je voudrais probablement un conteneur de contenu ici, et il utiliserait les tailles des 3 menus pour définir sa propre position. Cela signifie que les 3 doivent être au sommet de l'arbre, et s'il y a une reconstruction, j'ai besoin de la vue entière pour reconstruire, sans la contourner. Il s'agit essentiellement de votre échafaudage de bureau classique.

Bien sûr, nous pourrions commencer à déchirer l'arbre, toujours une option, nous l'utilisons beaucoup pour les widgets plus grands, mais je n'ai jamais vu cela améliorer réellement la capacité de grokabilité en un coup d'œil, il y a une étape cognitive supplémentaire que le lecteur doit faire à cela point. À la seconde où vous brisez l'arbre, je suis soudainement une piste de miettes de pain d'affectations variables pour comprendre ce qui se passe ici et comment le tout s'emboîte devient occlus. Présenter l'arbre comme un arbre digestible est toujours plus facile à raisonner d'après mon expérience.

Probablement un bon cas d'utilisation pour un RenderObject dédié.

Ou 3 AnimatorObjects faciles à gérer qui peuvent s'intégrer au cycle de vie du widget :D

Le fait que ces widgets soient leurs propres widgets améliore les performances.

Puisque nous devenons concrets ici : dans ce cas, parce qu'il s'agit d'un échafaudage, chaque sous-vue est déjà son propre widget, ce type est responsable de la mise en page et de la traduction des enfants. Les enfants seraient BottomMenu(), ChannelMenu(), SettingsView(), MainContent() etc.

Dans ce cas, nous enveloppons un tas de widgets autonomes, avec une autre couche de widgets autonomes, simplement pour gérer le passe-partout autour de leur déplacement. Je ne crois pas que ce soit une victoire de performance ? Dans ce cas, nous sommes poussés vers ce que le framework _pense_ que nous voulons faire, et non ce que nous voulons réellement faire, qui est d'écrire une vue tout aussi performante d'une manière plus succincte et cohérente.

[Modifier] Je vais mettre à jour les exemples pour ajouter ce contexte

La raison pour laquelle je suggère un RenderObject dédié est que cela améliorerait la mise en page. Je suis d'accord que l'aspect animation serait dans un widget avec état, il transmettrait simplement les trois doubles 0..1 (valeur1, valeur2, valeur3) à l'objet de rendu au lieu d'avoir le calcul Stack. Mais c'est surtout un sideshow pour cette discussion ; au moment où vous faites cela, vous voudriez toujours faire la simplification offerte par les crochets ou quelque chose de similaire dans ce widget avec état.

Sur une note plus pertinente, j'ai eu du mal à créer une démo pour le projet de https://github.com/TimWhiting/local_widget_state_approaches/pull/1
Je suis curieux de savoir à quoi ressemblerait une version Hooks. Je ne suis pas sûr de pouvoir voir un bon moyen de simplifier les choses, en particulier en maintenant les caractéristiques de performance (ou en les améliorant ; il y a un commentaire là-dedans montrant un endroit où il est actuellement sous-optimal).

(Je suis également très curieux de savoir si c'est le genre de chose où si nous trouvions un moyen de le simplifier, nous aurions fini ici, ou s'il manque des choses critiques que nous aurions besoin de résoudre avant d'avoir terminé.)

Les éléments de restauration sont-ils essentiels pour cet exemple ? J'ai du mal à suivre à cause de cela et je ne sais pas vraiment ce que fait RestorationMixin. Je suppose qu'il va falloir un peu de temps pour comprendre cela pour moi. Je suis sûr que Remi lancera la version à crochets en 4 secondes chrono :)

L'utilisation de l'API de restauration en utilisant HookWidget plutôt que StatefulHookWidget n'est pas prise en charge pour le moment.

Idéalement, nous devrions pouvoir changer

final value = useState(42);

dans:

final value = useRestorableInt(42);

Mais cela nécessite une réflexion, car l'API de restauration actuelle n'a pas vraiment été conçue avec des crochets à l'esprit.

En remarque, les crochets React sont livrés avec une fonctionnalité "clé", généralement utilisée comme suit :

int userId;

Future<User> user = useMemo(() => fetchUser(id), [id]);

où ce code signifie "mettre en cache le résultat du rappel et réévaluer le rappel chaque fois que quelque chose à l'intérieur du tableau change"

Flutter_hooks a fait une réimplémentation 1to1 de ceci (car il ne s'agit que d'un port), mais je ne pense pas que ce soit ce que nous voudrions faire pour un code optimisé pour Flutter.

Nous voudrions probablement :

int userId;

Future<User> user = useMemo1(id, (id) => fetchUser(id));

qui ferait la même chose, mais enlèverait la pression mémoire en évitant une allocation de liste et en utilisant une fonction arrachage

Ce n'est pas critique à ce stade, mais cela vaut la peine de le mentionner si nous prévoyons d'utiliser flutter_hooks pour des exemples.

@Hixie J'ai porté votre exemple d'animation sur des crochets

C'était un exemple intéressant, bravo d'y avoir pensé !
C'est un bon exemple en ce sens que, par défaut, la mise en œuvre de "actif" et de "durée" est omniprésente (et dépend l'une de l'autre).
Au point où il y a de nombreux appels "if (actif)" / "controller.repeat"

Alors qu'avec les hooks, toute la logique est gérée de manière déclarative et concentrée en un seul endroit, sans doublon.

L'exemple montre également comment les hooks peuvent être utilisés pour mettre facilement en cache des objets, ce qui a résolu le problème de la reconstruction trop fréquente de ExpensiveWidget.
Nous bénéficions des avantages des constructeurs const, mais cela fonctionne avec des paramètres dynamiques.

Nous obtenons également un meilleur rechargement à chaud. Nous pouvons modifier la durée Timer.periodic pour la couleur d'arrière-plan et voir immédiatement les changements en vigueur.

@rrousselGit avez-vous un lien ? Je n'ai rien vu de nouveau dans le référentiel de @TimWhiting .

L'utilisation de l'API de restauration en utilisant HookWidget plutôt que StatefulHookWidget n'est pas prise en charge pour le moment.

Quelle que soit la solution que nous proposons, nous devons nous assurer qu'elle n'a pas besoin de connaître chaque dernier mixin. Si quelqu'un souhaite utiliser notre solution en combinaison avec un autre package qui introduit un mixin comme TickerProviderStateMixin ou RestorationMixin, il devrait pouvoir le faire.

https://github.com/TimWhiting/local_widget_state_approaches/pull/3

D'accord, mais ça ne m'inquiète pas. useAnimationController n'oblige pas les utilisateurs à se soucier de SingleTickerProvider par exemple.

AutomaritKeepAlive pourrait bénéficier du même traitement.
Une des choses auxquelles je pensais était d'avoir un crochet "useKeepAlive(bool)"

Cela évite à la fois le mixin et le "super.build(context)" (ce dernier étant assez déroutant)

Un autre point intéressant concerne les changements requis lors du refactoring.

Par exemple, nous pouvons comparer la différence entre les changements nécessaires pour implémenter TickerMode pour l'approche brute vs les hooks :

D'autres choses sont mélangées dans le diff, mais nous pouvons voir que :

  • StatefulWidget requis pour déplacer la logique vers un cycle de vie complètement différent
  • Les changements de hooks sont purement additifs. Les lignes existantes n'ont pas été modifiées/déplacées.

Imo, c'est très important et une victoire clé de ce style d'objets d'état autonomes. Avoir tout basé sur des contextes imbriqués dans un arbre est fondamentalement plus difficile et plus compliqué à refactoriser et à modifier, ce qui, au cours d'un projet, a un effet invisible mais certain sur la qualité finale de la base de code.

D'accord!
Cela rend également les revues de code beaucoup plus faciles à lire :

final value = useSomething();
+ final value2 = useSomethingElse();

return Container(
  color: value.color,
-  child: Text('${value.name}'),
+  child: Text('${value.name} $value2'),
);

vs:

return SomethingBuilder(
  builder: (context, value) {
-    return Container(
-      color: value.color,
-      child: Text('$value'),
+    return SomethingElseBuilder(
+      builder: (context, value2) {
+        return Container(
+          color: value.color,
+          child: Text('${value.name} $value2'),
+        );
+      }
    );
  },
);

Il n'est pas clair dans le deuxième diff que Container est inchangé et que seul le Text changé

@rrousselGit Pensez-vous qu'il soit logique d'essayer de

Une autre chose à noter, c'est que là où tout cela toucherait vraiment à la maison, c'est si vous aviez plusieurs widgets, que tous devaient partager cette logique de «couleur pas à pas», mais utilisaient la couleur résultante de manière totalement différente. Avec une approche de style hooks, nous regroupons simplement toute logique logique à réutiliser, et nous l'utilisons simplement. Il n'y a pas de coins architecturaux dans lesquels nous sommes contraints, c'est vraiment agnostique et flexible.

Dans l'approche Stateful, nous sommes obligés de

  • copier et coller la logique (très non maintenable)
  • en utilisant un constructeur (pas super lisible, surtout lors de l'utilisation de l'imbrication)
  • mixage (ne compose pas bien, très facile pour différents mixins d'entrer en conflit dans leur état partagé)

L'essentiel, je pense, est que dans ce dernier cas, vous rencontrez immédiatement un problème d'architecture, où dois-je mettre cela dans mon arbre, comment puis-je l'encapsuler au mieux, devrait-il s'agir d'un constructeur ou peut-être d'un widget personnalisé ? Avec le premier, la seule décision est de savoir dans quel fichier enregistrer ce morceau de logique réutilisé, il n'y a aucun impact sur votre arbre. C'est très agréable sur le plan architectural lorsque vous souhaitez utiliser quelques-unes de ces encapsulations logiques ensemble, les déplacer de haut en bas dans votre hiérarchie ou passer à un widget frère, etc.

Je ne suis en aucun cas un développeur expert. Mais ce nouveau style de réutilisation de la logique et de son écriture au même endroit est vraiment pratique.

Je n'ai pas utilisé Vue.js depuis longtemps, mais ils ont même leur propre API (inspirée de Hooks) pour leur prochaine version, ça vaut peut-être le coup d'y jeter un œil.

Et CMIIW, je pense que les règles des crochets de réaction (ne pas utiliser de conditionnel), ne s'appliquent pas avec l'API de composition. Ainsi, vous n'avez pas besoin d'utiliser linter pour appliquer les règles.

Encore une fois, la section sur la motivation renforce fortement le PO ici :

MOTIVATION
Le code des composants complexes devient plus difficile à raisonner à mesure que les fonctionnalités évoluent avec le temps. Cela se produit en particulier lorsque les développeurs lisent du code qu'ils n'ont pas écrit eux-mêmes.
[Il y avait un] Manque d'un mécanisme propre et gratuit pour extraire et réutiliser la logique entre plusieurs composants.

Les mots clés là-bas étant "propre" et "gratuit". Les mixins sont gratuits mais ils ne sont pas propres. Les constructeurs sont propres, mais ils ne sont pas gratuits au sens de la lisibilité, ni au sens de l'architecture des widgets, car ils sont plus difficiles à déplacer dans l'arbre et à raisonner en termes de hiérarchie.

Je pense également qu'il est important de noter dans la discussion sur la lisibilité : "_arrive particulièrement lorsque les développeurs lisent du code qu'ils n'ont pas écrit_". Bien sûr, _votre_ constructeur imbriqué peut être facile à lire, vous savez ce qu'il y a et pouvez facilement le sauter, c'est lire le code de quelqu'un d'autre, comme vous le faites sur n'importe quel projet plus important, ou votre propre code d'il y a des semaines/mois, quand il devient assez ennuyeux/difficile à analyser et refactoriser ces choses.

Quelques autres sections particulièrement pertinentes.

Pourquoi le simple fait d'avoir des composants ne suffit pas :

Créer ... des composants nous permet d'extraire des parties répétables de l'interface couplées à ses fonctionnalités en morceaux de code réutilisables. Cela seul peut amener notre application assez loin en termes de maintenabilité et de flexibilité. Cependant, notre expérience collective a prouvé que cela seul pourrait ne pas suffire, surtout lorsque votre application devient vraiment volumineuse - pensez à plusieurs centaines de composants. Lorsqu'il s'agit d'applications aussi volumineuses, le partage et la réutilisation du code deviennent particulièrement importants.

Pourquoi réduire la fragmentation logique et encapsuler les choses plus fortement est une victoire :

la fragmentation est ce qui rend difficile la compréhension et la maintenance d'un composant complexe. La séparation des options masque les préoccupations logiques sous-jacentes. De plus, lorsque nous travaillons sur une seule préoccupation logique, nous devons constamment "sauter" autour des blocs d'options pour le code pertinent. Ce serait beaucoup mieux si nous pouvions colocaliser le code lié à la même préoccupation logique.

Je suis curieux de savoir s'il y a d'autres propositions d'exemples canoniques que nous essayons d'améliorer autres que celle que j'ai soumise.
Si c'est le point de départ que les gens veulent utiliser, c'est parfait. Cependant, je dirais que le plus gros problème avec cela en ce moment est la verbosité ; il n'y a pas beaucoup de code à réutiliser dans cet exemple. Donc, ce n'est pas clair pour moi si c'est une bonne représentation du problème tel que décrit dans le PO.

J'ai réfléchi un peu plus à la façon d'exprimer les caractéristiques que je rechercherais personnellement dans une solution, et cela m'a fait réaliser l'un des gros problèmes que je vois avec la proposition actuelle de Hooks, ce qui est l'une des raisons pour lesquelles je ne voudrais pas voulez le fusionner dans le cadre Flutter: Locality and Encapsulation, ou plutôt, l'absence de celui-ci. La conception de Hooks utilise l'état global (par exemple, le statique pour suivre quel widget est en cours de construction). À mon humble avis, c'est une caractéristique de conception que nous devrions éviter. En général, nous essayons de rendre les API autonomes, donc si vous appelez une fonction avec un paramètre, vous devez être sûr qu'elle ne pourra rien faire avec des valeurs en dehors de ce paramètre (c'est pourquoi nous passons le BuildContext autour , plutôt que d'avoir l'équivalent de useContext ). Je ne dis pas que c'est une caractéristique que tout le monde voudrait nécessairement ; et bien sûr, les gens peuvent utiliser des Hooks si cela ne leur pose pas de problème. Juste que c'est quelque chose que j'aimerais éviter de faire plus dans Flutter. Chaque fois que nous avons eu un état global (par exemple dans les fixations), nous avons fini par le regretter.

Les crochets pourraient probablement être des méthodes sur le contexte (je pense qu'ils étaient dans la première version), mais honnêtement, je ne vois pas beaucoup d'intérêt à les fusionner comme ils le sont actuellement. La fusion aurait besoin d'avantages pour séparer les packages, comme des performances accrues ou des outils de débogage spécifiques au hook. Sinon, ils ne feraient qu'ajouter à la confusion, par exemple vous auriez 3 manières officielles d'écouter un objet écoutable : AnimatedBuilder, StatefulWidget & useListenable.

Donc, pour moi, la voie à suivre est d'améliorer la génération de code - j'ai proposé quelques modifications : https://github.com/flutter/flutter/issues/63323

Si ces suggestions étaient réellement mises en œuvre, les personnes qui souhaitaient des solutions magiques de type SwiftUI dans leur application pourraient simplement créer un package et ne déranger personne d'autre.

Discuter de la validité des crochets est un peu hors sujet à ce stade, car nous ne sommes toujours pas d'accord sur le problème pour autant que je sache.

Comme indiqué à plusieurs reprises dans ce numéro, il existe de nombreuses autres solutions, dont plusieurs sont des fonctionnalités linguistiques.
Les crochets sont simplement un port d'une solution existante d'une autre technologie qui est relativement bon marché à mettre en œuvre dans sa forme la plus basique.

Cette fonctionnalité pourrait prendre un chemin complètement différent des crochets, comme ce que font SwiftUI ou Jetpack Compose ; la proposition "named mixin", ou la syntaxe sucre pour la proposition Builders.

J'insiste sur le fait que ce problème en son cœur demande une simplification des modèles comme StreamBuilder :

  • StreamBuilder a une mauvaise lisibilité/écriture en raison de son imbrication
  • Les mixins et les fonctions ne sont pas une alternative possible à StreamBuilder
  • Copier-coller l'implémentation de StreamBuilder dans tous les StatefulWidgets n'est pas raisonnable

Tous les commentaires jusqu'ici mentionnaient des alternatives de StreamBuilder, que ce soit pour des comportements différents (créer un objet jetable, faire des requêtes HTTP, ...) ou proposer des syntaxes différentes.

Je ne sais pas ce qu'il y a d'autre à dire, donc je ne vois pas comment nous pouvons progresser davantage.
Qu'est-ce que vous ne comprenez pas / n'êtes pas d'accord avec cette déclaration @Hixie ?

@rrousselGit Pourriez-vous créer une application de démonstration qui montre cela ? J'ai essayé de créer une application de démonstration qui montrait ce que je croyais être le problème, mais apparemment je n'ai pas bien compris. (Je ne sais pas quelle est la différence entre les "modèles comme StreamBuilder" et ce que j'ai fait dans l'application de démonstration.)

  • StreamBuilder a une mauvaise lisibilité/écriture en raison de son imbrication

Vous avez déjà dit que la verbosité n'est pas le problème. L'imbrication n'est qu'un autre aspect du verbe verbeux. Si l'imbrication est vraiment un problème, nous devrions rechercher les widgets Padding, Expanded, Flexible, Center, SizedBox et tous les autres widgets qui ajoutent de l'imbrication sans raison réelle. Mais l'imbrication peut être facilement résolue en divisant les widgets monolithiques.

Copier-coller l'implémentation de StreamBuilder dans tous les StatefulWidgets n'est pas raisonnable

Vous voulez dire copier-coller les lignes de code qui créent et suppriment les flux dont les StatefulWidgets ont besoin pour créer et supprimer ? Oui, c'est tout à fait raisonnable.

Si vous avez des 10 ou des centaines de StatefulWidgets personnalisés _différents_ qui doivent créer/disposer leurs propres flux - "utiliser" dans la terminologie des crochets-, vous avez de plus gros problèmes à vous soucier que la réutilisation ou l'imbrication "" logique ". pourquoi mon application doit créer autant de flux différents en premier lieu.

Pour être juste, je pense que c'est bien pour quelqu'un de penser qu'un modèle particulier n'est pas raisonnable dans son application. (Cela ne signifie pas nécessairement que le framework doit prendre en charge cela de manière native, mais il serait bon au moins de permettre à un package de le résoudre.) Si quelqu'un ne veut pas utiliser stream.listen ou StreamBuilder(stream) , c'est leur droit, et peut-être qu'en conséquence nous pouvons trouver un modèle qui est meilleur pour tout le monde.

Pour être juste, je pense que c'est bien pour quelqu'un de penser qu'un modèle particulier n'est pas raisonnable dans son application.

Je suis à 100% sur la même longueur d'onde que toi.
Bien sûr, les gens peuvent faire ce qu'ils veulent dans leurs applications. Ce que j'essaie de comprendre, c'est que tous les problèmes et difficultés décrits dans ce fil sont le résultat de mauvaises habitudes de programmation et ont en fait très peu à voir avec Dart ou Flutter. C'est juste mon opinion, mais je dirais que si quelqu'un écrit une application qui crée des dizaines de flux partout, devrait peut-être revoir la conception de son application avant de demander que le cadre soit "amélioré".

Par exemple, l'implémentation du hook qui a été intégrée au référentiel d'exemple.

  <strong i="10">@override</strong>
  Widget build(BuildContext context) {
    final value = useAnimation(animation);

    final screenHeight = MediaQuery.of(context).size.height;
    final textHeight =
        useMemoized(() => math.sqrt(screenHeight), [screenHeight]);

    return Text(
      'Change Duration',
      style: TextStyle(fontSize: 10.0 + value * textHeight),
    );
  }

J'ai un mauvais pressentiment à ce sujet, j'ai donc vérifié certains des éléments internes et ajouté des impressions de débogage pour vérifier ce qui se passe.
Vous pouvez voir à partir de la sortie ci-dessous que le crochet Listenable vérifie qu'il a été mis à jour à chaque tick d'animation. Comme dans, le widget qui "utilise" cet écoutable a-t-il été mis à jour ? La durée a-t-elle été modifiée ? L'instance a-t-elle été remplacée ?
Le crochet mémorisé, je ne sais même pas quel est le problème avec ça. Il est probablement destiné à mettre en cache un objet, mais à chaque construction, le widget vérifie si l'objet a changé ? Quoi? Pourquoi? Bien sûr, parce qu'il est utilisé dans un widget avec état et qu'un autre widget dans l'arborescence peut changer la valeur, nous devons donc interroger les modifications. Il s'agit d'un comportement d'interrogation littéral qui est l'exact opposé de la programmation « réactive ».

Pire encore, les crochets "nouveau" et "ancien" ont tous deux le même type d'instance, les deux ont les mêmes valeurs, et pourtant la fonction itère sur les valeurs pour vérifier si elles ont changé. Sur _chaque tick d'animation_.

C'est le résultat que j'obtiens, à l'infini.

/flutter (28121): Use hook:_ListenableHook
I/flutter (28121): Is this the current hook:false
I/flutter (28121): 1: --- inside shouldPreserveState ----
I/flutter (28121): Hook1:Instance of '_ListenableHook'
I/flutter (28121): Hook1 keys:null
I/flutter (28121): Hook2 :Instance of '_ListenableHook'
I/flutter (28121): Hook2 keys:null
I/flutter (28121): 2. Shoud we preserve the  state of _ListenableHook:true
I/flutter (28121): 3: --------------
I/flutter (28121): checking if the listenable did change
I/flutter (28121): Did the listenable change?false
I/flutter (28121): Use hook:_MemoizedHook<double>
I/flutter (28121): Is this the current hook:false
I/flutter (28121): 1: --- inside shouldPreserveState ----
I/flutter (28121): Hook1:Instance of '_MemoizedHook<double>'
I/flutter (28121): Hook1 keys:[1232.0]
I/flutter (28121): Hook2 :Instance of '_MemoizedHook<double>'
I/flutter (28121): Hook2 keys:[1232.0]
I/flutter (28121): iterating over the hooks keys
I/flutter (28121): 2. Shoud we preserve the  state of _MemoizedHook<double>:true
I/flutter (28121): Use hook:_ListenableHook
I/flutter (28121): Is this the current hook:false
I/flutter (28121): 1: --- inside shouldPreserveState ----
I/flutter (28121): Hook1:Instance of '_ListenableHook'
I/flutter (28121): Hook1 keys:null
I/flutter (28121): Hook2 :Instance of '_ListenableHook'
I/flutter (28121): Hook2 keys:null
I/flutter (28121): 2. Shoud we preserve the  state of _ListenableHook:true

Tout ce travail est effectué sur chaque tick d'animation. Si j'ajoute un autre crochet comme "couleur finale = useAnimation(animationColor);", pour animer aussi la couleur, maintenant le widget vérifie _deux_ fois s'il a été mis à jour.

Je suis assis ici à regarder un texte s'animer d'avant en arrière sans changement dans l'état de l'application ou l'un des widgets ou l'arborescence des widgets, et les crochets vérifient toujours si l'arborescence/les widgets ont été mis à jour. Avoir chaque widget qui "utilise" ces crochets particuliers fait ce comportement d'interrogation est mauvais.

La gestion de la logique d'initialisation/mise à jour/élimination des objets d'état à l'intérieur de la méthode de construction n'est qu'une mauvaise conception. Aucune quantité d'améliorations acquises en termes de réutilisabilité, de rechargement à chaud ou de charge cognitive ne justifie l'impact sur les performances.
Encore une fois, à mon avis. Les hooks étant un package, n'importe qui peut les utiliser s'il pense que les gains justifient les frais généraux.

De plus, je ne pense pas qu'une quantité de fonctionnalités de langage, de magie du compilateur ou d'abstraction puisse empêcher de telles vérifications inutiles, si nous commençons à essayer de tout abstraire à l'intérieur du processus de construction. Il nous reste donc des alternatives comme l'extension du StatefulWidget. Quelque chose qui peut déjà être fait, et a été rejeté d'innombrables fois.

@Hixie tu n'as pas répondu à la question. Qu'est-ce que vous ne comprenez pas/avec quoi vous n'êtes pas d'accord dans la liste à puces ci-dessus ?

Je ne peux pas faire un exemple sans savoir ce que vous voulez que je démontre.

@Rudiksz Bien sûr, toute solution que nous envisagerions devrait être profilée et comparée pour s'assurer qu'elle @TimWhiting est destinée à couvrir exactement les types de modèles qui peuvent être faciles à gâcher. (Et, comme indiqué précédemment, cela laisse place à l'amélioration, voir le TODO dans le code.)

@rrousselGit Je ne voulais pas vraiment entrer dans les mauvaises herbes à ce sujet parce que c'est tellement subjectif mais puisque vous demandez:

  • Tout d'abord, j'éviterais d'utiliser Streams en général. ValueListenable est à mon humble avis un modèle bien meilleur pour plus ou moins les mêmes cas d'utilisation.
  • Je ne pense pas que StreamBuilder soit particulièrement difficile à lire ou à écrire ; mais, comme @satvikpendem l'a commenté plus tôt , mon histoire est avec des arbres profondément imbriqués en HTML, et je regarde les arbres Flutter depuis 4 ans maintenant (j'ai déjà dit que la compétence de base de Flutter est de savoir comment marcher efficacement sur des arbres géants), donc j'ai probablement une tolérance plus élevée que la plupart des gens, et mon opinion ici n'est pas vraiment pertinente.
  • Quant à savoir si les mixins et les fonctions pourraient être une alternative possible à StreamBuilder, je pense que Hooks démontre assez bien que vous pouvez certainement utiliser des fonctions pour écouter des flux, et les mixins peuvent clairement faire tout ce que les classes peuvent faire ici, donc je ne vois pas pourquoi ils le feraient pas être une solution non plus.
  • Enfin, concernant les implémentations de copier-coller, c'est une question subjective. Personnellement, je ne copie pas et ne colle pas la logique dans initState/didUpdateWidget/dispose/build, je l'écris à chaque fois, et cela semble généralement bien. Quand cela devient "hors de contrôle", je l'intègre dans un widget comme StreamBuilder. Donc, encore une fois, mon opinion n'est probablement pas pertinente ici.

En règle générale, que je rencontre le problème que vous voyez ou non n'est pas pertinent. Votre expérience est valable quelle que soit mon opinion. Je suis heureux de travailler à trouver des solutions aux problèmes que vous rencontrez, même si je ne les ressens pas. Le seul effet du fait que je ne rencontre pas le problème moi-même est qu'il m'est plus difficile de bien comprendre le problème, comme nous l'avons vu dans cette discussion.

Ce que je voudrais que vous démontriez, c'est le modèle de codage que vous jugez inacceptable, dans une démo suffisamment importante pour que lorsque quelqu'un crée une solution, vous ne la rejetiez pas en disant qu'elle peut devenir difficile à utiliser dans une situation différente (c'est-à-dire inclure toutes les situations pertinentes, par exemple, assurez-vous d'inclure les parties "mise à jour" ou toute autre partie que vous jugez importante à gérer), ou dire que la solution fonctionne dans un cas mais pas dans le cas général (par exemple pour prendre vos commentaires précédents, en vous assurant que les paramètres proviennent de plusieurs endroits et se mettent à jour dans plusieurs situations, de sorte qu'il est évident qu'une solution comme Property ci-dessus ne prendrait pas en compte le code commun).

Le problème est que vous demandez des exemples pour quelque chose qui est du domaine "évident" pour moi.

Cela ne me dérange pas de faire quelques exemples, mais je n'ai aucune idée de ce que vous attendez, car je ne comprends pas ce que vous ne comprenez pas.

J'ai déjà dit tout ce que j'avais à dire.
La seule chose que je puisse faire sans comprendre ce que vous ne comprenez pas, c'est de me répéter.

Je peux faire fonctionner certains des extraits ici, mais cela équivaut à me répéter.
Si l'extrait n'était pas utile, je ne vois pas pourquoi le fait de pouvoir l'exécuter changerait quoi que ce soit.

Quant à savoir si les mixins et les fonctions pourraient être une alternative possible à StreamBuilder, je pense que Hooks démontre assez bien que vous pouvez certainement utiliser des fonctions pour écouter des flux, et les mixins peuvent clairement faire tout ce que les classes peuvent faire ici, donc je ne vois pas pourquoi ils le feraient pas être une solution non plus.

Les crochets ne doivent pas être considérés comme des fonctions.
Ils sont une nouvelle construction de langage semblable à Iterable/Stream

Les fonctions ne peuvent pas faire ce que font les hooks - elles n'ont pas d'état ou la capacité de faire reconstruire les widgets.

Le problème avec les mixins est démontré dans l'OP. TL;DR : conflit de nom sur les variables et il est impossible de réutiliser le même mixin plusieurs fois.

@rrousselGit Eh bien, puisque cela ne vous dérange pas de faire des exemples, et puisque les exemples que je demande sont évidents, commençons par certains de ces exemples évidents et itérons à partir de là.

Je n'ai pas dit que les exemples étaient évidents, mais le problème l'est.
Ce que je voulais dire, c'est que je ne peux pas créer de nouveaux exemples. Tout ce que j'ai à dire est déjà dans ce fil :

Je ne vois rien à ajouter à ces exemples.

Mais FWIW, je travaille sur une application météo open source utilisant Riverpod. Je le lierai ici quand ce sera fait.


J'ai fait un sondage sur Twitter en posant quelques questions sur les constructeurs liés aux problèmes discutés ici :

https://twitter.com/remi_rousselet/status/1295453683640078336

Le sondage est toujours en attente, mais voici les chiffres actuels :

Screenshot 2020-08-18 at 07 01 44

Le fait que 86% des 200 personnes souhaitent une manière d'écrire des constructeurs qui n'implique pas d'imbrication parle de lui-même.

Pour être clair, je n'ai jamais suggéré que nous ne devrions pas aborder cette question. Si je pensais que nous ne devrions pas l'aborder, le problème serait clos.

Je suppose que je vais essayer de faire un exemple qui utilise les extraits auxquels vous avez lié.

Je peux vous aider à faire des exemples basés sur les extraits liés, mais j'ai besoin de savoir pourquoi ces extraits n'étaient pas assez bons
Sinon, la seule chose que je puisse faire est de compiler ces extraits, mais je doute que ce soit ce que vous voulez.

Par exemple, voici un aperçu des nombreux ValueListenableBuilder+TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

Par exemple, voici un aperçu des nombreux ValueListenableBuilder+TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

FWIW, cet exemple particulier peut être plus facilement implémenté dans mobx.
C'est en fait plus court que votre implémentation de hooks.

Les observables de Mobx sont des ValueNotifiers sous stéroïdes et son widget Observer est l'évolution de ValueListenableBuilder de Flutter - il peut écouter plus d'un ValueNotifier.
Être un remplacement instantané pour le combo ValueNotifier/ValueListenableBuilder, signifie que vous écrivez toujours du code Flutter idiomatique, ce qui est en fait un facteur important.

Puisqu'il utilise toujours le générateur Tween intégré de Flutter, il n'est pas nécessaire d'apprendre / d'implémenter de nouveaux widgets / crochets (en d'autres termes, il n'a pas besoin de nouvelles fonctionnalités) et il n'a aucun des résultats de performance des crochets.

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'counters.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  <strong i="13">@override</strong>
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home1(),
    );
  }
}

var counters = Counters();

class Home1 extends StatelessWidget {
  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Observer(
              builder: (context) => TweenAnimationBuilder<int>(
                duration: const Duration(seconds: 5),
                curve: Curves.easeOut,
                tween: IntTween(end: counters.firstCounter),
                builder: (context, value, child) {
                  return Text('$value');
                },
              ),
            ),
            RaisedButton(
              onPressed: () => counters.firstCounter += 100,
              child: Text('+'),
            ),
            // Both counters have voluntarily a different Curve and duration
            Observer(
              builder: (context) => TweenAnimationBuilder<int>(
                duration: const Duration(seconds: 2),
                curve: Curves.easeInOut,
                tween: IntTween(end: counters.secondCounter),
                builder: (context, value, child) {
                  return Text('$value');
                },
              ),
            ),
            RaisedButton(
              onPressed: () => counters.secondCounter += 100,
              child: Text('+'),
            ),
            const Text('total:'),
            // The total must take into account the animation of both counters
            Observer(
              builder: (context) => TweenAnimationBuilder<int>(
                duration: const Duration(seconds: 5),
                curve: Curves.easeOut,
                tween: IntTween(
                    end: counters.firstCounter + counters.secondCounter),
                builder: (context, value, child) {
                  return Text('$value');
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Counters.dart étant aussi simple que...

part 'counters.g.dart';

class Counters = _Counters with _$Counters;
abstract class _Counters with Store {
  <strong i="18">@observable</strong>
  int firstCounter = 0;

  <strong i="19">@observable</strong>
  int secondCounter = 0;
}

Voici une autre implémentation qui n'a même pas besoin d'animationbuilders. La méthode de construction du widget est aussi pure que possible, presque comme un fichier html sémantique... comme un modèle.

https://gist.github.com/Rudiksz/cede1a5fe88e992b158ee3bf15858bd9

@Rudiksz Le comportement du champ "total" est cassé dans votre extrait. Cela ne correspond pas à l'exemple, où les deux compteurs peuvent s'animer ensemble mais terminer l'animation à des moments différents et avec des courbes différentes.
De même, je ne suis pas sûr de ce que cet exemple ajoute à la variante ValueListenableBuilder .

En ce qui concerne votre dernier point essentiel, TickerProvider est cassé car il ne prend pas en charge TickerMode - ni les écouteurs ni les contrôleurs supprimés.

Et Mobx est probablement hors sujet. Nous ne discutons pas de la manière d'implémenter l'état ambiant / ValueListenable vs Stores vs Streams, mais plutôt de savoir comment gérer l'état local / les constructeurs imbriqués - que Mobx ne résout en aucun cas

––––

De plus, gardez à l'esprit que dans l'exemple des hooks, useAnimatedInt pourrait/devrait être extrait dans un package et qu'il n'y a pas de doublon de la durée/courbe entre l'animation de texte individuelle et le total.

En ce qui concerne les performances, avec les Hooks, nous ne reconstruisons qu'un seul élément, tandis qu'avec les Builders, nous reconstruisons 2-4 Builders.
Donc Hooks pourrait très bien être plus rapide.

Le comportement du champ "total" est cassé dans votre extrait. Cela ne correspond pas à l'exemple, où les deux compteurs peuvent s'animer ensemble mais terminer l'animation à des moments différents et avec des courbes différentes.

Vous n'avez clairement même pas essayé d'exécuter l'exemple. Il se comporte exactement comme votre code.

En ce qui concerne votre dernier point essentiel, TickerProvider est cassé car il ne prend pas en charge TickerMode.

Je ne sais pas ce que vous entendez par là. J'ai remanié _votre_ exemple, qui n'utilise pas TickerMode. Vous modifiez à nouveau les exigences.

En ce qui concerne les performances, avec les Hooks, nous ne reconstruisons qu'un seul élément, tandis qu'avec les Builders, nous reconstruisons 2-4 Builders. Donc Hooks pourrait très bien être plus rapide.

Non, juste non. Vos widgets de crochet sondent constamment les modifications apportées à chaque version. Les constructeurs basés sur valuelistenables sont "réactifs".

De même, je ne suis pas sûr de ce que cet exemple ajoute à la variante ValueListenableBuilder .

Et Mobx est probablement hors sujet. Nous ne discutons pas de la manière d'implémenter l'état ambiant / ValueListenable vs Stores vs Streams, mais plutôt de savoir comment gérer l'état local / les constructeurs imbriqués - que Mobx ne résout en aucun cas

Tu te moques de moi. J'ai pris votre exemple et j'ai "traité" les ValueListenableBuilders imbriqués * et les constructeurs d'interpolation ? ! Un point que vous avez spécifiquement soulevé comme problème.
Mais cette phrase ici, décrit toute votre attitude envers cette discussion. Si ce ne sont pas des crochets, c'est "hors sujet", mais vous dites que vous vous en fichez si ce sont des crochets qui seront utilisés comme solution.
Laisse-moi tranquille.

Vous n'avez clairement même pas essayé d'exécuter l'exemple. Il se comporte exactement comme votre code.

Ce n'est pas le cas. Le premier compteur s'anime sur 5 secondes et le second sur 2 secondes - et les deux utilisent également une courbe différente.

Avec les deux extraits que j'ai donnés, vous pouvez incrémenter les deux compteurs en même temps, et pendant chaque image de l'animation, le "total" serait correct. Même lorsque le deuxième compteur cesse de s'animer alors que le premier compteur s'anime toujours

D'un autre côté, votre implémentation ne considère pas ce cas, car elle a fusionné les 2 TweenAnimationBuilders en un seul.
Pour y remédier, il faudrait écrire :

Observer(
  builder: (context) {
    return TweenAnimationBuilder<int>(
      duration: const Duration(seconds: 5),
      curve: Curves.easeOut,
      tween: IntTween(end: counters.firstCounter),
      builder: (context) {
        return Observer(
          valueListenable: secondCounter,
          builder: (context, value2, child) {
            return TweenAnimationBuilder<int>(
              duration: const Duration(seconds: 2),
              curve: Curves.easeInOut,
              tween: IntTween(end: counters.secondCounter),
              builder: (context, value2, child) {
                return Text('${value + value2}');
              },
            );
          },
        );
      },
    );
  },
)

Les deux TweenAnimationBuilders sont nécessaires pour respecter le fait que les deux compteurs peuvent s'animer individuellement. Et les deux Observer sont nécessaires car le premier Observer ne peut pas observer counters.secondCounter


Non, juste non. Vos widgets de crochet sondent constamment les modifications apportées à chaque version. Les constructeurs basés sur valuelistenables sont "réactifs".

Vous ignorez ce que fait Element , ce qui s'avère être la même chose que ce que font les hooks : comparer runtimeType et les clés, et décider s'il faut créer un nouvel élément ou mettre à jour l'existant

J'ai pris votre exemple et j'ai "traité" les ValueListenableBuilders imbriqués * et les constructeurs d'interpolation

En supposant que le problème avec le nombre total soit résolu, quelle imbrication a été supprimée ?

Observer(
  builder: (context) => TweenAnimationBuilder<int>(
    duration: const Duration(seconds: 5),
    curve: Curves.easeOut,
    tween: IntTween(end: counters.firstCounter),
    builder: (context, value, child) {
      return Text('$value');
    },
  ),
),

n'est pas différent de :

ValueListenableBuilder<int>(
  valueListenable: firstCounter,
  builder: (context, value, child) => TweenAnimationBuilder<int>(
    duration: const Duration(seconds: 5),
    curve: Curves.easeOut,
    tween: IntTween(end: value),
    builder: (context, value, child) {
      return Text('$value');
    },
  ),
),

en termes de nidification.

Si vous faites référence à l'essentiel, alors comme je l'ai mentionné précédemment, cette approche casse TickerProvider / TickerMode . Le vsync doit être obtenu à l'aide de SingleTickerProviderClientStateMixin sinon il ne prend pas en charge la logique d'inhibition, ce qui peut entraîner des problèmes de performances.
J'ai expliqué cela dans un de mes articles : https://dash-overflow.net/articles/why_vsync/

Et avec cette approche, nous devons réimplémenter la logique Tween dans chaque emplacement qui voudrait à l'origine un TweenAnimationBuilder. Cela conduit à un doublon important, d'autant plus que la logique n'est pas si triviale

Avec les deux extraits que j'ai donnés, vous pouvez incrémenter les deux compteurs en même temps, et pendant chaque image de l'animation, le "total" serait correct. Même lorsque le deuxième compteur cesse de s'animer alors que le premier compteur s'anime toujours

D'un autre côté, votre implémentation ne considère pas ce cas, car elle a fusionné les 2 TweenAnimationBuilders en un seul.

Oui, c'est un compromis que j'étais prêt à faire. Je pourrais facilement imaginer un cas où l'animation ne serait qu'un retour visuel du changement et la précision n'est pas importante. Tout dépend des exigences.

Je soupçonnais que vous vous y opposeriez, d'où la deuxième version qui résout exactement ce problème, tout en rendant le code encore plus propre.

Si vous faites référence à l'essentiel, alors comme je l'ai mentionné précédemment, cette approche casse TickerProvider/TickerMode. Le vsync doit être obtenu à l'aide de SingleTickerProviderClientStateMixin, sinon il ne prend pas en charge la logique d'inhibition, ce qui peut entraîner des problèmes de performances.

Vous créez donc le tickerprovider dans le widget et le transmettez aux compteurs. Je ne disposais pas non plus des contrôleurs d'animation. Ces détails sont si simples à mettre en œuvre que je n'avais pas l'impression qu'ils ajouteraient quoi que ce soit à l'exemple. Mais là, on les pinaille.

J'ai implémenté la classe Counter() pour faire ce que fait votre exemple et rien de plus.

Et avec cette approche, nous devons réimplémenter la logique Tween dans chaque emplacement qui voudrait à l'origine un TweenAnimationBuilder. Cela conduit à un doublon important, d'autant plus que la logique n'est pas si triviale

Quoi? Pourquoi? Veuillez expliquer pourquoi je n'ai pas pu créer plus d'une instance de la classe Counter et les utiliser dans divers widgets ?

@Rudiksz Je ne suis pas sûr que vos solutions résolvent réellement les problèmes posés. Vous dites

J'ai implémenté la classe Counter() pour faire ce que fait votre exemple et rien de plus.

et encore

Oui, c'est un compromis que j'étais prêt à faire. Je pourrais facilement imaginer un cas où l'animation ne serait qu'un retour visuel du changement et la précision n'est pas importante. Tout dépend des exigences.

Vous créez donc le tickerprovider dans le widget et le transmettez aux compteurs. Je ne disposais pas non plus des contrôleurs d'animation. Ces détails sont si simples à mettre en œuvre que je n'avais pas l'impression qu'ils ajouteraient quoi que ce soit à l'exemple. Mais là, on les pinaille.

Vous avez fourni du code qui n'est qu'apparemment équivalent à la version hook de @rrousselGit , mais ce n'est pas réellement équivalent, car vous omettez des parties que la version hook inclut. Dans ce cas, ils ne sont pas vraiment comparables, non ? Si vous souhaitez comparer votre solution à celle suggérée, il serait préférable de la faire correspondre exactement aux exigences au lieu de dire pourquoi vous ne les avez pas incluses. C'est quelque chose qui est la raison de la création du référentiel de

Vous n'avez clairement même pas essayé d'exécuter l'exemple. Il se comporte exactement comme votre code.

Non, juste non.

Tu te moques de moi. J'ai pris votre exemple et j'ai "traité" les ValueListenableBuilders imbriqués * et les constructeurs d'interpolation

Veuillez vous abstenir de telles accusations, elles ne servent qu'à rendre ce fil vitriolique sans résoudre les problèmes sous-jacents. Vous pouvez faire valoir vos points sans être accusateur, désobligeant ou en colère contre l'autre partie, ce qui est l'effet des commentaires que je vois dans ce fil, je ne vois pas un tel comportement des autres envers vous. Je ne suis pas non plus sûr de l'effet de réagir par emoji à votre propre message.

@rrousselGit

Je peux vous aider à faire des exemples basés sur les extraits liés, mais j'ai besoin de savoir pourquoi ces extraits n'étaient pas assez bons
Sinon, la seule chose que je puisse faire est de compiler ces extraits, mais je doute que ce soit ce que vous voulez.

La raison pour laquelle je demande une application plus élaborée qu'un simple extrait est que lorsque j'ai posté un exemple qui traitait l'un de vos extraits, vous avez dit que ce n'était pas assez bon car il ne traitait pas également un autre cas qui était 't dans votre extrait (par exemple, n'a pas géré didUpdateWidget, ou n'a pas géré le fait d'avoir deux de ces extraits côte à côte, ou d'autres choses parfaitement raisonnables que vous voudriez gérer). J'espère qu'avec une application plus élaborée, nous pourrons l'élaborer suffisamment pour qu'une fois que nous ayons une solution, il n'y ait pas de moment de « gêne » où un nouveau problème est soulevé et doit également être traité. Évidemment, il est toujours possible que nous manquions tous quelque chose qui doit être géré, mais l'idée est de minimiser les risques.

En ce qui concerne les récents messages moins qu'entièrement accueillants, s'il vous plaît, concentrons-nous simplement sur la création d'exemples sur le dépôt de

Vous pouvez y soumettre votre solution si vous pensez avoir satisfait à toutes les exigences.

Les exigences ont été modifiées après coup. J'ai proposé deux solutions. Un qui fait un compromis (un très raisonnable) et un qui met en œuvre le comportement exact de l'exemple fourni.

Vous avez fourni du code qui n'est qu'apparemment équivalent à la version hook de @rrousselGit , mais ce n'est pas réellement équivalent,

Je n'ai pas implémenté la "solution des crochets", j'ai implémenté l'exemple ValueListenableBuilder, en me concentrant spécifiquement sur le "problème d'imbrication". Il ne fait pas tout ce que font les crochets, j'ai simplement montré comment un élément de la liste à puces des griefs peut être simplifié à l'aide d'une solution alternative.

Si vous êtes autorisé à introduire des packages externes dans la discussion, alors moi aussi.

Réutilisabilité : jetez un œil à l'exemple dans le dépôt ci-dessous
https://github.com/Rudiksz/cbl_example

Noter:

  • il s'agit d'une vitrine de la façon dont vous pouvez encapsuler la "logique" à l'extérieur des widgets et avoir des widgets allégés, presque html
  • il ne couvre pas tout le couvercle des crochets. Ce n'est pas le propos. Je pensais que nous discutions d'alternatives au framework Flutter d'origine, et non au package hooks.
  • Il ne couvre pas _tous_ les cas d'utilisation que chaque équipe marketing peut proposer
  • mais, l'objet Counter lui-même est assez flexible, il peut être utilisé de manière autonome (voir le titre AppBar), dans le cadre d'un widget complexe qui doit calculer les totaux de différents compteurs, être rendu réactif ou répondre aux entrées de l'utilisateur.
  • C'est aux widgets consommateurs de personnaliser le montant, la valeur initiale, la durée, le type d'animation des compteurs qu'ils souhaitent utiliser.
  • les détails de la façon dont les animations sont gérées peuvent avoir des inconvénients. Si l'équipe Flutter dit que oui, l'utilisation de contrôleurs d'animation et d'interpolations en dehors des widgets casse en quelque sorte le cadre, je reconsidérerai. Il y a certainement des choses à améliorer. Remplacer le fournisseur de ticker personnalisé que j'utilise par un créé par le mixin du widget consommateur est trivial. Je ne l'ai pas fait.
  • Ceci n'est qu'une solution alternative de plus au modèle "Builder". Si vous dites que vous avez besoin ou que vous souhaitez utiliser Builders, rien de tout cela ne s'applique.
  • Pourtant, le fait est qu'il existe des moyens de simplifier le code, sans fonctionnalités supplémentaires. Si vous ne l'aimez pas, ne l'achetez pas. Je ne préconise rien de tout cela pour en faire un cadre.

Edit : Cet exemple a un bug, si vous lancez un "incrément" alors que le changement est animé, le compteur est réinitialisé et est incrémenté à partir de la valeur actuelle. Je ne l'ai pas fait exprès, car je ne connais pas les exigences exactes que vous pourriez avoir pour ces "compteurs". Encore une fois, il est trivial de modifier les méthodes d'incrémentation/décrémentation pour résoudre ce problème.

C'est tout.

@Hixie Dois-je interpréter votre commentaire comme un moyen de dire que mon exemple (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/hooks/animated_counter.dart) n'est pas assez bon ?

Aussi, pourrions-nous avoir un rendez-vous Zoom/Google ?

Je ne suis pas non plus sûr de l'effet de réagir par emoji à votre propre message.

Rien. Cela n'a aucun rapport avec quoi que ce soit. Pourquoi l'avoir soulevé ?

@rrousselGit Vous seul pouvez savoir si c'est assez bon. Si nous trouvons un moyen de refactoriser cet exemple pour qu'il soit propre et court et qu'il n'y ait pas de code en double, serez-vous satisfait ? Ou y a-t-il des choses que vous pensez que nous devrions prendre en charge qui ne sont pas gérées par cet exemple que nous devons gérer pour résoudre ce bogue ?

Toi seul peux savoir si c'est assez bien

Je ne peux pas en être juge. Pour commencer, je ne pense pas que nous puissions saisir le problème en utilisant un ensemble fini d'applications.

Cela ne me dérange pas de travailler comme vous le souhaitez, mais j'ai besoin de conseils car je ne comprends pas comment cela nous fera progresser.

Je pense que nous avons fourni une tonne d'extraits de code qui montrent le problème de notre point de vue. Je ne pense vraiment pas que cela deviendra plus clair à travers plus d'exemples de code, si ceux affichés ne le font pas.

Par exemple, si le fait de voir plusieurs générateurs imbriqués qui sont horribles à lire, ou 50 lignes de simple passe-partout qui offrent de nombreuses opportunités de bogues, ne démontre pas suffisamment le problème, il n'y a nulle part où aller.

Il est très étrange de proposer des mixins et des fonctions sont une solution ici, lorsque l'ensemble de la demande est encapsulé dans un état , qui est réutilisable. Les fonctions ne peuvent pas maintenir l'état. Les mixins ne sont pas encapsulés. Cette suggestion passe à côté de tous les exemples et justifications fournis et montre toujours une profonde incompréhension de la demande.

Pour moi, je pense que nous avons battu 2 points dans la terre, et je ne pense pas que l'on puisse sérieusement discuter avec l'un ou l'autre.

  1. Les constructeurs imbriqués sont intrinsèquement difficiles à lire
  2. À part les constructeurs imbriqués, il n'y a _aucun moyen_ d'encapsuler et de partager l'état qui a des crochets de cycle de vie de widget.

Comme indiqué à plusieurs reprises, et même dans le sondage de Rémi, en termes simples : nous voulons les capacités du constructeur, sans la verbosité et les fermetures imbriquées du constructeur. Cela ne résume-t-il pas complètement ? Je suis totalement confus pourquoi un autre exemple de code est nécessaire pour continuer ici.

Le sondage de Remi nous montre qu'environ 80% des développeurs de Flutter préféreraient une sorte de capacité pour éviter les constructeurs imbriqués dans leur code lorsque cela est possible. Cela parle vraiment de lui-même imo. Vous n'avez pas besoin de nous le retirer dans ce fil, lorsque le sentiment de la communauté est si clair.

De mon point de vue, les problèmes sont clairs, et ils le sont encore plus lorsque vous examinez les cadres concurrents qui consacrent des paragraphes à la description de la justification ici. Vue, React, Flutter, ce sont tous des cousins, ils sont tous dérivés de React, et ils sont tous confrontés à ce problème avec l'état de réutilisation qui doit être lié au cycle de vie du widget. Ils décrivent tous pourquoi ils ont mis en œuvre quelque chose comme ça en détail. Tout va bien là. Tout est pertinent.

@rrousselGit pourriez-vous faire un exemple d'avoir plusieurs crochets multiples ? Par exemple, je fais une animation avec peut-être des dizaines d'AnimationControllers. Avec Flutter régulier, je peux faire :

List<AnimationController> controllers = [];
int numAnimationControllers = 50;

<strong i="7">@override</strong>
void initState() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers.add(AnimationController(...));
}

<strong i="8">@override</strong>
void dispose() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers[i].dispose();
}

Mais avec les crochets, je ne peux pas appeler useAnimationController en boucle. Je suppose qu'il s'agit d'un exemple trivial, mais je n'ai trouvé nulle part la solution pour ce type de cas d'utilisation.

@satvikpendem

quelques exemples de mes applications qui sont en production (certains des crochets comme l'envoi de requête avec pagination peuvent fusionner/refactoriser en un seul crochet mais ce n'est pas pertinent ici):

récupération de données simple avec pagination :

    final selectedTab = useState(SelectedTab.Wallet);
    final isDrawerOpen = useValueNotifier(false);
    final pageNo = useValueNotifier(0);
    final generalData = useValueNotifier(initialData);
    final services = useXApi();
    final isLoading = useValueNotifier(false);
    final waybillData = useValueNotifier<List<WaybillResponseModel>>([]);
    final theme = useTheme();
    final router = useRouter();

    fetchNextPage() async {
      if (isLoading.value || selectedTab.value != SelectedTab.Wallet) return;
      isLoading.value = true;
      final request = WaybillRequestModel()..pageNo = pageNo.value;
      final result = await services.waybillGetList(model: request);
      if (result.isOk && result.data.length > 0) {
        pageNo.value += 1;
        waybillData.value = [...waybillData.value, ...result.data];
      }
      isLoading.value = false;
    }

    // first fetch
    useEffect(() {
      fetchNextPage();
      return () {};
    }, []);

logique de formulaire (formulaire de connexion avec vérification du numéro de téléphone et minuterie de renvoi) :

    final theme = useTheme();
    final loginState = useValueNotifier(LoginState.EnteringNumber);
    final error = useValueNotifier<String>(null);
    final phoneNumberController = useTextEditingController(text: "");
    final phoneNumberFocusNode = useMemoized(() => FocusNode(), []);
    final otpFocusNode = useMemoized(() => FocusNode(), []);
    final otpController = useTextEditingController(text: "");
    final appState = Provider.of<AppStateController>(context);
    final services = useXApi();
    final router = useRouter();
    final resendTimerValue = useValueNotifier(0);
    useEffect(() {
      var timer = Timer.periodic(Duration(seconds: 1), (t) async {
        if (resendTimerValue.value > 0) resendTimerValue.value--;
      });
      return () => timer.cancel();
    }, []);

    final doLogin = () async {
      // login
      loginState.value = LoginState.LoggingIn;
      final loginResult = await services.authLoginOrRegister(
        mobileNumber: phoneNumberController.text,
      );
      if (loginResult.isOk) {
        loginState.value = LoginState.EnteringOtpCode;
        WidgetsBinding.instance.addPostFrameCallback((_) {
          FocusScope.of(context).requestFocus(otpFocusNode);
        });
        resendTimerValue.value = 30;
      } else {
        error.value = loginResult.errorMessage;
        loginState.value = LoginState.EnteringNumber;
        WidgetsBinding.instance.addPostFrameCallback((_) {
          FocusScope.of(context).requestFocus(phoneNumberFocusNode);
        });
      }
    };

pour l'animation, je pense que @rrousselGit a déjà fourni assez d'exemple.

Je ne veux pas parler de la façon dont la nature composable des crochets peut rendre la refactorisation au-dessus du code beaucoup plus facile, réutilisable et plus propre, mais si vous le souhaitez, je peux également publier des versions refactorisées.

Comme indiqué à plusieurs reprises, et même dans le sondage de Rémi, en termes simples : nous voulons les capacités du constructeur, sans la verbosité et les fermetures imbriquées du constructeur. Cela ne résume-t-il pas complètement ? Je suis totalement confus pourquoi un autre exemple de code est nécessaire pour continuer ici.

J'ai littéralement fourni des exemples de la façon dont vous pouvez réduire la verbosité et éviter l'imbrication des constructeurs à l'aide d'un exemple fourni par Remi.
J'ai pris son code, je l'ai collé dans mon application, je l'ai exécuté et je l'ai réécrit. Le résultat final en ce qui concerne la fonctionnalité était presque identique - autant que j'ai pu le comprendre en exécutant simplement le code, car le code n'était pas fourni avec des exigences. Bien sûr, nous pouvons discuter des cas limites et des problèmes potentiels, mais à la place, cela a été appelé hors sujet.

Pour les cas d'utilisation simples, j'utilise Builders, pour les cas d'utilisation complexes, je n'utilise pas Builders. L'argument ici est que sans utiliser Builders, il n'y a pas de moyen facile d'écrire du code concis et réutilisable. Implicitement, cela signifie également que les constructeurs sont indispensables et le seul moyen de développer des applications Flutter. C'est manifestement faux.

Je viens de montrer un code de preuve de concept fonctionnel qui le démontre. Il n'a pas utilisé de constructeurs ou de crochets, et il ne couvre pas 100% de "l'ensemble infini de problèmes" que ce problème particulier de github semble vouloir résoudre. Cela s'appelait hors sujet.
Sidenote, il est également très efficace, même sans aucune référence, je suppose qu'il bat même les widgets Builder. Je suis heureux de changer d'avis si je me trompe, et si jamais je trouve que Mobx devient un goulot d'étranglement de performance, j'abandonnerai Mobx et passerai aux constructeurs vanille en un clin d'œil.

Hixie travaille pour Google, il doit être patient et poli avec vous et ne peut pas vous dénoncer votre manque d'engagement. Le mieux qu'il puisse faire est de pousser pour plus d'exemples.

Je n'ai appelé personne, ni provoqué d'attaques personnelles. Je n'ai jamais réagi qu'aux arguments présentés ici, partagé mon opinion
(que je sais impopulaire et minoritaire) et a même essayé de présenter des contre-exemples réels avec du code. Je pourrais faire plus, je suis prêt à discuter des lacunes de mes exemples et à voir comment nous pouvons les améliorer, mais oui, être appelé hors sujet est un peu rebutant.

Je n'ai rien à perdre, à part peut-être être banni, donc je me fiche de vous appeler.
Il est évident que vous êtes tous les deux convaincus que les crochets sont la seule solution ("parce que React le fait") quels que soient les problèmes que vous rencontrez et qu'à moins qu'une alternative ne remplisse 100% de "l'ensemble infini de problèmes" que vous imaginez, vous n'envisagerez même pas de vous engager.

Ce n'est pas raisonnable et montre un manque de désir de vraiment s'engager.


Bien sûr, tout ce qui précède "n'est que mon opinion".

Je vois l'utilité des crochets dans cet exemple, mais je suppose que je ne comprends pas comment cela fonctionnerait dans mon cas où il semble que vous voudriez initialiser plusieurs objets à la fois, dans ce cas AnimationController s mais en réalité, cela peut être n'importe quoi. Comment les crochets gèrent-ils ce cas?

Fondamentalement, y a-t-il un moyen de faire tourner cela

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

Dans

var xs = []
for (int i = 0; i < 3; i++)
     xs[i] = useState(i);

Sans enfreindre les règles du crochet ? Parce que j'ai listé l'équivalent dans Flutter normal. Je ne suis pas trop expérimenté avec les crochets dans Flutter, alors soyez indulgents avec moi.

Je veux simplement créer un tableau d'objets hook (AnimationControllers par exemple) à la demande avec tous ses initState et disposer déjà instanciés, je ne sais pas comment cela fonctionne dans les hooks.

@satvikpendem pense aux crochets comme des propriétés sur une classe. les définissez-vous en boucle ou les nommez-vous manuellement un par un ?

dans votre exemple définissant comme ceci

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

est utile pour ce cas d'utilisation :

var isLoading = useState(1);
var selectedTab = useState(2);
var username = useState(3); // text field

voyez-vous comment chaque useState est lié à une partie nommée de votre logique d'état ? (comme useState d'isLoading est connecté lorsque l'application est en état de chargement)

dans votre deuxième extrait, vous appelez useState en boucle. vous pensez à useState comme un détenteur de valeur ne faisant pas partie de votre logique d'état. cette liste est-elle nécessaire pour afficher un tas d'éléments dans un ListView ? si oui, vous devriez considérer chaque élément de la liste comme un état et non individuellement.

final listData = useState([]);

c'est juste pour le useState et je peux voir quelques cas d'utilisation (que je pense qu'ils sont très rares) pour appeler des crochets dans une condition ou dans une boucle. pour ce genre de crochets, il devrait y avoir un autre crochet pour gérer la liste de données au lieu d'un. par exemple:

var single = useTest("data");
var list = useTests(["data1", "data2"]);
// which is equivalent to
var single1 = useTest("data1");
var single2 = useTest("data2");

Je vois, donc avec les crochets, il semble que nous devions créer un crochet séparé pour gérer les cas avec un tableau d'éléments, tels que plusieurs AnimationControllers.

Voici ce que j'avais au départ qui ne semble pas fonctionner :

  final animationControllers = useState<List<AnimationController>>([]);

  animationControllers.value = List<AnimationController>.generate(
    50,
    (_) => useAnimationController(),
  );

mais je suppose que si j'écris mon propre crochet pour gérer plusieurs éléments, cela devrait fonctionner, non?

class _MultipleAnimationControllerHook extends Hook<MultipleAnimationController> {
  const _MultipleAnimationControllerHook({
    this.numControllers,
    this.duration,
    this.debugLabel,
    this.initialValue,
    this.lowerBound,
    this.upperBound,
    this.vsync,
    this.animationBehavior,
    List<Object> keys,
  }) : super(keys: keys);

  /// Take in number of controllers wanted
  /// This hook assumes all `AnimationController`s will have the same parameters
  final int numControllers; 

  final Duration duration;
  final String debugLabel;
  final double initialValue;
  final double lowerBound;
  final double upperBound;
  final TickerProvider vsync;
  final AnimationBehavior animationBehavior;

  <strong i="10">@override</strong>
  _AnimationControllerHookState createState() =>
      _AnimationControllerHookState();
}

class _AnimationControllerHookState
    extends HookState<AnimationController, _AnimationControllerHook> {
  List<AnimationController> _multipleAnimationController; // return a list instead of a singular item

  <strong i="11">@override</strong>
  void initHook() {
    super.initHook();
    for (int i = 0; i < hook.numControllers) // just added loops 
        _multipleAnimationController[i] = AnimationController(
          vsync: hook.vsync,
          duration: hook.duration,
          debugLabel: hook.debugLabel,
          lowerBound: hook.lowerBound,
          upperBound: hook.upperBound,
          animationBehavior: hook.animationBehavior,
          value: hook.initialValue,
        );
  }

  <strong i="12">@override</strong>
  void didUpdateHook(_AnimationControllerHook oldHook) {
      for (int i = 0; i < numControllers; i++) {
        if (hook.vsync != oldHook[i].vsync) {
           _multipleAnimationController[i].resync(hook.vsync);
        }

        if (hook.duration != oldHook[i].duration) {
          _multipleAnimationController[i].duration = hook.duration;
        }
      }
  }

  <strong i="13">@override</strong>
  MultipleAnimationController build(BuildContext context) {
    return _multipleAnimationController;
  }

  <strong i="14">@override</strong>
  void dispose() {
    _multipleAnimationController.map((e) => e.dispose());
  }
}

Cela signifie-t-il que si nous avons une version singulière d'un crochet, nous ne pouvons tout simplement pas l'utiliser pour une version avec plusieurs éléments et devons à la place réécrire la logique ? Ou y a-t-il une meilleure façon de faire cela?

Si quelqu'un souhaite également donner un exemple sans crochets, j'aimerais également le savoir, je me suis posé des questions sur cette pièce du puzzle de la réutilisabilité. Il existe peut-être un moyen d'encapsuler ce comportement dans une classe, qui possède son propre champ AnimationController, mais si cela est créé à l'intérieur d'une boucle, le crochet le serait également, enfreignant les règles. Peut-être pourrions-nous considérer comment Vue le fait, qui n'est pas affecté par les conditions et les boucles pour son implémentation des hooks.

@satvikpendem

Je ne pense pas que mon relevé soit valide pour AnimationController ou useAnimationController

car bien que vous puissiez avoir plus d'un AnimationController mais vous ne les stockez pas nécessairement dans un tableau pour les utiliser dans la méthode de classe. par exemple:

useSingleTickProvider();
final animation1 = useAnimationController();
final animation2 = useAnimationController();
final animation3 = useAnimationController();
// ...
useEffect(() async {
  await animation1.forward();
  await Future.sleep(100);
  await animation1.reverse();
  await animation2.forward();
  await animation3.forward();
}, []);

(vous ne créez pas de liste et ne les référencez pas comme animation[0] )

Honnêtement, dans mon expérience de réaction et de flottement avec des crochets, j'ai rarement eu besoin d'appeler une sorte de crochet dans une boucle. même alors, la solution était simple et facile à mettre en œuvre. maintenant j'y pense, cela pourrait certainement être résolu d'une meilleure manière, comme créer un composant (widget) pour chacun d'eux, qui est l'OMI est la solution "la plus propre".

pour répondre à votre question s'il existe un moyen plus simple de gérer plusieurs AnimationController , oui, il existe :

final ticker = useTickerProvider();
final controllers = useMemo(() => [AnimationController(ticker), AnimationController(ticker)], []);

useEffect(() {
  controllers.forEach(x => x.resync(ticker));
  return () => controllers.forEach(x => x.dispose());
}, [ticker, controllers]);

  • vous pouvez également utiliser useState si les AnimationController sont dynamiques.

(il se resynchronise également lorsque le ticker est modifié)

@rrousselGit pourriez-vous faire un exemple d'avoir plusieurs crochets multiples ? Par exemple, je fais une animation avec peut-être des dizaines d'AnimationControllers. Avec Flutter régulier, je peux faire :

List<AnimationController> controllers = [];
int numAnimationControllers = 50;

<strong i="8">@override</strong>
void initState() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers.add(AnimationController(...));
}

<strong i="9">@override</strong>
void dispose() {
    for (int i = 0; i < numAnimationControllers; i++)
        controllers[i].dispose();
}

Mais avec les crochets, je ne peux pas appeler useAnimationController en boucle. Je suppose qu'il s'agit d'un exemple trivial, mais je n'ai trouvé nulle part la solution pour ce type de cas d'utilisation.

Les crochets font cela différemment.

Nous ne créons plus de liste de contrôleurs, nous déplaçons plutôt la logique du contrôleur vers l'élément :

Widget build(context) {
  return ListView(
    children: [
      for (var i = 0; i < 50; i++)
        HookBuilder(
          builder: (context) {
            final controller = useAnimationController();
          },
        ),
    ],
  );
}

Nous avons toujours créé nos 50 contrôleurs d'animation, mais ils appartiennent à un widget différent.

Peut-être pourriez-vous partager un exemple de la raison pour laquelle vous en aviez besoin, et nous pourrions essayer de convertir en crochets et de l'ajouter au dépôt de Tim ?

Hixie travaille pour Google, il doit être patient et poli avec vous et ne peut pas vous dénoncer votre manque d'engagement. Le mieux qu'il puisse faire est de pousser pour plus d'exemples.

@Hixie , si c'est ce que vous ressentez, veuillez le dire (soit ici, soit contactez-moi en privé).

J'ai littéralement fourni des exemples de la façon dont vous pouvez réduire la verbosité et éviter l'imbrication des constructeurs à l'aide d'un exemple fourni par Remi.

Merci, mais je ne sais pas comment extraire un modèle commun de ce code pour appliquer cette logique à différents cas d'utilisation.

Dans l'OP, j'ai mentionné qu'actuellement, nous avons 3 choix:

  • utiliser Builders et avoir du code imbriqué
  • ne factorisez pas du tout le code, qui ne s'adapte pas à une logique d'état plus complexe (je dirais que StreamBuilder et son AsyncSnapshot est une logique d'état complexe).
  • essayez de créer une architecture en utilisant mixins/oop/..., mais vous obtenez une solution trop spécifique au problème que tout cas d'utilisation légèrement différent nécessitera une réécriture.

Il me semble que vous avez utilisé le 3e choix (qui est dans la même catégorie que les premières itérations des propositions Property ou addDispose ).

J'ai précédemment fait une grille d'évaluation pour juger du pattern :

Pourriez-vous exécuter votre variante là-dessus ? Surtout le deuxième commentaire sur la mise en œuvre de toutes les fonctionnalités de StreamBuilder sans code en double s'il est utilisé plusieurs fois.

Mon plan à ce stade sur ce bogue est:

  1. Prenez les exemples de https://github.com/flutter/flutter/issues/51752#issuecomment -675285066 et créez une application à l'aide de Flutter pur qui montre ces différents cas d'utilisation ensemble.
  2. Essayez de concevoir une solution qui permet la réutilisation du code pour ces exemples qui satisfasse aux diverses exigences clés qui ont été discutées ici et qui correspond à nos principes de conception.

Si quelqu'un souhaite aider avec l'un ou l'autre de ceux-ci, je suis vraiment heureux d'avoir de l'aide. Il est peu probable que j'y arrive bientôt car je travaille d'abord sur la transition NNBD.

@rrousselGit Bien sûr, je Box es), et ils devraient pouvoir se déplacer indépendamment les uns des autres (il doit donc y avoir au moins un AnimationController pour chaque Box ). Voici une version que j'ai créée avec un seul AnimationController partagé entre les multiples Widgets, mais à l'avenir, je pourrai animer chaque Widget indépendamment, par exemple pour effectuer des transformations compliquées telles que l'implémentation d'un CupertinoPicker , avec son effet de molette de défilement personnalisé .

Il y a trois cases dans une pile qui se déplacent de haut en bas lorsque vous cliquez sur un FloatingActionButton.

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main(List<String> args) => runApp(const App());

class App extends HookWidget {
  const App({Key key});

  static const Duration duration = Duration(milliseconds: 500);
  static const Curve curve = Curves.easeOutBack;

  <strong i="11">@override</strong>
  Widget build(BuildContext context) {
    final AnimationController controller =
        useAnimationController(duration: duration);
    final Animation<double> animation = Tween<double>(
      begin: 0,
      end: 300,
    )
        .chain(
          CurveTween(
            curve: curve,
          ),
        )
        .animate(controller);
    final ValueNotifier<bool> isDown = useState<bool>(false);
    final ValueNotifier<int> numBoxes = useState<int>(3);

    return MaterialApp(
      home: SafeArea(
        child: Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              if (!isDown.value) {
                controller.forward();
                isDown.value = true;
              } else {
                controller.reverse();
                isDown.value = false;
              }
            },
          ),
          body: AnimatedBuilder(
            animation: animation,
            builder: (_, __) => Boxes(
              numBoxes: numBoxes.value,
              animation: animation,
            ),
          ),
        ),
      ),
    );
  }
}

class Boxes extends StatelessWidget {
  Boxes({
    <strong i="12">@required</strong> this.numBoxes,
    <strong i="13">@required</strong> this.animation,
  });

  final int numBoxes;
  final Animation<double> animation;

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return Stack(
      children: List<Widget>.generate(
        numBoxes,
        (int index) => Positioned(
          top: (animation.value) + (index * (100 + 10)),
          left: (MediaQuery.of(context).size.width - 100) / 2,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
        ),
      ),
      // ],
    );
  }
}

Dans ce cas, chaque case se déplace à l'unisson, mais on peut imaginer un scénario plus complexe comme créer une visualisation pour une fonction de tri par exemple, ou déplacer des éléments dans une liste animée, où le widget parent connaît les données sur l'endroit où chaque Box devrait être et il devrait pouvoir animer chacun comme bon lui semble.

Le problème semble être que les AnimationControllers et les Box es qui les utilisent pour piloter leur mouvement ne sont pas dans la même classe, donc il faudrait soit passer par l'AnimationController en gardant un tableau d'entre eux à utiliser dans un Builder, ou demandez à chaque Box maintenir son propre AnimationController.

Avec les hooks, étant donné que les Box es et le widget parent ne sont pas dans la même classe, comment pourrais-je faire une liste de AnimationControllers pour le premier cas où chaque Box est passé dans un AnimationController ? Cela ne semble pas nécessaire d'après votre réponse ci-dessus avec HookBuilder, mais si je descends l'état dans le widget enfant comme vous le dites, et que je choisis de faire en sorte que chaque Box ait son propre AnimationController via useAnimationController , je rencontre un autre problème : comment exposer le contrôleur d'animation créé à la classe parente pour qu'il coordonne et exécute les animations indépendantes pour chaque enfant ?

Dans Vue, vous pouvez émettre un événement vers le parent via le modèle emit , donc dans Flutter ai-je besoin d'une solution de gestion d'état supérieure comme Riverpod ou Rx où le parent met à jour l'état global et l'enfant écoute le global Etat? Il semble que je ne devrais pas, du moins pour un exemple simple comme celui-ci. Merci d'avoir dissipé mes confusions.

@satvikpendem Désolé, je n'ai pas été clair. Pourriez-vous montrer comment vous le feriez sans crochets, plutôt que le problème où vous bloquez avec des crochets ?

Je veux avoir une compréhension claire de ce que vous essayez de faire plutôt que de savoir où vous êtes bloqué

Mais à titre de supposition rapide, je pense que vous recherchez plutôt la courbe d' intervalle et que vous disposez d'un seul contrôleur d'animation.

@rrousselGit Bien sûr, c'est ici

import 'package:flutter/material.dart';

void main(List<String> args) => runApp(const App());

class Animator {
  Animator({this.controller, this.animation});
  AnimationController controller;
  Animation<double> animation;
}

class App extends StatefulWidget {
  const App({Key key});

  static const Duration duration = Duration(milliseconds: 500);
  static const Curve curve = Curves.easeOutBack;

  <strong i="7">@override</strong>
  _AppState createState() => _AppState();
}

class _AppState extends State<App> with TickerProviderStateMixin {
  List<Animator> animators = [];
  bool isDown = false;
  int numBoxes = 3;

  <strong i="8">@override</strong>
  void initState() {
    for (int i = 0; i < numBoxes; i++) {
      final AnimationController c = AnimationController(
        duration: App.duration,
        vsync: this,
      );
      animators.add(
        Animator(
          controller: c,
          animation: Tween<double>(
            begin: 0,
            end: 300,
          )
              .chain(
                CurveTween(
                  curve: App.curve,
                ),
              )
              .animate(c),
        ),
      );
    }
    super.initState();
  }

  <strong i="9">@override</strong>
  void dispose() {
    for (int i = 0; i < numBoxes; i++) {
      animators[i].controller.dispose();
    }
    super.dispose();
  }

  <strong i="10">@override</strong>
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SafeArea(
        child: Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              if (!isDown) {
                for (final Animator animator in animators) {
                  animator.controller.forward();
                }
                setState(() {
                  isDown = true;
                });
              } else {
                for (final Animator animator in animators) {
                  animator.controller.reverse();
                }
                setState(() {
                  isDown = false;
                });
              }
            },
          ),
          body: Stack(
            children: List<Box>.generate(
              numBoxes,
              (int index) => Box(
                index: index,
                animation: animators[index].animation,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class Box extends StatelessWidget {
  Box({
    <strong i="11">@required</strong> this.animation,
    <strong i="12">@required</strong> this.index,
  });

  final int index;
  final Animation<double> animation;

  <strong i="13">@override</strong>
  Widget build(BuildContext context) {
    return Positioned(
      top: (animation.value) + (index * (100 + 10)),
      left: (MediaQuery.of(context).size.width - 100) / 2,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    );
  }
}

En fait, je veux plusieurs contrôleurs d'animation, un pour chaque widget, car ils peuvent se déplacer indépendamment les uns des autres, avec leurs propres durées, courbes, etc. Notez que le code ci-dessus semble avoir un bogue que je ne pouvais pas comprendre, où il devrait s'animer proprement, mais fondamentalement, il devrait animer 3 cases de haut en bas sur un clic de bouton. On peut imaginer un scénario où au lieu qu'elles aient chacune la même courbe, je leur donne à chacune une courbe différente, ou je fais 100 cases, chacune avec une durée plus ou moins longue que la précédente, ou je fais monter les paires et les impairs descendent, et ainsi de suite.

Avec Flutter normal, initState et dispose peuvent tous les deux avoir des boucles, mais ce n'est pas le cas avec les crochets, alors je me demande simplement comment on peut lutter contre cela. De plus, je ne veux pas mettre la classe Box dans le widget parent, car je ne veux pas les encapsuler étroitement tous les deux ; Je devrais pouvoir garder la logique parente la même mais échanger Box avec Box2 par exemple.

Merci!
J'ai poussé votre exemple vers le dépôt de

TL;DR, avec des crochets (ou constructeurs), nous pensons déclarativement au lieu d'impératif. Donc, plutôt que d'avoir une liste de contrôleurs sur un widget, alors les piloter impérativement - ce qui déplace le contrôleur vers l'élément et implémente une animation implicite.

Merci @rrousselGit ! J'ai eu du mal avec ce type d'implémentation pendant un petit moment après avoir commencé à utiliser des hooks mais je comprends maintenant comment cela fonctionne. Je viens d'ouvrir un PR pour une version avec une cible différente pour chaque contrôleur d'animation car cela pourrait être plus convaincant pour comprendre pourquoi les crochets sont utiles comme je l'avais dit ci-dessus :

On peut imaginer un scénario où au lieu qu'elles aient chacune la même courbe, je leur donne à chacune une courbe différente, ou je fais 100 cases, chacune avec une durée plus ou moins longue que la précédente, ou je fais monter les paires et les impairs descendent, et ainsi de suite.

J'avais essayé de faire la version déclarative mais je suppose que ce que je ne comprenais pas était la méthode de cycle didUpdateWidget/Hook vie

Je suis tombé sur un exemple du monde réel dans ma base de code aujourd'hui, alors j'ai pensé que je pourrais aussi bien le partager.

Donc, dans ce scénario, je travaille avec Firestore et j'ai un passe-partout que je souhaite exécuter avec chaque StreamBuilder, j'ai donc créé mon propre générateur personnalisé. J'ai aussi besoin de travailler avec un ValueListenablequi permet à l'utilisateur de réorganiser la liste. Pour des raisons de coût monétaire liées à Firestore, cela nécessite une implémentation très spécifique (chaque élément ne peut pas stocker sa propre commande, à la place la liste doit l'enregistrer en tant que champ d'identifiants concaténés), c'est parce que firestore facture chaque écriture, donc vous pouvez potentiellement économiser beaucoup d'argent de cette façon. Il finit par lire quelque chose comme ceci :

return ClipRRect(
      borderRadius: BorderRadius.circular(CornerStyles.dialog),
      child: Scaffold(
        backgroundColor: state.theme.scaffoldBackgroundColor,
        body: FamilyStreamBuilder<DocumentSnapshot>(
          stream: state.listRef.snapshots(),
          builder: (context, AsyncSnapshot<DocumentSnapshot> documentSnapshot) {
            //When a list is updated, we need to update the listOrder
            state.updateListOrderFromSnapshot(documentSnapshot);
            return FamilyStreamBuilder<QuerySnapshot>(
                stream: state.itemsCollection.snapshots(),
                builder: (_, AsyncSnapshot<QuerySnapshot> itemsSnapshot) {
                  //Sort the list items by the idField on the list-doc
                  List<DocumentSnapshot> items = itemsSnapshot.data.documents;
                  state.handleDocsSync(items);
                  return ValueListenableBuilder(
                    valueListenable: state.listOrderNotifier,
                    builder: (_, List<String> listOrder, __) {
                      List<DocumentSnapshot> ordered = state.sortItems(items, listOrder);
                     //Set the firstCompleted item if we have one
                      state.firstCompletedItem = ordered
                          ?.firstWhere((element) => element.data[FireParams.kIsComplete] == true, orElse: () => null)
                          ?.documentID;
                      return _buildItemList(items, ordered);
                    },
                  );
                });
          },
        ),
      ),
    );

J'ai l'impression qu'il serait beaucoup plus facile de raisonner si je pouvais l'écrire plus comme :

    DocumentSnapshot list = useFamilyStream(state.listRef.snapshots());
    List<DocumentSnapshot> items = useFamilyStream(state.itemsCollection.snapshots()).data.documents;
    List<String> listOrder = useValueListenable(state.listOrderNotifier);

    //When a list is updated, we need to update the listOrder
    state.updateListOrderFromSnapshot(list);

   //Sort the list items by the idField on the list-doc
    state.handleDocsSync(items);
    List<DocumentSnapshot> ordered = state.sortItems(items, listOrder);

    //Set the firstCompleted item if we have one
    state.firstCompletedItem = ordered
        ?.firstWhere((element) => element.data[FireParams.kIsComplete] == true, orElse: () => null)
        ?.documentID;

    return ClipRRect(
      borderRadius: BorderRadius.circular(CornerStyles.dialog),
      child: Scaffold(
          backgroundColor: state.theme.scaffoldBackgroundColor,
          body: _buildItemList(items, ordered)
      ));

Cela perd l'optimisation concernant les reconstructions granulaires, mais cela ne ferait pas un IRL différent puisque tous les éléments visuels sont au nœud feuille le plus bas, tous les wrappers sont à l'état pur.

Comme pour de nombreux scénarios du monde réel, le conseil de "N'utilisez pas X" n'est pas réaliste, car Firebase n'a qu'une seule méthode de connexion qui est Streams, et chaque fois que je veux ce comportement de type socket, je n'ai pas d'autre choix que de utiliser un flux. C'est la vie.

Cela perd l'optimisation concernant les reconstructions granulaires, mais cela ne ferait pas un IRL différent puisque tous les éléments visuels sont au nœud feuille le plus bas, tous les wrappers sont à l'état pur.

Cela fait quand même une différence. Qu'un nœud soit visuel ou non n'affecte pas le coût de sa reconstruction.

Je prendrais probablement cet exemple en compte dans différents widgets (les IDE ont des outils de refactorisation en un clic pour rendre cela vraiment facile). _buildItemList devrait probablement être un widget, tout comme la partie enracinée à FamilyStreamBuilder .

Nous ne perdons pas vraiment la reconstruction granulaire.
En fait, les hooks améliorent cet aspect, en permettant de mettre facilement en cache l'instance du widget en utilisant useMemoized .

Il y a quelques exemples sur le repo de Tim qui le font.

Je prendrais probablement cet exemple en compte dans différents widgets (les IDE ont des outils de refactorisation en un clic pour rendre cela vraiment facile). _buildItemList devrait probablement être un widget, tout comme la partie enracinée à FamilyStreamBuilder .

Le fait est que je n'ai pas vraiment envie de le faire, car je n'ai aucun problème de performance dans cette vue. Donc, 100% du temps, je privilégierai la localité et la cohérence du code à la micro-optimisation comme celle-ci. Cette vue ne se reconstruit que lorsque l'utilisateur lance une action (~moyenne une fois toutes les 10 secondes), ou lorsque les données du backend changent et qu'ils regardent une liste ouverte (ne se produit presque jamais). Il se trouve également que ce n'est qu'une simple vue qui est principalement une liste, et la liste a une tonne de ses propres optimisations en cours en interne. Je me rends compte que build() peut techniquement se déclencher à tout moment, mais en pratique, les reconstructions aléatoires sont assez rares.

imo, il est beaucoup plus facile de travailler et de déboguer cette vue si toute cette logique est regroupée dans un seul widget, principalement dans le but de me faciliter la vie lorsque j'y reviendrai à l'avenir :)

Une autre chose à noter est que l'imbrication m'a essentiellement "forcé" de la méthode de construction, car il n'y avait aucun moyen que je puisse commencer à construire mon arbre à l'intérieur de 3 fermetures et 16 espaces dans le trou.

Et oui, vous pourriez dire qu'il est logique de passer ensuite à un widget séparé. Mais pourquoi ne pas rester dans la méthode build ? Si nous pouvions réduire le passe-partout à ce qu'il doit vraiment être, alors il n'y a pas besoin d'avoir la lisibilité et les soucis de maintenance de diviser les choses sur 2 fichiers. (En supposant que les performances ne soient pas un problème, ce qui n'est souvent pas le cas)

N'oubliez pas que dans ce scénario, j'ai déjà créé un widget personnalisé pour gérer mon Stream Builder. Maintenant j'aurais besoin d'en faire un autre pour gérer la composition de ces constructeurs ?? Cela semble un peu exagéré.

parce que je n'ai aucun problème de performance de ce point de vue

Oh, je ne le refactoriserais pas en widgets pour les performances, les constructeurs devraient déjà s'en occuper. Je le refactoriserais pour la lisibilité et la réutilisation. Je ne dis pas que c'est la "bonne" façon de le faire, je dis juste comment je structurerais le code. De toute façon, ce n'est ni ici ni là.

aucun moyen que je puisse commencer à construire mon arbre à l'intérieur de 3 fermetures et 16 espaces dans le trou

J'ai peut-être juste un moniteur plus large que toi... :-/

alors il n'y a pas besoin d'avoir la lisibilité et les tracas de maintenance de diviser les choses sur 2 fichiers

Je mettrais les widgets dans le même fichier, FWIW.

Quoi qu'il en soit, c'est un bel exemple, et je pense que vous préféreriez utiliser un widget avec une syntaxe différente plutôt que d'utiliser plusieurs widgets.

J'ai peut-être juste un moniteur plus large que toi... :-/

J'ai un ultrawide :D, mais dartfmt nous limite évidemment tous à 80. Donc perdre 16 est important. Le problème principal est que la fin de ma déclaration est que ce },);});},),),); n'est pas vraiment amusant quand quelque chose ne va pas. Je dois être extrêmement prudent chaque fois que je modifie cette hiérarchie, et les aides IDE courantes comme swap with parent cessent de fonctionner.

Je mettrais les widgets dans le même fichier, FWIW.

100%, mais je trouve toujours que sauter verticalement dans un seul fichier est plus difficile à maintenir. Incontournable bien sûr, mais nous essayons de réduire lorsque cela est possible et de « garder les choses ensemble ».

Mais surtout, même si je refactorise la liste principale dans son propre widget (ce qui, je suis d'accord, est plus lisible qu'une méthode de construction imbriquée), c'est toujours beaucoup plus lisible sans l'imbrication dans le widget parent. Je peux entrer, comprendre toute la logique d'un coup d'œil, voir clairement le widget _MyListView() et y accéder directement, confiant de comprendre le contexte environnant. Je peux également ajouter/supprimer des dépendances supplémentaires avec une relative facilité, donc cela s'adapte très bien.

dartfmt nous limite évidemment tous à 80

Je veux dire, c'est l'une des raisons pour lesquelles je n'utilise généralement pas dartfmt, et quand je le fais, je le mets sur 120 ou 180 caractères...

Votre expérience ici est tout à fait valable.

Moi aussi en fait, 120 toute la journée :) Mais pub.dev baisse activement les taux des plugins qui ne sont pas formatés à 80, et j'ai l'impression que je (nous) sommes minoritaires lorsque nous modifions cette valeur.

Eh bien, c'est absurde, nous devrions corriger cela.

pub.dev ne déclasse pas les plugins qui ne respectent pas dartfmt. Il n'affiche qu'un commentaire dans la page de score, mais le score n'est pas impacté
Mais sans doute, il y a plus de problèmes avec dartfmt que juste la longueur de ligne.

Une longueur de ligne trop importante conduit à des éléments plus lisibles sur plusieurs lignes pour être sur une seule ligne, tels que :

object
  ..method()
  ..method2();

qui peut devenir :

object..method()..method2();

Je vois ça ?
image
Paquet en question : https://pub.dev/packages/sized_context/score

Intéressant - ce n'était certainement pas comme ça avant, car le fournisseur n'a pas utilisé dartfmt pendant un certain temps.
Je me suis trompé.

Oui, c'est définitivement un nouveau comportement, lorsque j'ai publié pour la première fois au printemps dernier, je me suis assuré de cocher toutes les cases, et dartfmt n'était pas nécessaire.

Après toutes ces discussions, j'espère que nous verrons un support natif pour la solution de type crochet dans Flutter. soit useHook ou use Hook ou tout ce que l'équipe flottante peut sentir que leur fonctionnalité n'est pas comme React 😁🤷‍♂️

nous utilisons des crochets d'une manière comme final controller = useAnimationController(duration: Duration(milliseconds: 800));
N'est-il pas préférable d'utiliser la nouvelle fonctionnalité du programme Darts _Extension_ copiée de kotlin/swift pour magnifiquement cette syntaxe ?

quelque chose comme : final controller = AnimationController.use(duration: Duration(milliseconds: 800));
avec cette approche, lorsque l'équipe flutter/dart décide d'ajouter use Hook au lieu de la syntaxe actuellement disponible useHook , je pense qu'un Annotation à cette fonction d'extension l'a fait lire pour l'utiliser comme
final controller = use AnimationController(duration: Duration(milliseconds: 800));

il est également compréhensible/significatif d'avoir use mot-clé const et new :
new Something
const Something
use Something

en prime à cette recommandation, je pense enfin que même les fonctions constructeur/générateur peuvent utiliser/bénéficier de ce Annotation proposé. puis le compilateur de fléchettes avec une certaine personnalisation le convertit pour prendre en charge le mot-clé use .

Tellement beau et caractéristique spécifique au flottement / fléchette 😉

Ai-je raison de supposer que les exemples dans https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful sont désormais représentatifs des problèmes que les gens veulent résoudre ?

Je ne suis pas sûr de ce que tout le monde ressent, mais je pense que les problèmes y sont quelque peu représentés (ce qui signifie que je ne peux pas en être sûr car quelqu'un pourrait signaler quelque chose qui n'est pas représenté).

J'ai essayé une solution intermédiaire dans ce référentiel. Il est composable comme les crochets, mais ne dépend pas de l'ordre des appels de fonction ou de l'interdiction des boucles, etc. Il utilise directement StatefulWidgets. Cela implique un mixin, ainsi que des propriétés avec état qui sont identifiées de manière unique par des clés. Je n'essaie pas de promouvoir cela comme la solution ultime, mais comme un juste milieu entre les deux approches.

Je l'ai appelée l'approche lifecycleMixin, elle est très proche de l'approche LateProperty qui a été discutée ici, mais la principale différence est qu'elle a plus de cycles de vie implémentés et qu'elle peut facilement composer. (sur la partie des cycles de vie, je n'ai pas utilisé de cycles de vie de widgets autres que initState et dispose beaucoup, donc j'ai peut-être totalement foiré là-bas).

J'aime cette approche parce que :

  1. Il a très peu de pénalité d'exécution.
  2. Il n'y a pas de logique/fonctions créant ou gérant l'état dans le chemin de construction (les versions peuvent être pures - seulement récupérer l'état).
  3. La gestion du cycle de vie est plus claire lors de l'optimisation des reconstructions via un générateur. (Mais vous ne sacrifiez pas la réutilisabilité et la composabilité de petits morceaux d'état).
  4. Étant donné que vous pouvez réutiliser la création de bits d'état, une bibliothèque peut être constituée de bits d'état communs qui doivent être créés et éliminés de certaines manières, de sorte qu'il y a moins de passe-partout dans votre propre code.

Je n'aime pas cette approche (par rapport aux hooks) pour les raisons suivantes :

  1. Je ne sais pas si cela couvre tout ce que les crochets peuvent faire.
  2. Vous devez utiliser des clés pour identifier de manière unique les propriétés. (Donc, lors de la composition des éléments de logique qui construisent un état, vous devez ajouter à la clé pour identifier de manière unique chaque partie de l'état - faire de la clé un paramètre de position requis aide, mais j'aimerais une solution au niveau de la langue pour accéder à un identifiant unique pour une variable).
  3. Il utilise fortement des extensions pour créer des fonctions réutilisables afin de créer des bits d'état communs. Et les extensions ne peuvent pas être importées automatiquement par les IDE.
  4. Vous pouvez vous tromper si vous mélangez les cycles de vie de différents widgets / y accédez entre les widgets sans les gérer explicitement correctement.
  5. La syntaxe du générateur est un peu étrange, de sorte que l'état créé est dans la portée de la fonction de génération, mais en laissant la fonction de génération pure.
  6. Je n'ai pas encore implémenté tous les exemples, il pourrait donc y avoir un cas d'utilisation que je ne peux pas couvrir.

Exemple de compteur simple .
Exemple de compteurs animés

cadre
bits communs de la logique de composition d'état réutilisable

Je ne sais pas combien de temps il me reste, les études supérieures m'occupent toujours, mais j'aimerais avoir des commentaires. @rrousselGit À quel point est-ce proche des crochets, voyez-vous des trous évidents dans la réutilisabilité ou la composabilité ?

Je n'essaie pas de promouvoir ma solution, mais plutôt d'encourager une discussion positive sur un terrain d'entente. Si nous pouvons nous mettre d'accord sur ce qui manque ou sur ce que cette solution nous apporte, je pense que nous ferons de bons progrès.

@TimWhiting Le principal problème que j'ai avec cette approche est le manque de robustesse. Un grand facteur ici est le besoin de fiabilité des constructeurs, sous une forme succincte. L'identifiant magique et la possibilité d'entrer en conflit sur le cycle de vie créent tous deux de nouveaux vecteurs pour l'apparition de bogues, et je continuerais à recommander à mon équipe d'utiliser des constructeurs, car bien qu'ils soient assez désagréables à lire, au moins nous savons qu'ils sont 100 % sans bug.

En ce qui concerne les exemples, je pense toujours que l'exemple parfait consiste simplement à utiliser un AnimationController, avec une valeur de durée liée au widget. Cela reste simple et familier. Il n'y a pas besoin d'être plus ésotérique que cela, c'est un petit cas d'utilisation parfait pour un passe-partout réutilisable, il a besoin de crochets de cycle de vie, et toutes les solutions pourraient facilement être jugées par leur capacité à utiliser plusieurs animations de manière succincte.

Tout le reste n'est qu'une variante de ce même cas d'utilisation 'Stateful Controller'. Je veux faire X dans initState et Y dans l'état de disposition, et mettre à jour Z lorsque mes dépendances changent. Peu importe ce que sont X, Y et Z.

Je me demande si @rrousselGit pourrait fournir un aperçu ici, ou a des données sur les crochets actuellement les plus utilisés. Je suppose que c'est 80% de flux et d'animations, mais ce serait bien de savoir ce que les gens utilisent le plus.

En ce qui concerne la reconstruction de portions de l'arbre, les constructeurs sont naturellement adaptés à cette tâche de toute façon, nous devrions simplement les laisser faire. Un contrôleur avec état peut facilement être connecté à des moteurs de rendu sans état de toute façon si c'est ce que vous voulez (bonjour à toutes les classes de transition).

Tout comme nous pourrions le faire :

var anim = get AnimationController();
return Column(
  _someExpensiveBuildMethod(),
  FadeTransition(opacity: anim, child: ...)
)

On pourrait toujours faire :

var foo = get ComplicatedThingController();
return Column(
  _someExpensiveBuildMethod(),
  ComplicatedThing(controller: foo, child: ...)
)

@esDotDev Je suis d'accord, les clés et la syntaxe du constructeur sont le principal inconvénient de l'approche lifecycleMixin. Je ne sais pas si vous pouvez contourner cela, sauf en utilisant une approche de style hooks avec ses restrictions associées, ou un changement de langage permettant d'associer des déclarations de variables à des bits d'état avec des cycles de vie. C'est pourquoi je continuerai à utiliser des crochets et laisserai les autres utiliser des widgets avec état, à moins qu'une meilleure solution ne soit trouvée. Cependant, je pense que c'est une alternative intéressante pour ceux qui n'aiment pas les restrictions des crochets, même si cela vient avec ses propres restrictions.

Ai-je raison de supposer que les exemples dans https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful sont désormais représentatifs des problèmes que les gens veulent résoudre ?

Je ne suis honnêtement pas sûr.
Je dirais _oui_. Mais cela dépend vraiment de la façon dont vous interpréterez ces exemples.

Dans ce fil, nous avons l'habitude de ne pas nous comprendre, donc je ne peux pas garantir que cela ne se reproduira plus.

C'est en partie pourquoi je n'aime pas utiliser des exemples de code et j'ai suggéré d'extraire un ensemble de règles à la place.
Les exemples sont subjectifs et ont plusieurs solutions, dont certaines peuvent ne pas résoudre le problème plus large.

Je me demande si @rrousselGit pourrait fournir un aperçu ici, ou a des données sur les crochets actuellement les plus utilisés. Je suppose que c'est 80% de flux et d'animations, mais ce serait bien de savoir ce que les gens utilisent le plus.

Je pense que c'est très homogène.

Bien que useStream et les animations soient probablement les moins utilisés :

  • useStream a généralement un meilleur équivalent en fonction de votre architecture. Pourrait utiliser context.watch , useBloc , useProvider , ...
  • peu de gens prennent le temps de faire des animations. C'est rarement la priorité, et TweenAnimationBuilder autres widgets implicitement animés couvrent une grande partie du besoin.
    Peut-être que cela changerait si j'ajoutais mes crochets useImplicitlyAnimatedInt dans flutter_hooks.

@esDotDev vient de supprimer le besoin de clés/identifiants dans l'approche lifecycleMixin. C'est encore un peu gênant dans la syntaxe du constructeur. Mais peut-être que cela pourrait être aidé par la suite aussi. Le seul problème que je rencontre est avec le système de type. Il essaie de présenter les choses de certaines manières qui ne fonctionnent pas. Mais il a probablement juste besoin d'un casting ou d'une maîtrise du système de type. En ce qui concerne le mélange des cycles de vie, je pense que cela pourrait être amélioré en lançant des exceptions raisonnables lorsqu'un état particulier auquel vous essayez d'accéder n'est pas accessible par le cycle de vie de ce widget. Ou une peluche qui, dans un constructeur de cycle de vie, ne devrait accéder qu'au cycle de vie du constructeur.

Merci Remi, cela me surprend, je pense que les gens utiliseraient très fréquemmentAnimation pour piloter la grande collection de widgets Transition dans le noyau, mais je suppose que la plupart des gens utilisent simplement les différents implicites, car ils sont assez agréables à lire et n'ont pas d'imbrication .

Malgré le fait qu'AnimatorController soit très bien servi avec une suite de widgets implicites et explicites, je pense toujours que c'est un excellent exemple d'une "chose qui doit maintenir l'état et être liée aux paramètres et au cycle de vie des widgets". Et sert de parfait petit exemple du problème à résoudre (le fait est qu'il est totalement résolu dans Flutter avec une douzaine de widgets nonobstant), que nous pouvons tous discuter et rester concentrés sur l'architecture et non sur le contenu.

Par exemple, considérez comment, si var anim = AnimationController.use(context, duration: widget.duration ?? _duration); était un citoyen de première classe, pratiquement aucune de ces animations implicites ou explicites n'a vraiment besoin d'exister. Cela les rend redondants car ils sont tous créés pour gérer le problème central : composer facilement un objet avec état (AnimationController) dans le contexte d'un widget. TAB devient presque inutile, puisque vous pouvez faire la même chose avec AnimatedBuilder + AnimatorController.use() .

Cela illustre vraiment la nécessité du cas d'utilisation général si vous regardez l'énorme masse de widgets qui ont surgi autour des animations. Précisément parce qu'il est si fastidieux / sujet aux bugs de réutiliser la logique de configuration / démontage de base, nous avons plus de 15 widgets gérant tous des choses très spécifiques, mais la majorité d'entre eux répètent la même animation passe-partout avec seulement une poignée de lignes de code uniques dans de nombreux cas.

Cela sert à montrer que oui, nous pourrions également faire cette chose pour réutiliser notre propre logique avec état : créer un widget pour chaque permutation d'utilisation. Mais quelle galère et quel casse tête d'entretien ! C'est tellement plus agréable d'avoir un moyen simple de composer de petits objets avec état, avec des crochets de cycle de vie, et si nous voulons créer des widgets dédiés pour le rendu, ou un constructeur réutilisable, nous pouvons facilement les superposer.

Pour ce que ça vaut, j'utilise beaucoup quelque chose comme useAnimation dans mon application plutôt que les widgets d'animation normaux. C'est parce que j'utilise un SpringAnimation qui n'est pas bien pris en charge avec des widgets comme AnimatedContainer par exemple ; ils supposent tous une animation basée sur le temps, avec curve et duration plutôt qu'une animation basée sur la simulation, qui accepterait un argument Simulation .

J'ai fait une abstraction sur useAnimation mais avec des ressorts, alors je l'ai appelée useSpringAnimation . Le widget wrapper avec lequel j'ai utilisé ce crochet est similaire à un AnimatedContainer mais c'était beaucoup plus facile à faire car je pouvais réutiliser tout le code d'animation comme vous le dites @esDotDev , car la logique est la même. Je pouvais même créer ma propre version de tous les widgets animés en utilisant à nouveau useSpringAnimation mais je n'en avais pas nécessairement besoin pour mon projet. Cela montre une fois de plus la puissance de la réutilisation de la logique du cycle de vie fournie par les crochets.

Par exemple, considérez comment, if var anim = AnimationController.use(context, duration: widget.duration ?? _duration); étaient un citoyen de première classe, pratiquement aucune de ces animations implicites ou explicites n'a vraiment besoin d'exister. Cela les rend redondants car ils sont tous créés pour gérer le problème central : composer facilement un objet avec état (AnimationController) dans le contexte d'un widget. TAB devient presque inutile, puisque vous pouvez faire la même chose avec AnimatedBuilder + AnimatorController.use().

En lisant mes commentaires ci-dessus, cela semble être essentiellement ce que j'ai fait avec mon crochet d'animation printanier. J'ai encapsulé la logique, puis j'ai simplement utilisé AnimatedBuilder. Pour les rendre implicites, afin que lorsque je changeais l'accessoire comme on le fait sur AnimatedContainer, il s'anime, j'ai juste ajouté la méthode didUpdateWidget (appelée didUpdateHook dans flutter_hooks ) à exécuter l'animation de l'ancienne valeur à la nouvelle valeur.

Ai-je raison de supposer que les exemples dans https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful sont désormais représentatifs des problèmes que les gens veulent résoudre ?

Je ne suis honnêtement pas sûr.
Je dirais _oui_. Mais cela dépend vraiment de la façon dont vous interpréterez ces exemples.

Dans ce fil, nous avons l'habitude de ne pas nous comprendre, donc je ne peux pas garantir que cela ne se reproduira plus.

C'est en partie pourquoi je n'aime pas utiliser des exemples de code et j'ai suggéré d'extraire un ensemble de règles à la place.
Les exemples sont subjectifs et ont plusieurs solutions, dont certaines peuvent ne pas résoudre le problème plus large.

Je dirais également que nous devrions inclure tous les exemples de code dans ce problème qui ont été discutés, je pense qu'il y a une liste ci-dessus quelque part que @rrousselGit a faite. Je pourrais faire un PR en les ajoutant au référentiel local_state, mais ce ne sont pas tous des exemples de code complets, ils pourraient donc ne pas tous être réellement compilés et exécutés. Mais ils montrent au moins les problèmes potentiels.

Je pourrais faire un PR en les ajoutant au référentiel local_state

Ce serait très utile.

Je voudrais souligner que ce fil n'a pas défini la réutilisation ou à quoi ressemble la réutilisation. Je pense que nous devrions être extrêmement précis dans la définition de cela, de peur que la conversation ne perde de vue.

Nous avons seulement montré ce que la réutilisation _n'est pas_ en ce qui concerne Flutter.

Il y a eu pas mal d'exemples d'utilisation, et les hooks fournissent clairement un exemple total de réutilisation de l'état des widgets. Je ne sais pas d'où vient la confusion car cela semble simple à première vue.

La réutilisation peut être simplement définie comme : _Tout ce qu'un widget de générateur peut faire._

La demande concerne un objet avec état qui peut exister dans n'importe quel widget, qui :

  • Encapsule son propre état
  • Peut se configurer/démonter lui-même en fonction des appels initState/dispose
  • Peut réagir lorsque les dépendances changent dans le widget

Et le fait d'une manière simple et succincte, sans passe-partout, comme :
AnimationController anim = AnimationController.stateful(duration: widget.duration);
Si cela fonctionne dans les widgets Stateless et Stateful. S'il se reconstruit lorsque widget.quelquechose change, s'il peut exécuter ses propres init() et dispose(), alors vous avez essentiellement un gagnant et je suis sûr que tout le monde l'apprécierait.

La principale chose avec laquelle je me bats est de savoir comment le faire de manière efficace. Par exemple, ValueListenableBuilder prend un argument enfant qui peut être utilisé pour améliorer les performances de manière mesurable. Je ne vois pas comment faire cela avec l'approche Propriété.

Je suis presque sûr que ce n'est pas un problème. Nous ferions cela de la même manière que les widgets XTransition fonctionnent maintenant. Si j'ai un état complexe et que je veux qu'il ait un enfant cher, je ferais juste un petit widget d'emballage pour cela. Tout comme nous pourrions faire :
FadeTransition(opacity: anim, child: someChild)

Nous pouvons tout aussi bien le faire avec n'importe quelle chose que nous voulons rendre, en passant la "chose" dans un Widget pour la re-rendre.
MyThingRenderer(value: thing, child: someChild)

  • Cela ne nécessite pas d'imbrication comme le fait le constructeur, mais il le prend en charge en option (.child pourrait être un fxn de build)
  • Il conserve la possibilité d'être utilisé directement sans widget d'emballage
  • Nous pouvons toujours créer un générateur et utiliser cette syntaxe dans le générateur pour le garder plus propre. Il ouvre également la porte à plusieurs types de constructeurs, construits autour du même objet principal, qui n'implique pas de copier-coller de code partout.

D'accord avec @esDotDev. Comme je l'ai mentionné précédemment, un autre titre pour cela serait "Syntaxe du sucre pour les constructeurs".

La principale chose avec laquelle je me bats est de savoir comment le faire de manière efficace. Par exemple, ValueListenableBuilder prend un argument enfant qui peut être utilisé pour améliorer les performances de manière mesurable. Je ne vois pas comment faire cela avec l'approche Propriété.

Je suis presque sûr que ce n'est pas un problème. Nous ferions cela de la même manière que les widgets XTransition fonctionnent maintenant. Si j'ai un état complexe et que je veux qu'il ait un enfant cher, je ferais juste un petit widget d'emballage pour cela. Tout comme nous pourrions faire :

Il n'y a pas besoin de ça.
L'un des avantages de cette fonctionnalité est que nous pouvons avoir une logique d'état qui consiste à "mettre en cache l'instance du widget si ses paramètres n'ont pas changé".

Avec des crochets, cela donnerait useMemo dans React :

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

Avec ce code, myWidget reconstruira _uniquement_ lorsque value change. Même si le widget qui appelle useMemo reconstruit pour d'autres raisons.

C'est similaire à un constructeur const pour les widgets, mais permet des paramètres dynamiques.

Il y a un exemple qui fait cela dans le repo de Tim.

La demande concerne un objet avec état qui peut exister dans n'importe quel widget, qui :

  • Encapsule son propre état
  • Peut se configurer/démonter lui-même en fonction des appels initState/dispose
  • Peut réagir lorsque les dépendances changent dans le widget

Je suppose que j'ai du mal à voir pourquoi, d'après ces paramètres, StatefulWidget ne fait pas le travail mieux qu'il ne le fait. C'est pourquoi j'ai posé la question sur ce que nous recherchons vraiment ici dans une solution. En tant que personne qui utilise flutter_hooks je trouve qu'ils sont plus amusants à travailler que StatefulWidget , mais c'est juste pour éviter la verbosité - pas parce que je pense en termes de crochets. En fait, je trouve difficile de raisonner sur les mises à jour de l'interface utilisateur avec les crochets par rapport aux Widget s.

  • Peut réagir lorsque les dépendances changent dans le widget

Vous voulez dire une dépendance qui a été créée/acquise dans le widget ? Ou une dépendance bien en dessous du widget dans l'arborescence ?

Je ne nie pas qu'il y ait un problème qui provoque la verbosité/la confusion dans Flutter, j'hésite simplement à compter sur le fait que tout le monde a en fait le même modèle mental de ce qu'est la "réutilisation". Je suis très reconnaissant pour l'explication; et quand les gens ont des modèles différents, ils créent des solutions différentes.

Parce que l'utilisation d'un SW pour faire cela est bien pour un cas d'utilisation spécifique, mais pas pour faire abstraction de la logique réutilisable du cas d'utilisation à travers de nombreux SW. Prenons l'exemple de la configuration/démontage de l'animation. Ce n'est pas un logiciel en lui-même, c'est quelque chose que nous voulons utiliser à travers eux. Sans prise en charge de première classe pour le partage de l'état encapsulé, vous devez créer un générateur, par exemple TweenAnimationBuilder, ou créer une tonne de widgets spécifiques, par exemple AnimatedContainer, etc. Vraiment beaucoup plus élégant si vous pouvez simplement regrouper cette logique et la réutiliser comme vous le souhaitez à l'intérieur d'un arbre.

En termes de dépendance au widget, je veux simplement dire que si widget.foo change, la chose avec état a la possibilité de faire toute mise à jour qu'elle doit faire. Dans le cas de stateful AnimationController, il vérifiera si la durée a changé et, si c'est le cas, mettra à jour son instance interne d'AnimatorController. Cela évite à chaque implémenteur de l'animation d'avoir à gérer le changement de propriété.

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

Avec ce code, myWidget reconstruira _uniquement_ lorsque value change. Même si le widget qui appelle useMemo reconstruit pour d'autres raisons.

Ah je vois, Memoized renvoie un Widget lui-même, puis vous passez [valeur] comme déclencheur de reconstruction, chouette !

La clé d'AnimatedOpacity n'est ni le parent ni l'enfant reconstruit. En fait, lorsque vous déclenchez une animation à l'aide d'AnimatedOpacity, rien ne se reconstruit littéralement après la première image où vous déclenchez l'animation. Nous sautons entièrement la phase de construction et faisons tout dans l'objet de rendu (et dans l'arbre de rendu, il ne s'agit que de repeindre, pas de relayer, et en fait, il utilise un calque, donc même la peinture est assez minime). Cela fait une différence significative dans les performances et l'utilisation de la batterie. Quelle que soit la solution que nous proposons ici, elle doit pouvoir maintenir ce type de performances si nous voulons l'intégrer dans le cadre de base.

Malheureusement, je n'ai pas eu le temps de rassembler les exemples de ce numéro dans le dépôt de l'État local, mon mauvais. Je ne pourrai peut-être pas y arriver à court terme, donc si quelqu'un d'autre veut le reprendre, je serais d'accord.

En ce qui concerne les performances d'avoir des crochets définis dans la méthode de construction / rendu (que je pense que quelqu'un a mentionné plus tôt dans ce numéro), j'ai lu la documentation de React et j'ai vu cette FAQ, cela pourrait être utile. Fondamentalement, il demande si les crochets sont lents en raison de la création de fonctions dans chaque rendu, et ils disent non pour plusieurs raisons, dont l'une est de pouvoir mémoriser des fonctions à l'aide d'un crochet comme useMemo ou useCallback .

https://reactjs.org/docs/hooks-faq.html#are -hooks-slow-because-of-creating-functions-in-render

Fondamentalement, il demande si les crochets sont lents en raison de la création de fonctions dans chaque rendu, et ils disent non pour plusieurs raisons, dont l'une est de pouvoir mémoriser des fonctions à l'aide d'un crochet comme useMemo ou useCallback .

Le souci n'est pas le coût de création des fermetures, celles-ci sont en effet relativement bon marché. C'est la différence entre exécuter n'importe quel code et ne pas exécuter du tout de code qui est la clé des performances que Flutter affiche dans les cas optimaux aujourd'hui. Nous avons consacré beaucoup d'efforts à la création d'algorithmes qui évitent littéralement d'exécuter certains chemins de code (par exemple, la phase de construction est entièrement ignorée pour AnimatedOpacity, ou la façon dont nous évitons de parcourir l'arbre pour effectuer des mises à jour mais ciblent plutôt les nœuds affectés).

Je suis d'accord. Je ne connais pas très bien les internes de Flutter ni les internes de crochet, mais vous avez raison de dire que les crochets devront (s'ils ne le font pas déjà) déterminer quand ils doivent fonctionner ou non, et les performances ne doivent pas régresser.

C'est la différence entre exécuter n'importe quel code et ne pas exécuter du tout de code qui est la clé des performances que Flutter affiche dans les cas optimaux aujourd'hui.

Comme mentionné précédemment à quelques reprises, les crochets améliorent cela.
L'exemple animé sur le repo de Tim en est la preuve. La variante hooks se reconstruit moins souvent que la variante StatefulWidget grâce à useMemo

Puisqu'il est question de solutions à ce problème quelque part dans ce fil de discussion, je le qualifie également de proposition.

J'aimerais vraiment voir des crochets incorporés dans le flutter comme cela a été fait avec React. Je regarde l'état en flutter de la même manière que lorsque j'ai utilisé réagir pour la première fois. Depuis que j'utilise des crochets, je n'y retournerais personnellement jamais.

C'est tellement plus lisible OMI. Actuellement, vous devez déclarer deux classes avec un widget avec état plutôt que des crochets où vous venez de déposer dans usestate.

Cela apporterait également une certaine familiarité au flutter que les développeurs n'ont souvent pas lorsqu'ils regardent le code flutter. De toute évidence, comparer flutter avec réagir est une voie dangereuse, mais je pense vraiment que mon expérience de développeur avec les crochets est meilleure que mon expérience sans eux.

Je ne déteste pas flutter d'ailleurs, c'est en fait mon framework préféré mais je pense que c'est une très bonne opportunité d'augmenter la lisibilité et l'expérience de développement.

Je pense qu'il y a certainement une opportunité d'améliorer les conventions de nommage et de les rendre plus fluides.

Des choses comme UseMemoized et UseEffect semblent assez étrangères, et il semble que nous voulions un moyen de ne pas avoir à exécuter le code init() dans le build fxn.

Actuellement, l'initialisation avec des crochets est comme ceci (je pense?):

Widget build(){
   useEffect(
      (){
          // Do init stuff
         return (){  //Do dispose stuff };
      }, [ ] ) //<-- pass an empty list of rebuild triggers, so this can only fire once. Passing null here would let it fire every time.
   );
}

J'apprécie la brièveté de ce code, mais il est certainement loin d'être idéal du point de vue de la lisibilité et du "code auto-documenté". Il y a beaucoup de magie implicite ici. Idéalement, nous avons quelque chose d'explicite à propos de ses hooks init/dispose, et ne se force pas à construire lorsqu'il est utilisé avec un widget sans état.

Des choses comme useMemoized et useEffect pourraient peut-être être mieux nommées plus explicitement hook ComputedValue() et hook SideEffect()

Widget build(BuildContext context){
   List<int> volumes = hook ComputedValue(
        execute: ()=>_getVolumeFromAudioSamples(widget.bytes), 
        dependencies: [ widget.bytes ]);

   hook SideEffect(
       execute: ()=>_recordSongChangedAnalytics()
       dependencies: [ widget.songUrl ]);
   )

   return SongVisualizer(volumes: volumes);
}

J'aime ça, je ne sais pas ce que je pense de l'utilisation du mot-clé hook , et je ne pense pas que cela résolve le problème des concepts étrangers. L'introduction de nouveaux mots clés ne me semble pas la meilleure approche, withSideEffect ou withComputedValue ? Je ne suis pas un concepteur de langage, donc mes mots ne valent rien.

J'ai l'impression que la fonctionnalité de crochet dans Flutter sera d'une grande aide pour lisser la courbe d'apprentissage pour les développeurs React, qui est vraiment le public cible lorsque les entreprises prennent la décision entre ReactNative et Flutter.

Faisant écho à @lemusthelroy , Flutter est de loin mon framework préféré et je suis plus qu'excité de voir les directions qu'il prend. Mais je pense que les concepts de programmation fonctionnelle pourraient être d'une grande aide pour développer le cadre dans une direction encore relativement inexplorée. Je pense que certaines personnes rejettent l'idée dans le but de se distancer de React, ce qui est regrettable, mais compréhensible.

Ya il y a deux côtés à cette pièce je pense. Un nouveau mot-clé est un événement majeur, donc la propagation des connaissances serait très rapide, mais l'autre côté est certainement que c'est maintenant quelque chose de nouveau pour _tout le monde_. Si c'est possible sans c'est cool aussi ! Je ne suis pas sûr que ce soit le cas... du moins pas aussi élégamment.

Opinion: La tendance de la communauté à nommer les crochets comme solution de facto à ce problème provient d'un biais pour les fonctions. Les fonctions sont plus simples à composer que les objets, en particulier dans un langage à typage statique. Je pense que le modèle mental des widgets pour de nombreux développeurs n'est en fait que la méthode build .

Je pense que si vous définissez le problème en termes de base, vous êtes plus susceptible de concevoir une solution qui fonctionne bien dans le reste de la bibliothèque.

Quant au mot-clé hook en termes de base ; on pourrait le considérer à la fois comme déclarant et définissant une fonction à partir d'une sorte de modèle (une macro), et le préfixe hook indique en réalité que la fonction intégrée a un état interne (statistiques de style c. )

Je me demande s'il n'y a pas une sorte d'art antérieur dans Swift FunctionBuilders.

Pendant que nous rêvons, je vais clarifier ma supposition sur ce que serait le code nécessaire :

Hook SideEffect(void Function() execute, List<Object> dependencies) {
  // Whatever happens each build.
}

Widget build(BuildContext context){
   List<int> volumes = hook ComputedValue(
        execute: ()=>_getVolumeFromAudioSamples(widget.bytes), 
        dependencies: [ widget.bytes ]);

   SideEffect(
       execute: ()=>_recordSongChangedAnalytics()
       dependencies: [ widget.songUrl ]);
   )

   return SongVisualizer(volumes: volumes);
}

Hook est un hack au niveau du système de type qui aide à analyser statiquement que le hook résultant a été appelé conformément à ce que les développeurs familiers du hook appellent les lois du hook. Comme ce genre de chose, le type Hook peut être documenté comme quelque chose qui ressemble beaucoup à une fonction, mais a un état mutable interne statique.

Je recule un peu en écrivant ceci parce que c'est tellement étrange du point de vue de la langue. Là encore, Dart est le langage né pour écrire des interfaces utilisateur. Si ce genre d'étrangeté doit exister quelque part, c'est peut-être l'endroit. Mais pas cette bizarrerie en particulier.

Opinion: La tendance de la communauté à nommer les crochets comme solution de facto à ce problème provient d'un biais pour les fonctions. Les fonctions sont plus simples à composer que les objets, en particulier dans un langage à typage statique. Je pense que le modèle mental des widgets pour de nombreux développeurs n'est en fait que la méthode de construction.

Je ne sais pas ce que vous voulez dire avec ça. L'approche hook que j'utilise également avec mon get_it_mixin rend simplement l'arborescence des widgets plus facile à lire que d'utiliser un Builder.

Article intéressant sur les crochets React

@ nt4f04uNd Tous vos points ont été abordés précédemment, y compris les performances, pourquoi il doit s'agir d'une fonctionnalité de base, les widgets de style fonctionnel par rapport à la classe et pourquoi des choses autres que les crochets ne semblent pas fonctionner. Je vous suggère de lire toute la conversation pour comprendre les différents points.

Je vous suggère de lire toute la conversation pour comprendre les différents points.

C'est juste à dire étant donné qu'ils n'ont pas lu tout le fil, mais je ne suis pas sûr que cela rende les choses plus claires de lire le reste du fil. Il y a des gens dont la priorité est de garder les widgets tels qu'ils sont, et un autre groupe qui veut faire autre chose ou rendre les widgets plus modulaires.

Bien que cela puisse être vrai, ce problème montre qu'il existe des problèmes qui ne peuvent pas être résolus avec les widgets tels qu'ils sont actuellement, donc si nous voulons résoudre les problèmes, nous n'avons pas d'autre choix que de faire quelque chose de nouveau. C'est le même concept que d'avoir des Future s et d'introduire plus tard la syntaxe async/await , cette dernière rend possible des choses qui n'étaient tout simplement pas possibles sans une nouvelle syntaxe.

Cependant, les gens suggèrent que nous l'intégrions dans le cadre. React ne peut pas ajouter de nouvelle syntaxe à Javascript car ce n'est pas le seul framework disponible (enfin, il le peut via les transformations Babel), mais Dart est spécifiquement conçu pour fonctionner avec Flutter (Dart 2 au moins, pas la version originale) donc nous avons un beaucoup plus d'une capacité à faire fonctionner les crochets avec le langage sous-jacent. React, par exemple, a besoin de Babel pour JSX, et il doit utiliser un linter pour les erreurs useEffect , alors que nous pourrions en faire une erreur de compilation. Avoir un package rend l'adoption beaucoup plus difficile, car vous pouvez imaginer l'attrait que les crochets React n'auraient pas obtenu s'il s'agissait d'un package tiers.

Il n'y aurait aucun problème s'il pouvait y avoir un troisième type de widget, c'est-à-dire HookWidget, en plus des widgets actuels sans état et avec état. Laissez la communauté décider lequel utiliser. Il existe déjà un package de Remi mais il a forcément des limites. Je l'ai essayé et cela a considérablement réduit le passe-partout, mais j'ai dû le laisser tomber par inadvertance en raison de limitations. Je dois créer des widgets avec état pour n'utiliser que la méthode init. Il pourrait y avoir de grands avantages supplémentaires s'il fait partie du framework de base avec le support du langage. En outre, un HookWidget peut permettre à la communauté de créer des applications plus optimales et plus performantes.

Je dois créer des widgets avec état pour n'utiliser que la méthode init.

Vous n'êtes pas obligé de le faire, useEffect() est capable de faire l'initCall à l'intérieur de la construction. Les docs ne font aucun effort pour expliquer cela, et supposent fondamentalement que vous êtes un développeur React qui sait déjà comment fonctionnent les crochets.

J'utilisais cette méthode mais j'avais d'autres problèmes avec les limitations du package et je ne me souviens pas exactement de quoi il s'agissait.

Cette page vous a été utile?
0 / 5 - 0 notes