Flutter: Die Wiederverwendung von Zustandslogik ist entweder zu ausführlich oder zu schwierig

Erstellt am 2. März 2020  ·  420Kommentare  ·  Quelle: flutter/flutter

.

Bezogen auf die Diskussion um Haken #25280

TL;DR: Es ist schwierig, die State Logik wiederzuverwenden. Am Ende haben wir entweder eine komplexe und tief verschachtelte build Methode oder müssen die Logik über mehrere Widgets kopieren und einfügen.

Es ist weder möglich, eine solche Logik durch Mixins noch Funktionen wiederzuverwenden.

Problem

Die Wiederverwendung einer State Logik über mehrere StatefulWidget ist sehr schwierig, sobald diese Logik auf mehreren Lebenszyklen beruht.

Ein typisches Beispiel wäre die Logik der Erstellung eines TextEditingController (aber auch AnimationController , implizite Animationen und vieles mehr). Diese Logik besteht aus mehreren Schritten:

  • Definieren einer Variablen auf State .
    dart TextEditingController controller;
  • Erstellen des Controllers (normalerweise innerhalb von initState), mit möglicherweise einem Standardwert:
    dart <strong i="25">@override</strong> void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • hat den Controller verworfen, wenn State verworfen wird:
    dart <strong i="30">@override</strong> void dispose() { controller.dispose(); super.dispose(); }
  • mit dieser Variablen in build tun, was immer wir wollen.
  • (optional) Machen Sie diese Eigenschaft auf debugFillProperties :
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

Dies ist an sich nicht komplex. Das Problem beginnt, wenn wir diesen Ansatz skalieren wollen.
Eine typische Flutter-App kann Dutzende von Textfeldern haben, was bedeutet, dass diese Logik mehrmals dupliziert wird.

Das Kopieren und Einfügen dieser Logik überall "funktioniert", erzeugt jedoch eine Schwachstelle in unserem Code:

  • Es kann leicht sein, dass man vergisst, einen der Schritte neu zu schreiben (wie etwa das Vergessen, dispose anzurufen).
  • es fügt dem Code viel Rauschen hinzu

Das Mixin-Problem

Der erste Versuch, diese Logik zu faktorisieren, wäre die Verwendung eines Mixins:

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));
  }
}

Dann so verwendet:

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,
    );
  }
}

Aber das hat verschiedene Mängel:

  • Ein Mixin kann nur einmal pro Klasse verwendet werden. Wenn unser StatefulWidget mehrere TextEditingController , können wir den Mixin-Ansatz nicht mehr verwenden.

  • Der vom Mixin deklarierte "Zustand" kann mit einem anderen Mixin oder dem State selbst kollidieren.
    Genauer gesagt, wenn zwei Mixins ein Mitglied mit demselben Namen deklarieren, kommt es zu einem Konflikt.
    Im schlimmsten Fall schlägt dies stillschweigend fehl, wenn die in Konflikt stehenden Member denselben Typ haben.

Dies macht Mixins sowohl unideal als auch zu gefährlich, um eine echte Lösung zu sein.

Verwenden des "Builder"-Musters

Eine andere Lösung könnte sein, das gleiche Muster wie StreamBuilder & co zu verwenden.

Wir können ein TextEditingControllerBuilder Widget erstellen, das diesen Controller verwaltet. Dann kann unsere Methode build sie frei verwenden.

Ein solches Widget würde normalerweise so implementiert:

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);
  }
}

Dann so verwendet:

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

Dies löst die Probleme, die bei Mixins auftreten. Aber es schafft andere Probleme.

  • Die Verwendung ist sehr ausführlich. Das sind effektiv 4 Codezeilen + zwei Einrückungsebenen für eine einzelne Variablendeklaration.
    Dies ist noch schlimmer, wenn wir es mehrmals verwenden möchten. Während wir einmal ein TextEditingControllerBuilder in einem anderen erstellen können, verringert dies die Lesbarkeit des Codes drastisch:

    <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),
              ],
            );
          },
        );
      },
    );
    }
    

    Das ist ein sehr eingerückter Code, nur um zwei Variablen zu deklarieren.

  • Dies führt zu zusätzlichem Overhead, da wir eine zusätzliche Instanz von State und Element .

  • Es ist schwierig, das TextEditingController außerhalb von build .
    Wenn wir wollen, dass ein State Lebenszyklus eine Operation auf diesen Controllern ausführt, dann benötigen wir einen GlobalKey um auf sie zuzugreifen. Zum Beispiel:

    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

Hilfreichster Kommentar

Ich werde ein paar Gedanken aus der React-Perspektive hinzufügen.
Verzeihen Sie, wenn sie nicht relevant sind, aber ich wollte kurz erklären, wie wir über Hooks denken.

Haken sind definitiv "versteckende" Dinge. Oder kapseln Sie sie, je nachdem, wie Sie es betrachten. Insbesondere kapseln sie den lokalen Zustand und die Auswirkungen (ich denke, unsere "Effekte" sind die gleichen Dinge wie "Wegwerfartikel"). Die "Implizitheit" besteht darin, dass sie die Lebensdauer automatisch an die Komponente anhängen, in der sie aufgerufen werden.

Diese Implizitheit ist dem Modell nicht inhärent. Sie können sich vorstellen, dass ein Argument explizit durch alle Aufrufe gefädelt wird – von der Komponente selbst über benutzerdefinierte Hooks bis hin zu jedem primitiven Hook. Aber in der Praxis haben wir festgestellt, dass das laut und nicht wirklich nützlich ist. Also haben wir die derzeit ausgeführte Komponente zum impliziten globalen Zustand gemacht. Dies ist vergleichbar damit, wie throw in einer VM nach oben nach dem nächstgelegenen catch Block sucht, anstatt dass Sie errorHandlerFrame im Code herumreichen.

Okay, es sind also Funktionen mit impliziten versteckten Zuständen darin, das scheint schlecht zu sein? Aber in React sind Komponenten im Allgemeinen so. Das ist der springende Punkt von Components. Es sind Funktionen, denen eine Lebensdauer zugeordnet ist (die einer Position im UI-Baum entspricht). Der Grund, warum Komponenten selbst keine Waffe in Bezug auf den Zustand sind, ist, dass Sie sie nicht einfach aus zufälligem Code aufrufen. Sie rufen sie von anderen Komponenten auf. Ihre Lebensdauer ist also sinnvoll, da Sie im Kontext des UI-Codes bleiben.

Jedoch sind nicht alle Probleme komponentenförmig. Komponenten kombinieren zwei Fähigkeiten: Zustand+Effekte und eine an die Baumposition gebundene Lebenszeit. Aber wir haben festgestellt, dass die erste Fähigkeit allein schon nützlich ist. So wie Funktionen im Allgemeinen nützlich sind, weil sie Code kapseln können, fehlte uns ein Primitiv, mit dem wir Zustands+Effekt-Bündel kapseln (und wiederverwenden) können, ohne unbedingt einen neuen Knoten im Baum zu erstellen. Das sind Hooks. Komponenten = Hooks + zurückgegebene Benutzeroberfläche.

Wie ich bereits erwähnt habe, ist eine willkürliche Funktion, die den Kontextstatus verbirgt, beängstigend. Aus diesem Grund setzen wir eine Konvention über einen Linter durch. Hooks haben "Farbe" — wenn Sie einen Hook verwenden, ist Ihre Funktion auch ein Hook. Und der Linter erzwingt, dass nur Komponenten oder andere Hooks Hooks verwenden dürfen. Dadurch wird das Problem beseitigt, dass beliebige Funktionen den kontextabhängigen UI-Zustand verbergen, da sie jetzt nicht mehr implizit sind als die Komponenten selbst.

Vom Konzept her betrachten wir Hook-Aufrufe nicht als einfache Funktionsaufrufe. Wie useState() ist mehr use State() wenn wir die Syntax hätten. Es wäre eine Sprachfunktion. Sie können so etwas wie Hooks mit algebraischen Effekten in Sprachen mit wären sie reguläre Funktionen, aber die Tatsache, dass sie State "verwenden", wäre ein Teil ihrer Typsignatur. Dann können Sie sich React selbst als "Handler" für diesen Effekt vorstellen. Wie auch immer, dies ist sehr theoretisch, aber ich wollte auf den Stand der Technik in Bezug auf das Programmiermodell hinweisen.

Praktisch gesehen gibt es hier einiges. Zunächst ist es erwähnenswert, dass Hooks keine "zusätzliche" API für React sind. Sie sind an dieser Stelle die React-API zum Schreiben von Komponenten. Ich denke, ich würde zustimmen, dass sie als zusätzliches Feature nicht sehr überzeugend wären. Ich weiß also nicht, ob sie für Flutter wirklich Sinn machen, das ein wohl anderes Gesamtparadigma hat.

Was sie zulassen, ist meiner Meinung nach die Hauptfunktion, die zustands- und effektvolle Logik zu kapseln und sie dann wie bei der regulären Funktionskomposition miteinander zu verketten. Da die Primitive zum Komponieren entwickelt wurden, können Sie eine Hook-Ausgabe wie useState() , sie als Eingabe an ein benutzerdefiniertes useGesture(state) und diese dann als Eingabe an mehrere benutzerdefinierte useSpring(gesture) Aufrufe, die Ihnen gestaffelte Werte liefern, und so weiter. Jedes dieser Stücke ist sich der anderen völlig nicht bewusst und kann von verschiedenen Leuten geschrieben werden, aber sie komponieren gut zusammen, weil Zustand und Effekte gekapselt sind und an die umgebende Komponente "angehängt" werden. Hier ist eine kleine Demo von so etwas und ein Artikel, in dem ich kurz zusammenfasse, was Hooks sind.

Ich möchte betonen, dass es hier nicht um die Reduzierung des Boilerplates geht, sondern um die Fähigkeit, Pipelines zustandsbehafteter gekapselter Logik dynamisch zusammenzustellen. Beachten Sie, dass es vollständig reaktiv ist – dh es wird nicht einmal ausgeführt, sondern reagiert im Laufe der Zeit auf alle Änderungen der Eigenschaften. Man kann sie sich vorstellen, dass sie wie Plugins in einer Audiosignal-Pipeline sind. Während ich in der Praxis die Vorsicht bei "Funktionen mit Erinnerungen" völlig verstehe, haben wir dies nicht als Problem empfunden, da sie vollständig isoliert sind. Tatsächlich ist diese Isolation ihr Hauptmerkmal. Es würde sonst auseinanderfallen. Jede Co-Abhängigkeit muss also explizit ausgedrückt werden, indem Werte zurückgegeben und an das nächste Ding in der Kette übergeben werden. Und die Tatsache, dass jeder benutzerdefinierte Hook Status oder Effekte hinzufügen oder entfernen kann, ohne seine Verbraucher zu beeinträchtigen (oder sogar zu beeinträchtigen), ist ein weiteres wichtiges Merkmal aus der Sicht der Bibliotheken von Drittanbietern.

Ich weiß nicht, ob dies überhaupt hilfreich war, hoffe aber, dass es etwas Perspektive auf das Programmiermodell wirft.
Beantworte gerne weitere Fragen.

Alle 420 Kommentare

cc @dnfield @Hixie
Wie gewünscht, das sind die vollständigen Details zu den Problemen, die von Hooks gelöst werden.

Ich befürchte, dass jeder Versuch, dies innerhalb des Frameworks zu vereinfachen, tatsächlich die Komplexität verbergen wird, über die Benutzer nachdenken sollten.

Es scheint, dass einiges davon für Bibliotheksautoren verbessert werden könnte, wenn wir Klassen, die mit einer Art von abstract class Disposable entsorgt werden müssen, stark typisieren. In einem solchen Fall sollten Sie leichter eine einfachere Klasse wie diese schreiben können, wenn Sie dazu neigen:

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();
  }
}

Was einige wiederholte Codezeilen beseitigt. Sie könnten eine ähnliche abstrakte Klasse für Debugeigenschaften schreiben, und sogar eine, die beides kombiniert. Ihr Init-Zustand könnte am Ende etwa so aussehen:

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

Fehlen uns nur solche Tippinformationen für Einwegklassen?

Ich befürchte, dass jeder Versuch, dies innerhalb des Frameworks zu vereinfachen, tatsächlich die Komplexität verbergen wird, über die Benutzer nachdenken sollten.

Widgets verbergen die Komplexität, über die Benutzer nachdenken müssen.
Ich bin mir nicht sicher, ob das wirklich ein Problem ist.

Am Ende liegt es an den Benutzern, sie nach Belieben zu faktorisieren.


Das Problem betrifft nicht nur Einwegartikel.

Dadurch wird der Update-Teil des Problems vergessen. Die Stufenlogik könnte sich auch auf Lebenszyklen wie didChangeDependencies und didUpdateWidget stützen.

Einige konkrete Beispiele:

  • SingleTickerProviderStateMixin mit einer Logik in didChangeDependencies .
  • AutomaticKeepAliveClientMixin, das auf super.build(context)

Es gibt viele Beispiele im Framework, in denen wir die Zustandslogik wiederverwenden möchten:

  • StreamBuilder
  • TweenAnimationBuilder
    ...

Dies sind nichts anderes als eine Möglichkeit, den Zustand mit einem Aktualisierungsmechanismus wiederzuverwenden.

Aber sie leiden unter dem gleichen Problem wie die im Abschnitt "Builder" genannten.

Das verursacht viele Probleme.
Eines der häufigsten Probleme bei Stackoverflow ist beispielsweise, dass Leute versuchen, StreamBuilder für Nebenwirkungen zu verwenden, wie zum Beispiel "eine Route bei Änderung verschieben".

Und letztendlich besteht ihre einzige Lösung darin, StreamBuilder "auszuwerfen".
Das beinhaltet:

  • Konvertieren des Widgets in zustandsbehaftet
  • Hören Sie den Stream manuell in initState+didUpdateWidget+didChangeDependencies
  • das vorherige Abonnement von didChangeDependencies/didUpdateWidget kündigen, wenn sich der Stream ändert
  • Abonnement bei Entsorgung kündigen

Das ist _eine Menge Arbeit_, und es ist praktisch nicht wiederverwendbar.

Problem

Die Wiederverwendung einer State Logik über mehrere StatefulWidget ist sehr schwierig, sobald diese Logik auf mehreren Lebenszyklen beruht.

Ein typisches Beispiel wäre die Logik der Erstellung eines TextEditingController (aber auch AnimationController , implizite Animationen und vieles mehr). Diese Logik besteht aus mehreren Schritten:

  • Definieren einer Variable auf State .
    dart TextEditingController controller;
  • Erstellen des Controllers (normalerweise innerhalb von initState), mit möglicherweise einem Standardwert:
    dart <strong i="19">@override</strong> void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • hat den Controller verworfen, wenn State verworfen wird:
    dart <strong i="24">@override</strong> void dispose() { controller.dispose(); super.dispose(); }
  • mit dieser Variablen in build tun, was immer wir wollen.
  • (optional) Machen Sie diese Eigenschaft auf debugFillProperties :
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

Dies ist an sich nicht komplex. Das Problem beginnt, wenn wir diesen Ansatz skalieren wollen.
Eine typische Flutter-App kann Dutzende von Textfeldern haben, was bedeutet, dass diese Logik mehrmals dupliziert wird.

Das Kopieren und Einfügen dieser Logik überall "funktioniert", erzeugt jedoch eine Schwachstelle in unserem Code:

  • Es kann leicht sein, dass man vergisst, einen der Schritte neu zu schreiben (wie etwa das Vergessen, dispose anzurufen).
  • es fügt dem Code viel Rauschen hinzu

Ich habe wirklich Schwierigkeiten zu verstehen, warum das ein Problem ist. Ich habe viele Flutter-Anwendungen geschrieben, aber es scheint wirklich kein so großes Problem zu sein? Selbst im schlimmsten Fall sind es vier Zeilen, um eine Eigenschaft zu deklarieren, zu initialisieren, zu entsorgen und an die Debug-Daten zu melden (und tatsächlich sind es normalerweise weniger, da Sie sie normalerweise in derselben Zeile deklarieren können, in der Sie sie initialisieren, Apps im Allgemeinen müssen Sie sich nicht darum kümmern, den Debug-Eigenschaften einen Status hinzuzufügen, und viele dieser Objekte haben keinen Status, der verworfen werden muss).

Ich stimme zu, dass ein Mixin pro Eigenschaftstyp nicht funktioniert. Ich stimme zu, dass das Builder-Muster nicht gut ist (es verwendet buchstäblich die gleiche Anzahl von Zeilen wie das oben beschriebene Worst-Case-Szenario).

Mit NNBD (insbesondere mit late final damit Initialisierer auf this verweisen können) können wir Folgendes tun:

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));
  }
}

Sie würden es so verwenden:

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),
      ),
    );
  }
}

Scheint die Sache nicht wirklich besser zu machen. Es sind immer noch vier Zeilen.

Widgets verbergen die Komplexität, über die Benutzer nachdenken müssen.

Was verbergen sie?

Das Problem ist nicht die Anzahl der Zeilen, sondern was diese Zeilen sind.

StreamBuilder kann ungefähr so ​​viele Zeilen haben wie stream.listen + setState + subscription.close .
Aber das Schreiben eines StreamBuilder kann sozusagen ohne jede Überlegung erfolgen.
Dabei ist kein Fehler möglich. Es ist nur "den Stream übergeben und Widgets daraus erstellen".

Während das manuelle Schreiben des Codes viel mehr Gedanken erfordert:

  • Kann sich der Stream im Laufe der Zeit ändern? Wenn wir das vergessen haben, haben wir einen Fehler.
  • Haben wir vergessen, das Abonnement zu schließen? Noch ein Fehler
  • Welchen Variablennamen verwende ich für das Abonnement? Dieser Name ist möglicherweise nicht verfügbar
  • Was ist mit Testen? Muss ich den Test duplizieren? Mit StreamBuilder müssen keine Unit-Tests zum Abhören des Streams geschrieben werden, das wäre überflüssig. Aber wenn wir es die ganze Zeit manuell schreiben, ist es durchaus möglich, einen Fehler zu machen
  • Wenn wir zwei Streams gleichzeitig abhören, haben wir jetzt mehrere Variablen mit sehr ähnlichen Namen, die unseren Code verschmutzen, was zu Verwirrung führen kann.

Was verbergen sie?

  • FutureBuilder/StreamBuilder verbirgt den Abhörmechanismus und verfolgt den aktuellen Snapshot.
    Die Logik des Umschaltens zwischen zwei Future ist auch ziemlich komplex, wenn man bedenkt, dass es kein subscription.close() .
  • AnimatedContainer verbirgt die Logik zum Erstellen eines Tweens zwischen den vorherigen und neuen Werten.
  • Listview verbirgt die Logik des "Einhängen eines Widgets, wie es erscheint"

Apps müssen sich im Allgemeinen nicht darum kümmern, den Debug-Eigenschaften einen Status hinzuzufügen

Sie tun es nicht, weil sie sich nicht mit der Komplexität der Wartung der debugFillProperties-Methode befassen möchten.
Aber wenn wir den Entwicklern sagten: "Möchten Sie, dass alle Ihre Parameter und Zustandseigenschaften auf Flutters Devtool verfügbar sind?" Ich bin sicher, sie würden ja sagen

Viele Leute haben mir gegenüber ihren Wunsch nach einem echten Äquivalent zu Reacts Devtool geäußert. Flutters Devtool ist noch nicht da.
In React können wir den gesamten Status eines Widgets + seine Parameter sehen und es bearbeiten, ohne etwas zu tun.

Ebenso waren die Leute ziemlich überrascht, als ich ihnen sagte, dass bei Verwendung von provider + einigen anderen Paketen von mir standardmäßig ihr gesamter Anwendungsstatus für sie sichtbar ist, ohne etwas tun zu müssen ( modulo dieser nervige Devtool-Bug ).

Ich muss zugeben, dass ich kein großer Fan von FutureBuilder bin, es verursacht viele Bugs, weil die Leute nicht darüber nachdenken, wann sie die Future auslösen sollen. Ich denke, es wäre nicht unvernünftig, die Unterstützung dafür einzustellen. StreamBuilder ist in Ordnung, denke ich, aber dann denke ich, dass Streams selbst zu kompliziert sind (wie Sie in Ihrem Kommentar oben erwähnen), also ...

Warum muss jemand über die Komplexität der Erstellung von Tweens nachdenken?

ListView verbirgt die Logik des Mountens eines Widgets nicht wirklich, wie es erscheint; es ist ein großer Teil der API.

Das Problem ist nicht die Anzahl der Zeilen, sondern was diese Zeilen sind.

Ich verstehe die Sorge hier wirklich nicht. Die Zeilen wirken wie eine einfache Boilerplate. Ding deklarieren, Ding initialisieren, Ding entsorgen. Wenn es nicht an der Anzahl der Zeilen liegt, was ist dann das Problem?

Ich stimme Ihnen zu, dass FutureBuilder problematisch ist.

Es ist ein bisschen off-topic, aber ich würde vorschlagen, dass Flutter in der Entwicklung alle paar Sekunden einen gefälschten Hot-Reload auslösen sollte. Dies würde Missbrauch von FutureBuilder, Schlüsseln und vielem mehr aufzeigen.

Warum muss jemand über die Komplexität der Erstellung von Tweens nachdenken?

ListView verbirgt die Logik des Mountens eines Widgets nicht wirklich, wie es erscheint; es ist ein großer Teil der API.

Darin sind wir uns einig. Mein Punkt war, dass wir so etwas wie Hooks nicht mit "es verbirgt Logik" kritisieren können, da das, was Hooks tun, genau dem entspricht, was ein TweenAnimationBuilder / AnimatedContainer /... tut.
Die Logik ist nicht versteckt

Letztlich sind Animationen meiner Meinung nach ein guter Vergleich. Animationen haben dieses Konzept von implizit vs. explizit.
Implizite Animationen werden wegen ihrer Einfachheit, Zusammensetzbarkeit und Lesbarkeit geliebt.
Explizite Animationen sind flexibler, aber komplexer.

Wenn wir dieses Konzept auf das Hören von Streams übertragen, ist StreamBuilder ein _implizites Hören_, während stream.listen _explizit_ ist.

Genauer gesagt, mit StreamBuilder Sie _nicht_ vergessen, das Szenario zu bearbeiten, in dem sich der Stream ändert, oder vergessen, das Abonnement zu schließen.
Sie können auch mehrere StreamBuilder miteinander kombinieren

stream.listen ist etwas fortgeschrittener und fehleranfälliger.

Builder sind mächtig, um die Anwendung zu vereinfachen.
Aber wie wir vorher vereinbart haben, ist das Builder-Muster nicht ideal. Es ist sowohl ausführlich zu schreiben als auch zu verwenden.
Bei diesem Problem und was Hooks lösen, geht es um eine alternative Syntax für *Builders

flutter_hooks hat beispielsweise ein striktes Äquivalent zu FutureBuilder und StreamBuilder :

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

In der Fortsetzung könnte AnimatedContainer & alike durch ein useAnimatedSize / useAnimatedDecoractedBox / ... dargestellt werden, so dass wir:

double opacity;

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

Mein Punkt war, dass wir so etwas wie Hooks nicht mit "es verbirgt Logik" kritisieren können.

Das ist nicht das Argument. Das Argument ist "es verbirgt Logik , über die Entwickler nachdenken sollten ".

Haben Sie ein Beispiel für eine solche Logik, über das die Entwickler nachdenken sollten?

Zum Beispiel, wem gehört der TextEditingController (wer erstellt ihn, wer entsorgt ihn).

Wie mit diesem Code?

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

Der Haken erzeugt es und entsorgt es.

Ich bin mir nicht sicher, was daran unklar ist.

Ja genau. Ich habe keine Ahnung, wie der Lebenszyklus des Controllers mit diesem Code ist. Dauert es bis zum Ende des lexikalischen Geltungsbereichs? Die Lebenszeit des Staates? Etwas anderes? Wem gehört es? Wenn ich es an jemand anderen weitergebe, können diese dann das Eigentum übernehmen? Nichts davon ist im Code selbst offensichtlich.

Es sieht so aus, als ob Ihr Argument eher durch mangelndes Verständnis dafür, was Hooks tun, als durch ein echtes Problem verursacht wird.
Diese Fragen haben eine klar definierte Antwort, die mit allen Hooks übereinstimmt:

Ich habe keine Ahnung, wie der Lebenszyklus des Controllers mit diesem Code aussieht

Da muss man sich auch keine Gedanken machen. Es liegt nicht mehr in der Verantwortung des Entwicklers.

Dauert es bis zum Ende des lexikalischen Geltungsbereichs? Die Lebenszeit des Staates

Die Lebenszeit des Staates

Wem gehört es?

Der Hook besitzt den Controller. Es ist Teil der API von useTextEditingController , der der Controller gehört.
Dies gilt für useFocusNode , useScrollController , useAnimationController , ...

In gewisser Weise treffen diese Fragen auf StreamBuilder :

  • Wir müssen nicht über die Lebenszyklen des StreamAbonnements nachdenken
  • Das Abonnement gilt für die Lebenszeit des Staates
  • der StreamBuilder besitzt das StreamSubscription

Im Allgemeinen können Sie sich Folgendes vorstellen:

final value = useX(argument);

als striktes Äquivalent zu:

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

  },
);

Sie haben die gleichen Regeln und das gleiche Verhalten.

Es liegt nicht mehr in der Verantwortung des Entwicklers

Ich denke, das ist im Grunde die Meinungsverschiedenheit. Eine funktionsähnliche API zu haben, die einen Wert zurückgibt, der eine definierte Lebensdauer hat, die nicht klar ist, ist IMHO grundlegend anders als eine API, die auf der Übergabe dieses Werts an eine Closure basiert.

Ich habe kein Problem damit, dass jemand ein Paket erstellt, das diesen Stil verwendet, aber es ist ein Stil, der der Art widerspricht, die ich in die Kern-Flatter-API aufnehmen möchte.

@Hixie
Ich glaube nicht, dass @rrousselGit gesagt hat, dass sie dasselbe sind, sondern nur, dass sie "die gleichen Regeln und das gleiche Verhalten" in Bezug auf den Lebenszyklus haben? Richtig?

Sie lösen jedoch nicht die gleichen Probleme.

Vielleicht liege ich hier falsch, aber beim Ausprobieren von Flattern im letzten Herbst glaube ich, dass, wenn ich drei dieser Builder in einem Widget benötigt hätte, es viel Verschachtelung gewesen wäre. Im Vergleich zu drei Haken (drei Linien).
Ebenfalls. Hooks sind zusammensetzbar. Wenn Sie also eine aus mehreren Hooks bestehende Zustandslogik teilen müssen, können Sie einen neuen Hook erstellen, der andere Hooks und etwas zusätzliche Logik verwendet, und einfach den einen neuen Hook verwenden.

Solche Dinge wie das einfache Teilen von Zustandslogik zwischen Widgets waren etwas, das ich beim Ausprobieren von Flatter im Herbst 2019 vermisste.

Es könnte natürlich noch viele andere mögliche Lösungen geben. Vielleicht wurde es schon gelöst und ich habe es nur in den Dokumenten nicht gefunden.
Aber wenn nicht, könnte viel getan werden, um die Entwicklung erheblich zu beschleunigen, wenn Dinge wie Hooks oder eine andere Lösung für die gleichen Probleme als Bürger erster Klasse verfügbar wären.

Ich schlage definitiv nicht vor, den Builder-Ansatz zu verwenden, wie das OP erwähnt, der alle Arten von Problemen hat. Ich würde vorschlagen, nur initState/dispose zu verwenden. Ich verstehe nicht wirklich, warum das ein Problem ist.

Ich bin gespannt, wie die Leute über den Code in https://github.com/flutter/flutter/issues/51752#issuecomment -664787791 denken. Ich denke nicht, dass es besser ist als initState/dispose, aber wenn Leute Hooks mögen, mögen sie das auch? Ist Haken besser? Schlechter?

@Hixie Hooks sind gut zu verwenden, da sie den Lebenszyklus in einen einzigen Funktionsaufruf unterteilen. Wenn ich einen Hook verwende, sagen wir useAnimationController , muss ich nicht mehr über initState und disponieren nachdenken. Es entzieht dem Entwickler die Verantwortung. Ich muss mir keine Sorgen machen, ob ich jeden einzelnen von mir erstellten Animationscontroller entsorgt habe.

initState und dispose sind für eine einzelne Sache in Ordnung, aber stellen Sie sich vor, Sie müssen mehrere und unterschiedliche Zustandstypen im Auge behalten. Hooks komponieren basierend auf der logischen Einheit der Abstraktion, anstatt sie im Lebenszyklus der Klasse zu verteilen.

Ich denke, Ihre Frage ist gleichbedeutend mit der Frage, warum Funktionen vorhanden sind, wenn wir uns jedes Mal manuell um die Effekte kümmern können. Ich stimme zu, dass es nicht genau dasselbe ist, aber es fühlt sich im Großen und Ganzen ähnlich an. Es scheint , dass Sie nicht Haken verwendet haben , so dass die Probleme auf Sie nicht zu offensichtlich erscheinen, so dass ich Sie ermutigt , würde ein kleines oder mittleres Größe Projekt mit Haken zu tun, mit dem flutter_hooks vielleicht verpacken und sehen wie es sich anfühlt. Ich sage dies mit allem Respekt, als Benutzer von Flutter bin ich auf diese Probleme gestoßen, für die Hooks Lösungen bieten, wie auch für andere. Ich bin mir nicht sicher, wie ich Sie davon überzeugen kann, dass diese Probleme wirklich für uns existieren. Lassen Sie uns wissen, ob es einen besseren Weg gibt.

Ich werde ein paar Gedanken aus der React-Perspektive hinzufügen.
Verzeihen Sie, wenn sie nicht relevant sind, aber ich wollte kurz erklären, wie wir über Hooks denken.

Haken sind definitiv "versteckende" Dinge. Oder kapseln Sie sie, je nachdem, wie Sie es betrachten. Insbesondere kapseln sie den lokalen Zustand und die Auswirkungen (ich denke, unsere "Effekte" sind die gleichen Dinge wie "Wegwerfartikel"). Die "Implizitheit" besteht darin, dass sie die Lebensdauer automatisch an die Komponente anhängen, in der sie aufgerufen werden.

Diese Implizitheit ist dem Modell nicht inhärent. Sie können sich vorstellen, dass ein Argument explizit durch alle Aufrufe gefädelt wird – von der Komponente selbst über benutzerdefinierte Hooks bis hin zu jedem primitiven Hook. Aber in der Praxis haben wir festgestellt, dass das laut und nicht wirklich nützlich ist. Also haben wir die derzeit ausgeführte Komponente zum impliziten globalen Zustand gemacht. Dies ist vergleichbar damit, wie throw in einer VM nach oben nach dem nächstgelegenen catch Block sucht, anstatt dass Sie errorHandlerFrame im Code herumreichen.

Okay, es sind also Funktionen mit impliziten versteckten Zuständen darin, das scheint schlecht zu sein? Aber in React sind Komponenten im Allgemeinen so. Das ist der springende Punkt von Components. Es sind Funktionen, denen eine Lebensdauer zugeordnet ist (die einer Position im UI-Baum entspricht). Der Grund, warum Komponenten selbst keine Waffe in Bezug auf den Zustand sind, ist, dass Sie sie nicht einfach aus zufälligem Code aufrufen. Sie rufen sie von anderen Komponenten auf. Ihre Lebensdauer ist also sinnvoll, da Sie im Kontext des UI-Codes bleiben.

Jedoch sind nicht alle Probleme komponentenförmig. Komponenten kombinieren zwei Fähigkeiten: Zustand+Effekte und eine an die Baumposition gebundene Lebenszeit. Aber wir haben festgestellt, dass die erste Fähigkeit allein schon nützlich ist. So wie Funktionen im Allgemeinen nützlich sind, weil sie Code kapseln können, fehlte uns ein Primitiv, mit dem wir Zustands+Effekt-Bündel kapseln (und wiederverwenden) können, ohne unbedingt einen neuen Knoten im Baum zu erstellen. Das sind Hooks. Komponenten = Hooks + zurückgegebene Benutzeroberfläche.

Wie ich bereits erwähnt habe, ist eine willkürliche Funktion, die den Kontextstatus verbirgt, beängstigend. Aus diesem Grund setzen wir eine Konvention über einen Linter durch. Hooks haben "Farbe" — wenn Sie einen Hook verwenden, ist Ihre Funktion auch ein Hook. Und der Linter erzwingt, dass nur Komponenten oder andere Hooks Hooks verwenden dürfen. Dadurch wird das Problem beseitigt, dass beliebige Funktionen den kontextabhängigen UI-Zustand verbergen, da sie jetzt nicht mehr implizit sind als die Komponenten selbst.

Vom Konzept her betrachten wir Hook-Aufrufe nicht als einfache Funktionsaufrufe. Wie useState() ist mehr use State() wenn wir die Syntax hätten. Es wäre eine Sprachfunktion. Sie können so etwas wie Hooks mit algebraischen Effekten in Sprachen mit wären sie reguläre Funktionen, aber die Tatsache, dass sie State "verwenden", wäre ein Teil ihrer Typsignatur. Dann können Sie sich React selbst als "Handler" für diesen Effekt vorstellen. Wie auch immer, dies ist sehr theoretisch, aber ich wollte auf den Stand der Technik in Bezug auf das Programmiermodell hinweisen.

Praktisch gesehen gibt es hier einiges. Zunächst ist es erwähnenswert, dass Hooks keine "zusätzliche" API für React sind. Sie sind an dieser Stelle die React-API zum Schreiben von Komponenten. Ich denke, ich würde zustimmen, dass sie als zusätzliches Feature nicht sehr überzeugend wären. Ich weiß also nicht, ob sie für Flutter wirklich Sinn machen, das ein wohl anderes Gesamtparadigma hat.

Was sie zulassen, ist meiner Meinung nach die Hauptfunktion, die zustands- und effektvolle Logik zu kapseln und sie dann wie bei der regulären Funktionskomposition miteinander zu verketten. Da die Primitive zum Komponieren entwickelt wurden, können Sie eine Hook-Ausgabe wie useState() , sie als Eingabe an ein benutzerdefiniertes useGesture(state) und diese dann als Eingabe an mehrere benutzerdefinierte useSpring(gesture) Aufrufe, die Ihnen gestaffelte Werte liefern, und so weiter. Jedes dieser Stücke ist sich der anderen völlig nicht bewusst und kann von verschiedenen Leuten geschrieben werden, aber sie komponieren gut zusammen, weil Zustand und Effekte gekapselt sind und an die umgebende Komponente "angehängt" werden. Hier ist eine kleine Demo von so etwas und ein Artikel, in dem ich kurz zusammenfasse, was Hooks sind.

Ich möchte betonen, dass es hier nicht um die Reduzierung des Boilerplates geht, sondern um die Fähigkeit, Pipelines zustandsbehafteter gekapselter Logik dynamisch zusammenzustellen. Beachten Sie, dass es vollständig reaktiv ist – dh es wird nicht einmal ausgeführt, sondern reagiert im Laufe der Zeit auf alle Änderungen der Eigenschaften. Man kann sie sich vorstellen, dass sie wie Plugins in einer Audiosignal-Pipeline sind. Während ich in der Praxis die Vorsicht bei "Funktionen mit Erinnerungen" völlig verstehe, haben wir dies nicht als Problem empfunden, da sie vollständig isoliert sind. Tatsächlich ist diese Isolation ihr Hauptmerkmal. Es würde sonst auseinanderfallen. Jede Co-Abhängigkeit muss also explizit ausgedrückt werden, indem Werte zurückgegeben und an das nächste Ding in der Kette übergeben werden. Und die Tatsache, dass jeder benutzerdefinierte Hook Status oder Effekte hinzufügen oder entfernen kann, ohne seine Verbraucher zu beeinträchtigen (oder sogar zu beeinträchtigen), ist ein weiteres wichtiges Merkmal aus der Sicht der Bibliotheken von Drittanbietern.

Ich weiß nicht, ob dies überhaupt hilfreich war, hoffe aber, dass es etwas Perspektive auf das Programmiermodell wirft.
Beantworte gerne weitere Fragen.

Ich schlage definitiv nicht vor, den Builder-Ansatz zu verwenden, wie das OP erwähnt, der alle Arten von Problemen hat. Ich würde vorschlagen, nur initState/dispose zu verwenden. Ich verstehe nicht wirklich, warum das ein Problem ist.

Ich bin gespannt, wie die Leute über den Code in #51752 (Kommentar) denken . Ich denke nicht, dass es besser ist als initState/dispose, aber wenn Leute Hooks mögen, mögen sie das auch? Ist Haken besser? Schlechter?

Das Schlüsselwort late macht die Dinge besser, aber es leidet immer noch unter einigen Problemen:

Solche Property können für Zustände nützlich sein, die in sich geschlossen sind oder nicht von Parametern abhängen, die sich im Laufe der Zeit ändern können. In einer anderen Situation kann die Verwendung jedoch schwierig werden.
Genauer gesagt fehlt der "Update"-Teil.

Bei StreamBuilder der gehörte Stream im Laufe der Zeit ändern. Aber es gibt keine einfache Lösung, so etwas hier zu implementieren, da das Objekt nur einmal initialisiert wird.

In ähnlicher Weise haben Hooks ein Äquivalent zu Key von Widget – was dazu führen kann, dass ein Teil des Zustands zerstört und neu erstellt wird, wenn sich dieser Schlüssel ändert.

Ein Beispiel dafür ist useMemo , ein Hook, der eine Objektinstanz zwischenspeichert.
In Kombination mit Schlüsseln können wir useMemo um einen impliziten Datenabruf zu ermöglichen.
Zum Beispiel kann unser Widget eine Nachrichten-ID erhalten – die wir dann verwenden, um die Nachrichtendetails abzurufen. Diese Nachrichten-ID kann sich jedoch im Laufe der Zeit ändern, sodass wir die Details möglicherweise erneut abrufen müssen.

Mit useMemo kann dies wie folgt aussehen:

String messageId;

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

}

In dieser Situation wird der Datenabruf nicht erneut ausgeführt, auch wenn die Build-Methode 10 Mal erneut aufgerufen wird, solange sich messageId nicht ändert.
Aber wenn sich das messageId ändert, wird ein neues Future erstellt.


Es ist erwähnenswert, dass ich nicht denke, dass flutter_hooks in seinem aktuellen Zustand für Dart verfeinert ist. Meine Implementierung ist eher ein POC als eine vollwertige Architektur.
Aber ich glaube, dass wir ein Problem mit der Wiederverwendbarkeit von Code von StatefulWidgets haben.

Ich erinnere mich nicht wo, aber ich erinnere mich, dass ich vorgeschlagen habe, dass Hooks in der idealen Welt ein benutzerdefinierter Funktionsgenerator neben async* & sync* , was ähnlich dem sein könnte, was Dan mit use State vorschlägt useState

@gaearon

Ich möchte betonen, dass es hier nicht um die Reduzierung des Boilerplates geht, sondern um die Fähigkeit, Pipelines zustandsbehafteter gekapselter Logik dynamisch zusammenzustellen.

Das ist nicht das Problem, das hier diskutiert wird. Ich empfehle, einen separaten Fehler einzureichen, um über die Unfähigkeit zu sprechen, die Sie beschreiben. (Das klingt nach einem ganz anderen Problem und ehrlich gesagt nach einem überzeugenderen als das hier beschriebene.) Bei diesem Fehler geht es speziell darum, dass ein Teil der Logik zu ausführlich ist.

Nein, er hat recht, meine Formulierung kann verwirrend sein.
Wie ich bereits erwähnt habe, geht es hier nicht um die Anzahl der Codezeilen, sondern um die Codezeilen selbst.

Hier geht es um die Faktorisierung des Staates.

Dieser Fehler ist sehr klar bezüglich des Problems "Die Wiederverwendung von Zustandslogik ist zu ausführlich/schwierig" und es geht darum, dass zu viel Code in einem Zustand vorhanden ist, wenn Sie eine Eigenschaft haben, die Code haben muss, um sie in initState zu deklarieren, in disponieren und in debugFillProperties. Wenn das Problem, das Sie interessiert, ein anderes ist, empfehle ich, einen neuen Fehler einzureichen, der dieses Problem beschreibt.

Ich empfehle wirklich dringend, Hooks (oder jede Lösung) zu vergessen, bis Sie das Problem, das Sie lösen möchten, vollständig verstanden haben. Nur wenn Sie ein klares Verständnis des Problems haben, können Sie ein überzeugendes Argument für ein neues Feature formulieren, denn wir müssen Features anhand der Probleme bewerten, die sie lösen.

Ich glaube, Sie missverstehen, was ich damals in dieser Ausgabe gesagt habe.

Das Problem ist keineswegs Boilerplate, sondern die Wiederverwendbarkeit.

Boilerplate ist eine Folge eines Problems mit der Wiederverwendbarkeit, nicht die Ursache

Was dieses Problem beschreibt, ist:

Wir möchten möglicherweise Zustandslogik wiederverwenden/zusammenstellen. Aber die verfügbaren Optionen sind entweder Mixins, Builder oder die Nichtwiederverwendung – alle haben ihre eigenen Probleme.

Die Probleme der vorhandenen Optionen können mit Boilerplate zusammenhängen, aber das Problem, das wir zu lösen versuchen, ist es nicht.
Während die Reduzierung des Boilerplates von Builders ein Weg ist (was Hooks tun), kann es einen anderen geben.

Zum Beispiel wollte ich eine Weile vorschlagen, Methoden hinzuzufügen wie:

context.onDidChangeDependencies(() {

});
context.onDispose(() {

});

Aber diese haben ihre eigenen Probleme und lösen das Problem nicht vollständig, also habe ich es nicht getan.

@rrousselGit , Sie können die ursprüngliche Problembeschreibung hier oben bearbeiten, um das Problem besser widerzuspiegeln. Fühlen Sie sich auch frei, ein Design-Dokument zu erstellen: https://flutter.dev/docs/resources/design-docs , über das wir gemeinsam iterieren können (wiederum, wie @Hixie vorschlägt, konzentrieren wir uns engstmögliche Darstellung der Problemstellung ). Ich würde mich freuen, wenn Sie sich genauso stark fühlen wie jeder andere Flutter-Ingenieur – Sie sind Teil des Teams, also lassen Sie uns gemeinsam iterieren!

Ich habe das Thema noch ein paar Mal durchgesehen. Ehrlich gesagt verstehe ich nicht, woher das Missverständnis kommt, daher bin ich mir nicht sicher, was ich verbessern soll.
Der ursprüngliche Kommentar erwähnt immer wieder den Wunsch nach Wiederverwendbarkeit/Faktorisierung. Die Erwähnungen über Boilerplate sind nicht "Flattern ist ausführlich", sondern "Einige Logiken sind nicht wiederverwendbar".

Ich denke nicht, dass der Vorschlag der Designdokumentation fair ist. Es dauert sehr lange, ein solches Dokument zu schreiben, und das mache ich in meiner Freizeit.
Ich persönlich bin mit Haken zufrieden. Ich verfasse diese Themen nicht in meinem Interesse, sondern um das Bewusstsein für ein Problem zu schärfen, das eine beträchtliche Anzahl von Menschen betrifft.

Vor ein paar Wochen wurde ich eingestellt, um über die Architektur einer bestehenden Flutter-App zu diskutieren. Ihre wahrscheinlich war genau das, was hier erwähnt wird:

  • Sie haben eine gewisse Logik, die in mehreren Widgets wiederverwendet werden muss (Verarbeitung von Ladezuständen / Markierung von "Nachrichten" als gelesen, wenn einige Widgets sichtbar werden / ...)
  • Sie versuchten, Mixins zu verwenden, was zu großen Architekturfehlern führte.
  • Sie versuchten auch, das "Erstellen/Aktualisieren/Entsorgen" manuell zu handhaben, indem diese Logik an mehreren Stellen neu geschrieben wurde, aber es verursachte Fehler.
    An einigen Stellen wurde vergessen, Abonnements zu schließen. In anderen haben sie das Szenario, in dem sich ihre stream Instanz ändert, nicht gehandhabt
  • Markieren von "Nachrichten" als gelesen, wenn einige Widgets sichtbar werden

Das ist ein interessanter Fall, weil es ähnlich zu Problemen ist, die ich in einer meiner eigenen Apps hatte, also habe ich mir angesehen, wie ich den Code dort implementiert habe, und ich sehe wirklich nicht viele der Probleme, die dieser Fehler beschreibt, was möglicherweise Deshalb habe ich Probleme, das Problem zu verstehen. Dies ist der betreffende Code:

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

Haben Sie Beispiele für tatsächliche Apps, die ich studieren könnte, um das Problem in Aktion zu sehen?

(Übrigens, im Allgemeinen würde ich dringend empfehlen, Streams überhaupt nicht zu verwenden. Ich denke, sie machen die Dinge im Allgemeinen schlimmer.)

(Übrigens, im Allgemeinen würde ich dringend empfehlen, Streams überhaupt nicht zu verwenden. Ich denke, sie machen die Dinge im Allgemeinen schlimmer.)

(Ich stimme voll und ganz zu. Aber die Community hat derzeit die gegenteilige Reaktion. Vielleicht würde es helfen, ChangeNotifier/Listenable/ValueNotifier aus Flutter in ein offizielles Paket zu extrahieren)

Haben Sie Beispiele für tatsächliche Apps, die ich studieren könnte, um das Problem in Aktion zu sehen?

Traurigerweise Nein. Ich kann nur die Erfahrung teilen, die ich gemacht habe, als ich anderen geholfen habe. Ich habe keine App zur Hand.

Das ist ein interessanter Fall, weil es ähnlich zu Problemen ist, die ich in einer meiner eigenen Apps hatte, also habe ich mir angesehen, wie ich den Code dort implementiert habe, und ich sehe wirklich nicht viele der Probleme, die dieser Fehler beschreibt, was möglicherweise Deshalb habe ich Probleme, das Problem zu verstehen. Dies ist der betreffende Code:

In Ihrer Implementierung ist die Logik an keinen Lebenszyklus gebunden und wird in _build_ platziert, sodass das Problem irgendwie umgangen wird.
In diesem speziellen Fall kann es sinnvoll sein. Ich bin mir nicht sicher, ob das Beispiel gut war.

Ein besseres Beispiel könnte Pull-to-Refresh sein.

In einem typischen Pull-to-Refresh wollen wir:

  • Behandeln Sie beim ersten Build Lade-/Fehlerzustände
  • beim Aktualisieren:

    • Wenn sich der Bildschirm im Fehlerzustand befand, zeigen Sie den Ladebildschirm erneut an

    • Wenn die Aktualisierung während des Ladens durchgeführt wurde, brechen Sie ausstehende HTTP-Anfragen ab.

    • wenn der Bildschirm einige Daten anzeigt:

    • Zeigen Sie die Daten weiterhin an, während der neue Status geladen wird

    • Wenn die Aktualisierung fehlschlägt, zeigen Sie weiterhin die zuvor erhaltenen Daten an und zeigen Sie eine Snackbar mit dem Fehler an

    • Wenn der Benutzer auftaucht und den Bildschirm erneut betritt, während die Aktualisierung aussteht, zeigen Sie den Ladebildschirm an

    • Stellen Sie sicher, dass der RefreshIndicator sichtbar ist, während die Aktualisierung aussteht

Und wir möchten eine solche Funktion für alle Ressourcen und mehrere Bildschirme implementieren. Darüber hinaus möchten einige Bildschirme möglicherweise mehrere Ressourcen gleichzeitig aktualisieren.

ChangeNotifier + provider + StatefulWidget wird eine Menge Schwierigkeiten haben, diese Logik zu faktorisieren.

Während meine neuesten Experimente (die auf Unveränderlichkeit basieren und auf flutter_hooks basieren) unterstützen das gesamte Spektrum sofort:

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]);
          },
        );
      },
    ),
  );
}

Diese Logik ist völlig in sich geschlossen. Es kann mit jeder Ressource in jedem Bildschirm wiederverwendet werden.

Und wenn ein Bildschirm mehrere Ressourcen gleichzeitig aktualisieren möchte, können wir Folgendes tun:

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

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

Ich würde empfehlen, all diese Logik in den App-Status außerhalb der Widgets zu setzen und nur den App-Status den aktuellen App-Status widerspiegeln zu lassen. Pull to refresh benötigt keinen Status innerhalb des Widgets, es muss nur dem Umgebungsstatus mitteilen, dass eine Aktualisierung ansteht und dann warten, bis ihre Zukunft abgeschlossen ist.

Es liegt nicht in der Verantwortung des Umgebungszustands zu bestimmen, wie ein Fehler vs. Laden vs. Daten dargestellt wird

Diese Logik im Umgebungszustand zu haben entfernt nicht alle Logiken aus der Benutzeroberfläche
Die Benutzeroberfläche muss noch bestimmen, ob der Fehler im Vollbildmodus oder in einer Snackbar angezeigt wird
Es muss immer noch erzwungen werden, dass Fehler aktualisiert werden, wenn die Seite neu geladen wird

Und das ist weniger wiederverwendbar.
Wenn die Rendering-Logik in den Widgets und nicht im Umgebungszustand vollständig definiert ist, funktioniert sie mit _any_ Futureund kann sogar direkt in Flutter eingebunden werden.

Ich verstehe nicht wirklich, wofür Sie in Ihrem letzten Kommentar plädieren. Mein Punkt ist, dass Sie keine Änderungen am Framework benötigen, um etwas so Einfaches wie den Aktualisierungsindikatorcode oben zu tun, wie der oben zitierte Code zeigt.

Wenn wir viele dieser Arten von Interaktionen haben, nicht nur für Aktualisierungsindikatoren, sondern auch für Animationen und andere, ist es besser, sie dort zu kapseln, wo sie am nächsten sind, anstatt sie in den App-Zustand zu versetzen, da der App-Zustand muss nicht die Besonderheiten jeder einzelnen Interaktion in der App kennen, wenn sie nicht an mehreren Stellen in der App benötigt wird.

Ich glaube nicht, dass wir uns über die Komplexität des Features und seine Wiederverwendbarkeit einig sind.
Haben Sie ein Beispiel, das zeigt, dass eine solche Funktion einfach ist?

Ich habe die Quelle einer App verlinkt, die ich oben geschrieben habe. Es ist sicherlich kein perfekter Code, und ich plane, Teile davon für die nächste Version neu zu schreiben, aber ich hatte nicht die Probleme, die Sie in dieser Ausgabe beschreiben.

Aber Sie sind einer der technischen Leiter von Flutter.
Selbst wenn Sie mit einem Problem konfrontiert sind, haben Sie genug Geschick, um sofort eine Lösung zu finden.

Auf der anderen Seite verstehen jedoch viele Leute nicht, was mit dem folgenden Code nicht stimmt:

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

Diese Tatsache wird dadurch bewiesen, wie beliebt eine auf StackOverflow erstellte Q/AI ist.

Das Problem ist nicht, dass es unmöglich ist, Zustandslogik auf eine wiederverwendbare und robuste Weise zu abstrahieren (sonst macht es keinen Sinn, dieses Problem zu stellen).
Das Problem ist, dass dies sowohl Zeit als auch Erfahrung erfordert.

Durch die Bereitstellung einer offiziellen Lösung verringert dies die Wahrscheinlichkeit, dass eine Anwendung nicht mehr gewartet werden kann – was die Gesamtproduktivität und das Entwicklererlebnis erhöht.
Nicht jeder konnte mit Ihrem Property- Vorschlag

Das Problem ist, dass es wirklich davon abhängt, was Ihre App ist, wie Ihr Status aussieht und so weiter. Wenn die Frage hier nur "wie verwaltet man den App-Status" lautet, dann ist die Antwort nicht so etwas wie Hooks, sondern viele Dokumentationen, die über verschiedene Vorgehensweisen sprechen und verschiedene Techniken für verschiedene Situationen empfehlen ... Dokumente: https://flutter.dev/docs/development/data-and-backend/state-mgmt

Es gibt einen kurzlebigen und einen App-Zustand, aber es scheint auch einen anderen Anwendungsfall zu geben: einen Zustand, der sich nur auf einen einzigen Widget-Typ bezieht, den Sie aber dennoch zwischen diesen Widget-Typen teilen möchten.

Beispielsweise kann ein ScrollController eine Art von Animation aufrufen, aber es ist nicht unbedingt angemessen, diese in den globalen App-Zustand zu versetzen, da es sich nicht um Daten handelt, die in der gesamten App verwendet werden müssen. Mehrere ScrollController s können jedoch dieselbe Logik haben, und Sie möchten diese Lebenszykluslogik zwischen ihnen teilen. Der Zustand ist immer noch für nur ScrollController s, also kein globaler App-Zustand, aber das Kopieren und Einfügen der Logik ist fehleranfällig.

Darüber hinaus möchten Sie diese Logik möglicherweise verpacken, um sie für Ihre zukünftigen Projekte, aber auch für andere, besser zusammensetzbar zu machen. Wenn Sie sich die Site useHooks ansehen , sehen Sie viele Logikelemente, die bestimmte allgemeine Aktionen useAuth , schreiben Sie es einmal und müssen sich keine Gedanken darüber machen, ob Sie einen initState oder dispose Aufruf verpasst haben oder ob die Async-Funktion einen then -Aufruf hat catch . Die Funktion wird nur einmal geschrieben, so dass der Raum für Fehler im Grunde verschwindet. Daher ist diese Art von Lösung nicht nur für mehrere Teile derselben App und zwischen mehreren Apps besser zusammensetzbar, sondern auch für den Endprogrammierer sicherer.

Ich habe nichts dagegen, dass Leute Hooks benutzen. Soweit ich das beurteilen kann, steht dem nichts im Wege. (Wenn etwas das verhindert, dann melde bitte einen Fehler dazu.)

Bei diesem Fehler geht es nicht um Hooks, sondern um "Die Wiederverwendung von Zustandslogik ist zu ausführlich/schwierig", und ich habe immer noch Probleme zu verstehen, warum dies Änderungen an Flutter erfordert. Es gibt viele Beispiele (einschließlich Hooks), die zeigen, wie es möglich ist, die Ausführlichkeit zu vermeiden, indem man seine Anwendung auf die eine oder andere Weise strukturiert, und es gibt bereits eine Menge Dokumentation darüber.

Ich verstehe, Sie fragen sich also, warum es eine First-Party-Lösung für Hooks geben muss, wenn es so etwas wie ein Hooks-Paket gibt, das bereits ohne Änderungen an Flutter erstellt wurde? Ich nehme an, @rrousselGit kann dies besser beantworten, aber die Antwort beinhaltet wahrscheinlich einen besseren Support, einen stärker integrierten Support und mehr Leute, die sie verwenden.

Ich kann Ihnen zustimmen, dass ich darüber hinaus auch verwirrt bin, warum an Flutter grundlegende Änderungen vorgenommen werden müssen, um Hooks zu unterstützen, da das Paket Flutter_hooks angeblich bereits existiert.

Ich habe immer noch Probleme zu verstehen, warum dies Änderungen an Flutter erfordert.

Zu sagen, dass dieses Problem gelöst ist, weil die Community ein Paket erstellt hat, ist wie zu sagen, dass Dart keine Datenklassen + Unionstypen benötigt, weil ich habe .
Freezed mag von der Community als Lösung für diese beiden Probleme sehr geschätzt werden, aber wir können es noch besser machen.

Das Flutter-Team hat viel mehr Einfluss, als die Community jemals haben wird. Sie haben beide die Möglichkeit, den gesamten Stapel zu ändern; Menschen, die Experten für jeden einzelnen Teil sind; und ein Gehalt, um die erforderliche Arbeit zu finanzieren.

Dieses Problem braucht das.
Denken Sie daran: Eines der Ziele des React-Teams ist es, dass Hooks Teil der Sprache werden, ähnlich wie bei JSX.

Auch ohne Sprachunterstützung brauchen wir noch Arbeit im Analyzer; Dartpad; flattern/devtools; und viele Hooks, um all die verschiedenen Dinge zu vereinfachen, die Flutter macht (z. B. für implizite Animationen, Formen und mehr).

Das ist ein gutes Argument, dem stimme ich zu, auch wenn die allgemeine Philosophie von Flutter einen kleinen Kern hat. Aus diesem Grund fügen wir zunehmend neue Funktionen als Pakete hinzu, auch wenn sie von Google stammen, cf Charaktere und Animationen . Das gibt uns mehr Flexibilität, um im Laufe der Zeit zu lernen und uns zu verändern. Wir würden dasselbe für diesen Bereich tun, es sei denn, es gibt einen zwingenden technischen Grund, warum ein Paket nicht ausreicht (und bei Erweiterungsmethoden ist dies noch unwahrscheinlicher als je zuvor).

Es ist schwierig, Dinge in den Kern von Flutter zu stecken. Eine Herausforderung besteht, wie Sie aus erster Hand wissen, darin, dass der Zustand ein Bereich ist, der sich weiterentwickelt, da wir alle mehr darüber erfahren, was in einer reaktiven UI-Architektur gut funktioniert. Wenn wir vor zwei Jahren gezwungen gewesen wären, einen Gewinner auszuwählen, hätten wir vielleicht BLoC ausgewählt, aber dann hat natürlich Ihr Provider-Paket übernommen und ist jetzt unsere Standardempfehlung.

Ich könnte mir gut vorstellen, dass bei Google tätige Mitwirkende flutter_hooks oder ein ähnliches Hooks-Paket unterstützen, das Traktion hatte (obwohl wir natürlich viele andere Arbeiten haben, die um unsere Aufmerksamkeit konkurrieren ). Insbesondere sollten wir Wenn Sie eine Übernahme durch uns suchen, ist das natürlich eine andere Frage.

Interessantes Argument, @timsneath. Auch die Rust-Community macht etwas Ähnliches, denn einmal in die Kern- oder Standardbibliothek einer Sprache oder eines Frameworks eingeführt, ist es sehr schwierig, sie wieder herauszunehmen. Im Fall von Rust ist dies unmöglich, da die Abwärtskompatibilität für immer aufrechterhalten werden soll. Deshalb warten sie, bis Pakete angekommen sind und gegeneinander angetreten sind, bis nur noch wenige Gewinner herauskommen, dann falten sie das in die Sprache.

Dies könnte bei Flutter ein ähnlicher Fall sein. Es könnte später etwas Besseres als Hooks geben, so wie React von Klassen zu Hooks wechseln musste, aber immer noch Klassen pflegen musste und die Leute migrieren mussten. Dann ist es möglicherweise besser, konkurrierende State-Management-Lösungen zu haben, bevor sie zum Core hinzugefügt werden. Und vielleicht sollten wir, die Community, Innovationen auf den Punkt bringen oder versuchen, noch bessere Lösungen zu finden.

Ich verstehe diese Bedenken, aber es geht hier nicht um eine State-Management-Lösung.

Diese Funktion ist näher an Inheritedwidget & StatefulWidget. Es ist ein Primitiv auf niedriger Ebene, das so niedrig wie ein Sprachmerkmal sein könnte.

Hooks können unabhängig vom Framework sein, aber das ist nur durch Glück möglich.
Wie ich bereits erwähnt habe, kann ein anderer Weg zu diesem Problem sein:

context.onDispose(() {

});

Und ähnliche Ereignis-Listener.
Aber das ist außerhalb des Rahmens nicht umsetzbar.

Ich weiß nicht, was sich das Team einfallen lassen würde.
Wir können jedoch nicht ausschließen, dass sich eine solche Lösung direkt neben Element befinden muss

Helfen Erweiterungen dabei?

(Vielleicht sollten wir aber in einem anderen Thema darüber sprechen. Es ist hier irgendwie nicht zum Thema. Ich würde es wirklich vorziehen, wenn wir ein Problem pro Problem hätten, das die Leute sehen, damit wir Lösungen an der richtigen Stelle diskutieren könnten. Ist es nicht klar, wie context.onDispose bei der Ausführlichkeit helfen würde.)

Ich vermute stark, dass wir diesbezüglich einige wirklich gute Sprachvorschläge machen könnten.

Ich denke, es wäre hilfreich, genauer über sie zu sprechen, als darüber, wie sie ein bestimmtes Idiom des Staatsmanagements ermöglichen könnten. Wir könnten dann ernsthafter überlegen, was sie ermöglichen und welche Kompromisse sie mit sich bringen könnten.

Insbesondere könnten wir überlegen, wie und ob sie sowohl in der VM- als auch in der JS-Laufzeit funktionieren könnten

Es ist nicht klar, wie context.onDispose bei der Ausführlichkeit helfen würde.)

Wie ich bereits erwähnt habe, geht es bei diesem Problem mehr um die Wiederverwendbarkeit von Code als um die Ausführlichkeit. Wenn wir jedoch mehr Code wiederverwenden können, sollte dies implizit die Ausführlichkeit reduzieren.

Die Art und Weise, wie context.onDispose mit diesem Problem zusammenhängt, ist mit der aktuellen Syntax, die wir haben:

AnimationController controller;

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

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

Das Problem ist:

  • dies ist eng an die Klassendefinition gekoppelt, kann also nicht wiederverwendet werden
  • Wenn das Widget wächst, wird die Beziehung zwischen der Initialisierung und dem Entsorgen schwieriger zu lesen, da sich Hunderte von Codezeilen in der Mitte befinden.

Mit einem context.onDispose könnten wir Folgendes tun:

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

Der interessante Teil ist:

  • Dies ist nicht mehr eng mit der Klassendefinition verbunden, sodass es in eine Funktion extrahiert werden kann.
    Wir könnten theoretisch eine halbkomplexe Logik haben:
    ```Pfeil
    AnimationController someReusableLogic(BuildContext-Kontext) {
    endgültiger Controller = AnimationController(...);
    controller.onDispose (controller.dispose);
    controller.forward();
    Leerer Hörer () {}
    controller.addListener (Listener);
    context.onDispose(() => controller.removeListener(Listener));
    }
    ...

@überschreiben
void initState() {
Controller = someReusableLogic (Kontext);
}
```

  • die ganze Logik ist gebündelt. Selbst wenn das Widget 300 lang wird, ist die Logik von controller immer noch gut lesbar.

Das Problem bei diesem Ansatz ist:

  • context.myLifecycle(() {...}) ist nicht heiß nachladbar
  • Es ist unklar, wie someReusableLogic Eigenschaften von StatefulWidget someReusableLogic lesen kann, ohne die Funktion eng an die Widget-Definition zu koppeln.
    Beispielsweise können die AnimationController Duration AnimationController als Parameter des Widgets übergeben werden. Wir müssen also mit dem Szenario umgehen, in dem sich die Dauer ändert.
  • Es ist unklar, wie eine Funktion implementiert werden kann, die ein Objekt zurückgibt, das sich im Laufe der Zeit ändern kann, ohne auf ein ValueNotifier zurückgreifen und mit Listenern umgehen zu müssen

    • Dies ist besonders wichtig für berechnete Zustände.


Ich werde über einen Sprachvorschlag nachdenken. Ich habe ein paar Ideen, aber nichts, worüber es im Moment zu reden lohnt.

Wie ich bereits erwähnt habe, geht es bei diesem Problem mehr um die Wiederverwendbarkeit von Code als um die Ausführlichkeit

Okay. Können Sie bitte einen neuen Fehler einreichen, der speziell darüber spricht? Dieser Fehler heißt wörtlich "Die Wiederverwendung der Zustandslogik ist zu ausführlich/schwierig". Wenn die Ausführlichkeit nicht das Problem ist, ist _dies_ nicht das Problem.

Mit einem context.onDispose könnten wir Folgendes tun:

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

Ich bin mir nicht sicher, warum context diesbezüglich relevant ist (und onDispose gegen unsere Namenskonventionen verstößt). Wenn Sie jedoch nur eine Möglichkeit haben möchten, Dinge zu registrieren, die während der Entsorgung ausgeführt werden, können Sie dies heute ganz einfach tun:

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();
  }
}

Nennen Sie es so:

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);
}

Das kannst du auch machen:

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);
}

Das Problem bei diesem Ansatz ist:

  • context.myLifecycle(() {...}) ist nicht heiß nachladbar

In diesem Zusammenhang scheint es keine Rolle zu spielen, da es nur für Dinge gilt, die in initState? Verpasse ich etwas?

  • Es ist unklar, wie someReusableLogic Eigenschaften von StatefulWidget someReusableLogic lesen kann, ohne die Funktion eng an die Widget-Definition zu koppeln.
    Zum Beispiel kann der AnimationController ‚s Duration als Parameter des Widgets übergeben werden kann. Wir müssen also mit dem Szenario umgehen, in dem sich die Dauer ändert.

Es ist ziemlich einfach, eine didChangeWidget-Warteschlange hinzuzufügen, genau wie die Dispose-Warteschlange:

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;
}

So verwendet:

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)),
      ),
    );
  }
}
  • Es ist unklar, wie man eine Funktion implementiert, die ein Objekt zurückgibt, das sich im Laufe der Zeit ändern kann, ohne auf ein ValueNotifier zurückgreifen und mit Listenern umgehen zu müssen

    • Dies ist besonders wichtig für berechnete Zustände.

Nicht sicher, was das hier bedeutet, was stimmt nicht mit ValueNotifier und beispielsweise einem ValueListenableBuilder?

Wie ich bereits erwähnt habe, geht es bei diesem Problem mehr um die Wiederverwendbarkeit von Code als um die Ausführlichkeit

Okay. Können Sie bitte einen neuen Fehler einreichen, der speziell darüber spricht? Dieser Fehler heißt wörtlich "Die Wiederverwendung der Zustandslogik ist zu ausführlich/schwierig". Wenn die Ausführlichkeit nicht das Problem ist, ist dies nicht das Problem.

Ich beginne mich bei dieser Diskussion ziemlich unwohl zu fühlen. Diesen Punkt habe ich schon einmal beantwortet:
Das Thema dieser Ausgabe ist die Wiederverwendbarkeit, und die Ausführlichkeit wird als Folge eines Wiederverwendbarkeitsproblems diskutiert. nicht als Hauptthema.

Es gibt nur einen einzigen Aufzählungspunkt im oberen Kommentar, der Ausführlichkeit erwähnt, und zwar mit StreamBuilder, der hauptsächlich auf die 2 Ebenen der Einrückungen abzielt.

Ich bin mir nicht sicher, warum der Kontext in diesem Zusammenhang relevant ist [...]. Wenn Sie jedoch nur eine Möglichkeit haben möchten, Dinge zu registrieren, die während der Entsorgung ausgeführt werden, können Sie dies heute ganz einfach tun:

Als ich context.onDispose , erwähnte ich ausdrücklich, dass ich das für keine gute Lösung halte.
Ich habe es erklärt, weil Sie gefragt haben, wie es mit der Diskussion zusammenhängt.

Warum context anstelle von StateHelper , liegt daran, dass dies flexibler ist (wie die Arbeit mit StatelessWidget).

context.myLifecycle(() {...}) ist nicht hot-reloadable

In diesem Zusammenhang scheint es keine Rolle zu spielen, da es nur für Dinge gilt, die in initState? Verpasse ich etwas?

Wir können ändern:

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

hinein:

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

Dadurch werden die Änderungen nicht auf den myLifecycle Rückruf angewendet.

Aber wenn wir verwendet haben:

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

Dann würde Hot-Reload funktionieren.

Nicht sicher, was das hier bedeutet, was stimmt nicht mit ValueNotifier und beispielsweise einem ValueListenableBuilder?

Diese Syntax wurde entwickelt, um zu vermeiden, dass Builder verwendet werden müssen, daher sind wir zum ursprünglichen Problem zurückgekehrt.

Wenn wir unsere Funktion wirklich zusammensetzbar machen wollen, müssen Funktionen anstelle Ihres ValueGetter + queueDidUpdateWidget Vorschlags einen ValueNotifier als Parameter annehmen:

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

da wir isAnimating von einem anderen Ort als didUpdateWidget erhalten möchten, je nachdem, welches Widget diese Funktion verwendet.
An einer Stelle kann es didUpdateWidget sein; in einem anderen kann es didChangeDependencies sein; und an einer anderen Stelle kann es sich im Callback eines stream.listen .

Aber dann brauchen wir eine Möglichkeit, diese Szenarien einfach in ValueNotifier umzuwandeln und unsere Funktion auf einen solchen Notifier zu hören.
Damit machen wir uns das Leben deutlich schwerer.
Es ist zuverlässiger und einfacher, ein ConditionalAnimatorBuilder als dieses Muster zu verwenden, denke ich.

Warum context anstelle von StateHelper , liegt daran, dass dies flexibler ist (wie die Arbeit mit StatelessWidget).

StatelessWidget ist für zustandslose Widgets gedacht. Der springende Punkt ist, dass sie keinen Zustand erstellen, Dinge entsorgen, auf didUpdateWidget reagieren usw.

Re die Sache mit dem heißen Nachladen, ja. Aus diesem Grund verwenden wir Methoden, anstatt Closures in initState zu setzen.

Es tut mir leid, dass ich das immer wieder sage, und ich verstehe, dass es frustrierend sein muss, aber ich verstehe immer noch nicht, was das Problem ist, das wir hier zu lösen versuchen. Ich dachte, es sei Ausführlichkeit, gemäß der ursprünglichen Fehlerzusammenfassung und einem großen Teil der ursprünglichen Beschreibung, aber ich verstehe, dass das nicht der Fall ist. Also, wo liegt das Problem? Es hört sich so an, als gäbe es hier viele sich gegenseitig ausschließende Wünsche, verteilt auf die vielen vielen Kommentare in diesem Fehler:

  • Die Angabe, wie etwas entsorgt werden soll, sollte an derselben Stelle erfolgen, an der es zugewiesen wird...
  • ...und der Ort, der ihn zuweist, muss nur einmal ausgeführt werden, da er ihn zuweist...
  • ...und es muss mit Hot Reload funktionieren (was per Definition keinen Code erneut ausführt, der nur einmal ausgeführt wird) ...
  • ...und es muss in der Lage sein, einen Zustand zu erstellen, der mit zustandslosen Widgets funktioniert (die per Definition keinen Zustand haben) ...
  • ...und es muss das Einhängen in Dinge wie didUpdateWidget und didChangeDependencies ermöglichen...

Dieser iterative Tanz, an dem wir hier beteiligt sind, ist kein produktiver Weg, um Dinge zu erledigen. Wie ich bereits versucht habe zu sagen, ist der beste Weg, hier etwas zu bekommen, das Problem, mit dem Sie konfrontiert sind, so zu beschreiben, dass wir es verstehen, mit allen Anforderungen an einem Ort und mit Anwendungsfällen erklärt. Ich empfehle, keine Lösungen aufzulisten, insbesondere keine Lösungen, von denen Sie wissen, dass sie nicht Ihren Bedürfnissen entsprechen. Stellen Sie nur sicher, dass die Notwendigkeit, die diese Lösungen unangemessen macht, in der Beschreibung aufgeführt ist.

Um ehrlich zu sein, klingt es für mich grundsätzlich so, als ob Sie nach einem ganz anderen Framework-Design fragen. Das ist völlig in Ordnung, aber es ist kein Flutter. Wenn wir ein anderes Framework machen würden, wäre es ein anderes Framework, und wir haben noch viel zu tun an _diesem_ Framework. Tatsächlich ist vieles von dem, was Sie beschreiben, dem Design von Jetpack Compose sehr ähnlich. Ich bin kein großer Fan dieses Designs, weil es Compiler-Magie erfordert, also ist es wirklich schwierig, zu debuggen, was vor sich geht, aber vielleicht liegt es eher an Ihnen?

Es hört sich so an, als gäbe es hier viele sich gegenseitig ausschließende Wünsche, verteilt auf die vielen vielen Kommentare in diesem Fehler:

Sie schließen sich nicht gegenseitig aus. Haken machen jeden einzelnen von diesen. Ich werde nicht ins Detail gehen, da wir uns nicht auf Lösungen konzentrieren wollen, aber sie kreuzen alle Kästchen an.

Wie ich bereits versucht habe zu sagen, ist der beste Weg, hier etwas zu bekommen, das Problem, mit dem Sie konfrontiert sind, so zu beschreiben, dass wir es verstehen, mit allen Anforderungen an einem Ort und mit Anwendungsfällen erklärt.

Ich verstehe immer noch nicht, warum dieser Top-Kommentar das nicht tut.
Mir ist nicht klar, was anderen nicht klar ist.

Tatsächlich ist vieles von dem, was Sie beschreiben, dem Design von Jetpack Compose sehr ähnlich. Ich bin kein großer Fan dieses Designs, weil es Compiler-Magie erfordert, also ist es wirklich schwierig, zu debuggen, was vor sich geht, aber vielleicht liegt es eher an Ihnen?

Ich kenne mich damit nicht aus, aber bei einer kurzen Suche würde ich _ja_ sagen.

Sie schließen sich nicht gegenseitig aus.

Sind alle Aufzählungspunkte, die ich oben aufgelistet habe, Teil des Problems, das wir hier zu lösen versuchen?

aber sie kreuzen alle Kästchen an

Kannst du die Boxen auflisten?

Ich verstehe immer noch nicht, warum dieser Top-Kommentar das nicht tut.

Zum Beispiel sagt das OP ausdrücklich, dass es sich bei dem Problem um StatefulWidgets handelt, aber einer der jüngsten Kommentare zu diesem Problem sagte, dass ein bestimmter Vorschlag nicht gut sei, da er mit StatelessWidgets nicht funktionierte.

Im OP sagst du:

Es ist schwierig, die State Logik wiederzuverwenden. Am Ende haben wir entweder eine komplexe und tief verschachtelte build Methode oder müssen die Logik über mehrere Widgets kopieren und einfügen.

Daher gehe ich davon aus, dass die Anforderungen Folgendes umfassen:

  • Lösung darf nicht tief verschachtelt sein.
  • Die Lösung darf an Stellen, an denen versucht wird, einen Status hinzuzufügen, nicht viel ähnlichen Code erfordern.

Der erste Punkt (über das Verschachteln) scheint in Ordnung zu sein. Auf keinen Fall versuchen wir vorzuschlagen, dass wir Dinge tun sollten, die tief verschachtelt sind. (Allerdings sind wir uns möglicherweise nicht einig, was tief verschachtelt ist; das wird hier nicht definiert. Andere Kommentare deuten später darauf hin, dass Builder tief verschachtelten Code verursachen, aber meiner Erfahrung nach sind Builder ziemlich gut, wie in dem Code gezeigt, den ich zuvor zitiert habe.)

Der zweite Punkt scheint eine Anforderung zu sein, dass wir keine Ausführlichkeit haben. Aber dann haben Sie mehrmals erklärt, dass es hier nicht um Ausführlichkeit geht.

Die nächste Aussage des OP, die ein Problem beschreibt, ist:

Die Wiederverwendung einer State Logik über mehrere StatefulWidget ist sehr schwierig, sobald diese Logik auf mehreren Lebenszyklen beruht.

Ehrlich gesagt weiß ich nicht genau was das bedeutet. "Schwierig" bedeutet für mich normalerweise, dass etwas viel komplizierte Logik beinhaltet, die schwer zu verstehen ist, aber das Zuordnen, Entsorgen und Reagieren auf Lebenszyklusereignisse ist sehr einfach. Die nächste Anweisung, die ein Problem aufwirft (hier überspringe ich das Beispiel, das explizit als "nicht komplex" bezeichnet wird und daher vermutlich keine Beschreibung des Problems darstellt):

Das Problem beginnt, wenn wir diesen Ansatz skalieren wollen.

Dies deutete für mich darauf hin, dass Sie mit "sehr schwierig" "sehr ausführlich" meinten und dass die Schwierigkeit daher kam, dass viele ähnliche Codes vorkommen, da der einzige Unterschied zwischen dem von Ihnen angegebenen "nicht komplexen" Beispiel und dem "sehr schwierigen" besteht " Ergebnis der Skalierung des Beispiels ist buchstäblich, dass derselbe Code viele Male vorkommt (dh Ausführlichkeit, Boilerplate-Code).

Dies wird durch die nächste Anweisung, die ein Problem beschreibt, weiter unterstützt:

Das Kopieren und Einfügen dieser Logik überall "funktioniert", erzeugt jedoch eine Schwachstelle in unserem Code:

  • es kann leicht sein, dass man vergisst, einen der Schritte neu zu schreiben (wie etwa das Vergessen, dispose anzurufen).

Vermutlich ist es also sehr schwierig, weil die Ausführlichkeit es leicht macht, beim Kopieren und Einfügen des Codes Fehler zu machen? Aber noch einmal, als ich versuchte, dieses Problem anzugehen, das ich als "Ausführlichkeit" bezeichnen würde, sagten Sie, das Problem sei nicht Ausführlichkeit.

  • es fügt dem Code viel Rauschen hinzu

Das hört sich wieder so an, als würde man mir nur Ausführlichkeit / Boilerplate sagen, aber Sie haben erneut erklärt, dass dies nicht der Fall ist.

Der Rest des OP beschreibt nur Lösungen, die Ihnen nicht gefallen, daher wird das Problem vermutlich nicht beschrieben.

Erklärt dies, wie das OP das Problem nicht erklärt? Alles im OP, das tatsächlich ein Problem beschreibt, scheint Ausführlichkeit zu beschreiben, aber jedes Mal, wenn ich vorschlage, dass dies das Problem ist, sagen Sie, dass dies nicht der Fall ist und dass es ein anderes Problem gibt.

Ich denke, das Missverständnis läuft auf die Wortbedeutung hinaus.
Zum Beispiel:

es fügt dem Code viel Rauschen hinzu

Das hört sich wieder so an, als würde man mir nur Ausführlichkeit / Boilerplate sagen, aber Sie haben erneut erklärt, dass dies nicht der Fall ist.

In diesem Punkt geht es nicht um die Anzahl der controller.dispose() , sondern um den Wert, den diese Codezeilen dem Leser bringen.
Diese Linie sollte immer da sein und ist immer gleich. Daher ist sein Wert für den Leser fast null.

Was zählt, ist nicht das Vorhandensein dieser Linie, sondern ihre Abwesenheit.

Das Problem ist, je mehr von solchen controller.dispose() wir haben, desto wahrscheinlicher ist es, dass wir ein tatsächliches Problem in unserer Entsorgungsmethode übersehen.
Wenn wir 1 Controller haben und 0 disponieren, ist es leicht zu fangen
Wenn wir 100 Controller haben und 99 verfügen, ist es schwierig, den fehlenden zu finden.

Dann haben wir:

Vermutlich ist es also sehr schwierig, weil die Ausführlichkeit es leicht macht, beim Kopieren und Einfügen des Codes Fehler zu machen? Aber noch einmal, als ich versuchte, dieses Problem anzugehen, das ich als "Ausführlichkeit" bezeichnen würde, sagten Sie, das Problem sei nicht Ausführlichkeit.

Wie ich im vorherigen Punkt erwähnt habe, sind nicht alle Codezeilen gleich.

Wenn wir vergleichen:

+ 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) {

+    },
+ );

dann haben diese beiden Schnipsel die gleiche Anzahl von Zeilen und machen dasselbe.
Aber ValueListenableBuilder ist vorzuziehen.

Der Grund dafür ist, dass nicht die Anzahl der Zeilen zählt, sondern was diese Zeilen sind.

Der erste Ausschnitt hat:

  • 1 Eigenschaftserklärung
  • 1 Methodendeklaration
  • 1 Aufgabe
  • 2 Methodenaufrufe
  • die alle über 2 verschiedene Lebenszyklen verteilt sind. 3 wenn wir build einbeziehen

Der zweite Ausschnitt hat:

  • 1-Klassen-Instanziierung
  • 1 anonyme Funktion
  • kein Lebenszyklus. 1 wenn wir build einbeziehen

Das macht den ValueListenableBuilder _einfacher_.

Es gibt auch, was diese Zeilen nicht sagen:
ValueListenableBuilder verarbeitet valueListenable sich im Laufe der Zeit ändern.
Selbst in dem Szenario, in dem sich widget.valueNotifier im Laufe der Zeit nicht ändert, schadet es nicht.
Eines Tages könnte sich diese Aussage ändern. In diesem Fall behandelt ValueListenableBuilder das neue Verhalten elegant, während wir beim ersten Snippet jetzt einen Fehler haben.

Der ValueListenableBuilder ist also nicht nur einfacher, sondern auch widerstandsfähiger gegenüber Änderungen im Code – bei exakt der gleichen Anzahl von Zeilen.


Damit sind wir uns, denke ich, einig, dass ValueListenableBuilder vorzuziehen ist.
Die Frage lautet dann: "Warum nicht für jede wiederverwendbare Zustandslogik ein Äquivalent zu ValueListenableBuilder?"

Statt zum Beispiel:

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

Wir würden haben:

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

  },
);

mit dem zusätzlichen Vorteil, dass Änderungen an initialText im laufenden Betrieb neu geladen werden können.

Dieses Beispiel mag etwas trivial sein, aber wir könnten dieses Prinzip für etwas fortgeschrittenere wiederverwendbare Zustandslogiken verwenden (wie Ihr ModeratorBuilder ).

Das ist "in Ordnung" in kleinen Ausschnitten. Es verursacht jedoch einige Probleme, da wir den Ansatz skalieren möchten:

  • Bauherren kehren zum Thema "zu viel Lärm" zurück.

Ich habe zum Beispiel gesehen, wie einige Leute ihr Modell auf diese Weise verwalten:

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

Aber dann möchte ein Widget vielleicht sowohl name , age als auch gender anhören.
Was bedeutet, dass wir tun müssten:

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

Dies ist offensichtlich nicht ideal. Wir haben die Verschmutzung in initState / dispose , um unsere Methode build zu verschmutzen.

(Ignorieren wir Listenable.merge für das Beispiel. Es spielt hier keine Rolle, es geht mehr um die Zusammensetzung)

Wenn wir Builders ausgiebig verwendet haben, ist es leicht, uns in genau diesem Szenario zu sehen – und ohne Entsprechung zu Listenable.merge (nicht dass ich diesen Konstruktor mag, um mit anzufangen 😛 )

  • Einen Custom Builder zu schreiben ist mühsam

    Es gibt keine einfache Lösung, um einen Builder zu erstellen. Hier hilft uns kein Refactoring-Tool – wir können nicht einfach „als Builder extrahieren“.
    Außerdem ist es nicht unbedingt intuitiv. Einen benutzerdefinierten Builder zu erstellen ist nicht das Erste, woran die Leute denken werden – zumal viele gegen die Boilerplate sind (obwohl ich es nicht bin).

    Es ist wahrscheinlicher, dass die Leute eine benutzerdefinierte Zustandsverwaltungslösung erstellen und möglicherweise mit schlechtem Code enden.

  • Einen Baum von Baumeistern zu manipulieren ist mühsam

    Angenommen, wir wollten in unserem vorherigen Beispiel ein ValueListenableBuilder entfernen oder ein neues hinzufügen, das ist nicht einfach.
    Wir können ein paar Minuten damit verbringen, zu zählen () und {}, um zu verstehen, warum unser Code nicht kompiliert wird.


Hooks sind dazu da, die eben erwähnten Builder-Probleme zu lösen.

Wenn wir das vorherige Beispiel in Hooks umgestalten, hätten wir:

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

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

Es ist identisch mit dem vorherigen Verhalten, aber der Code hat jetzt eine lineare Einrückung.
Was bedeutet:

  • der Code drastisch lesbarer
  • es ist einfacher zu bearbeiten. Wir brauchen keine Angst zu haben (){}; um eine neue Zeile hinzuzufügen.

Das ist einer der wichtigsten, den provider gefällt. Durch die Einführung von MultiProvider viele Verschachtelungen entfernt.

In ähnlicher Weise profitieren wir im Gegensatz zum initState / dispose Ansatz vom Hot-Reload.
Wenn wir ein neues useValueListenable hinzufügen, wird die Änderung sofort übernommen.

Und natürlich haben wir immer noch die Möglichkeit, wiederverwendbare Primitive zu extrahieren:

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);
}

und eine solche Änderung kann mit extract as function automatisiert werden, was in den meisten Szenarien funktionieren würde.


Beantwortet das deine Frage?

Sicher. Das Problem mit so etwas ist jedoch, dass es einfach nicht genug Informationen hat, um tatsächlich das Richtige zu tun. Zum Beispiel:

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

...wäre auf verwirrende Weise fehlerhaft.

Sie können das mit Compiler-Magie umgehen (so macht es Compose), aber für Flutter verstößt dies gegen einige unserer grundlegenden Designentscheidungen. Sie können es mit Schlüsseln umgehen, aber dann leidet die Leistung stark (da die Variablensuche schließlich Map-Lookups, Hashes usw. umfasst), was für Flutter einige unserer grundlegenden Designziele verletzt.

Die zuvor vorgeschlagene Property-Lösung oder etwas davon abgeleitetes scheint die Compiler-Magie zu vermeiden und gleichzeitig die von Ihnen beschriebenen Ziele zu erreichen, den gesamten Code an einem Ort zu haben. Ich verstehe nicht wirklich, warum es dafür nicht funktionieren sollte. (Natürlich würde es so erweitert, dass es sich auch in didChangeDependencies einklinkt und so weiter, um eine vollständige Lösung zu sein.) (Wir würden dies nicht in das Basis-Framework aufnehmen, da es unsere Leistungsanforderungen verletzen würde.)

Gerade wegen der eventuell auftretenden Fehler, wie Sie sagen, sollten Hooks nicht bedingt aufgerufen werden. Weitere Informationen finden Sie im Dokument Rules of Hooks von ReactJS. Der grundlegende Kern ist, dass, da sie in ihrer Implementierung nach Aufrufreihenfolge verfolgt werden, ihre bedingte Verwendung diese Aufrufreihenfolge unterbricht und somit keine korrekte Verfolgung ermöglicht. Um den Hook richtig zu verwenden, rufen Sie sie auf der obersten Ebene in build ohne bedingte Logik auf. In der JS-Version bekommst du zurück

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

Das Dart-Äquivalent kann ähnlich sein, es ist nur länger, weil es nicht wie bei JS entpackt wird:

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

Wenn Sie bedingte Logik wünschen, können Sie sich dann entscheiden, title in der Build-Methode zu verwenden, _nachdem sie auf der obersten Ebene aufgerufen wurden_, da die Aufrufreihenfolge jetzt noch erhalten bleibt. Viele dieser von Ihnen angesprochenen Probleme wurden in dem Hooks-Dokument erläutert, das ich oben verlinkt habe.

Sicher. Und das können Sie in einem Paket tun. Ich sage nur, dass eine solche Anforderung gegen unsere Designphilosophie verstoßen würde, weshalb wir dies nicht in das Framework Flutter aufnehmen würden. (Insbesondere optimieren wir auf Lesbarkeit und Fehlersuche; Code zu haben, der aussieht, als würde er funktionieren, aber aufgrund einer Bedingung (die im Code möglicherweise nicht offensichtlich ist) manchmal nicht funktioniert, möchten wir in der Kernrahmen.)

Das Debugging / bedingte Verhalten ist kein Problem. Deshalb ist ein Analyse-Plugin wichtig. Ein solches Plugin würde:

  • warnen, wenn eine Funktion einen Hook verwendet, ohne useMyFunction
  • warnen, wenn ein Haken bedingt verwendet wird
  • warnen, wenn ein Hook in einem Loop/Callback verwendet wird.

Damit sind alle möglichen Fehler abgedeckt. React hat bewiesen, dass dies machbar ist.

Dann bleiben uns die Vorteile:

  • besser lesbarer Code (wie zuvor gezeigt)
  • besser heiß nachladen
  • mehr wiederverwendbarer/zusammensetzbarer Code
  • flexibler – wir können leicht berechnete Zustände erstellen.

In Bezug auf berechnete Zustände sind Hooks sehr mächtig, um die Instanz eines Objekts zwischenzuspeichern. Dies kann verwendet werden, um ein Widget nur dann neu zu erstellen, wenn sich seine Parameter ändern.

Wir können zum Beispiel haben:

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);
  }  
}

Ein solcher useMemo Hook ermöglicht einfache Leistungsoptimierungen und die deklarative Behandlung von Init + Update, wodurch auch Fehler vermieden werden.

Dies ist etwas, das der Vorschlag von Property / context.onDispose vermisst.
Sie sind für deklarative Zustände schwer zu verwenden, ohne die Logik eng an einen Lebenszyklus zu koppeln oder den Code mit ValueNotifier komplexieren.

Mehr dazu, warum der Vorschlag von ValueGetter nicht praktikabel ist:

Wir möchten vielleicht umgestalten:

final int userId;

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

hinein:

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

Mit Hooks funktioniert diese Änderung einwandfrei, da useMemo an keinen Lebenszyklus gebunden ist.

Aber mit Property + ValueGetter müssten wir die Implementierung von Property ändern, damit dies funktioniert – was unerwünscht ist, da der Property Code möglicherweise nicht erwünscht ist an mehreren Stellen wiederverwendet. So haben wir wieder die Wiederverwendbarkeit verloren.

FWIW ist dieses Snippet äquivalent zu:

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);
      },
    );
  }
}

Ich nehme an, wir müssen eine Lösung finden, die die gleichen Probleme löst, die @rrousselGit erwähnt, aber auch Lesbarkeit und Debugging im Auge hat. Vue verfügt über eine eigene Implementierung, die möglicherweise eher dem entspricht, was Sie suchen, wo Bedingungen oder Aufrufreihenfolge keine Fehler wie in React verursachen.

Vielleicht besteht ein nächster Schritt darin, eine für Flutter einzigartige Lösung zu erstellen, die die Hook-Version dieses Frameworks angesichts der Einschränkungen von Flutter ist, so wie Vue ihre Version aufgrund der Einschränkungen von Vue erstellt hat. Ich benutze die Hooks von React regelmäßig und würde sagen, dass es manchmal nicht ausreichen kann, nur ein Analyse-Plugin zu haben, es sollte wahrscheinlich mehr in die Sprache integriert werden.

Ich glaube jedenfalls, dass wir nie zu einem Konsens kommen werden. Es hört sich so an, als ob wir uns nicht einig sind, selbst was lesbar ist

Zur Erinnerung, ich teile dies nur, weil ich weiß, dass die Community einige Probleme mit diesem Problem hat. Mir persönlich macht es nichts aus, wenn Flutter nichts dagegen unternimmt (obwohl ich das irgendwie traurig finde), solange wir:

  • ein richtiges Analyse-Plugin-System
  • die Möglichkeit, Pakete innerhalb von Dartpad zu verwenden

Wenn Sie das Hooks-Plugin verwenden möchten, das ich dringend empfehle, aber auf einige Probleme stößt, empfehle ich, Probleme für diese Probleme einzureichen und PRs einzureichen, um diese Probleme zu beheben. Daran arbeiten wir gerne mit Ihnen zusammen.

Hier ist eine neue Version der früheren Idee von Property . Es handhabt didUpdateWidget und die Entsorgung (und kann leicht dazu gebracht werden, andere Dinge wie didChangeDependencies zu handhaben); es unterstützt Hot-Reload (Sie können den Code ändern, der die Eigenschaft und Hot-Reload registriert, und es wird das Richtige tun); es ist typsicher, ohne explizite Typen zu benötigen (basiert auf Inferenz); es hat alles an einem Ort außer der Eigenschaftsdeklaration und -verwendung, und die Leistung sollte einigermaßen gut sein (wenn auch nicht ganz so gut wie die ausführlicheren Vorgehensweisen).

Property/PropertyManager:

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();
  }
}

So würden Sie es verwenden:

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);
      },
    );
  }
}

Der Einfachheit halber können Sie vorbereitete Property-Unterklassen für Dinge wie AnimationControllers usw. erstellen.

Sie können wahrscheinlich eine Version davon erstellen, die auch in State.build-Methoden funktioniert ...

Ich teile einige der Zweifel, die @Hixie auf den Tisch bringt. Auf der anderen Seite sehe ich die klaren Vorteile von Hooks und es scheint ziemlich vielen Entwicklern zu gefallen.
Mein Problem mit dem von @timsneath vorgeschlagenen
Wenn Pakete beginnen, Dinge zu implementieren, die der Reaktionsfähigkeit des Frameworks entsprechen sollten, werden wir viele verschiedene Flutter-Dialekte bekommen, was das Erlernen neuer Codebasen erschwert. Also für mich würde ich wahrscheinlich anfangen, Hooks zu verwenden, sobald sie Teil von Flutter werden.
Es ist meiner aktuellen Ansicht über das eingefrorene Paket sehr ähnlich. Ich liebe die Funktionalität, aber wenn Unions und Datenklassen nicht Teil von Dart sind, möchte ich sie nicht in meine Codebasis aufnehmen, da es für die Leute schwieriger würde, meinen Code zu lesen.

@escamoteur Nur damit ich es verstehe, schlagen Sie vor, dass wir die Funktionsweise von Widgets grundlegend ändern? Oder schlagen Sie vor, dass es bestimmte neue Fähigkeiten geben sollte? Angesichts der Tatsache, dass Dinge wie Hooks und der oben genannte Property-Vorschlag ohne Änderungen am Core-Framework möglich sind, ist mir nicht klar, was Sie eigentlich ändern möchten.

Es ist orthogonal zu dem Gespräch über jede vorgeschlagene Änderung selbst, aber ich denke, was ich von @escamoteur , @rrousselGit und anderen hier und anderswo gehört habe, ist, dass das _im_ Framework als wichtiger Weg angesehen wird, die Legitimität einer bestimmten Person zu begründen sich nähern. Korrigieren Sie mich, wenn Sie anderer Meinung sind.

Ich verstehe diese Denkweise -- da viel mit dem Framework zu tun hat (z. B. unterstützt DartPad heute keine Pakete von Drittanbietern, einige Kunden sind misstrauisch, wie viele Pakete sie nach dem Brennen mit NPM benötigen, es fühlt sich "offizieller" an, es wird garantiert mit Änderungen wie Null-Sicherheit vorankommen).

Die Einbeziehung ist aber auch mit erheblichen Kosten verbunden: Insbesondere verknöchert sie einen Ansatz und eine API. Deshalb legen wir beide die Messlatte für das, was wir hinzufügen, sehr hoch, insbesondere wenn es keine einstimmige Zustimmung gibt (vgl. staatliche Verwaltung), wo die Wahrscheinlichkeit einer Evolution besteht oder wir so leicht etwas als Paket hinzufügen können.

Ich frage mich, ob wir unsere Package-First-Philosophie dokumentieren müssen, aber _wo_ es hingeht, ist getrennt von einer Diskussion darüber, _was_ wir vielleicht ändern möchten, um die Wiederverwendung der Zustandslogik zu verbessern.

Unsere Paketrichtlinie ist hier dokumentiert: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#deciding -where-to-put-code

Ich verstehe den Paket-zuerst-Ansatz voll und ganz und stimme zu, dass dies eine wichtige Sache ist.
Aber ich glaube auch, dass einige Probleme im Kern gelöst werden müssen, nicht durch Pakete.

Deshalb argumentiere ich nicht, dass provider in Flutter zusammengeführt werden sollte, sondern glaube auch, dass dieses Problem ein Problem beschreibt, das Flutter nativ lösen sollte (natürlich nicht unbedingt mit Hooks).

Mit Provider liefert Flutter ein eingebautes Primitiv, um diese Art von Problem zu lösen: InheritedWidgets.
Der Anbieter fügt nur eine eigenwillige Ebene oben hinzu, um es "schöner" zu machen.

Haken sind anders. Sie _sind_ die Primitiven. Sie sind eine uneinsichtige Low-Level-Lösung für ein bestimmtes Problem: die Wiederverwendung von Logik über mehrere Zustände hinweg.
Sie sind nicht das Endprodukt, sondern etwas, von dem erwartet wird, dass die Leute benutzerdefinierte Pakete erstellen (wie ich es mit Hooks_riverpod getan

Es wäre hilfreich für mich (in Bezug auf das Verständnis der Wünsche hier und der Bedürfnisse, die Hooks erfüllt usw.), wenn jemand eine detaillierte Übersicht darüber geben könnte, wie der oben skizzierte Property-Ansatz im Vergleich zu Hooks vergleicht. (Mein Ziel mit der Property-Idee ist es, die Meinung über das Framework zu legen, um das Problem der Wiederverwendung von Logik über mehrere Zustände hinweg zu lösen.)

Ich denke, der Property-Vorschlag löst ein wichtiges Ziel dieses Problems nicht: Die Zustandslogik sollte sich nicht darum kümmern, woher die Parameter kommen und in welcher Situation sie aktualisiert werden.

Dieser Vorschlag erhöht die Lesbarkeit bis zu einem gewissen Grad, indem er die gesamte Logik an einem Ort neu gruppiert; aber es löst das Problem der Wiederverwendbarkeit nicht

Genauer gesagt können wir nicht extrahieren:

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

aus _ExampleState herausnehmen und in einem anderen Widget wiederverwenden, da die Logik an Example und initState + didUpdateWidget gebunden ist

Wie würde es mit Haken aussehen?

Ich stimme @timsneath zu, nachdem ich etwas Ähnliches in der Rust-Community gesehen habe. Es ist sehr schwierig, etwas aus dem Kern herauszuholen, wenn es einmal drin ist. Das BLoC-Muster wurde festgelegt, bevor Provider auf den Markt kam, aber jetzt ist Provider die empfohlene Version. Vielleicht kann flatter_hooks auf die gleiche Weise die "gesegnete" Version sein. Ich sage dies, weil es in Zukunft Verbesserungen gegenüber Hooks geben könnte, die sich die Leute einfallen lassen. React, der jetzt Haken hatte, kann sie nicht wirklich ändern oder aus ihnen herauskommen. Sie müssen sie, ähnlich wie Klassenkomponenten, im Wesentlichen für immer unterstützen, da sie im Kern enthalten sind. Daher stimme ich der Paketphilosophie zu.

Das Problem scheint zu sein, dass die Akzeptanz gering sein wird und die Leute alles verwenden, was ihnen passt. Dies kann wie gesagt gelöst werden, indem den Leuten empfohlen wird, flatter_hooks zu verwenden. Auch dies dürfte kein großes Problem sein, wenn wir uns analog ansehen, wie viele State-Management-Lösungen es gibt, auch wenn viele Leute Provider nutzen. Ich habe auch einige Probleme und "Gotchas" mit Hooks in anderen Frameworks erlebt, die angegangen werden sollten, um eine überlegene Lösung für zusammensetzbare und wiederverwendbare Lebenszykluslogik zu schaffen.

Wie würde es mit Haken aussehen?

Ohne die von React/flatter_hooks gelieferten primitiven Hooks zu verwenden, könnten wir Folgendes haben:

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);
  }
}

Dann verwendet:

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);
  }
}

In dieser Situation ist die Logik völlig unabhängig von Example und den Lebenszyklen eines StatefulWidget .
Wir könnten es also in einem anderen Widget wiederverwenden, das seine userId anders verwaltet. Vielleicht ist dieses andere Widget ein StatefulWidget , das seine userId intern verwaltet. Vielleicht erhält es stattdessen userId von einem InheritedWidget.

Diese Syntax sollte deutlich machen, dass Hooks wie unabhängige State Objekte mit ihren eigenen Lebenszyklen sind.

Nebenbei bemerkt, ein Nachteil des Package-First-Ansatzes ist: Paketautoren veröffentlichen weniger wahrscheinlich Pakete, die sich auf Hooks verlassen, um Probleme zu lösen.

Ein häufiges Problem, mit dem Provider-Benutzer konfrontiert sind, besteht beispielsweise darin, dass sie den Status eines Providers automatisch löschen möchten, wenn er nicht mehr verwendet wird.
Das Problem ist, dass Provider-Benutzer auch die Syntax context.watch / context.select mögen, im Gegensatz zur ausführlichen Syntax Consumer(builder: ...) / Selector(builder:...) .
Aber wir können nicht sowohl diese nette Syntax _und_ das zuvor erwähnte Problem ohne Hooks lösen (oder https://github.com/flutter/flutter/pull/33213, was abgelehnt wurde).

Das Problem ist:
Der Anbieter kann sich nicht auf flutter_hooks , um dieses Problem zu lösen.
Aufgrund der Popularität des Providers wäre es unzumutbar, sich auf Hooks zu verlassen.

Also entschied ich mich letztendlich für:

  • forking Provider (unter dem Codenamen Riverpod )
  • dadurch freiwillig die "Flatter-Favorit"/Google-Empfehlung verlieren
  • dieses Problem lösen (und einige mehr)
  • Fügen Sie eine Abhängigkeit von Hooks hinzu, um eine Syntax bereitzustellen, die Leute, die Spaß an context.watch , gerne hätten.

Ich bin sehr zufrieden mit dem, was ich herausgefunden habe, da es meiner Meinung nach eine erhebliche Verbesserung gegenüber Provider bringt (es macht InheritedWidgets kompilierungssicher).
Aber der Weg dorthin hinterließ bei mir einen schlechten Beigeschmack.

Soweit ich das beurteilen kann, gibt es im Wesentlichen drei Unterschiede zwischen der Hooks-Version und der Property-Version:

  • Die Hooks-Version ist viel mehr Backing-Code
  • Die Property-Version ist viel mehr Boilerplate-Code
  • Die Hooks-Version hat das Problem in Build-Methoden, bei denen, wenn Sie die Hooks in der falschen Reihenfolge aufrufen, die Dinge schief gehen und es keine Möglichkeit gibt, dies sofort aus dem Code zu erkennen.

Ist der Boilerplate-Code wirklich so eine große Sache? Ich meine, Sie können die Property jetzt einfach wiederverwenden, der Code ist an einem Ort. Also ist es jetzt wirklich _nur_ ein Ausführlichkeitsargument.

Ich denke, eine gute Lösung sollte nicht davon abhängen, dass andere Pakete davon wissen. Es sollte keine Rolle spielen, ob es im Rahmen ist oder nicht. Personen, die es nicht verwenden, sollten kein Problem sein. Wenn Leute es nicht benutzen, ist das ein Problem, IMHO, ein rotes Flag für die API.

Ich meine, Sie können die Property jetzt einfach wiederverwenden, der Code ist an einem Ort.

Der Code an einem Ort bedeutet nicht, dass er wiederverwendbar ist.
Würde es Ihnen etwas ausmachen, ein sekundäres Widget zu erstellen, das den Code, der sich derzeit in _ExampleState in einem anderen Widget wiederverwendet?
Mit einer Wendung: Dieses neue Widget sollte seine Benutzer-ID intern innerhalb seines Zustands verwalten, sodass wir Folgendes haben:

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

Wenn Leute es nicht benutzen, ist das ein Problem, IMHO, ein rotes Flag für die API.

Leute, die etwas nicht verwenden, weil es nicht offiziell ist, bedeutet nicht, dass die API schlecht ist.

Es ist völlig legitim, keine zusätzlichen Abhängigkeiten hinzufügen zu wollen, da dies zusätzlicher Wartungsaufwand ist (aufgrund von Versionierung, Lizenz, Abschreibung und anderen Dingen).
Soweit ich mich erinnere, muss Flutter so wenig Abhängigkeiten wie möglich haben.

Sogar beim Provider selbst, der mittlerweile weithin akzeptiert und fast offiziell ist, habe ich gesehen, wie Leute sagten: "Ich bevorzuge es, die integrierten InheritedWidgets zu verwenden, um das Hinzufügen einer Abhängigkeit zu vermeiden".

Würde es Ihnen etwas ausmachen, ein sekundäres Widget zu erstellen, das den Code, der sich derzeit in _ExampleState in einem anderen Widget wiederverwendet?

Bei dem fraglichen Code geht es darum, eine userId von einem Widget abzurufen und an eine fetchUser-Methode zu übergeben. Der Code zum Verwalten der Benutzer-ID, die sich lokal im selben Objekt ändert, wäre anders. Das scheint in Ordnung zu sein? Ich bin mir nicht ganz sicher, welches Problem du hier lösen willst.

Für die Aufzeichnung würde ich Property nicht verwenden, um das zu tun, was Sie beschreiben, es würde einfach so aussehen:

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);
      },
    );
  }
}

Leute, die etwas nicht verwenden, weil es nicht offiziell ist, bedeutet nicht, dass die API schlecht ist.

Einverstanden.

Die Tatsache, dass die Leute nichts selbst als schlecht verwenden, bedeutet, dass die API schlecht ist. Wenn Sie sagen, dass "Paketautoren weniger wahrscheinlich Pakete veröffentlichen, die auf Hooks angewiesen sind, um Probleme zu lösen", bedeutet dies, dass Hooks davon abhängig sind, dass andere Leute sie verwenden, um für Sie nützlich zu sein. Eine gute API wird IMHO nicht schlecht, wenn sie von niemand anderem übernommen wird; es sollte halten, auch wenn sonst niemand davon weiß. Das obige Property Beispiel hängt beispielsweise nicht davon ab, dass andere Pakete es verwenden, um selbst nützlich zu sein.

Sogar beim Provider selbst, der mittlerweile weithin akzeptiert und fast offiziell ist, habe ich gesehen, wie Leute sagten: "Ich bevorzuge es, die integrierten InheritedWidgets zu verwenden, um das Hinzufügen einer Abhängigkeit zu vermeiden".

Was ist falsch an Leuten, die es vorziehen, InheritedWidget zu verwenden? Ich möchte den Leuten keine Lösung aufzwingen. Sie sollten das verwenden, was sie verwenden möchten. Du beschreibst wörtlich ein Nicht-Problem. Die Lösung für Leute, die es vorziehen, InheritedWidget zu verwenden, besteht darin, ihnen aus dem Weg zu gehen und sie InheritedWidget verwenden zu lassen.

. Eine gute API wird IMHO nicht schlecht, wenn sie von niemand anderem übernommen wird; es sollte halten, auch wenn sonst niemand davon weiß. Das obige Property-Beispiel hängt beispielsweise nicht davon ab, dass andere Pakete es verwenden, um selbst nützlich zu sein.

Es gibt ein Missverständnis.

Das Problem besteht nicht darin, dass Leute im Allgemeinen keine Hooks verwenden.
Es geht darum, dass Provider keine Hooks verwenden können, um Probleme zu beheben, da Hooks nicht offiziell sind, während Provider es ist.


Der Code zum Verwalten der Benutzer-ID, die sich lokal im selben Objekt ändert, wäre anders. Das scheint in Ordnung zu sein? Ich bin mir nicht ganz sicher, welches Problem du hier lösen willst.

Für die Aufzeichnung würde ich Property nicht verwenden, um das zu tun, was Sie beschreiben, es würde einfach so aussehen:

Dies beantwortet die Frage nicht. Ich habe dies speziell gefragt, um die Wiederverwendbarkeit von Code zwischen Hooks und Property zu vergleichen.

Mit Hooks könnten wir FetchUser wiederverwenden:

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

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

Mit Hooks könnten wir FetchUser wiederverwenden:

Ich verstehe nicht, warum das wünschenswert ist. FetchUser hat keinen interessanten Code, es ist nur ein Adapter von Hooks zur fetchUser Funktion. Warum rufen Sie nicht einfach fetchUser direkt an? Der Code, den Sie wiederverwenden, ist kein interessanter Code.

Es geht darum, dass Provider keine Hooks verwenden können, um Probleme zu beheben, da Hooks nicht offiziell sind, während Provider es ist.

IMHO müsste eine gute Lösung für das Code-Wiederverwendungsproblem überhaupt nicht vom Provider übernommen werden. Sie wären völlig orthogonale Konzepte. Davon spricht der Flutter-Styleguide unter der Überschrift „Komplettierung vermeiden“.

Ich verstehe nicht, warum das wünschenswert ist. FetchUser hat keinen interessanten Code, es ist nur ein Adapter von Hooks zur fetchUser-Funktion. Warum rufen Sie nicht einfach fetchUser direkt auf? Der Code, den Sie wiederverwenden, ist kein interessanter Code.

Es spielt keine Rolle. Wir versuchen, die Wiederverwendbarkeit von Code zu demonstrieren. fetchUser könnte alles sein – einschließlich ChangeNotifier.addListener zum Beispiel.

Wir könnten eine alternative Implementierung haben, die nicht von fetchUser abhängt, und einfach eine API für den impliziten Datenabruf bereitstellen:

int userId;

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

IMHO müsste eine gute Lösung für das Code-Wiederverwendungsproblem überhaupt nicht vom Provider übernommen werden. Sie wären völlig orthogonale Konzepte. Davon spricht der Flutter-Styleguide unter der Überschrift „Komplettierung vermeiden“.

Deshalb habe ich erwähnt, dass Haken primitiv sind

Als Metapher:
package:animations hängt von Animation . Aber das ist kein Problem, denn das ist primitiv.
Es wäre eine andere Sache, wenn stattdessen package:animations eine Abzweigung von Animation die von der Community verwaltet wird

@escamoteur Nur damit ich es verstehe, schlagen Sie vor, dass wir die Funktionsweise von Widgets grundlegend ändern? Oder schlagen Sie vor, dass es bestimmte neue Fähigkeiten geben sollte? Angesichts der Tatsache, dass Dinge wie Hooks und der oben genannte Property-Vorschlag ohne Änderungen am Core-Framework möglich sind, ist mir nicht klar, was Sie eigentlich ändern möchten.

@Hixie nein, mein Punkt ist, dass wir, wenn Hooks noch beliebter werden, darüber nachdenken sollten, sie in das Framework aufzunehmen und sie allen beizubringen, damit wir ein gemeinsames Verständnis dafür haben, wie Flutter-Code aussieht und sich verhält.
Ich teile Ihre Bedenken sehr, aber auf der anderen Seite sieht ein Widget mit Haken wirklich elegant aus.
Es würde nicht verbieten, Dinge wie zuvor zu tun.

Es würde nicht verbieten, Dinge wie zuvor zu tun.

Ich denke, es wird eine gute Idee für das Flutter-Team sein, zu sagen "Hey, wir empfehlen jetzt Flatter-Hooks, aber Sie können immer noch Dinge wie zuvor tun", die Leute werden darüber verwirrt sein. Auch wenn das Flutter-Team in Zukunft Hooks empfiehlt, müssen sie auch aufhören, den tatsächlichen Flutter-Code als Beispiele zu veröffentlichen.

Die Leute folgen immer der "offiziellen Art", Dinge zu tun, und ich denke, es sollte nicht zwei offizielle Arten geben, Flutter zu verwenden.

Es spielt keine Rolle. Wir versuchen, die Wiederverwendbarkeit von Code zu demonstrieren. fetchUser könnte alles sein – einschließlich ChangeNotifier.addListener zum Beispiel.

Sicher. Dafür sind Funktionen gut: Code abstrahieren. Aber wir haben bereits Funktionen. Der obige Property-Code und der obige _setUserId-Code zeigen, dass Sie den gesamten Code, der diese Funktionen aufruft, an einen Ort bringen können, ohne besondere Hilfe vom Framework zu benötigen. Warum brauchen wir Hooks, um die Aufrufe dieser Funktionen zu umschließen?

IMHO müsste eine gute Lösung für das Code-Wiederverwendungsproblem überhaupt nicht vom Provider übernommen werden. Sie wären völlig orthogonale Konzepte. Davon spricht der Flutter-Styleguide unter der Überschrift „Komplettierung vermeiden“.

Deshalb habe ich erwähnt, dass Haken primitiv sind

Sie sind eine Bequemlichkeit, sie sind nicht primitiv. Wären sie primitiv, wäre die Frage "Was ist das Problem" viel einfacher zu beantworten. Du würdest sagen "Hier ist etwas, was ich tun möchte und ich kann es nicht".

Als Metapher:
package:animations hängt von Animation . Aber das ist kein Problem, denn das ist primitiv.
Es wäre eine andere Sache, wenn stattdessen package:animations eine Abzweigung von Animation die von der Community verwaltet wird

Die Animationsklassenhierarchie macht etwas Grundlegendes: Sie führt Ticker und eine Möglichkeit ein, sie zu kontrollieren und zu abonnieren. Ohne die Animationsklassenhierarchie müssen Sie so etwas wie die Animationsklassenhierarchie erfinden, um Animationen zu erstellen. (Idealerweise etwas Besseres. Es ist nicht unsere beste Arbeit.) Hooks führt keine neue grundlegende Funktion ein. Es bietet nur eine Möglichkeit, denselben Code anders zu schreiben. Es kann sein, dass dieser Code einfacher ist oder anders faktorisiert ist, als er es sonst wäre, aber es ist kein Primitiv. Sie benötigen kein Hooks-ähnliches Framework, um Code zu schreiben, der dasselbe tut wie Hooks-verwendender Code.


Grundsätzlich glaube ich nicht, dass das in dieser Ausgabe beschriebene Problem etwas ist, das das Framework beheben muss. Unterschiedliche Menschen werden sehr unterschiedliche Bedürfnisse haben, um damit umzugehen. Es gibt viele Möglichkeiten, das Problem zu beheben, wir haben in diesem Fehler bereits mehrere besprochen; Einige der Methoden sind sehr einfach und können in wenigen Minuten geschrieben werden. Es ist also kaum ein Problem, das so schwer zu lösen ist, dass es für uns von Wert ist, die Lösung zu besitzen und zu pflegen. Jeder der Vorschläge hat Stärken und Schwächen; die Schwächen sind in jedem Fall Dinge, die jemandem blockieren würde, um sie zu benutzen. Es ist nicht einmal wirklich klar, dass sich alle einig sind, dass das Problem überhaupt behoben werden muss.

Haken _sind_ Primitive
Hier ist ein Thread von Dan: https://twitter.com/dan_abramov/status/1093698629708251136 , der dies erklärt. Einige Formulierungen unterscheiden sich, aber die Logik gilt aufgrund der Ähnlichkeit zwischen den Komponenten der React-Klasse und den Flutter StatefulWidgets hauptsächlich für Flutter

Genauer gesagt können Sie sich flutter_hooks als dynamische State-Mixins vorstellen.

Wären sie primitiv, wäre die Frage "Was ist das Problem" viel einfacher zu beantworten. Du würdest sagen "Hier ist etwas, was ich tun möchte und ich kann es nicht".

Es steht im OP:

Es ist schwierig, die Zustandslogik wiederzuverwenden. Am Ende haben wir entweder eine komplexe und tief verschachtelte Build-Methode oder müssen die Logik über mehrere Widgets kopieren und einfügen.
Es ist weder möglich, eine solche Logik durch Mixins noch Funktionen wiederzuverwenden.

Es kann sein, dass dieser Code einfacher ist oder anders faktorisiert ist, als er es sonst wäre, aber es ist kein Primitiv. Sie benötigen kein Hooks-ähnliches Framework, um Code zu schreiben, der dasselbe tut wie Hooks-verwendender Code.

Sie brauchen keine Klassen, um ein Programm zu schreiben. Aber Klassen ermöglichen es Ihnen, Ihren Code zu strukturieren und sinnvoll zu faktorisieren.
Und Klassen sind Primitive.

Das Gleiche gilt für Mixins, die ebenfalls primitiv sind

Haken sind das Gleiche.

Warum brauchen wir Hooks, um die Aufrufe dieser Funktionen zu umschließen?

Denn wenn wir diese Logik nicht an _einer_ Stelle, sondern an _zwei_ Stellen aufrufen müssen.

Es ist weder möglich, eine solche Logik durch Mixins noch Funktionen wiederzuverwenden.

Geben Sie mir bitte ein konkretes Beispiel, wo dies der Fall ist. Bisher waren alle Beispiele, die wir studiert haben, einfach ohne Haken.

Bisher habe ich in diesem Thread keine andere Lösung als @rrousselGit- Hooks gesehen, die die Wiederverwendung und das Verfassen von Zustandslogik lösen und vereinfachen.

Zugegeben, ich habe in letzter Zeit nicht viel Dart und Flatter gemacht, so dass ich möglicherweise Dinge in den Codebeispielen für die Eigenschaftslösung oben vermisse, aber gibt es Lösungen? Welche Optionen gibt es heute, bei denen kein Kopieren und Einfügen anstelle einer Wiederverwendung erforderlich ist?
Wie lautet die Antwort auf die

Würde es Ihnen etwas ausmachen, ein sekundäres Widget zu erstellen, das den Code, der sich derzeit in _ExampleState befindet, in einem anderen Widget wiederverwendet?
Mit einer Wendung: Dieses neue Widget sollte seine Benutzer-ID intern innerhalb seines Status verwalten

Wenn es nicht möglich ist, eine so einfache Zustandslogik mit der obigen Eigenschaftslösung wiederzuverwenden, welche anderen Optionen gibt es?
Ist die Antwort einfach, dass es im Flattern nicht einfach wiederverwendbar sein sollte? Das ist völlig in Ordnung, aber IMHO ein bisschen traurig.

Übrigens, macht SwiftUI es auf eine neue/andere inspirierende Weise? Oder fehlt ihnen auch die gleiche Wiederverwendbarkeit der Zustandslogik? Ich selbst habe Swiftui noch nie benutzt. Vielleicht ist es einfach zu anders?

Alle Baumeister, im Grunde. Builder sind derzeit die einzige Möglichkeit, den Zustand wiederzuverwenden.
Hooks macht Builder lesbarer und einfacher zu erstellen


Hier ist eine Sammlung von benutzerdefinierten Hooks, die ich oder einige Kunden letzten Monat für verschiedene Projekte erstellt haben:

  • useQuery – das entspricht dem ImplicitFetcher Hook, den ich zuvor angegeben habe, führt aber stattdessen eine GraphQL-Abfrage durch.
  • useOnResume die einen Rückruf zum Ausführen einer benutzerdefinierten Aktion für AppLifecycleState.resumed ausgibt, ohne dass dies erforderlich ist
    Machen Sie sich die Mühe, einen WidgetsBindingObserver zu erstellen
  • useDebouncedListener die auf ein Listenable hört (normalerweise TextField oder ScrollController), aber mit einer Entprellung auf dem Listener
  • useAppLinkService , mit dem Widgets eine gewisse Logik für ein benutzerdefiniertes Ereignis ausführen können, ähnlich wie AppLifecycleState.resumed jedoch mit Geschäftsregeln
  • useShrinkUIForKeyboard für den reibungslosen Umgang mit der Tastaturdarstellung. Es gibt einen booleschen Wert zurück, der angibt, ob sich die Benutzeroberfläche an das untere Padding anpassen soll oder nicht (basierend auf dem Abhören eines focusNode).
  • useFilter , die useDebouncedListener und useState (ein primitiver Hook, der eine einzelne Eigenschaft deklariert) kombiniert, um einen Filter für eine Suchleiste anzuzeigen.
  • useImplicitlyAnimated<Int/Double/Color/...> – entspricht TweenAnimationBuilder als Haken

Apps verwenden auch viele Low-Level-Hooks für unterschiedliche Logik.

Statt zum Beispiel:

Whatever whatever;

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

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

Tun sie:

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

Dies vermeidet Duplikate zwischen initState / didUpdateWidget / didChangeDependencies .

Sie verwenden auch viele useProvider , von Riverpod, die sonst ein StreamBuilder / ValueListenableBuilder müssten


Der wichtige Teil ist, dass Widgets selten "nur einen Haken" verwenden.
Zum Beispiel kann ein Widget tun

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),
        ),
      ],
    );
  }
}

Es ist prägnant und sehr gut lesbar (vorausgesetzt, Sie haben natürlich Grundkenntnisse der API).
Die gesamte Logik kann von oben nach unten gelesen werden – es gibt keinen Sprung zwischen den Methoden, um den Code zu verstehen.
Und alle hier verwendeten Hooks werden an mehreren Stellen in der Codebasis wiederverwendet

Wenn wir ohne Hooks genau dasselbe tun würden, hätten wir:

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),
                    ),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }
}

Dies ist deutlich weniger lesbar.

  • Wir haben 10 Einrückungsebenen – 12, wenn wir FilterBuilder , um die Filterlogik wiederzuverwenden
  • Die Filterlogik ist so wie sie ist nicht wiederverwendbar.

    • wir könnten versehentlich vergessen, die timer zu stornieren

  • die Hälfte der Methode build ist für den Leser nicht nützlich. The Builders lenkt uns von dem ab, was zählt
  • Ich habe gute 5 Minuten verloren, um zu verstehen, warum der Code aufgrund einer fehlenden Klammer nicht kompiliert wird

Als Benutzer von flutter_hooks selbst werde ich meine Meinung dazu beitragen. Vor der Verwendung von Haken war ich mit Flutter zufrieden. Ich habe die Notwendigkeit für so etwas nicht gesehen. Nachdem ich darüber gelesen und ein Youtube-Video darüber gesehen hatte, war ich immer noch nicht überzeugt, es sah cool aus, aber ich brauchte etwas Übung oder Beispiele, um es wirklich zu motivieren. Aber dann ist mir was aufgefallen. Ich habe zustandsbehaftete Widgets um jeden Preis vermieden, es gab nur eine Menge Boilerplate und hüpfte in der Klasse herum, um Dinge zu finden. Aus diesem Grund hatte ich den größten Teil meines kurzlebigen Zustands zusammen mit dem Rest des App-Zustands in eine Zustandsverwaltungslösung verschoben und nur zustandslose Widgets verwendet. Dies führt jedoch dazu, dass die Geschäftslogik schnell von Flutter abhängig ist, da sie darauf angewiesen ist, die Navigator oder BuildContext für den Zugriff auf InheritedWidget s / Providers höher in . zu bekommen der Baum. Ich sage nicht, dass es ein guter Ansatz für das Staatsmanagement war, ich weiß, dass es das nicht war. Aber ich habe alles getan, um mich nicht um die Zustandsverwaltung in der Benutzeroberfläche kümmern zu müssen.

Nachdem ich für eine Weile Hooks verwendet hatte, war ich viel produktiver, viel glücklicher mit Flutter, indem ich den kurzlebigen Zustand (zusammen mit der Benutzeroberfläche) anstelle des App-Zustands an die richtige Stelle setzte.

Für mich ist es wie ein Garbage Collector für kurzlebige Zustände / Controller. Ich muss nicht daran denken, alle Abonnements in der Benutzeroberfläche zu löschen, obwohl ich mir immer noch sehr bewusst bin, dass flutter_hooks genau das für mich tut. Es macht es auch eine Menge einfacher, meinen Code zu warten und umzugestalten. Ich spreche vom Schreiben von ~ 10 Apps im letzten Jahr für meine Absolventenforschung und meinen Spaß.

Wie andere weiß ich nicht genau, was die Hauptmotivation sein sollte, es in das Flutter-SDK selbst aufzunehmen. Hier sind jedoch zwei Gedanken zu diesem Thema.

  1. Gelegentlich mache ich einen Hook, um es einfacher zu machen, ein Paket zu verwenden, das Controller enthält, die initialisiert / entsorgt werden müssen. (Zum Beispiel golden_layout oder zefyr ). Ich glaube, dass die anderen Benutzer, die flutter_hooks , von einem solchen Paket profitieren würden. Allerdings kann ich die Veröffentlichung eines Pakets, das buchstäblich 1-3 Funktionen enthält, nicht rechtfertigen. Die Alternative wäre, ein Kitchen-Sink-Paket zu erstellen, das viele Hooks für verschiedene Pakete enthält, die ich verwende um zu profitieren (was weniger auffindbar ist und wahrscheinlich Abhängigkeiten von Paketen enthält, die ihnen egal sind), oder von einem Paket, das 3 Funktionen enthält, oder ich veröffentliche ein Garden-Sink-Paket auf pub.dev. Alle Ideen erscheinen lächerlich und nicht sehr auffindbar. Die anderen Benutzer von flutter_hooks könnten diese Funktionen leicht kopieren und in ihren Code einfügen oder versuchen, die Logik selbst herauszufinden, aber das verfehlt den Sinn des Teilens von Code / Paketen völlig. Die Funktionen würden viel besser in die Originalpakete gehen und nicht in irgendein 'Erweiterungspaket'. Wenn flutter_hooks Teil des Frameworks wäre oder auch nur ein Paket, das vom Framework verwendet oder aus dem Framework exportiert wird, wie characters , dann würden die Autoren des Originalpakets viel wahrscheinlicher einen Pull-Request für einen einfachen Hook akzeptieren Funktionen, und wir werden nicht ein Durcheinander von 1-3 Funktionspaketen haben.
    Wenn flutter_hooks nicht von Flutter übernommen wird, sehe ich eine Reihe von 1-3 Funktionspaketen, die die Suchergebnisse von pub.dev überladen. Die Tatsache, dass diese Pakete wirklich klein wären, lässt mich @rrousselGit wirklich zustimmen, dass dies ein Primitiv ist. Wenn die 1228 Sterne im flutter_hooks Repository kein Hinweis darauf sind, dass die von @rrousselGit erwähnten Probleme weiß ich nicht, was ist.

  2. Ich habe mir ein Youtube-Video über den Beitrag zum Flutter-Repo angesehen, da ich daran interessiert war, zu sehen, was ich tun könnte, um zu helfen. Während ich zusah, hat die Person, die das Video erstellt hat, ziemlich einfach in der neuen Eigenschaft hinzugefügt, aber fast vergessen, sich um die Aktualisierung von dispose , didUpdateWidget und debugFillProperties zu kümmern. Die ganze Komplexität eines zustandsbehafteten Widgets noch einmal zu sehen und wie leicht es ist, etwas zu übersehen, hat mich wieder misstrauisch gemacht und mich nicht so aufgeregt, zum Haupt-Repositorium von Flutter beizutragen. Ich sage nicht, dass es mich völlig abgeschreckt hat, ich bin immer noch daran interessiert, einen Beitrag zu leisten, aber es fühlt sich an, als würde ich Boilerplate-Code erstellen, der schwer zu warten und zu überprüfen ist. Es geht nicht um die Komplexität des Schreibens des Codes, sondern um die Komplexität des Lesens des Codes und um zu überprüfen, ob Sie den ephemeren Zustand ordnungsgemäß entsorgt und gepflegt haben.

Entschuldigung für die langatmige Antwort, aber ich habe mir dieses Problem von Zeit zu Zeit angesehen und bin etwas verblüfft über die Antwort des Flutter-Teams. Es scheint, als hätten Sie sich nicht die Zeit genommen, eine App in beide Richtungen auszuprobieren und den Unterschied selbst zu sehen. Ich verstehe den Wunsch, eine zusätzliche Abhängigkeit nicht aufrechtzuerhalten oder zu sehr in das Framework zu integrieren. Der Kern des flutter_hook Frameworks besteht jedoch aus nur 500 Zeilen ziemlich gut dokumentiertem Code. Nochmals, Entschuldigung, wenn dies tangential zum Gespräch ist und ich hoffe, ich beleidige niemanden dafür, dass er meine 2 Cent gegeben und meine Stimme ausgesprochen hat. Ich habe mich nicht früher geäußert, weil ich das Gefühl hatte, dass

Entschuldigung für die langatmige Antwort, aber ich habe mir dieses Problem von Zeit zu Zeit angesehen und bin etwas verblüfft über die Antwort des Flutter-Teams. Es scheint, als hätten Sie sich nicht die Zeit genommen, eine App in beide Richtungen auszuprobieren und den Unterschied selbst zu sehen.

Um fair zu sein, dies ist ein unglaublich langer Thread und der Gründer des Frameworks hat mehrmals täglich aktiv mit mehreren Lösungen beigetragen, Feedback dazu angefordert, sich mit ihnen beschäftigt und daran gearbeitet, zu verstehen, was angefordert wird. Es fällt mir ehrlich gesagt schwer, mir ein klareres Beispiel dafür vorzustellen, dass ein Betreuer hilfreich ist.

Ich wünschte, dies wäre etwas mehr Geduld mit diesem Problem - ich verstehe Hooks nicht mehr, nachdem ich diesen Thread gelesen habe, außer dass sie eine andere Möglichkeit sind, die Lebensdauer von Einwegartikeln an einen Staat zu binden. Ich bevorzuge diesen Ansatz stilistisch nicht, und ich finde, es ist etwas grundlegend fehlerhaft, wenn die Position lautet: „Nehmen Sie sich einfach die Zeit, eine ganz neue App im Paradigma zu schreiben, dann werden Sie verstehen, warum sie in das Framework eingebaut werden muss! ' - Wie der React-Ingenieur in diesem Thread bemerkte, wäre es für Flutter wirklich nicht ratsam, und die in diesem Thread beschriebenen Vorteile sind im Vergleich zu den Kosten für die Art der Neuverdrahtung gering, was bedeutet, dass Sie eine ganz neue Codebasis benötigen, um den Nutzen zu sehen.

Es fällt mir ehrlich gesagt schwer, mir ein klareres Beispiel dafür vorzustellen, dass ein Betreuer hilfreich ist.

Einverstanden. Ich bin Hixie dankbar, dass er sich die Zeit nimmt, an dieser Diskussion teilzunehmen.

Ich verstehe Hooks nicht mehr, nachdem ich diesen Thread durchgelesen habe

Um fair zu sein, versucht dieses Thema explizit zu vermeiden, speziell über Hooks zu sprechen.
Hier geht es eher darum, das Problem zu erklären als die Lösung

Haben Sie das Gefühl, dass dies nicht gelingt?

Ich kann hier beide Seiten ( @rrousselGit und @Hixie) fühlen und wollte ein Feedback aus einer (meiner) Nutzungsseite / Perspektive des Flutter-Frameworks hinterlassen.

Der flutter_hooks Ansatz reduziert die Boilerplate ziemlich stark (nur von den hier gezeigten Beispielen, da wir solche Zustandskonfigurationen wiederverwenden können) und reduziert die Komplexität, indem nicht aktiv über das Initialisieren / Entsorgen von Ressourcen nachgedacht werden muss. Im Allgemeinen leistet es gute Arbeit, um den Entwicklungsfluss / die Geschwindigkeit zu verbessern und zu unterstützen ... obwohl es (subjektiv) nicht so gut in den "Kern" von Flutter selbst passt.

Wie mindestens >95% des Codes, den ich schreibe, ist die Build-Methode nur deklarativ, keine lokalen Variablen oder Aufrufe außerhalb des zurückgegebenen Widget-Unterbaums, der gesamte logische Teil befindet sich in diesen Zustandsfunktionen, um Ressourcen zu initialisieren, zuzuweisen und zu entsorgen und hinzuzufügen Hörer (in meinem Fall MobX-Reaktionen) und so logisches Zeug. Da dies auch größtenteils der Ansatz in Flutter selbst ist, fühlt es sich sehr heimisch an. Dies gibt Ihnen als Entwickler auch die Möglichkeit, immer explizit und offen zu sein, was Sie tun - es zwingt mich, solche Widgets immer in StatefulWidget umzuwandeln und ähnlichen Code in initState/dispose zu schreiben, aber es ist auch führt immer dazu, dass Sie direkt im verwendeten Widget genau aufschreiben, was Sie tun möchten. Für mich persönlich, wie @Hixie bereits selbst erwähnt hat, stört es mich in keiner Weise, diese Art von Boilerplate-Code zu schreiben und erlaubt mir als Entwickler zu entscheiden, wie ich damit anstatt mich auf etwas wie flutter_hooks , es für mich zu tun und nicht zu verstehen, warum sich etwas so verhält, wie es sich verhält. Das Extrahieren von Widgets in kleinen Bits stellt auch sicher, dass diese Art von Boilerplate genau dem Anwendungsfall entspricht, für den sie verwendet wird. Bei flutter_hooks müsste ich noch darüber nachdenken, welche Zustände es wert sind, als Hook geschrieben und daher wiederverwendet zu werden - verschiedene Geschmacksrichtungen könnten entweder zu verschiedenen "Single"-Use-Hooks oder gar keinen Hooks führen, da ich es könnte Konfigurationen nicht zu oft wiederverwenden, sondern eher benutzerdefinierte Konfigurationen schreiben.

Versteh mich nicht falsch, der Ansatz in solchen Hooks scheint sehr nett und nützlich zu sein, aber für mich fühlt es sich wie ein sehr grundlegendes Konzept an, das das Kernkonzept verändert, wie man damit umgeht. Es fühlt sich als Paket selbst sehr gut an, Entwicklern die Möglichkeit zu geben, diese Art von Ansatz zu verwenden, wenn sie nicht damit zufrieden sind, wie man es "nativ" macht, aber es wäre zumindest sauber, es Teil des Flutter-Frameworks selbst zu machen / vereinheitlicht, dazu führen, dass entweder große Teile von Flutter neu geschrieben werden, um dieses Konzept zu nutzen (viel Arbeit) oder es für zukünftige / ausgewählte Dinge verwenden (was bei so gemischten Ansätzen verwirrend sein könnte).

Wenn es in das Flutter-Framework selbst integriert und unterstützt / aktiv genutzt würde, würde ich natürlich darauf hüpfen. Da ich den aktuellen Ansatz verstehe und sogar mag und die (möglichen) Maßnahmen sehe, die erforderlich sind, um dies nativ umzusetzen, kann ich das Zögern und / oder warum es nicht getan werden sollte, verstehen und es lieber als Paket behalten.

Korrigiert mich, wenn ich falsch liege, aber in diesem Thread geht es um die Probleme der Wiederverwendung von Zustandslogik in mehreren Widgets auf lesbare und zusammensetzbare Weise. Nicht speziell Haken. Ich glaube, dieser Thread wurde wegen des Wunsches eröffnet, eine Diskussion über das Problem mit einem offenen Ansatz zu führen, was die Lösung sein sollte.

Hooks werden jedoch erwähnt, da sie eine Lösung sind und ich glaube, dass @rrousselGit sie hier verwendet hat, um zu versuchen, das Problem / Problem zu erklären, das sie lösen (da sie eine Lösung sind), damit eine andere Lösung gefunden werden könnte, die möglicherweise eher für Flattern

Wie gesagt, ich weiß im Moment nicht, wohin der Thread führt.
Ich denke, das Problem existiert wirklich. Oder diskutieren wir das?
Wenn wir uns alle einig sind, dass es heute schwierig ist, Zustandslogik auf eine zusammensetzbare Weise in mehreren Widgets mit dem Kern von Flatter wiederzuverwenden, welche Lösungen gibt es dann, die eine Kernlösung sein könnten? da Bauherren wirklich sind (um zu zitieren)

deutlich weniger lesbar

Die Eigenschaftslösung scheint nicht so leicht wiederverwendbar zu sein oder ist das eine falsche Schlussfolgerung, die ich gezogen habe (?), da es keine Antwort darauf gab, wie man sie verwendet, um:

ein sekundäres Widget erstellen, das den Code, der sich derzeit in _ExampleState befindet, in einem anderen Widget wiederverwendet?
Mit einer Wendung: Dieses neue Widget sollte seine Benutzer-ID intern innerhalb seines Status verwalten

Ich wäre bereit, mit einem Designdokument zu helfen, wie

Ich stehe der Idee, ein Designdokument zu erstellen, im Moment ziemlich skeptisch gegenüber.
Es ist offensichtlich, dass @Hixie derzeit dagegen ist, dieses Problem direkt in Flutter zu lösen.

Für mich sieht es so aus, als ob wir uns über die Bedeutung des Problems und die Rolle von Google bei der Lösung dieses Problems nicht einig sind.
Wenn sich beide Seiten nicht einig sind, sehe ich keine produktive Diskussion über die Lösung dieses Problems – wie auch immer die Lösung aussehen mag.

Dieser Thementhread war eine sehr interessante Lektüre und ich freue mich zu sehen, dass der Meinungsaustausch zivil geblieben ist. Ich bin jedoch etwas überrascht über die derzeitige Sackgasse.

Wenn es um Hooks geht, bin ich der Meinung, dass Flutter zwar nicht unbedingt die von @rrousselGit vorgestellte spezifische Hooks-Lösung

In Ermangelung anderer praktikabler Vorschläge, die die gleichen Fähigkeiten bieten, und angesichts der Tatsache, dass Hooks sich in React bewährt haben und es eine bestehende Lösung für Flutter gibt, auf der sie basieren könnte, glaube ich nicht, dass dies der Fall wäre eine schlechte Wahl, dies zu tun. Ich glaube nicht, dass das Hooks-Konzept als Primitiv, das auch im Flutter-SDK (oder sogar darunter) enthalten ist, Flutter etwas nehmen würde. Meiner Meinung nach würde es es nur bereichern und es noch einfacher machen, mit Flutter wartbare und ansprechende Apps zu entwickeln.

Obwohl das Argument, dass Hooks als Paket für diejenigen verfügbar ist, die seine Vorteile nutzen wollen, ein gültiger Punkt ist, denke ich, dass es für ein Primitiv wie Hooks nicht optimal ist. Hier ist warum.

Wenn wir sogar intern wiederverwendbare Pakete erstellen, diskutieren wir sehr oft, ob das Paket „rein“ sein muss, in dem Sinne, dass es möglicherweise nur vom Dart+Flutter SDK abhängt, oder ob wir einige andere Pakete darin zulassen und wenn ja, welche Einsen. Sogar Provider ist auf „reine“ Pakete aus, wird aber oft für höherstufige Pakete zugelassen. Bei einer App gibt es auch immer die gleiche Debatte, welche Pakete in Ordnung sind und welche nicht. Provider ist grün, aber so etwas wie Hooks ist immer noch ein Fragezeichen als Paket.

Wenn eine Hooks-ähnliche Lösung Teil des SDK wäre, wäre es eine naheliegende Wahl, die Möglichkeiten zu nutzen, die es bietet. Während ich Hooks verwenden und bereits jetzt als Paket zulassen möchte, bin ich auch besorgt, dass es einen Flutter-Codestil erstellt und Konzepte einführt, die Flutter-Entwicklern, die ihn nicht verwenden, möglicherweise nicht vertraut sind. Es fühlt sich ein bisschen wie eine Weggabelung an, wenn wir diesen Weg ohne Unterstützung im SDK gehen. Für kleinere persönliche Projekte ist es eine einfache Wahl, Hooks zu verwenden. Ich empfehle, es zusammen mit Riverpod auszuprobieren.

(Unser Paketkonservatismus kommt vermutlich davon, dass er in der Vergangenheit von Paketen und Abhängigkeiten von anderen Paketmanagern verbrannt wurde, wahrscheinlich nicht einzigartig.)

Ich sage nicht, dass Hooks der einzige Weg sind, das aktuelle Problem zu lösen, auch wenn es bisher die einzige funktionierende demonstrierte Lösung ist. Es könnte sicherlich interessant und ein gültiger Ansatz sein, Optionen auf einer allgemeineren Ebene zu untersuchen, bevor Sie sich auf eine Lösung festlegen. Dazu muss erkannt werden, dass das Flutter SDK _derzeit einen Fehler in Bezug auf einfach wiederverwendbare Zustandslogik aufweist_, den es trotz ausführlicher Erklärungen derzeit nicht zu geben scheint.

Für mich gibt es zwei Hauptgründe, Hooks nicht einfach in das Core-Framework zu integrieren. Der erste ist, dass die API gefährliche Fallen enthält. In erster Linie, wenn Sie die Hooks irgendwie in der falschen Reihenfolge aufrufen, werden die Dinge kaputt gehen. Das scheint mir ein fatales Problem zu sein. Ich verstehe, dass Sie dies mit Disziplin und Befolgung der Dokumentation vermeiden können, aber IMHO hätte eine gute Lösung für dieses Code-Wiederverwendungsproblem diesen Fehler nicht.

Der zweite ist, dass es wirklich keinen Grund geben sollte, nicht einfach Hooks (oder eine andere Bibliothek) zu verwenden, um dieses Problem zu lösen. Nun, speziell mit Hooks funktioniert das nicht, wie die Leute besprochen haben, weil das Schreiben des Hooks mühsam genug ist, dass die Leute sich wünschen, dass nicht verwandte Bibliotheken Hooks unterstützen würden. Aber ich denke, eine gute Lösung für dieses Problem würde das nicht brauchen. Eine gute Lösung würde für sich allein stehen und nicht jede andere Bibliothek benötigen, die davon weiß.

Wir haben dem Framework kürzlich RestorableProperties hinzugefügt. Es wäre interessant zu sehen, ob sie hier irgendwie genutzt werden könnten...

Ich stimme @Hixie zu, dass die API versteckte Probleme hat, für deren Lösung ein Analysator oder Linter erforderlich ist. Ich denke, dass wir, wie alle, die mitmachen wollen, verschiedene Lösungen prüfen sollten, vielleicht über den Vorschlag der Design-Dokumentation oder auf andere Weise zum Problem des wiederverwendbaren Lebenszyklusmanagements. Im Idealfall wäre es mehr Flutter-spezifisch und nutzt Flutter-APIs, während es gleichzeitig die Probleme löst, die die Hook-API macht. Ich denke, die Vue-Version ist ein gutes Startmodell, wie ich bereits erwähnt habe, da sie nicht von der Hook-Call-Reihenfolge abhängt. Hat noch jemand Interesse, mit mir zu recherchieren?

@Hixie, aber Sie stimmen dem Problem zu, dass es keine gute Möglichkeit gibt, die Zustandslogik auf zusammensetzbare Weise zwischen Widgets wiederzuverwenden? Aus diesem Grund haben Sie angefangen, darüber nachzudenken, ResuableProperties irgendwie zu nutzen?

Für mich gibt es zwei Hauptgründe, Hooks nicht einfach in das Core-Framework zu integrieren. Der erste ist, dass die API gefährliche Fallen enthält. In erster Linie, wenn Sie die Hooks irgendwie in der falschen Reihenfolge aufrufen, werden die Dinge kaputt gehen. Das scheint mir ein fatales Problem zu sein. Ich verstehe, dass Sie dies mit Disziplin und Befolgung der Dokumentation vermeiden können, aber IMHO hätte eine gute Lösung für dieses Code-Wiederverwendungsproblem diesen Fehler nicht.

Nachdem ich mit Hooks gearbeitet habe und mit anderen Leuten gearbeitet habe, die Hooks verwenden, ist dies IMHO wirklich kein so großes Problem. Und überhaupt nicht im Vergleich zu all den großen Vorteilen (große Gewinne in der Entwicklungsgeschwindigkeit, Wiederverwendbarkeit, Zusammensetzbarkeit und leicht lesbarem Code), die sie mit sich bringen.
Ein Hook ist ein Hook, so wie eine Klasse eine Klasse ist, nicht nur eine Funktion, und Sie können sie nicht bedingt verwenden. Das lernt man schnell. Und Ihr Redakteur kann auch bei diesem Problem helfen.

Der zweite ist, dass es wirklich keinen Grund geben sollte, nicht einfach Hooks (oder eine andere Bibliothek) zu verwenden, um dieses Problem zu lösen. Nun, speziell mit Hooks funktioniert das nicht, wie die Leute besprochen haben, weil das Schreiben des Hooks mühsam genug ist, dass die Leute sich wünschen, dass nicht verwandte Bibliotheken Hooks unterstützen würden. Aber ich denke, eine gute Lösung für dieses Problem würde das nicht brauchen. Eine gute Lösung würde für sich allein stehen und nicht jede andere Bibliothek benötigen, die davon weiß.

Schreibhaken sind nicht lästig.
Es ist immer noch einfacher als die jetzt verfügbaren Lösungen IMHO (um diesen Ausdruck noch einmal zu verwenden 😉).
Vielleicht interpretiere ich das was du schreibst falsch. Aber ich glaube das hat noch keiner gesagt?
Ich lese es so, als ob die Leute alle Vorteile, die die Hakenlösung mit sich bringt, wirklich schätzen und sich wünschen, sie könnten sie überall verwenden. Um alle Vorteile zu nutzen. Da ein Hook wiederverwendbar ist, wäre es großartig, wenn Entwickler von Drittanbietern sicher sein könnten, ihre eigenen Hooks zu programmieren und auszuliefern, ohne dass jeder seine eigenen Wrapper schreiben muss. Profitieren Sie von der Wiederverwendbarkeit der Zustandslogik.
Ich denke, @rrousselGit und @gaearon haben die primitive Sache bereits erklärt. Darauf gehe ich also nicht ein.
Vielleicht verstehe ich diese Aussage nicht, weil ich nicht sehen kann, dass sie eine gute Zusammenfassung dessen ist, was die Leute in diesem Thread geschrieben haben. Es tut mir Leid.

Hoffe es gibt einen Weg nach vorne. Aber ich denke, es ist an der Zeit, zumindest zuzustimmen, dass dies ein Problem ist, und entweder alternative Lösungen zu finden, die besser sind, da Hooks nicht einmal auf dem Tisch zu sein scheinen.
Oder entscheiden Sie sich einfach, das Problem im Flatterkern zu beheben.

Wer entscheidet über den weiteren Weg?
Was ist der nächste Schritt?

Das scheint mir ein fatales Problem zu sein. Ich verstehe, dass Sie dies mit Disziplin und Befolgung der Dokumentation vermeiden können, aber IMHO hätte eine gute Lösung für dieses Code-Wiederverwendungsproblem diesen Fehler nicht.

In React lösen wir dies mit einem Linter – einer statischen Analyse. Unserer Erfahrung nach war dieser Fehler nicht einmal in einer großen Codebasis wichtig. Es gibt andere Probleme, die wir als Fehler betrachten könnten, aber ich wollte nur darauf hinweisen, dass das Vertrauen auf die beständige Anrufreihenfolge zwar das ist, was die Leute intuitiv für ein Problem halten, aber in der Praxis sieht die Balance ziemlich anders aus.

Der wahre Grund, warum ich diesen Kommentar schreibe, ist jedoch, dass Flutter eine kompilierte Sprache verwendet. "Linting" ist nicht optional. Wenn es also eine Übereinstimmung zwischen der Hostsprache und dem UI-Framework gibt, ist es definitiv möglich, zu erzwingen, dass das "bedingte" Problem nie statisch auftritt. Das funktioniert aber nur, wenn das UI-Framework Sprachänderungen motivieren kann (zB Compose + Kotlin).

@Hixie, aber Sie stimmen dem Problem zu, dass es keine gute Möglichkeit gibt, die Zustandslogik auf zusammensetzbare Weise zwischen Widgets wiederzuverwenden? Aus diesem Grund haben Sie angefangen, darüber nachzudenken, ResuableProperties irgendwie zu nutzen?

Es ist sicherlich etwas, das die Leute hervorgebracht haben. Damit habe ich keine viszerale Erfahrung. Es ist kein Problem, das ich beim Schreiben meiner eigenen Apps mit Flutter empfunden habe. Das bedeutet jedoch nicht, dass es für manche Leute kein echtes Problem ist.

Da ein Hook wiederverwendbar ist, wäre es großartig, wenn Entwickler von Drittanbietern sicher sein könnten, ihre eigenen Hooks zu programmieren und auszuliefern, ohne dass jeder seine eigenen Wrapper schreiben muss

Mein Punkt ist, dass eine gute Lösung hier nicht erfordern würde, dass jemand Wrapper schreibt.

Was ist der nächste Schritt?

Es gibt viele nächste Schritte, zum Beispiel:

  • Wenn es spezielle Probleme mit Flutter gibt, über die wir hier nicht gesprochen haben, melden Sie die Probleme und beschreiben Sie die Probleme.
  • Wenn Sie eine gute Idee haben, wie Sie das Problem dieses Problems besser als Hooks lösen können, erstellen Sie ein Paket, das dies tut.
  • Wenn es Dinge gibt, die getan werden können, um Hooks zu verbessern, tun Sie dies.
  • Wenn Probleme mit Flutter auftreten, die Hooks daran hindern, sein volles Potenzial auszuschöpfen, archivieren Sie diese als neue Probleme.
    usw.

Dieser Thementhread war eine sehr interessante Lektüre und ich freue mich zu sehen, dass der Meinungsaustausch zivil geblieben ist.

Ich würde es hassen zu sehen, wie ein unziviler Thread dann aussieht. Es gibt so sehr wenig Empathie in diesem Thread, dass es schwer war, ihn von der Seitenlinie aus zu lesen und zu verfolgen

Mein Punkt ist, dass eine gute Lösung hier nicht erfordern würde, dass jemand Wrapper schreibt.

Sie müssen jedoch keine Wrapper schreiben. Vielleicht möchten Sie jedoch die Vorteile und die Wiederverwendbarkeit Ihres eigenen Codes nutzen, an den Sie sich gewöhnt haben. Sie können die Bibliotheken sicher weiterhin so verwenden, wie sie sind. Wenn Sie (wenn möglich) Hook-Wrap-Zeug schreiben, liegt das wahrscheinlich nicht daran, dass Sie denken, dass es eine Last ist, sondern dass es besser ist als die Alternative.

Das ist eigentlich ein guter Grund und ein Grund, warum eine Lösung des Problems in diesem Thread im Kern großartig wäre. Eine wiederverwendbare, zusammensetzbare Zustandslogik-Lösung im Kern würde bedeuten, dass Benutzer keine Wrapper schreiben müssten, da eine solche wiederverwendbare Logik sicher in allen Paketen ausgeliefert werden könnte, ohne Abhängigkeiten hinzuzufügen.

Eine wiederverwendbare, zusammensetzbare Zustandslogik-Lösung im Kern würde bedeuten, dass Benutzer keine Wrapper schreiben müssten, da eine solche wiederverwendbare Logik sicher in allen Paketen ausgeliefert werden könnte, ohne Abhängigkeiten hinzuzufügen.

Der Punkt, den ich zu machen versuche, ist, dass IMHO eine gute Lösung nicht erfordern würde, dass _jemand_ diese Logik schreibt. Es gäbe einfach keine redundante Logik zur Wiederverwendung. Wenn Sie sich beispielsweise das Beispiel "fetchUser" von vorhin ansehen, müsste niemand einen Hook oder ein Äquivalent schreiben, um die Funktion "fetchUser" aufzurufen, Sie würden einfach die Funktion "fetchUser" direkt aufrufen. In ähnlicher Weise müsste "fetchUser" nichts über Hooks wissen (oder was auch immer wir verwenden) und Hooks (oder was auch immer wir verwenden) müssten nichts über "fetchUser" wissen. Und das alles, während Sie die Logik, die Sie schreiben, trivial halten, wie es bei Hooks der Fall ist.

Die aktuellen Einschränkungen werden dadurch verursacht, dass Hooks ein Patch über den Sprachbeschränkungen sind.

In manchen Sprachen sind Hooks ein Sprachkonstrukt, wie zum Beispiel:

state count = 0;

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

Dies wäre eine Variante von async /sync- Funktionen, die einen bestimmten Status über Aufrufe hinweg beibehalten können.

Es erfordert keine bedingungslose Verwendung mehr, da wir als Teil der Sprache jede Variable nach ihrer Zeilennummer und nicht nach ihrem Typ unterscheiden können.

Ich möchte hinzufügen, dass die Hooks-Beschränkungen den --track-widget-creation-Beschränkungen ähnlich sind.

Dieses Flag unterbricht die Kanonalisierung des const-Konstruktors für Widgets. Aber das ist kein Problem, da Widgets deklarativ sind.

In diesem Sinne sind Haken gleich. Die Beschränkungen spielen keine Rolle, da sie deklarativ manipuliert werden.
Wir werden keinen ganz bestimmten Haken erhalten, ohne die anderen zu lesen.

Vielleicht ist das fetchuser-Beispiel nicht das ideale.
Aber useStream, useAnimstion oder useStreamCintroller machen den Widget-Baum viel sauberer und verhindern, dass Sie vergessen, zu entsorgen oder sich um dudChangeDependencues zu kümmern.
Daher hat der aktuelle Weg seine Fallen, in die Sie sich verfangen können. Ich denke also, das potenzielle Problem mit der Anrufsequenz ist nicht größer als das.
Ich bin mir nicht sicher, ob ich anfangen würde, meine eigenen Hooks zu schreiben, aber es wäre schön, eine Sammlung von oft benötigten einsatzbereiten innerhalb des Frameworks zu haben.
Es wäre nur eine alternative Möglichkeit, mit ihnen umzugehen.

@Hixie , tut mir wirklich leid, dass ich nicht begreifen kann, was du versuchst zu beschreiben, ich beschuldige, dass es hier spät am Abend ist, aber wahrscheinlich bin es nur ich 😳.. die Zustands-Geschäftslogik und die Lebenszeit-Ereignislogik, die die Lösung des Problems so umschließt/kapselt, dass sie leicht zusammengestellt und zwischen Widgets geteilt werden kann? Könnten Sie etwas erläutern, was eine gute Lösung bewirkt und wie sie Ihrer Meinung nach idealerweise funktionieren würde?

Ich möchte hier nur ein wenig einwerfen, da es in dieser Diskussion Erwähnungen über die Höflichkeit gibt. Ich persönlich finde nicht, dass hier irgendjemand unhöflich ist.

Trotzdem denke ich, es ist erwähnenswert, dass dies ein Thema ist, das den Menschen auf allen Seiten sehr am Herzen liegt.

  • @rrousselGit beantwortet seit Jahren Anfängerfragen zum Zustandsmanagement auf StackOverflow und zum package:provider Issue Tracker. Ich verfolge nur letztere dieser Feuerwehrschläuche und habe nur Respekt vor Remis Fleiß und Geduld.
  • @Hixie und andere legen großen Wert auf die API von Flutter, ihre Stabilität, Oberfläche, Wartbarkeit und Lesbarkeit. Dank dessen ist Flutters Entwicklererfahrung dort, wo sie heute ist.
  • Flutter-Entwickler legen großen Wert auf das Zustandsmanagement, da sie einen erheblichen Teil ihrer Entwicklungszeit damit verbringen.

Es ist klar, dass alle Parteien in dieser Diskussion einen guten Grund haben, für ihr Handeln zu argumentieren. Es ist auch verständlich, dass es Zeit braucht, bis die Botschaften rüberkommen.

Ich würde mich freuen, wenn die Diskussion hier oder in anderer Form fortgesetzt würde. Wenn ich irgendwie helfen kann, zum Beispiel mit einem formellen Dokument, lass es mich wissen.

Wenn die Leute andererseits der Meinung sind, dass die Diskussion hier aus dem Ruder läuft, dann lassen Sie uns innehalten und sehen, ob es eine bessere Art der Kommunikation gibt.

(Separat möchte ich @gaearon für die Teilnahme an dieser Diskussion

@emanuel-lundman

Aber wo würden sich in der von Ihnen beschriebenen guten Lösung die Zustandswerte, die Zustands-Geschäftslogik und die Lebenszeit-Ereignislogik befinden, die die Lösung des Problems umschließt/einkapselt, damit sie leicht zusammengestellt und zwischen Widgets geteilt werden kann? Könnten Sie etwas erläutern, was eine gute Lösung bewirkt und wie sie Ihrer Meinung nach idealerweise funktionieren würde?

Kann ich leider nicht näher ausführen, da ich es nicht weiß. :-)

@escamoteur

Vielleicht ist das fetchuser-Beispiel nicht das ideale.
Aber useStream, useAnimstion oder useStreamCintroller machen den Widget-Baum viel sauberer und verhindern, dass Sie vergessen, zu entsorgen oder sich um dudChangeDependencues zu kümmern.

Eine der Schwierigkeiten in dieser Ausgabe war das "Verschieben der Torpfosten", wo ein Problem beschrieben wird, dann wird das Problem bei der Analyse als nicht das eigentliche Problem abgetan und ein neues Problem beschrieben und so weiter. Was wirklich nützlich sein könnte, wäre, einige kanonische Beispiele zu finden, zB eine Demo-Flutter-Anwendung, die ein echtes Beispiel des Problems enthält, eines, das der Darstellung halber nicht übermäßig vereinfacht ist. Dann könnten wir das mit Hooks und anderen Vorschlägen neu implementieren und wirklich konkret gegeneinander bewerten. (Ich würde dies selbst tun, außer ich verstehe nicht genau, was das Problem ist, daher ist es wahrscheinlich am besten, wenn jemand, der sich für Hooks einsetzt, es tun würde.)

Was wirklich nützlich sein könnte, wäre, einige kanonische Beispiele zu finden, z

Was halten Sie von dem Beispiel, das ich hier gegeben habe? https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

Dies ist ein Code-Snippet aus der realen Welt.

Ich denke, das wäre ein toller Anfang. Können wir es in einen Zustand versetzen, in dem es als eigenständige App ausgeführt wird, mit einer Version, die keine Hooks verwendet, und einer Version, die dies tut?

Sorry, ich meinte als Code-Schnipsel, nicht als App.

Ich denke, eines der Probleme mit der Idee der "Demo Flutter-Anwendung" ist, dass die Beispiele in diesem Thread sehr real sind.
Sie sind nicht zu stark vereinfacht.
Der Hauptanwendungsfall von Hooks ist die Faktorisierung von Mikrozuständen wie Debounce, Event-Handler, Abonnements oder implizite Nebeneffekte – die miteinander kombiniert werden, um nützlichere Logiken zu erzielen.

Ich habe einige Beispiele auf Riverpod, wie zum Beispiel https://marvel.riverpod.dev/#/ wo der Quellcode hier ist: https://github.com/rrousselGit/river_pod/tree/master/examples/marvel/lib
Aber das wird sich nicht viel von dem unterscheiden, was bisher erwähnt wurde.

@Hixie

Ich habe wirklich Schwierigkeiten zu verstehen, warum das ein Problem ist. Ich habe viele Flutter-Anwendungen geschrieben, aber es scheint wirklich kein so großes Problem zu sein? Selbst im schlimmsten Fall sind es vier Zeilen, um eine Eigenschaft zu deklarieren, zu initialisieren, zu entsorgen und an die Debug-Daten zu melden (und tatsächlich sind es normalerweise weniger, da Sie sie normalerweise in derselben Zeile deklarieren können, in der Sie sie initialisieren, Apps im Allgemeinen müssen Sie sich nicht darum kümmern, den Debug-Eigenschaften einen Status hinzuzufügen, und viele dieser Objekte haben keinen Status, der verworfen werden muss).

Ich sitze im selben Boot.
Ich gebe zu, ich verstehe die hier beschriebenen Probleme auch nicht wirklich. Ich verstehe nicht einmal, was die "Zustandslogik" ist, auf die sich die Leute beziehen, die wiederverwendbar sein muss.

Ich habe viele zustandsbehaftete Formular-Widgets, einige mit Dutzenden von Formularfeldern, und ich muss die Textcontroller und Focusnodes selbst verwalten. Ich erstelle und entsorge sie in den Lifecycle-Methoden des Statelesswidgets. Obwohl es ziemlich mühsam ist, habe ich kein Widget, das die gleiche Anzahl von Controllern / FocusNodes verwendet oder für den gleichen Anwendungsfall verwendet wird. Die einzige Gemeinsamkeit zwischen ihnen ist das allgemeine Konzept, zustandsbehaftet zu sein und eine Form zu sein. Nur weil es ein Muster ist, heißt das nicht, dass der Code wiederholt wird.
Ich meine, in vielen Teilen meines Codes muss ich Arrays durchlaufen.

Ich liebe die Leistungsfähigkeit des StatefulWidget, das aus der Einfachheit seiner Lebenszyklus-API resultiert. Es ermöglicht mir, StatefulWidgets zu schreiben, die eine Sache tun und dies isoliert vom Rest der App. Der "Zustand" meiner Widgets ist immer privat für sie selbst, daher ist die Wiederverwendung meiner Widgets kein Problem, ebenso wenig die Wiederverwendung von Code.

Ich habe ein paar Probleme mit den hier angeführten Beispielen, die etwas mit Ihren Punkten übereinstimmen:

  • Das Erstellen mehrerer zustandsbehafteter Widgets mit genau derselben "Zustandslogik" scheint einfach falsch und widerspricht der Idee, Widgets in sich geschlossen zu haben. Aber auch hier bin ich verwirrt, was die Leute mit allgemeiner "Staatslogik" meinen.
  • Hooks scheinen nichts zu tun, was ich nicht bereits mit einfachen Dart- und grundlegenden Programmierkonzepten (wie Funktionen) tun kann.
  • die Probleme scheinen mit einem bestimmten Programmierstil verbunden oder verursacht zu sein, ein Stil, der den "wiederverwendbaren globalen Zustand" zu bevorzugen scheint.
  • Das Wegabstrahieren einiger Codezeilen riecht nach "vorzeitiger Codeoptimierung" und erhöht die Komplexität, um ein Problem zu lösen, das wenig bis gar nichts mit dem Framework und alles damit zu tun hat, wie die Leute es verwenden.

Dies ist deutlich weniger lesbar.

  • Wir haben 10 Einrückungsebenen – 12, wenn wir FilterBuilder , um die Filterlogik wiederzuverwenden
  • Die Filterlogik ist so wie sie ist nicht wiederverwendbar.

    • wir könnten versehentlich vergessen, die timer zu stornieren
  • die Hälfte der Methode build ist für den Leser nicht nützlich. The Builders lenkt uns von dem ab, was zählt
  • Ich habe gute 5 Minuten verloren, um zu verstehen, warum der Code aufgrund einer fehlenden Klammer nicht kompiliert wird

Ihr Beispiel ist eher ein Beispiel dafür, wie ausführlich Provider ist und warum der Missbrauch von InheritedWidgets für alles eine schlechte Sache ist, und nicht ein echtes Problem mit der StatelessWidget- und State-Lifecycle-API von Flutter.

@rrousselGit Entschuldigung, wenn ich nicht klar war. Der Vorschlag, den ich oben gemacht habe, war speziell, Vanilla Flutter-Anwendungen (mit StatefulWidget usw.) zu erstellen, die realistisch sind und die von Ihnen beschriebenen Probleme zeigen, damit wir dann Vorschläge basierend auf einer echten vollständigen Anwendung machen können. Die konkreten Beispiele, die wir hier besprochen haben, wie das Beispiel "fetchUser", endeten immer mit einer Diskussion nach dem Motto "Nun, Sie könnten diesen Fall so handhaben und es wäre einfach und würde keine Hooks benötigen" folgte von "na ja, das ist stark vereinfacht, in der realen Welt braucht man Hooks". Mein Punkt ist also, lassen Sie uns ein reales Beispiel erstellen, das wirklich Hooks _braucht_, das nicht zu stark vereinfacht ist, das die Schwierigkeiten bei der Wiederverwendung von Code zeigt, damit wir sehen können, ob es möglich ist, diese Probleme zu vermeiden, ohne neuen Code zu verwenden , oder ob wir neuen Code brauchen, und im letzteren Fall, ob er wie Hooks geformt werden muss oder ob wir eine noch bessere Lösung finden können.

Ich verstehe nicht einmal, was die "Zustandslogik" ist, auf die sich die Leute beziehen, die wiederverwendbar sein muss.

Meinetwegen
Eine Parallele zur Zustandslogik wäre die UI-Logik und was Widgets auf den Tisch bringen.

Wir könnten die Widget-Ebene technisch entfernen. In dieser Situation bleiben die RenderObjects übrig.

Zum Beispiel könnten wir einen minimalistischen Zähler haben:

var counter = 0;

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

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

Das ist nicht unbedingt komplex. Aber es ist fehleranfällig. Wir haben ein Duplikat für das counterLabel Rendering

Mit Widgets haben wir:

class _CounterState exends State {
  int counter = 0;

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

Das einzige, was dies tat, war die Faktorisierung der Text Logik, indem sie deklarativ gemacht wurde.
Es ist eine minimalistische Veränderung. Aber bei einer großen Codebasis ist das eine erhebliche Vereinfachung.

Haken machen genau das Gleiche.
Aber statt Text Sie benutzerdefinierte Hooks für die Zustandslogik. Dazu gehören Listener, Entprellen, HTTP-Anfragen, ...


Ihr Beispiel ist eher ein Beispiel dafür, wie ausführlich Provider ist und warum der Missbrauch von InheritedWidgets für alles eine schlechte Sache ist, und nicht ein echtes Problem mit der StatelessWidget- und State-Lifecycle-API von Flutter.

Dies hat nichts mit dem Anbieter zu tun (dieser Code verwendet schließlich keinen Anbieter).
Wenn überhaupt, hat es der Anbieter besser, weil er context.watch anstelle von Consumer .

Die Standardlösung wäre, Consumer durch ValueListenableBuilder zu ersetzen – was zu genau dem gleichen Problem führt.

Ich stimme zu @Hixie , ich denke, wir brauchen zwei Vergleiche nebeneinander, um die Wirksamkeit von nur Flutter im Vergleich zu Hooks zu beurteilen. Dies würde auch helfen, andere zu überzeugen, ob Hooks besser ist oder nicht, oder vielleicht ist eine andere Lösung noch besser, wenn die Vanilla-App mit dieser dritten Lösung erstellt wird. Dieses Vanilla-App-Konzept gibt es schon eine Weile, und Dinge wie TodoMVC zeigen den Unterschied zwischen verschiedenen Front-End-Frameworks, also ist es nicht unbedingt neu. Ich kann beim Erstellen dieser Beispiel-Apps helfen.

@satvikpendem
Ich wäre gerne bereit zu helfen.
Ich denke, die Beispiel-App im Repo flutter_hooks wahrscheinlich mehrere verschiedene Hooks und was sie einfacher machen / Probleme, die sie lösen, und wäre ein guter Ausgangspunkt.

Ich denke auch, dass wir auch einige der Beispiele und Ansätze verwenden könnten, die in dieser Ausgabe vorgestellt werden.

Update: Der Code ist hier, https://github.com/TimWhiting/local_widget_state_approaches
Ich bin mir nicht sicher, wie der richtige Name für das Repository lautet, also gehen Sie nicht davon aus, dass dies das Problem ist, das wir zu lösen versuchen. Ich habe die grundlegende Counter-App in Stateful und Hooks erstellt. Ich habe später heute Abend nicht viel Zeit, aber ich werde versuchen, weitere Anwendungsfälle hinzuzufügen, die das Problem veranschaulichen. Jeder, der dazu beitragen möchte, bitte um Zugang.

Die konkreten Beispiele, die wir hier besprochen haben, wie das Beispiel "fetchUser", endeten immer mit einer Diskussion nach dem Motto "Nun, Sie könnten diesen Fall so handhaben und es wäre einfach und würde keine Hooks benötigen" folgte von "na ja, das ist stark vereinfacht, in der realen Welt braucht man Hooks".

Ich bin nicht einverstanden. Ich glaube nicht, dass ich ein solches "Sie könnten diesen Fall so handhaben" gesehen habe und stimmte zu, dass der resultierende Code besser war als die Hook-Variante.

Mein Punkt war die ganze Zeit, dass wir zwar Dinge anders machen können, der resultierende Code jedoch fehleranfällig und/oder weniger lesbar ist.
Dies gilt auch für fetchUser

Haken machen genau das Gleiche.
Aber anstelle von Text Sie benutzerdefinierte Hooks für die Zustandslogik. Dazu gehören Listener, Entprellen, HTTP-Anfragen, ...

Nein, ich verstehe immer noch nicht, was diese allgemeine Zustandslogik sein soll. Ich meine, ich habe viele Widgets, die in ihren "initState/didUpdateDependency" -Methoden aus einer Datenbank lesen, aber ich kann keine zwei Widgets finden, die genau die gleiche Abfrage durchführen, daher ist ihre "Logik" nicht gleich.

Am Beispiel einer HTTP-Anfrage. Angenommen, ich habe irgendwo in meiner Serviceklasse ein "makeHTTPRequest(url, paramters)", das einige meiner Widgets verwenden müssen, warum sollte ich dann einen Hook verwenden, anstatt die Methode einfach direkt aufzurufen, wenn ich sie benötige? Wie unterscheidet sich die Verwendung von Hooks von einfachen Methodenaufrufen in diesem Fall?

Zuhörer. Ich habe keine Widgets, die auf die gleichen Dinge hören. Jedes meiner Widgets ist dafür verantwortlich, alles zu abonnieren, was es benötigt, und sicherzustellen, dass es sich abmeldet. Hooks mögen für die meisten Dinge syntaktischer Zucker sein, aber da meine Widgets nicht auf dieselbe Kombination von Objekten hören würden, müssten die Hooks irgendwie "parametrisiert" werden. Wie unterscheiden sich Hooks von einer einfachen alten Funktion?


Dies hat nichts mit dem Anbieter zu tun (dieser Code verwendet schließlich keinen Anbieter).
Wenn überhaupt, hat es der Anbieter besser, weil er context.watch anstelle von Consumer .

Hä? Ihr Gegenbeispiel zu dem, was Ihr HookWidget "ChatScreen" lösen soll, war folgendes:

<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) {

Inwiefern hat das nichts mit dem Anbieter zu tun? Ich bin verwirrt. Ich bin kein Experte für Provider, aber das scheint definitiv Code zu sein, der Provider verwendet.

Ich möchte darauf hinweisen, dass es in diesem Thema nicht um komplexe Zustände geht.
Es handelt sich um winzige Inkremente, die auf die gesamte Codebasis angewendet werden können.

Wenn wir mit dem Wert der hier gegebenen Beispiele nicht einverstanden sind, wird eine Anwendung nichts zur Konversation beitragen – da wir mit Hooks nichts tun können, was wir mit StatefulWidget nicht tun können.

Meine Empfehlung wäre, stattdessen Micro-Snippets wie ImplicitFetcher nebeneinander zu vergleichen und _objektiv_ anhand messbarer Metriken zu bestimmen, welcher Code besser ist, und dies für eine Vielzahl kleiner Snippets.


Inwiefern hat das nichts mit dem Anbieter zu tun? Ich bin verwirrt. Ich bin kein Experte für Provider, aber das scheint definitiv Code zu sein, der Provider verwendet.

Dieser Code stammt nicht von Provider, sondern von einem anderen Projekt, das keine InheritedWidgets verwendet.
Consumer Anbieters hat keinen provider Parameter.

Und wie bereits erwähnt, können Sie Consumer -> ValueListenableBuilder/StreamBuilder/BlocBuilder/Observer/... ersetzen.

in ihren "initState/didUpdateDependency" -Methoden, aber ich kann keine zwei Widgets finden, die genau die gleiche Abfrage machen, daher ist ihre "Logik" nicht gleich.

Die Zustandslogik, die wir wiederverwenden wollen, ist nicht "eine Abfrage durchführen", sondern "etwas tun, wenn sich x ändert". Das "Tue etwas" kann sich ändern, aber das "Wenn x sich ändert" ist üblich

Konkretes Beispiel:
Wir möchten vielleicht, dass ein Widget eine HTTP-Anfrage sendet, wenn sich die ID ändert.
Außerdem möchten wir die ausstehenden Anfragen mit der CancelableOperation von package:async stornieren.

Jetzt haben wir zwei Widgets, die genau dasselbe tun möchten, jedoch mit einer anderen HTTP-Anfrage.
Am Ende haben wir:

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();
}

Der einzige Unterschied ist, dass wir fetchUser mit fetchMessage geändert haben. Die Logik ist ansonsten zu 100% gleich. Aber wir können es nicht wiederverwenden, was fehleranfällig ist.

Mit Hooks könnten wir dies in einen useUnaryCancelableOperation Hook faktorisieren.

Was bedeutet, dass dies mit den gleichen zwei Widgets stattdessen tun würde:

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

VS

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

In diesem Szenario wird die gesamte Logik im Zusammenhang mit dem Stellen der Anforderung und dem Abbrechen gemeinsam verwendet. Uns bleibt nur noch ein bedeutsamer Unterschied, nämlich fetchUser vs. fetchMessage .
Wir könnten sogar ein Paket aus diesem useUnaryCancelableOperation , und jetzt kann es jeder in seiner App wiederverwenden.

Wenn wir mit dem Wert der hier gegebenen Beispiele nicht einverstanden sind, wird eine Anwendung nichts zur Konversation beitragen – da wir mit Hooks nichts tun können, was wir mit StatefulWidget nicht tun können.

Wenn das wirklich der Fall ist, sollten wir diesen Fehler wohl schließen, da wir die hier aufgeführten Beispiele bereits besprochen haben und sie nicht überzeugend waren. Ich würde die Situation jedoch wirklich gerne besser verstehen, und nach früheren Kommentaren zu diesem Fehler klang es so, als ob die Vorteile auf der Anwendungsebene liegen, daher schlage ich vor, dass wir Anwendungsbeispiele untersuchen.

Der einzige Unterschied besteht darin, dass wir fetchUser mit fetchMessage geändert haben. Die Logik ist ansonsten zu 100% gleich. Aber wir können es nicht wiederverwenden, was fehleranfällig ist.

Was ist fehleranfällig und was kann wiederverwendet werden? Eine ganz neue Abstraktionsebene und Klassenhierarchie zu implementieren, nur damit wir nicht drei Methoden in einer Klasse implementieren müssen, und das ist viel zu viel.

Auch hier gilt: Nur weil etwas ein allgemeines Muster ist, bedeutet dies nicht, dass Sie ein neues Feature dafür erstellen müssen. Außerdem können Sie in diesem Fall, wenn Sie sich wiederholenden Code reduzieren möchten, einfach die StatefulWidget*-Klasse erweitern und die initstate/didUpdateWidget-Methoden mit den gemeinsamen Bits überschreiben.

Mit Hooks könnten wir dies in einen useUnaryCancelableOperation Hook faktorisieren.

Was bedeutet, dass dies mit den gleichen zwei Widgets stattdessen tun würde:

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

VS

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

In diesem Szenario wird die gesamte Logik im Zusammenhang mit dem Stellen der Anforderung und dem Abbrechen gemeinsam verwendet. Uns bleibt nur noch ein bedeutsamer Unterschied, nämlich fetchUser vs. fetchMessage .
Wir könnten sogar ein Paket aus diesem useUnaryCancelableOperation erstellen, und jetzt kann es jeder in seiner App wiederverwenden.

Es tut mir leid, aber das ist ein klares Nein von mir. Abgesehen von der Tatsache, dass es nur eine geringe Menge an Coderedundanz einspart, ist das "Ausfaktorisieren" eines Codes, der konzeptionell zu den Lebenszyklusmethoden "initState" und "update" gehört, in die Build-Methode ein großes Nein.

Ich erwarte, dass meine Build-Methoden nur das Layout erstellen und sonst nichts. Das Einrichten und Abbauen von Abhängigkeiten gehört definitiv nicht in die Build-Methode, und ich bin sehr froh, den gleichen Codetyp explizit umschreiben zu müssen, um für mein zukünftiges Ich und andere klar zu machen, was mein Widget macht. Und lassen Sie uns nicht alles in die Build-Methode stecken.

Wenn das wirklich der Fall ist, sollten wir diesen Fehler wohl schließen

@Hixie Bitte nicht. Die Leute kümmern sich um dieses Thema. Ich habe mit dir auf reddit über dasselbe gesprochen , aber im Zusammenhang mit SwiftUI: https://github.com/szotp/SwiftUI_vs_Flutter

Es geht nicht um Hooks, sondern darum, zustandsbehaftete Widgets irgendwie zu verbessern. Die Leute hassen es einfach, Boilerplate zu schreiben. Für Entwickler, die SwiftUI-Code schreiben, die an RAII und das Kopieren der Semantik von Views gewöhnt sind, scheint die manuelle Verwaltung von Disposables gerade recht.

Daher ermutige ich das Flatter-Team, dies zumindest als Problem zu sehen und über alternative Lösungen / Verbesserungen nachzudenken.

Ich erwarte, dass meine Build-Methoden nur das Layout erstellen und sonst nichts. Das Auf- und Abbau von Abhängigkeiten gehört definitiv nicht in die Build-Methode,
Das ist ein wichtiger Punkt. Build-Methoden sollten rein sein. Trotzdem wünschte ich, wir könnten die Vorteile ohne die Schwierigkeiten haben

Ich verstehe den Drang nach mehr Beispielen hier wirklich nicht. Es ist klar im Gesicht.

Das durch Hooks gelöste Problem ist einfach und offensichtlich, es hält den Code DRY. Die Vorteile hiervon liegen auf der Hand, weniger Code == weniger Fehler, einfachere Wartung, weniger Versteckmöglichkeiten für Fehler, insgesamt eine geringere Zeilenanzahl erhöht die Lesbarkeit, Junior-Programmierer sind besser isoliert usw.

Wenn Sie von einem realen Anwendungsfall sprechen, handelt es sich um eine App, bei der Sie jedes Mal 12 Animator-Controller in 12 verschiedenen Ansichten einrichten und abreißen, die Tür offen lassen, um einen Dispose()-Aufruf zu verpassen oder einige zu überschreiben andere Lebenszyklusmethode. Wenden Sie dies dann auf Dutzende anderer zustandsbehafteter Instanzen an, und Sie sehen sich leicht Hunderte oder Tausende von sinnlosen Codezeilen an.

Flutter ist voll von diesen Fällen, in denen wir uns ständig wiederholen müssen, den Zustand von kleinen Objekten auf- und abzubauen, die alle möglichen Möglichkeiten für Fehler schaffen, die nicht existieren müssen, aber existieren, weil es derzeit keinen eleganten Ansatz dafür gibt Teilen dieser Routine-Setup/Teardown/Sync-Logik.

Sie können dieses Problem in buchstäblich _jedem_ Zustand sehen, der eine Einrichtungs- und Abbauphase hat oder einen Lebenszyklus-Hook hat, an den immer gebunden werden muss.

Ich selbst finde, dass die Verwendung von Widgets der beste Ansatz ist, ich benutze zum Beispiel selten AnimatorController, weil das Setup / Teardown so nervig, ausführlich und fehleranfällig ist, stattdessen benutze ich TweenAnimationBuilder, wo immer ich kann. Dieser Ansatz hat jedoch seine Grenzen, da Sie in einer bestimmten Ansicht zu einer höheren Anzahl von zustandsorientierten Objekten gelangen, was Verschachtelung und Ausführlichkeit erzwingt, wo wirklich keine erforderlich sein sollten.

@szotp Ich habe nicht ... Ich würde viel lieber eine oder mehrere Baseline-Apps einrichten, die das Problem demonstrieren, damit wir die Lösungen bewerten können. Ich würde es selbst tun, aber ich verstehe nicht genau, was wir zu lösen versuchen, also bin ich die falsche Person, um es zu tun.

@escamoteur Baseline-Apps würden uns helfen, Lösungen zu entwickeln, die diese Schwierigkeiten nicht haben.

@esDotDev Wir haben Fälle wie diesen in diesem Fehler bisher besprochen, aber jedes Mal werden andere Lösungen als Hooks verworfen, weil sie ein Problem nicht lösen, das nicht in dem Beispiel enthalten war, das die Lösung behandelt hat. Einfache Beschreibungen von Problemen scheinen daher nicht auszureichen, um das volle Ausmaß zu erfassen. Zum Beispiel könnte der Fall der "12 Animator-Controller" möglicherweise durch eine Reihe von Animations-Controllern und die Funktionsmerkmale in Dart gelöst werden. TweenAnimationBuilder könnte eine andere Lösung sein. Keines davon betrifft Hooks. Aber ich bin sicher, wenn ich das vorschlage, wird jemand auf etwas hinweisen, das ich übersehen habe, und sagen "es funktioniert nicht, weil ..." und dieses (neue, im Kontext des Beispiels) Problem ansprechen. Daher deckt die Notwendigkeit einer Basis-App, von der wir uns alle einig sind, die gesamte Ausbreitung des Problems ab.

Wenn jemand dies vorantreiben möchte, denke ich wirklich, dass der beste Weg, dies zu tun, das ist, was ich oben beschrieben habe (https://github.com/flutter/flutter/issues/51752#issuecomment-670249755 und https://github.com /flattern/flattern/issues/51752#issuecomment-670232842). Das wird uns einen Ausgangspunkt geben, von dem wir uns alle einig sind, dass er das Ausmaß des Problems darstellt, das wir zu lösen versuchen; dann können wir Lösungen entwickeln , dass Adresse diese Probleme in einer Weise , dass Adresse alle Wünsche (zB @rrousselGit ‚s Notwendigkeit für die Wiederverwendung, @Rudiksz‘ s Bedarf an sauberen Build Methoden, etc.), und vor allem wir diese Lösungen in der auswerten Kontext der Baseline-Apps.

Ich denke, wir könnten uns alle ziemlich leicht auf das Problem einigen:
_Es gibt keine elegante Möglichkeit, die Setup- / Teardown-Aufgaben für Dinge wie Streams, AnimatorController usw. zu teilen. Dies führt zu unnötiger Ausführlichkeit, Öffnungen für Fehler und reduzierter Lesbarkeit._

Ist damit jemand nicht einverstanden? Können wir nicht dort ansetzen und auf der Suche nach einer Lösung voranschreiten? Wir müssen uns zunächst darauf einigen, dass dies ein Kernproblem ist, das wir anscheinend immer noch nicht haben.

Während ich das schreibe, habe ich jedoch festgestellt, dass genau der Name des Themas übereinstimmt, das ein offenes Ende hat und Raum für Diskussionen lässt:
"Die Wiederverwendung von Zustandslogik ist entweder zu ausführlich oder zu schwierig "

Für mich ist das ein sehr offensichtliches Problem, und wir sollten schnell über die Debattenphase hinausgehen und darüber nachdenken, was funktionieren würde, wenn nicht Haken, was dann. Wir brauchen wiederverwendbare Mikrostaaten... Ich bin sicher, wir können etwas herausfinden. Es würde am Ende des Tages wirklich viele Flutter-Ansichten bereinigen und sie robuster machen.

@Hixie Bitte nicht. Die Leute kümmern sich um dieses Thema. Ich habe mit dir auf reddit über dasselbe gesprochen , aber im Zusammenhang mit SwiftUI: https://github.com/szotp/SwiftUI_vs_Flutter

Ihr SwiftUI-Beispiel kann in Dart in wenigen Codezeilen repliziert werden, indem Sie einfach die StatefulWidget-Klasse erweitern.

Ich habe StatefulWidgets, die keine Benachrichtigungen abonnieren und / oder externe Anrufe tätigen, und tatsächlich sind die meisten von ihnen so. Ich habe ungefähr 100 benutzerdefinierte Widgets (wenn auch nicht alle Stateful), und vielleicht 15 davon haben irgendeine Art von "gemeinsamer Zustandslogik", wie in den Beispielen hier beschrieben.

Auf lange Sicht ist das Schreiben einiger Codezeilen (auch Boilerplate genannt) ein kleiner Kompromiss, um unnötigen Overhead zu vermeiden. Und wieder scheint dieses Problem, die initState/didUpdate-Methoden implementieren zu müssen, viel zu überzogen. Wenn ich ein Widget erstelle, das eines der hier beschriebenen Muster verwendet, verbringe ich vielleicht zuerst 5-10 Minuten damit, die LifeCycle-Methoden zu "implementieren", und dann ein paar Tage, um das Widget selbst zu schreiben und zu polieren, ohne die Lifecycle-Methoden zu berühren. Die Zeit, die ich mit dem Schreiben des sogenannten Boilerplate-Setup/Teardown-Codes verbringe, ist im Vergleich zu meinem App-Code winzig.

Wie ich bereits sagte, macht die Tatsache, dass StatefulWidgets so wenige Annahmen über ihre Verwendung treffen, sie so leistungsfähig und effizient.

Das Hinzufügen eines neuen Widget-Typs zu Flutter, der StatefulWidget-Unterklassen (oder nicht) für diesen speziellen Anwendungsfall bildet, wäre in Ordnung, aber lassen Sie uns es nicht in das StatefulWidget selbst einbacken. Ich habe viele Widgets, die nicht den Overhead benötigen, der mit einem "Hooks" -System oder Mikrozuständen verbunden wäre.

@esDotDev Ich stimme zu, dass dies ein Problem ist, mit dem einige Leute konfrontiert sind. Ich habe sogar früher in dieser Ausgabe einige Lösungen vorgeschlagen (Suche nach meinen verschiedenen Versionen einer Property Klasse, könnte jetzt begraben sein, da GitHub nicht alle Kommentare anzeigen möchte). Die Schwierigkeit besteht darin, dass diese Vorschläge verworfen wurden, weil sie bestimmte Probleme nicht lösten (zB in einem Fall kein Hot-Reload, in einem anderen didUpdateWidget). Also habe ich weitere Vorschläge gemacht, aber dann wurden diese wieder abgewiesen, weil sie mit etwas anderem nicht fertig wurden (ich vergesse was). Aus diesem Grund ist es wichtig, eine Art Basislinie zu haben, von der wir uns einig sind, dass sie das _gesamte_ Problem darstellt, damit wir Lösungen für dieses Problem finden können.

Das Ziel hat sich nie geändert. Kritisiert wird, dass es den vorgeschlagenen Lösungen an Flexibilität mangelt. Keiner von ihnen arbeitet außerhalb des Snippets, für das sie implementiert wurden.

Deshalb steht in dieser Ausgabe im Titel „Schwierig“: Weil wir derzeit Probleme nicht flexibel lösen.


Eine andere Betrachtungsweise ist:

Dieses Problem argumentiert im Wesentlichen, dass wir eine Widget-Schicht für die Zustandslogik implementieren müssen.
Die vorgeschlagenen Lösungen sind "Aber Sie können es mit RenderObjects tun".

_Auf lange Sicht ist das Schreiben einiger Codezeilen (auch Boilerplate genannt) ein kleiner Kompromiss, um unnötigen Overhead zu vermeiden._

Paar Nissen mit dieser Aussage:

  1. Es sind nicht wirklich ein paar Zeilen, wenn Sie Klammern, Zeilenabstand @overides usw. berücksichtigen, können Sie nach 10-15+ Zeilen für einen einfachen Animator-Controller suchen. Das ist meiner Meinung nach nicht trivial... wie weit mehr als trivial. 3 Zeilen, um dies zu tun, nervt mich (in Unity ist es Thing.DOTween() ). 15 ist lächerlich.
  1. Es geht nicht ums Tippen, auch wenn das mühsam ist. Es geht um die Dummheit, eine 50-Zeilen-Klasse zu haben, von der 30 Zeilen aus reiner Boilerplate sind. Seine Verschleierung. Es geht um die Tatsache, dass, wenn Sie den Boilerplate _nicht_ schreiben, es keine Warnungen oder ähnliches gibt, Sie nur einen Fehler hinzugefügt haben.
  2. Ich sehe keinen Overhead, der es wert wäre, mit so etwas wie Hooks diskutiert zu werden. Wir sprechen von einem Array von Objekten mit einer Handvoll fxns auf jedem. In Dart, das sehr schnell ist. Dies ist ein Red Hering Imo.

@esDotDev

Für mich ist das ein sehr offensichtliches Problem, und wir sollten schnell über die Debattenphase hinausgehen und darüber nachdenken, was funktionieren würde, wenn nicht Haken, was dann.

Widgets erweitern. So wie ValueNotifier den ChangeNotifier erweitert, um ein allgemeines Nutzungsmuster zu vereinfachen, kann jeder seine eigenen Varianten von StatelessWidgets für seine spezifischen Anwendungsfälle schreiben.

Ja, ich stimme zu, dass dies ein effektiver Ansatz ist, aber er lässt zu wünschen übrig. Wenn ich 1 Animator habe, kann ich einfach einen TweenAnimationBuilder verwenden. Cool, es sind immer noch 5 Zeilen Code, aber was auch immer. es funktioniert... nicht so schlecht. Aber wenn ich 2 oder 3 habe? Jetzt bin ich in der Verschachtelungshölle, wenn ich aus irgendeinem Grund eine cpl andere zustandsbehaftete Objekte habe, ist alles irgendwie ein Durcheinander von Einrückungen, oder ich erstelle ein sehr spezifisches Widget, das eine zufällige Sammlung von Setup, Update und Teardown kapselt Logik.

Widgets erweitern. So wie ValueNotifier den ChangeNotifier erweitert, um ein allgemeines Nutzungsmuster zu vereinfachen, kann jeder seine eigenen Varianten von StatelessWidgets für seine spezifischen Anwendungsfälle schreiben.

Sie können jeweils nur eine Basisklasse erweitern. Das skaliert nicht

Mixins sind der nächste logische Versuch. Aber wie das OP erwähnt, skalieren sie auch nicht.

@esDotDev

oder ich erstelle ein sehr spezifisches Widget, das eine zufällige Sammlung von Setup-, Update- und Teardown-Logik kapselt.

Eine Art Widget, das 3-4 Arten von AnimationControllern einrichten muss, klingt nach einem sehr spezifischen Anwendungsfall und die Unterstützung einer zufälligen Sammlung von Setup- / Teardown-Logik sollte definitiv nicht Teil eines Frameworks sein. Tatsächlich werden deshalb die initState/didUpdateWidget-Methoden in erster Linie exponiert, damit Sie Ihre zufällige Sammlung von Setups nach Herzenslust durchführen können.

Meine längste initState-Methode besteht aus 5 Codezeilen, meine Widgets leiden nicht unter übermäßiger Verschachtelung, daher haben wir definitiv unterschiedliche Bedürfnisse und Anwendungsfälle. Oder einen anderen Entwicklungsstil.

@esDotDev

3. Ich sehe keinen Overhead, der es wert wäre, mit so etwas wie Hooks diskutiert zu werden. Wir sprechen von einem Array von Objekten mit einer Handvoll fxns auf jedem. In Dart, das sehr schnell ist. Dies ist ein Red Hering Imo.

Wenn die vorgeschlagene Lösung so etwas wie das flatter_hooks-Paket ist, ist das völlig falsch. Ja, konzeptionell ist es ein Array mit Funktionen darin, aber die Implementierung ist bei weitem nicht trivial oder effizient.

Ich meine, ich kann mich vielleicht irren, aber es scheint, als ob das HookElement prüft, ob es sich in seiner eigenen Build-Methode neu erstellen soll?!
Auch die Überprüfung, ob die Hooks bei jedem einzelnen Widget-Build initialisiert, neu initialisiert oder entsorgt werden sollten, scheint ein erheblicher Aufwand zu sein. Es fühlt sich einfach nicht richtig an, also hoffe ich, dass ich falsch liege.

Wäre es sinnvoll, eines der Architekturbeispiele von @brianegan als Basis-App zum Vergleich zu nehmen?

Wenn ich hier einwerfen darf, bin mir nicht sicher, ob das schon gesagt wurde. Aber in React denken wir nicht wirklich an den Lebenszyklus mit Hooks, und das mag beängstigend klingen, wenn Sie es gewohnt sind, Komponenten/Widgets zu erstellen, aber hier ist der Grund, warum der Lebenszyklus nicht wirklich wichtig ist.

In den meisten Fällen, wenn Sie Komponenten/Widgets mit Status oder Aktionen erstellen, die auf Requisiten basieren, möchten Sie, dass etwas passiert, wenn sich dieser Status/diese Eigenschaften ändern (z - die Details eines Benutzers abrufen, wenn sich die userId-Prop geändert hat). Es ist normalerweise viel natürlicher, dies als "Effekt" der Änderung der Benutzer-ID zu betrachten, anstatt als etwas, das passiert, wenn sich alle Requisiten des Widgets ändern.

Dasselbe gilt für die Bereinigung, es ist normalerweise viel natürlicher zu denken: "Ich muss diesen Zustand / Listener / Controller bereinigen, wenn sich diese Eigenschaft / dieser Zustand ändert" anstatt "Ich muss daran denken, X zu bereinigen, wenn sich alle Requisiten / Status ändern oder" wenn das gesamte Bauteil zerstört wird".

Ich habe Flutter schon eine Weile nicht mehr geschrieben, also versuche ich nicht so zu klingen, als ob ich das aktuelle Klima oder die Einschränkungen kenne, die dieser Ansatz auf die aktuelle Flutter-Denkweise haben würde, ich bin offen für unterschiedliche Meinungen. Ich denke nur, dass viele Leute, die nicht mit React-Hooks vertraut sind, die gleiche Verwirrung haben, die ich hatte, als ich ihnen vorgestellt wurde, weil meine Denkweise so tief im Lebenszyklus-Paradigma verwurzelt war.

@escamoteur @Rudiksz @Hixie es gab ein GitHub-Projekt, das von @TimWhiting erstellt wurde, zu dem ich eingeladen wurde, wo wir beginnen, diese Beispiele zu erstellen. Jede Person/Gruppe kann erstellen, wie sie ein vordefiniertes Problem lösen würde. Sie sind keine ausgewachsenen Apps, eher wie Seiten, aber wir können auch Apps hinzufügen, wenn sie dazu dienen, komplexere Beispiele zu zeigen. Dann können wir Probleme besprechen und eine bessere API erstellen. @TimWhiting kann jeden einladen, der daran interessiert ist,

https://github.com/TimWhiting/local_widget_state_approaches

Jetpack Compose hat auch ähnliche Hooks, die hier mit reagieren verglichen wurden .

@satvikpendem @TimWhiting Das ist großartig! Dankeschön.

@esDotDev
Ein sehr spezifischer Anwendungsfall und die Unterstützung einer zufälligen Sammlung von Setup-/Teardown-Logik sollten definitiv nicht Teil eines Frameworks sein.

Dies ist der Nagel, der Schläge auf den Kopf einhaken kann. Jeder Objekttyp ist für seinen eigenen Auf- und Abbau verantwortlich. Animatoren wissen, wie sie sich selbst erstellen, aktualisieren und zerstören, ebenso wie Streams und so weiter. Hooks löst speziell dieses Problem der "zufälligen Sammlungen" von staatlichen Gerüsten, die in Ihrer Ansicht verstreut sind. Dadurch kann sich der Großteil des Ansichtscodes auf die Geschäftslogik und die Layoutformatierung konzentrieren, was ein Gewinn ist. Es zwingt Sie nicht dazu, benutzerdefinierte Widgets zu erstellen, sondern nur, um einige generische Boilerplates auszublenden, die in jedem Projekt gleich sind.

Meine längste initState-Methode besteht aus 5 Codezeilen, meine Widgets leiden nicht unter übermäßiger Verschachtelung, daher haben wir definitiv unterschiedliche Bedürfnisse und Anwendungsfälle. Oder einen anderen Entwicklungsstil.

Meine auch. Aber es ist initState() + dispon() + didUpdateDependancies(), und das Fehlen einer der letzten 2 kann zu Fehlern führen.

Ich denke, das kanonische Beispiel wäre etwa: Schreiben Sie eine Ansicht, die 1 Streamcontroller und 2 Animator-Controller verwendet.

Sie haben, soweit ich das sehe, 3 Möglichkeiten:

  1. Fügen Sie Ihrer Klasse etwa 30 Zeilen Boilerplate und einige Mixins hinzu. Was nicht nur ausführlich ist, sondern anfangs ziemlich schwer zu befolgen ist.
  2. Verwenden Sie 2 TweenAnimationBuilder und einen StreamBuilder für etwa 15 Einrückungsebenen, bevor Sie überhaupt zu Ihrem Ansichtscode gelangen, und Sie haben immer noch viele Boilerplates für den Stream.
  3. Fügen Sie am Anfang von build() etwa 6 Zeilen nicht eingerückten Codes hinzu, um Ihre 3 zustandsbehafteten Unterobjekte zu erhalten, und definieren Sie benutzerdefinierten Init-/Destroy-Code

Vielleicht gibt es eine vierte Option, die ein SingleStreamBuilderDoubleAnimationWidget ist, aber dies ist nur ein Make-Work-Ding für Entwickler und im Allgemeinen ziemlich nervig.

Bemerkenswert ist auch, dass die kognitive Belastung von 3 für einen neuen Entwickler deutlich niedriger ist als die der anderen 2. Die meisten neuen Entwickler wissen nicht einmal, dass TweenAnimationBuilder existiert, und das Konzept von SingleTickerProvider einfach zu erlernen ist eine eigene Aufgabe. Einfach zu sagen: "Gib mir bitte einen Animator", ist ein einfacherer und robusterer Ansatz.

Ich werde heute versuchen, etwas zu codieren.

2. Verwenden Sie 2 TweenAnimationBuilder und einen StreamBuilder für etwa 15 Einrückungsebenen, bevor Sie überhaupt zu Ihrem Ansichtscode gelangen, und Sie haben immer noch viele Boilerplates für den Stream.

Rechts. Zeigen Sie uns ein reales Codebeispiel, das 15 Einrückungsebenen verwendet.

Wie reduziert das Ersetzen von 30 Codezeilen durch 6 Zeilen + Hunderte von Zeilen in einer Bibliothek die kognitive Belastung? Ja, ich kann die "Magie" der Bibliothek einfach ignorieren, aber nicht ihre Regeln. Zum Beispiel sagt Ihnen das Hooks-Paket unmissverständlich, dass Hooks nur in Build-Methoden verwendet werden dürfen. Jetzt müssen Sie sich um eine zusätzliche Einschränkung kümmern.

Ich habe wahrscheinlich weniger als 200 Zeilen Code, die Focusnodes, Textcontroller, Singletickerprovider oder die verschiedenen Lifecycle-Methoden meiner Statefulwidgets beinhalten, in einem Projekt mit 15k Codezeilen. Von welcher kognitiven Überlastung sprichst du?

@Rudiksz bitte hör auf, passiv aggressiv zu sein.
Wir können widersprechen, ohne zu kämpfen.


Hooks-Einschränkungen sind meine geringste Sorge.

Wir sprechen nicht speziell über Hooks, sondern über das Problem.
Wenn es sein muss, können wir eine andere Lösung finden.

Was zählt, ist das Problem, das wir lösen wollen.

Außerdem können Widgets nur innerhalb von Builds und bedingungslos verwendet werden (oder wir ändern die Baumtiefe anderweitig, was ein No-Go ist).

Das ist identisch mit den Hooks-Einschränkungen, aber ich glaube nicht, dass sich die Leute darüber beschwert haben.

Außerdem können Widgets nur innerhalb von Builds und bedingungslos verwendet werden (oder wir ändern die Baumtiefe anderweitig, was ein No-Go ist).

Das ist identisch mit den Hooks-Einschränkungen, aber ich glaube nicht, dass sich die Leute darüber beschwert haben.

Nein, es ist nicht identisch. Das hier vorgestellte Problem scheint mit Code zusammenzuhängen, der Widgets _vorbereitet_ bevor_ (neu) erstellt wird. Status, Abhängigkeiten, Streams, Controller usw. vorbereiten und Änderungen in der Baumstruktur behandeln. Nichts davon sollte in der build-Methode enthalten sein, selbst wenn es hinter einem einzelnen Funktionsaufruf verborgen ist.
Der Einstiegspunkt für diese Logik sollte niemals in der build-Methode liegen.

Mich dazu zu zwingen, Initialisierungslogik jeglicher Art in die Build-Methode einzufügen, ist nirgendwo dasselbe wie mich zu "zwingen", einen Widget-Baum in der Build-Methode zu erstellen. Der ganze Grund für die Build-Methode besteht darin, einen vorhandenen Zustand (Satz von Variablen) zu nehmen und einen Widget-Baum zu erstellen, der dann gezeichnet wird.

Umgekehrt wäre ich auch dagegen, mich dazu zu zwingen, Code hinzuzufügen, der Widgets innerhalb der initState/didUpdateWidget-Methoden erstellt.

So wie es jetzt ist, spielen Statefulwidget-Lebenszyklusmethoden eine sehr klare Rolle und machen es sehr einfach und unkompliziert, Code mit ganz anderen Anliegen zu trennen.

Konzeptionell beginne ich, die hier beschriebenen Probleme zu verstehen, aber ich sehe es immer noch nicht als tatsächliches Problem. Vielleicht können mir einige echte Beispiele (die nicht die Counter-App sind) helfen, meine Meinung zu ändern.

Als Randnotiz hat Riverpod , mein neuestes Experiment, einige sehr Hook-ähnliche Ideen, ohne die Einschränkungen.

Es löst zum Beispiel:

Consumer(
  provider: provider,
  builder: (context, value) {
    return Consumer(
      provider: provider2,
      builder: (context, value2) {
        return Text('$value $value2');
      },
    );
  },
)

indem:

Consumer(
  builder (context, watch) {
    final value = watch(provider);
    final value2 = watch(provider2);
  },
)

Wobei watch bedingt aufgerufen werden kann:

Consumer(
  builder: (context, watch) {
    final value = watch(provider);
    if (something) {
      final value2 = watch(provider2);
    }
  },
)

Wir könnten Consumer vollständig loswerden, indem wir eine benutzerdefinierte StatelessWidget / StatefulWidget Basisklasse haben:

class Example extends ConsumerStatelessWidget {
  <strong i="21">@override</strong>
  Widget build(ConsumerBuildContext context) {
    final value = context.watch(provider);
    final value2 = context.watch(provider2);
  }
}

Das Hauptproblem ist, dass dies spezifisch für eine Art von Objekt ist und auf der Tatsache beruht, dass die Objektinstanz über einen konsistenten HashCode über Neuaufbau hinweg verfügt.

Von der Flexibilität der Haken sind wir also noch weit entfernt

@rrousselGit Ich denke, ohne die Klassen StatelessWidget / StatefulWidget zu erweitern und etwas wie ConsumerStatelessWidget zu erstellen, ist es möglich, etwas wie context.watch indem Erweiterungsmethoden auf BuildContext -Klasse und die Bereitstellung der Überwachungsfunktion durch den Anbieter mit InheritedWidgets.

Das ist ein anderes Thema. Aber tl; dr, wir können uns nicht auf InheritedWidgets als Lösung für dieses Problem verlassen: https://github.com/flutter/flutter/issues/30062

Um dieses Problem zu lösen, würde uns die Verwendung von InheritedWidgets aufgrund von https://github.com/flutter/flutter/issues/12992 und https://github.com/flutter/flutter/pull/33213 blockieren

Konzeptionell beginne ich, die hier beschriebenen Probleme zu verstehen, aber ich sehe es immer noch nicht als tatsächliches Problem.

Wenn ich Flutter mit SwiftUI vergleiche, ist es für mich offensichtlich, dass es ein tatsächliches Problem gibt, oder besser gesagt - die Dinge sind nicht so großartig, wie sie sein könnten.

Es mag schwer zu erkennen sein, weil Flutter und andere hart daran gearbeitet haben: Wir haben Wrapper für jeden speziellen Fall: AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity usw. StatefulWidget funktioniert hervorragend für die Implementierung dieser kleinen wiederverwendbaren Dienstprogramme, aber es ist einfach zu niedrig für nicht wiederverwendbare, domänenspezifische Komponenten, bei denen Sie möglicherweise über eine Vielzahl von Textcontrollern, Animationen oder was auch immer die Geschäftslogik erfordert, verfügen. Die übliche Lösung besteht darin, entweder in den sauren Apfel zu beißen und all diese Boilerplates zu schreiben oder einen sorgfältig aufgebauten Baum von Anbietern und Zuhörern zu erstellen. Beide Ansätze sind nicht zufriedenstellend.

Es ist wie @rrousselGit sagt, in den alten Tagen (UIKit) waren wir gezwungen, unsere UIViews (das Äquivalent von RenderObjects) manuell zu verwalten und daran zu denken, Werte vom Modell in die Ansicht und zurück zu kopieren, nicht verwendete Ansichten zu löschen, zu recyceln und so weiter. Dies war keine Raketenwissenschaft, und viele Leute sahen dieses alte Problem nicht, aber ich denke, jeder hier würde zustimmen, dass Flutter die Situation eindeutig verbessert hat.
Bei Statefulness ist das Problem sehr ähnlich: Es ist langweilige, fehleranfällige Arbeit, die automatisiert werden könnte.

Und übrigens, ich glaube nicht, dass Hooks das überhaupt lösen. Es ist nur so, dass Hooks der einzige Ansatz sind, der möglich ist, ohne die Interna des Flatterns zu ändern.

StatefulWidget eignet sich hervorragend für die Implementierung dieser kleinen wiederverwendbaren Dienstprogramme, aber es ist einfach zu niedrig für nicht wiederverwendbare, domänenspezifische Komponenten, bei denen Sie möglicherweise eine Vielzahl von Textcontrollern, Animationen oder was auch immer die Geschäftslogik erfordert.

Ich bin verwirrt, wenn Sie sagen, dass Sie zum Erstellen Ihrer nicht wiederverwendbaren domänenspezifischen Komponenten ein Widget auf hoher Ebene benötigen. Normalerweise ist es genau umgekehrt.

AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity sind alle Widgets, die einen bestimmten Anwendungsfall implementieren. Wenn ich ein Widget mit einer so spezifischen Logik benötige, dass ich keines dieser Widgets auf höherer Ebene verwenden kann, gehe ich zu einer API auf niedrigerer Ebene, damit ich meinen eigenen spezifischen Anwendungsfall schreiben kann. Die sogenannte Boilerplate implementiert, wie mein einzigartiges Widget seine einzigartige Kombination aus Streams, Netzwerkaufrufen, Controllern und so weiter verwaltet.

Das Befürworten von Hooks, Hook-ähnlichem Verhalten oder einfach nur "Automatisierung" ist so, als würde man sagen, dass wir ein Low-Level- Widget brauchen, das High-Level- , nicht wiederverwendbare Logik verarbeiten kann, die jeder jemals haben möchte, ohne den sogenannten Boilerplate-Code schreiben zu müssen.

Bei Statefulness ist das Problem sehr ähnlich: Es ist langweilige, fehleranfällige Arbeit, die automatisiert werden könnte.

Wieder. Sie möchten __"nicht wiederverwendbare, domänenspezifische Komponenten automatisieren, bei denen Sie möglicherweise eine Vielzahl von Textcontrollern, Animationen oder was auch immer die Geschäftslogik erfordert"__?!

Es ist wie @rrousselGit sagt, in den alten Tagen (UIKit) waren wir gezwungen, unsere UIViews (das Äquivalent von RenderObjects) manuell zu verwalten und daran zu denken, Werte vom Modell in die Ansicht und zurück zu kopieren, nicht verwendete Ansichten zu löschen, zu recyceln und so weiter. Dies war keine Raketenwissenschaft, und viele Leute sahen dieses alte Problem nicht, aber ich denke, jeder hier würde zustimmen, dass Flutter die Situation eindeutig verbessert hat.

Ich habe vor 6-7 Jahren iOS- und Android-Entwicklung durchgeführt (etwa zu der Zeit, als Android zu ihrem Materialdesign wechselte) und ich kann mich nicht erinnern, dass diese Verwaltung und Wiederverwendung von Ansichten ein Problem war und Flutter scheint nicht besser oder schlechter zu sein. Ich kann nicht über die aktuellen Angelegenheiten sprechen, ich habe aufgehört, als Swift und Kotlin ins Leben gerufen wurden.

Die Boilerplate, die ich in meine StatefulWidgets schreiben muss, beträgt etwa 1% meiner Codebasis. Ist es fehleranfällig? Jede Codezeile ist eine potenzielle Fehlerquelle, also sicher. Ist es umständlich? 200 Codezeilen von 15000? Ich glaube wirklich nicht, aber das ist nur meine Meinung. Flutters Text-/Animations-Controller und Focusnodes haben alle Probleme, die verbessert werden können, aber ausführlich zu sein ist keines.

Ich bin wirklich gespannt, was die Leute entwickeln, dass sie so viel Boilerplate brauchen.

Einige der Kommentare hier zu hören, klingt so, als ob die Verwaltung von 5 Codezeilen anstelle von 1 fünfmal schwieriger ist. Es ist nicht.

Würden Sie nicht zustimmen, dass, anstatt beispielsweise initState einzurichten und für jeden AnimationController zu entsorgen, fehleranfälliger sein kann, als es nur einmal zu tun und diese Logik wiederzuverwenden? Gleiches Prinzip wie bei der Verwendung von Funktionen, Wiederverwendbarkeit. Ich stimme zu, dass es problematisch ist, Hooks in die Build-Funktion zu setzen, es gibt definitiv einen besseren Weg.

Es fühlt sich wirklich so an, als ob der Unterschied zwischen denen, die das Problem hier sehen und nicht sehen, darin besteht, dass erstere bereits Hook-ähnliche Konstrukte verwendet haben, wie in React, Swift und Kotlin, und letztere nicht, wie zum Beispiel in reinem Java oder Android. Ich denke, der einzige Weg, überzeugt zu sein, besteht meiner Erfahrung nach darin, Hooks auszuprobieren und zu sehen, ob Sie zum Standardweg zurückkehren können. Oftmals können viele Leute meiner Erfahrung nach nicht. Sie wissen es, wenn Sie es verwenden.

Zu diesem Zweck möchte ich Leute, die skeptisch sind, ermutigen, flatter_hooks für ein kleines Projekt zu verwenden und zu sehen, wie es abschneidet, und es dann auf die Standardmethode zu wiederholen. Es reicht nicht aus, dass wir einfach Versionen der App zum Lesen erstellen , wie in

Es reicht nicht aus, dass wir einfach Versionen der App zum Lesen erstellen , wie in

Ich habe Tage damit verschwendet, den Anbieter auszuprobieren, noch mehr Tage mit dem Versuch, Block zu versuchen, ich fand keine von beiden eine gute Lösung. Wenn es bei dir funktioniert, super.

Damit ich Ihren Lösungsvorschlag für ein Problem, das Sie haben, überhaupt ausprobieren Sie seine Vorteile aufzeigen. Ich habe mir Beispiele mit Flatterhaken angeschaut und mir die Umsetzung angeschaut. Einfach nein.

Unabhängig davon, welcher Code zur Reduzierung von Boilerplates dem Framework hinzugefügt wird, hoffe ich, dass die Stateful/StatelessWidgets unverändert bleiben. Ich kann diesem Gespräch nicht mehr viel hinzufügen.

Fangen wir noch einmal an, in einer hypothetischen Welt, in der wir Dart verändern können, ohne über Hooks zu sprechen.

Das diskutierte Problem ist:

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

Dieser Code ist nicht lesbar.

Wir könnten das Lesbarkeitsproblem beheben, indem wir ein neues Schlüsselwort einführen, das die Syntax in Folgendes ändert:

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');
}

Dieser Code ist deutlich lesbarer, hat nichts mit Hooks zu tun und leidet nicht unter seinen Einschränkungen.
Beim Lesbarkeitsgewinn geht es weniger um die Zeilenanzahl, sondern um die Formatierung und Einrückungen.


Aber was ist, wenn der Builder nicht das Root-Widget ist?

Beispiel:

Widget build(context) {
  return Scaffold(
    body: StreamBuilder(
      stream: stream,
      builder: (context, value) {
        return Consumer<Value2>(
          builder: (context, value2, child) {
            return Text('${value.data} $value2');
          },
        );
      },
    ),
  );
}

Wir könnten haben:

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword StreamBuilder(stream: stream);
      final value2 = keyword Consumer<Value2>();
      return Text('${value.data} $value2');
    }
  );
}

Aber was hat das mit der Frage der Wiederverwendbarkeit zu tun?

Der Grund dafür ist, dass Builder technisch gesehen eine Möglichkeit sind, Zustandslogik wiederzuverwenden. Aber ihr Problem ist, dass sie den Code nicht sehr lesbar machen, wenn wir _viele_ Builder verwenden möchten, wie in diesem Kommentar https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

Mit dieser neuen Syntax haben wir das Lesbarkeitsproblem behoben. Als solche können wir mehr Dinge in Builders extrahieren.

So könnte beispielsweise das in diesem Kommentar erwähnte useFilter lauten:

FilterBuilder(
  debounceDuration: const Duration(seconds: 2),
  builder: (context, filter) {
    return TextField(onChange: (value) => filter.value = value);
  }
)

Was wir dann mit dem neuen Schlüsselwort verwenden können:

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),
        ),
      ],
    );
  }
}

Was ist mit dem "Extrahieren als Funktion", über das wir mit Hooks gesprochen haben, um benutzerdefinierte Hooks/Builder zu erstellen?

Wir könnten dasselbe mit einem solchen Schlüsselwort tun, indem wir eine Kombination von Buildern in einer Funktion extrahieren:

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),
        ),
      ],
    );
  }
}

Offensichtlich wurde nicht viel über alle Implikationen einer solchen Syntax nachgedacht. Aber das ist die Grundidee.


Haken sind diese Funktion.
Die Einschränkungen von Hooks bestehen, weil sie als Paket und nicht als Sprachfeature implementiert werden.

Und das Schlüsselwort ist use , sodass aus keyword StreamBuilder use StreamBuilder , was letztendlich als useStream implementiert wird

Dieser Code ist deutlich lesbarer

Ich denke, das ist Ansichtssache. Ich stimme zu, dass einige Leute denken, dass Versionen, die Sie als lesbarer bezeichnen, besser sind; Persönlich bevorzuge ich die viel expliziteren magielosen Versionen. Aber ich habe nichts dagegen, den zweiten Stil zu ermöglichen.

Der nächste Schritt besteht jedoch darin, an der App von @TimWhiting (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/stateful/counter.dart) zu arbeiten, um daraus etwas zu machen, das alle Probleme hat die wir lösen wollen.

Was es wert ist, https://github.com/flutter/flutter/issues/51752#issuecomment -670959424 entspricht ziemlich genau der Inspiration für Hooks in React. Das Builder-Muster scheint identisch mit dem Render Props-Muster zu sein, das früher in React vorherrschte (aber zu ähnlich tiefen Bäumen führte). Später schlug @trueadm Syntax-Zucker für Render Props vor, und später führte dies zu Hooks (um unnötigen Laufzeit-Overhead zu entfernen).

`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');
            },
          );
        },
      );
    },
  );
}`

Wenn Lesbarkeit und Einrückung das Problem sind, kann dies umgeschrieben werden als

  <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'),
      );

Wenn Funktionen nicht Ihr Ding sind oder Sie Wiederverwendbarkeit benötigen, extrahieren Sie sie als 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');
      },
    );
  }
}

Es gibt nichts in Ihrem Beispiel, das ein neues Schlüsselwort oder eine neue Funktion rechtfertigt.

Ich weiß, was Sie sagen werden. Die Variable 'value' muss durch alle Widgets/Funktionsaufrufe geleitet werden, aber das ist nur ein Ergebnis der Architektur Ihrer Anwendung. Ich unterbreche meinen Code je nach Anwendungsfall sowohl mit "Build" -Methoden als auch mit benutzerdefinierten Widgets und muss nie dieselbe Variable an eine Kette von drei Aufrufen übergeben.

Wiederverwendbarer Code ist wiederverwendbar, wenn er so wenig wie möglich von externen Nebeneffekten abhängig ist (wie InheritedWidgets oder (semi)globaler Status).

@Rudiksz Ich glaube nicht, dass Sie der Diskussion hier etwas hinzufügen. Wir kennen Strategien, um diese Probleme zu mildern, weil wir sie den ganzen Tag lang anwenden. Wenn Sie das Gefühl haben, dass es kein Problem ist, können Sie die Dinge einfach so weiter verwenden, wie sie sind, und dies betrifft Sie überhaupt nicht.

Offensichtlich gibt es viele, viele Menschen, die dies als einen grundlegenden Schmerzpunkt ansehen, und nur hin und her zu radeln ist sinnlos. Sie werden die Leute nicht durch verschiedene Argumente davon überzeugen, dass sie nicht wollen, was sie wollen, oder die Meinung anderer hier ändern. Jeder in dieser Diskussion hat eindeutig Hunderte oder Tausende von Stunden in Flutter auf dem Buckel, und es wird nicht erwartet, dass wir uns alle einig sind.

Ich denke, das ist Ansichtssache. Ich stimme zu, dass einige Leute denken, dass Versionen, die Sie als lesbarer bezeichnen, besser sind; Persönlich bevorzuge ich die viel expliziteren magielosen Versionen. Aber ich habe nichts dagegen, den zweiten Stil zu ermöglichen.

Wenn es Ansichtssache ist, würde ich vermuten, dass es in eine Richtung ziemlich schief ist.

  1. Beide haben Magie. Ich weiß nicht unbedingt, was einer dieser Builder intern macht. Die nicht-magische Version schreibt die eigentliche Boilerplate in diese Builder. Die Verwendung eines SingleAnimationTIckerProvider-Mixins ist für 95% der Flutter-Entwickler ebenfalls magisch.
  2. Einer verschleiert sehr wichtige Variablennamen, die später im Baum verwendet werden, nämlich value1 und value2 , der andere hat sie vorne und in der Mitte oben im Build. Dies ist ein klarer Parsing- / Wartungsgewinn.
  3. Einer hat 6 Einrückungsebenen, bevor der Widget-Baum überhaupt beginnt, der andere hat 0
  4. Eine ist 5 vertikale Linien, die andere ist 15
  5. Einer zeigt den eigentlichen Inhaltsteil prominent (Text()), der andere versteckt ihn verschachtelt weit unten im Baum. Ein weiterer klarer Parsing-Gewinn.

Im Allgemeinen könnte ich sehen, dass dies vielleicht Geschmackssache ist. Aber Flutter hat im Allgemeinen ein Problem mit der Zeilenanzahl, der Einrückung und dem Signal-Rausch- Verhältnis. Obwohl ich die Fähigkeit, einen Baum in Dart-Code deklarativ zu bilden, absolut _liebe_, kann dies zu extrem unlesbarem/ausführlichem Code führen, insbesondere wenn Sie sich auf mehrere Schichten von umschlossenen Buildern verlassen. Im Kontext von Flutter selbst, wo wir diesen Kampf ständig ausfechten, ist diese Art der Optimierung also ein Killerfeature, da sie uns ein wirklich hervorragendes Werkzeug bietet, um dieses etwas allgegenwärtige Problem der allgemeinen Ausführlichkeit zu bekämpfen.

TL;DR - Alles, was die Einrückung und Zeilenzahl in Flutter deutlich reduziert, ist aufgrund der allgemein hohen Zeilenzahlen und Einrückungen in Flutter doppelt wertvoll.

@Rudiksz Ich glaube nicht, dass Sie der Diskussion hier etwas hinzufügen. Wir kennen Strategien, um diese Probleme zu mildern, weil wir sie den ganzen Tag lang anwenden. Wenn Sie das Gefühl haben, dass es kein Problem ist, können Sie die Dinge einfach so weiter verwenden, wie sie sind, und dies betrifft Sie überhaupt nicht.

Außer wenn es sich um eine Änderung im Kern-Framework handelt, betrifft es mich, nicht wahr?

Offensichtlich gibt es viele, viele Menschen, die dies als einen grundlegenden Schmerzpunkt ansehen, und nur hin und her zu radeln ist sinnlos. Sie werden die Leute nicht durch verschiedene Argumente davon überzeugen, dass sie nicht wollen, was sie wollen, oder die Meinung anderer hier ändern. Jeder in dieser Diskussion hat eindeutig Hunderte oder Tausende von Stunden in Flutter auf dem Buckel, und es wird nicht erwartet, dass wir uns alle einig sind.

Stimmt, dieses Pferd wurde schon unzählige Male zu Tode geprügelt, also tappe ich nicht in die Falle, noch weitere Kommentare zu beantworten.

Builder sind technisch gesehen eine Möglichkeit, Zustandslogik wiederzuverwenden. Aber ihr Problem ist, dass sie den Code nicht sehr lesbar machen.

Dies sagt es perfekt. Um es in Flutter-Begriffen zu betrachten, brauchen wir Single-Line-Builder.

Builder, die keine Dutzende von Registerkarten und Zeilen benötigen, aber dennoch die Einbindung einiger benutzerdefinierter Boilerplates in den Widget-Lebenszyklus ermöglichen.

Das Mantra "Alles ist ein Widget" ist hier nicht besonders gut. Der relevante Code in einem Builder besteht normalerweise nur aus den Setup-Props und der zustandsbehafteten Sache, die er zurückgibt, die der Build-FXN benötigt. Jeder einzelne Zeilenumbruch und Tabulator ist im Grunde sinnlos.

Außer wenn es sich um eine Änderung im Kern-Framework handelt, betrifft es mich, nicht wahr?

@Rudiksz Ich glaube nicht, dass jemand vorschlägt, Stateful-Widgets zu ändern. Sie können sie jederzeit in ihrer aktuellen Form verwenden, wenn Sie möchten. Welche Lösung wir auch immer finden würden, würde entweder Stateful-Widgets ohne Änderungen oder eine andere Art von Widget vollständig verwenden. Wir sagen nicht, dass zustandsbehaftete Widgets schlecht sind, sondern möchten nur einen anderen Widget-Typ, der einen besser zusammensetzbaren Widget-Status ermöglicht. Stellen Sie es sich als zustandsbehaftetes Widget vor, das anstelle eines ihm zugeordneten Zustandsobjekts mehrere Zustandsobjekte und eine separate Build-Funktion enthält, die jedoch Zugriff auf diese Zustandsobjekte hat. Auf diese Weise können Sie Bits des gemeinsamen Zustands (zusammen mit der diesem Zustand zugeordneten Zustandslogik) mit ihren bereits implementierten initState und dispose wiederverwenden. Im Wesentlichen modularerer Zustand, der in verschiedenen Situationen auf unterschiedliche Weise zusammengestellt werden kann. Auch dies ist kein konkreter Vorschlag, aber vielleicht eine andere Möglichkeit, darüber nachzudenken. Vielleicht könnte es zu einer Lösung werden, die flutter ähnlicher ist, aber ich weiß es nicht.

Der nächste Schritt besteht jedoch darin, an der App von @TimWhiting (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/stateful/counter.dart) zu arbeiten, um daraus etwas zu machen, das alle Probleme hat die wir lösen wollen.

Dies ist schwierig, da dieses Problem von Natur aus eines von einem Tod durch tausend Schnitte ist. Es fügt nur Blähungen hinzu und verringert die Lesbarkeit in der gesamten Codebasis. Am schlimmsten sind die Auswirkungen bei kleinen Widgets zu spüren, bei denen die gesamte Klasse <100 Zeilen umfasst und die Hälfte davon für die Verwaltung eines Animator-Controllers verwendet wird. Ich weiß also nicht, was das Anschauen von 30 dieser Beispiele zeigen wird, das ist bei einem nicht der Fall.

Es ist wirklich der Unterschied zwischen diesem:

<strong i="10">@override</strong>
  Widget build(BuildContext context) {
    final controller = get AnimationController(vsync: this, duration: widget.duration);
    //do stuff
  }

Und das:

  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
  }

Es gibt einfach keinen besseren Weg, dies zu veranschaulichen. Sie können diesen Anwendungsfall auf jedes beliebige Controller-Typ-Objekt erweitern. AnimatorController, FocusController und TextEditingController sind wahrscheinlich die gebräuchlichsten und lästigsten im täglichen Gebrauch. Stellen Sie sich jetzt nur 50 oder 100 davon auf meiner gesamten Codebasis vor.

  • Sie haben ungefähr 1000-2000 Zeilen, die einfach verschwinden könnten.
  • Sie haben wahrscheinlich Dutzende von Fehlern und RTEs (an verschiedenen Entwicklungspunkten), die nie existieren mussten, weil irgendeine Überschreibung fehlte.
  • Sie haben Widgets, die, wenn sie mit kalten Augen betrachtet werden, auf einen Blick viel schwieriger zu erkennen sind. Ich muss jede dieser Überschreibungen lesen, ich kann nicht einfach davon ausgehen, dass sie Standard sind.

Und Sie können dies natürlich auf benutzerdefinierte Controller erweitern. Das gesamte Konzept der Verwendung von Controllern ist in Flutter weniger ansprechend, da Sie wissen, dass Sie sie so booten, verwalten und zerstören müssen, was nervig und fehleranfällig ist. Dies führt dazu, dass Sie keine eigenen erstellen und stattdessen benutzerdefinierte StatefulWidgets/Builder erstellen. Es wäre schön, wenn Controller-Typ-Objekte einfach einfacher zu verwenden und robuster wären, da Builder Lesbarkeitsprobleme haben (oder zumindest deutlich ausführlicher und mit Leerzeichen beladen sind).

Das ist schwierig

Ja, API-Design ist knifflig. Willkommen in meinem Leben.

Ich weiß also nicht, was das Anschauen von 30 dieser Beispiele zeigen wird, das ist bei einem nicht der Fall.

Es sind nicht 30 Beispiele, die helfen, sondern ein Beispiel, das so ausführlich ist, dass es nicht so vereinfacht werden kann, dass Sie sagen würden: "Nun, klar, für dieses Beispiel funktioniert das, aber es funktioniert nicht für ein _echtes_ Beispiel".

Es sind nicht 30 Beispiele, die helfen, sondern ein Beispiel, das so ausführlich ist, dass es nicht so vereinfacht werden kann, dass Sie sagen würden: "Nun, klar, für dieses Beispiel funktioniert es, aber es funktioniert nicht für ein echtes Beispiel".

Ich habe es schon ein paar Mal gesagt, aber diese Art, Hooks zu beurteilen, ist unfair.
Bei Hooks geht es nicht darum, etwas zu ermöglichen, das vorher unmöglich war. Es geht darum, eine konsistente API bereitzustellen, um diese Art von Problem zu lösen.

Eine Anwendung anzufordern, die etwas zeigt, was nicht anders vereinfacht werden kann, bedeutet, einen Fisch nach seiner Fähigkeit zu beurteilen, auf Bäume zu klettern.

Wir versuchen nicht nur, Hooks zu beurteilen, wir versuchen, verschiedene Lösungen zu evaluieren, um zu sehen, ob es welche gibt, die den Bedürfnissen aller gerecht werden.

(Ich bin gespannt, wie Sie hier verschiedene Vorschläge bewerten würden, wenn Sie nicht in jedem der Vorschläge Bewerbungen schreiben und sie vergleichen. Welche Bewertungsmetrik würden Sie stattdessen vorschlagen?)

Ein angemessener Weg, um die Lösung für dieses Problem zu beurteilen, ist keine Anwendung (da jede einzelne Verwendung der API wie in den Beispielen hier abgewiesen wird).

Woran wir die vorgeschlagene Lösung beurteilen sollten, ist:

  • Ist der resultierende Code objektiv besser als die Standardsyntax?

    • Vermeidet es Fehler?

    • Ist es besser lesbar?

    • Ist es einfacher zu schreiben?

  • Wie wiederverwendbar ist der erzeugte Code?
  • Wie viele Probleme können mit dieser API gelöst werden?

    • Verlieren wir einige Vorteile für bestimmte Probleme?

Bei der Auswertung in diesem Raster kann der Vorschlag Property / addDispose ein gutes "Ist der resultierende Code besser?" Punktzahl, bewerten aber sowohl die Wiederverwendbarkeit als auch die Flexibilität schlecht.

Ich weiß nicht, wie ich diese Fragen beantworten soll, ohne jeden Vorschlag in der Praxis zu sehen.

Wieso den?

Ich musste keine Anwendungen mit Property erstellen, um zu wissen, dass dieser Vorschlag Schwierigkeiten haben wird, wirklich wiederverwendbaren Code zu erstellen und viele Probleme zu lösen.

Wir können jeden vorhandenen *Builder nehmen und versuchen, ihn mit der vorgeschlagenen Lösung neu zu implementieren.
Wir können auch versuchen, die in diesem Thread aufgeführten Hooks oder einige Hooks, die in der React-Community erstellt wurden, neu zu implementieren (es gibt zahlreiche Zusammenstellungen von Hooks online).

Ich musste keine Anwendungen mit Property erstellen, um zu wissen, dass dieser Vorschlag Schwierigkeiten haben wird, wirklich wiederverwendbaren Code zu erstellen und viele Probleme zu lösen.

Leider teile ich Ihre Instinkte hier nicht (wie die Tatsache zeigt, dass ich dachte, dass Property (https://github.com/flutter/flutter/issues/51752#issuecomment-667737471) großartig funktioniert, bis Sie sagten, es müsse mit Werten umgehen sowohl von einem Widget als auch von einem lokalen Zustand mit derselben API, von der ich nicht wusste, dass sie eine Einschränkung darstellt, bis Sie sie aufgerufen haben). Wenn ich eine Version von Property anbiete, die auch dieses Problem löst, wird es dann definitiv in Ordnung sein oder wird es ein neues Problem geben, das es nicht abdeckt? Ohne ein Ziel sind wir uns alle einig, ist das Ziel, ich weiß nicht, wofür wir Lösungen entwickeln.

Wir können jeden vorhandenen *Builder nehmen und versuchen, ihn mit der vorgeschlagenen Lösung neu zu implementieren.

Ganz klar nicht _irgendwie_. Zum Beispiel haben Sie im OP einen gegeben, und als ich meinen ersten Property-Vorschlag gemacht habe (https://github.com/flutter/flutter/issues/51752#issuecomment-664787791), haben Sie auf Probleme hingewiesen, die nicht von illustriert wurden der ursprüngliche Builder.

Wir können auch versuchen, die in diesem Thread aufgeführten Hooks oder einige Hooks, die in der React-Community erstellt wurden, neu zu implementieren (es gibt zahlreiche Zusammenstellungen von Hooks online).

Es ist mir egal, wo wir anfangen. Wenn Sie ein besonders gutes Beispiel haben, das Ihrer Meinung nach das Problem veranschaulicht und Hooks verwendet, dann fügen wir es dem Repository von

Es sind nicht 30 Beispiele, die helfen, sondern ein Beispiel, das so ausführlich ist, dass es nicht so vereinfacht werden kann, dass Sie sagen würden: "Nun, klar, für dieses Beispiel funktioniert das, aber es funktioniert nicht für ein _echtes_ Beispiel".

Es wird nie etwas Aufwändigeres geben als den einfachen Wunsch, einen AnimatorController (oder jede andere wiederverwendbare zustandsbehaftete Komponente, die Sie sich vorstellen können) ohne einen unlesbaren Builder oder eine Menge fehleranfälliger Lebenszyklus-Boilerplates zu verwenden.

Es wurde keine Lösung vorgeschlagen, die die geforderten Lesbarkeits- und Robustheitsvorteile auf eine allgemeine Art und Weise berücksichtigt.

Ich bestehe darauf, dass _jeder_ Builder ausreicht, da dieses Thema in "Wir brauchen Syntaxzucker für Builder" umbenannt werden und zu derselben Diskussion führen könnte.

Alle anderen Argumente (wie das Erstellen und Verwerfen eines AnimationController ) basieren darauf, dass sie ebenfalls in einen Builder extrahiert werden könnten:

Widget build(context) {
  return AnimationControllerBuilder(
    duration: Duration(seconds: 2),
    builder: (context, animationController) {

    }
  );
}

Am Ende denke ich, dass das perfekte Beispiel darin besteht, StreamBuilder vollständig neu zu implementieren und in verschiedenen Szenarien zu testen:

  • der Stream kommt aus dem Widget
  • // von einem InheritedWidget
  • vom lokalen Staat

und testen Sie jeden einzelnen Fall gegen "der Strom kann sich im Laufe der Zeit ändern", also:

  • didUpdateWidget mit neuem Stream
  • das InheritedWidget aktualisiert
  • wir nannten setState

Dies ist derzeit mit Property oder onDispose lösbar

@esDotDev Können Sie "die angeforderten Lesbarkeits- und Robustheitsvorteile" aufzählen? Wenn jemand einen Vorschlag macht, der AnimationController mit diesen Lesbarkeits- und Robustheitsvorteilen handhabt, dann sind wir hier fertig?

@rrousselGit Ich befürworte kein Eigentum, wie Sie bereits sagten, dass es Ihre Probleme nicht löst. Aber wenn jemand eine Lösung erstellen würde, die alles macht, was StreamBuilder tut, aber ohne die Einrückung, wäre das das? Sie würden sich freuen?

Aber wenn jemand eine Lösung erstellen würde, die alles macht, was StreamBuilder tut, aber ohne die Einrückung, wäre das das? Sie würden sich freuen?

Sehr wahrscheinlich ja

Wir müssten diese Lösung natürlich mit anderen Lösungen vergleichen. Aber das würde das akzeptable Niveau erreichen.

@esDotDev Können Sie "die angeforderten Lesbarkeits- und Robustheitsvorteile" aufzählen?

Robustheit, da es die Boilerplate um Abhängigkeiten und den Lebenszyklus vollständig kapseln kann. dh. Ich sollte fetchUser nicht jedes Mal mitteilen müssen, dass es wahrscheinlich neu erstellt werden sollte, wenn sich die ID ändert, es weiß das intern. Sollte einer Animation nicht sagen müssen, dass sie sich jedes Mal selbst verwerfen soll, wenn ihr übergeordnetes Widget verworfen wird usw. (Ich verstehe nicht ganz, ob Property dies tun kann tbh). Dies verhindert, dass Entwickler in der gesamten Codebasis Fehler bei Routineaufgaben machen (einer der Hauptvorteile der Verwendung von Buildern derzeit imo).

Lesbarkeit bedeutet, dass wir das zustandsbehaftete Ding mit einer einzigen Zeile nicht eingerückten Codes erhalten und die Variable für das Ding hochgezogen und deutlich sichtbar ist.

@esDotDev Wenn jemand einen Vorschlag macht, der AnimationController mit diesen Lesbarkeits- und Robustheitsvorteilen handhabt, dann sind wir hier fertig?

Wenn Sie speziell AnimationController Nr. Wenn Sie ein AnimationController/FocusController/TextEditingController-ähnliches Objekt meinen, dann ja.

Eine funktionsähnliche API zu haben, die einen Wert zurückgibt, der eine definierte Lebensdauer hat, die nicht klar ist, ist

Ich denke, das ist ein zentrales Missverständnis. Die Lebensdauer eines Hooks ist klar, da es sich per Definition um Unterzustände handelt. Sie existieren _immer_ für die Lebenszeit des Staates, der sie "benutzt". Deutlicher könnte es eigentlich nicht sein. Die Syntax ist vielleicht seltsam und ungewohnt, aber es fehlt ihr sicherlich nicht an Klarheit.

Ähnlich wie die Lebensdauer eines TweenAnimationBuilder() ist auch klar. Es wird verschwinden, wenn die Eltern weg sind. Wie ein untergeordnetes Widget sind dies untergeordnete Zustände. Sie sind vollständig unabhängige Zustands-"Komponenten", die wir mit Leichtigkeit zusammenbauen und wiederverwenden können, und wir verwalten ihre Lebensdauer ausdrücklich nicht, weil wir möchten, dass sie natürlich an den Zustand gebunden ist, in dem sie deklariert sind, ein Feature, kein Fehler.

@esDotDev

etc

Kannst du genauer sein? (Aus diesem Grund habe ich vorgeschlagen, eine Demoanwendung zu entwickeln, die alle Grundlagen abdeckt. Ich denke weiterhin, dass dies der beste Weg ist.) Gibt es andere Funktionen, als nur den Initialisierer aufzurufen, es sei denn, die Konfiguration hat sich geändert und automatisch entsorgt? zugewiesene Ressourcen, wenn das Hostelement verworfen wird?

TextEditingController-ähnliches Objekt

Kannst du genauer sein? Ist TextEditingController in irgendeiner Weise aufwendiger als AnimationController?


@rrousselGit

Aber wenn jemand eine Lösung erstellen würde, die alles macht, was StreamBuilder tut, aber ohne die Einrückung, wäre das das? Sie würden sich freuen?

Sehr wahrscheinlich ja

Hier ist eine Lösung, die alles macht, was StreamBuilder tut, ohne Einrücken:

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);
}

Ich vermute jedoch, dass dies gegen eine andere Einschränkung verstößt. Deshalb würde ich es vorziehen, etwas zu haben, bei dem wir uns alle einig sind, dass es eine vollständige Beschreibung des Problems ist, bevor wir versuchen, es zu lösen.

Es sind einfach die gleichen Einschränkungen, die Builder @Hixie haben, niemand verlangt mehr als das. Ein Builder kann sich mit widget.whatever verbinden, ein Builder kann jeden internen Zustand, der im Kontext des Widget-Baums erforderlich ist, vollständig verwalten. Das ist alles, was ein Hook tun kann, und alles, was irgendjemand nach einem Mikrostaat oder wie immer man ihn nennen will, verlangt.

Kannst du genauer sein? Ist TextEditingController in irgendeiner Weise aufwendiger als AnimationController?

Nein, aber es könnte verschiedene Dinge in init/dispose tun, oder es wird an verschiedene Eigenschaften gebunden, und ich möchte diesen spezifischen Boilerplate kapseln.

@esDotDev Sie möchten also dasselbe wie ein Builder, jedoch ohne Einrücken und in einer Zeile (abzüglich des Builder-Rückrufs selbst vermutlich)? Das Beispiel, das ich gerade gepostet habe (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483) macht das heute mit Buildern, also gibt es vermutlich darüber hinaus zusätzliche Einschränkungen?

(FWIW, ich glaube nicht, dass Builder oder ähnliche Builder, sondern auf einer Zeile eine gute Lösung sind, da sie erfordern, dass für jeden Datentyp ein eigener Builder erstellt wird .)

(FWIW, ich glaube nicht, dass Builder oder ähnliche Builder, sondern auf einer Zeile eine gute Lösung sind, da sie erfordern, dass für jeden Datentyp ein eigener Builder erstellt wird .)

Ich verstehe nicht, was das bedeutet. Könnten Sie es umformulieren? 🙏

Sie müssen einen AnimationBuilder für Animationen und einen StreamBuilder für Streams usw. erstellen. Anstatt nur einen einzigen Builder zu haben und zu sagen "So erhalten Sie einen neuen, so entsorgen Sie ihn, so erhalten Sie die Daten" usw., wenn Sie Ihr StatefulWidget erstellen.

Ich vermute jedoch, dass dies gegen eine andere Einschränkung verstößt. Deshalb würde ich es vorziehen, etwas zu haben, bei dem wir uns alle einig sind, dass es eine vollständige Beschreibung des Problems ist, bevor wir versuchen, es zu lösen.

Ich denke, es verstößt ziemlich offensichtlich gegen jede Anforderung nach lesbarem Code, was letztendlich das Ziel hier ist, sonst würden wir alle einfach eine Million speziell typisierte Builder verwenden, sie für immer verschachteln und damit Schluss machen.

Ich denke, was angefordert wird, ist etwas wie (Habe mit mir, ich benutze nicht viel 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)]);
}

Das ist der ganze Code. Es gäbe nichts mehr, da der Stream für uns erstellt wird, der Stream für uns entsorgt wird, wir uns selbst nicht in den Fuß schießen können UND der Code wesentlich besser lesbar ist.

Sie müssen einen AnimationBuilder für Animationen und einen StreamBuilder für Streams usw. erstellen.

Ich sehe das nicht als Problem. Wir haben bereits RestorableInt vs RestorableString vs RestorableDouble

Und Generika können das lösen:

GenericBuilder<Stream<int>>(
  create: (ref) {
    var controller = StreamController<int>();
    ref.onDispose(controller.close);
    return controller.stream;
  }
  builder: (context, Stream<int> stream) {

  }
)

In ähnlicher Weise könnten Flutter oder Dart eine Disposable Schnittstelle enthalten, wenn dies wirklich ein Problem darstellt.

@esDotDev

Ich denke, es wird um folgendes gebeten:

Das würde einige der recht vernünftigen Einschränkungen verletzen, die andere aufgelistet haben (zB @Rudiksz), nämlich zu garantieren, dass während des Aufrufs der build-Methode niemals Initialisierungscode passiert.

@rrousselGit

Ich sehe das nicht als Problem. Wir haben bereits RestorableInt vs RestorableString vs RestorableDouble

Und wir haben AnimationBuilder und StreamBuilder und so weiter, ja. In beiden Fällen ist es schade.

GenericBuilder

Das ist ähnlich dem, was ich für Property vorgeschlagen habe, aber wenn ich Ihre Bedenken dort verstanden habe, hielten Sie das für zu ausführlich.

Zuvor haben Sie gesagt, dass Sie wahrscheinlich glücklich wären, wenn jemand eine Lösung erstellen würde, die alles macht, was StreamBuilder tut, jedoch ohne die Einrückung. Sie haben meinen Versuch, dies zu tun, nicht kommentiert (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483). Sind Sie mit dieser Lösung zufrieden?

@esDotDev

Ich denke, es wird um folgendes gebeten:

Das würde einige der recht vernünftigen Einschränkungen verletzen, die andere aufgelistet haben (zB @Rudiksz), nämlich zu garantieren, dass während des Aufrufs der build-Methode niemals Initialisierungscode passiert.

Es ist nicht wichtig, dass dieser Code im Build ist. Der wichtige Teil ist das

  1. Ich bin nicht gezwungen, meinen Baum einzurücken oder eine Menge zusätzlicher Zeilen hinzuzufügen.
  2. Der für dieses Ding spezifische Lebenszykluscode ist gekapselt.

Das wäre erstaunlich:

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)]));
}

Oder nicht so prägnant, aber immer noch viel lesbarer als die Verwendung von Buildern und weniger ausführlich und fehleranfällig als dies direkt zu tun:

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)]);
}

Ich verstehe nicht, warum wir immer wieder zur Ausführlichkeit zurückkommen.
Ich habe mehrmals ausdrücklich gesagt, dass dies nicht das Problem ist und dass das Problem Wiederverwendbarkeit vs. Lesbarkeit vs. Flexibilität ist.

Ich habe sogar ein Raster erstellt, um die Lösung zu bewerten https://github.com/flutter/flutter/issues/51752#issuecomment -671000137 und einen Testfall https://github.com/flutter/flutter/issues/51752#issuecomment - 671002248


Zuvor haben Sie gesagt, dass Sie wahrscheinlich glücklich wären, wenn jemand eine Lösung erstellen würde, die alles macht, was StreamBuilder tut, jedoch ohne die Einrückung. Sie haben meinen Versuch, dies zu tun, nicht kommentiert (#51752 (Kommentar)). Sind Sie mit dieser Lösung zufrieden?

Das erreicht das minimal akzeptable Maß an Flexibilität.

Die Auswertung gemäß https://github.com/flutter/flutter/issues/51752#issuecomment -671000137 ergibt:

  • Ist der resultierende Code objektiv besser als die Standardsyntax?

    • Vermeidet es Fehler?

      _Die Standardsyntax (StreamBuilder ohne Hacking) ist weniger fehleranfällig. Diese Lösung vermeidet keine Fehler, sie schafft einige_ ❌

    • Ist es besser lesbar?

      _Es ist eindeutig nicht lesbarer_ ❌

    • Ist es einfacher zu schreiben?

      _Es ist nicht leicht zu schreiben_ ❌

  • Wie wiederverwendbar ist der erzeugte Code?
    _StreamBuilder ist nicht an Widget/Status/Lebenszyklen gebunden, also dieser Pass_
  • Wie viele Probleme können mit dieser API gelöst werden?
    _Wir können kundenspezifische Builder herstellen und dieses Muster verwenden. Also dieser Pass_. ✅
  • Verlieren wir einige Vorteile für bestimmte Probleme?
    _Nein, die Syntax ist relativ konsistent_. ✅
  1. Dieses Feature-IMO könnte sich auf alle Builder-Widgets erstrecken, einschließlich beispielsweise LayoutBuilder.
  2. Es muss eine Möglichkeit geben, das Abhören zu deaktivieren, damit Sie 10x-Controller erstellen und sie zum Wiederaufbau an Leafs übergeben können, oder Flattern muss irgendwie wissen, welcher Teil des Baums den vom Builder erhaltenen Wert verwendet hat.
  3. Dies sollte nicht viel ausführlicher sein als Hooks.
  4. Compiler muss erweitert werden, um dies richtig zu handhaben.
  5. Debugging-Helfer werden benötigt. Nehmen wir an, Sie setzen Breakpoints in eines Ihrer Widgets, die dies verwenden. Beim Erreichen eines Haltepunkts innerhalb einer Build-Methode konnte die IDE zusätzliche Informationen für jeden verwendeten Builder anzeigen, weil einer der Builder ausgelöst wurde:
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)]);
}

Auch @Hixie

Das würde einige der recht vernünftigen Einschränkungen verletzen, die andere aufgelistet haben (zB @Rudiksz), nämlich zu garantieren, dass während des Aufrufs der build-Methode niemals Initialisierungscode passiert.

Wir tun dies bereits implizit, indem wir *Builders verwenden. Wir brauchen nur einen Syntaxzucker, um sie einzurücken. Es ist sehr ähnlich wie Async/Await und Futures, denke ich.

@esDotDev Was Sie beschreiben, klingt sehr ähnlich zu dem, was ich zuvor mit Property vorgeschlagen habe (siehe z. B. https://github.com/flutter/flutter/issues/51752#issuecomment-664787791 oder https://github.com/flutter/flutter/ Ausgaben/51752#issuecomment-667737471). Gibt es etwas, das verhindert, dass diese Art von Lösung als Paket erstellt wird? Das heißt, welche Änderung müsste das Kern-Framework vorgenommen haben, damit Sie diese Art von Funktion verwenden können?

@rrousselGit Wie bei Shawn würde ich dich dann dasselbe fragen. Wenn der einzige Unterschied zwischen der aktuellen StreamBuilder-Funktion und einer, die Ihre Anforderungen erfüllen würde, eine andere Syntax ist, was benötigen Sie dann von der Kernsyntax, damit Sie eine solche Funktion verwenden können? Wäre es nicht ausreichend, einfach die gewünschte Syntax zu erstellen und diese zu verwenden?

Das Problem, das ich mit Ihrem Raster habe, ist, dass ich, wenn ich es auf die bisherigen Lösungen anwenden würde, Folgendes erhalten würde, von dem ich annehme, dass es sich stark von dem unterscheidet, was Sie erhalten würden:

StatefulWidget

  • Ist der resultierende Code objektiv besser als die Standardsyntax?

    • Vermeidet es Fehler?

      _Sie entspricht der Standardsyntax, die nicht besonders fehleranfällig ist._ 🔷

    • Ist es besser lesbar?

      _Es ist das gleiche, also genauso gut lesbar, was einigermaßen lesbar ist._ 🔷

    • Ist es einfacher zu schreiben?

      _Es ist das gleiche, also ist es genauso einfach zu schreiben, was relativ einfach ist._ 🔷

  • Wie wiederverwendbar ist der erzeugte Code?
    _Es gibt nur sehr wenig Code, der wiederverwendet werden kann._
  • Wie viele Probleme können mit dieser API gelöst werden?
    _Das ist die Grundlinie._ 🔷
  • Verlieren wir einige Vorteile für bestimmte Probleme?
    _Scheint nicht so._ ✅

Variationen der Eigenschaft

  • Ist der resultierende Code objektiv besser als die Standardsyntax?

    • Vermeidet es Fehler?

      _Es verschiebt den Code an eine andere Stelle, reduziert aber die Anzahl der Fehler nicht besonders._ 🔷

    • Ist es besser lesbar?

      _Es platziert Initialisierungscode und Bereinigungscode und anderen Lebenszykluscode an derselben Stelle, sodass es weniger klar ist._ ❌

    • Ist es einfacher zu schreiben?

      _Es mischt Initialisierungscode und Bereinigungscode und anderen Lebenszykluscode, sodass es schwieriger zu schreiben ist._ ❌

  • Wie wiederverwendbar ist der erzeugte Code?
    _Genau so wiederverwendbar wie StatefulWidget, nur an verschiedenen Stellen._
  • Wie viele Probleme können mit dieser API gelöst werden?
    _Das ist syntaktischer Zucker für StatefulWidget, also kein Unterschied._
  • Verlieren wir einige Vorteile für bestimmte Probleme?
    _Leistung und Speicherauslastung würden leicht leiden._

Variationen über Builder

  • Ist der resultierende Code objektiv besser als die Standardsyntax?

    • Vermeidet es Fehler?

      _Es ist im Grunde die StatefulWidget-Lösung, aber ausgeklammert; Fehler sollten ungefähr gleich sein._ 🔷

    • Ist es besser lesbar?

      _Build-Methoden sind komplexer, der Rest der Logik verschiebt sich zu einem anderen Widget, also ungefähr gleich._ 🔷

    • Ist es einfacher zu schreiben?

      _Schwerer zu schreiben beim ersten Mal (Erstellen des Builder-Widgets), danach etwas einfacher, also ungefähr gleich._ 🔷

  • Wie wiederverwendbar ist der erzeugte Code?
    _Genau so wiederverwendbar wie StatefulWidget, nur an verschiedenen Stellen._
  • Wie viele Probleme können mit dieser API gelöst werden?
    _Das ist syntaktischer Zucker für StatefulWidget, also meistens kein Unterschied. In einigen Aspekten ist es sogar besser, zum Beispiel reduziert es die Menge an Code, die ausgeführt werden muss, wenn eine Abhängigkeitsänderung verarbeitet wird._ ✅
  • Verlieren wir einige Vorteile für bestimmte Probleme?
    _Scheint nicht so._ ✅

Hakenartige Lösungen

  • Ist der resultierende Code objektiv besser als die Standardsyntax?

    • Vermeidet es Fehler?

      _Ermutigt schlechte Muster (zB Konstruktion in der Build-Methode), riskiert Fehler, wenn sie versehentlich mit Bedingungen verwendet werden._ ❌

    • Ist es besser lesbar?

      _Erhöht die Anzahl der Konzepte, die bekannt sein müssen, um eine Build-Methode zu verstehen._ ❌

    • Ist es einfacher zu schreiben?

      _Entwickler müssen lernen, Hooks zu schreiben, was ein neues Konzept ist, also schwieriger._ ❌

  • Wie wiederverwendbar ist der erzeugte Code?
    _Genau so wiederverwendbar wie StatefulWidget, nur an verschiedenen Stellen._
  • Wie viele Probleme können mit dieser API gelöst werden?
    _Das ist syntaktischer Zucker für StatefulWidget, also kein Unterschied._
  • Verlieren wir einige Vorteile für bestimmte Probleme?
    _Leistung und Speicherverbrauch leiden._ ❌

Ich verstehe nicht, warum wir immer wieder zur Ausführlichkeit zurückkommen.
Ich habe mehrmals ausdrücklich gesagt, dass dies nicht das Problem ist und dass das Problem Wiederverwendbarkeit vs. Lesbarkeit vs. Flexibilität ist.

Entschuldigung, ich habe mich falsch erinnert, wer gesagt hat, dass Property zu ausführlich sei. Sie haben Recht, Ihre Sorge war nur, dass es einen neuen Anwendungsfall gab, der noch nicht aufgeführt war und der nicht behandelt wurde, obwohl ich denke, es wäre trivial, Property zu erweitern, um auch diesen Anwendungsfall zu behandeln (ich habe nicht versucht, es scheint besser zu warten, bis wir eine klare Demo-App haben, damit wir die Dinge ein für alle Mal lösen können, anstatt immer wieder iterieren zu müssen, wenn die Anforderungen angepasst werden).

@szotp

  1. Dieses Feature-IMO könnte sich auf alle Builder-Widgets erstrecken, einschließlich beispielsweise LayoutBuilder.

LayoutBuilder ist ein ganz anderes Widget als die meisten Builder, FWIW. Keiner der bisher besprochenen Vorschläge würde für LayoutBuilder-ähnliche Probleme funktionieren, und keine der vor Ihrem Kommentar beschriebenen Anforderungen enthält LayoutBuilder. Wenn wir diese neue Funktion auch für den Umgang mit LayoutBuilder verwenden sollten, ist das wichtig zu wissen; Ich empfehle, mit @TimWhiting zusammenzuarbeiten , um sicherzustellen, dass die Beispiel-App, auf der wir Vorschläge

  1. Es muss eine Möglichkeit geben, das Abhören zu deaktivieren, damit Sie 10x-Controller erstellen und sie zum Wiederaufbau an Leafs übergeben können, oder Flattern muss irgendwie wissen, welcher Teil des Baums den vom Builder erhaltenen Wert verwendet hat.

Ich bin mir nicht sicher, was das genau bedeutet. Soweit ich das beurteilen kann, können Sie dies heute mit Listenern und Buildern tun (zB verwende ich ValueListenableBuilder in der oben zitierten App, um genau dies zu tun).

Das würde einige der recht vernünftigen Einschränkungen verletzen, die andere aufgelistet haben (zB @Rudiksz), nämlich zu garantieren, dass während des Aufrufs der build-Methode niemals Initialisierungscode passiert.

Wir tun dies bereits implizit, indem wir *Builders verwenden.

Ich glaube nicht, dass das richtig ist. Es hängt vom Builder ab, aber einige arbeiten sehr hart daran, initState, didChangeDependencies, didUpdateWidget und Build-Logik zu trennen, sodass nur ein Minimum an Code für die Ausführung jedes Builds basierend auf den Änderungen erforderlich ist. ValueListenableBuilder registriert die Listener beispielsweise nur beim ersten Erstellen, sein Builder kann erneut ausgeführt werden, ohne dass entweder das übergeordnete Element oder der initState des Builders erneut ausgeführt wird. Das kann Hooks nicht.

@esDotDev Was Sie beschreiben, klingt sehr ähnlich zu dem, was ich zuvor mit Property vorgeschlagen habe (siehe zB #51752 (Kommentar) oder #51752 (Kommentar) ).

Wenn ich das richtig verstehe, könnten wir UserProperty erstellen, das automatisch die DidDependancyChange für UserId oder AnimationProperty oder eine andere Eigenschaft verarbeitet, die wir für die Init/Update/Dispose für diesen Typ benötigen? Dann kommt mir das gut vor. Die gängigsten Anwendungsfälle konnten schnell erstellt werden.

Das einzige, was mich abschreckt, ist der zukünftige Baumeister hier. Aber ich denke, das liegt nur an dem Beispiel, das Sie gewählt haben?

Könnte ich das zum Beispiel erstellen?

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: ...),
   ])
  }
}

Wenn ja, das ist total LGTM! In Bezug auf das Hinzufügen zum Framework geht es darum, ob dies zu einem erstklassigen syntaktischen Ansatz befördert werden sollte (was bedeutet, dass er in etwa einem Jahr zur allgemeinen Praxis wird) oder ob es als Plugin existiert, das ein einstelliger Prozentsatz der Entwickler verwenden.

Es geht darum, ob Sie die ausführlichen und (leicht?) fehleranfälligen Beispiele mit einer besseren, prägnanteren Syntax aktualisieren möchten. Die Notwendigkeit, Eigenschaften manuell zu synchronisieren und Dinge manuell zu entsorgen () führt zu Fehlern und kognitiver Belastung.

Imo wäre es schön, wenn ein Entwickler Animatoren verwenden könnte, mit den richtigen didUpdate und disponieren und debugFillProperties und das Ganze funktioniert, ohne jemals darüber nachdenken zu müssen (genau wie wir es jetzt tun, wenn wir TweenAnimationBuilder verwenden, was der Hauptgrund ist, den wir empfehlen alle unsere Entwickler verwenden es standardmäßig anstelle der manuellen Verwaltung von Animatoren).

Wenn ja, das ist total LGTM! In Bezug auf das Hinzufügen zum Framework geht es darum, ob dies zu einem erstklassigen syntaktischen Ansatz befördert werden sollte (was bedeutet, dass er in etwa einem Jahr zur allgemeinen Praxis wird) oder ob es als Plugin existiert, das ein einstelliger Prozentsatz der Entwickler verwenden.

Angesichts der Tatsache, wie trivial Property ist, würde ich jemandem, der diesen Stil mag, empfehlen, einfach seinen eigenen zu erstellen (vielleicht mit meinem Code beginnen, wenn das hilft) und ihn direkt in seiner App zu verwenden, wie er es für richtig hält, und ihn anzupassen um auf ihre Bedürfnisse einzugehen. Es könnte zu einem Paket gemacht werden, wenn es auch vielen Leuten gefällt, obwohl es mir wiederum für etwas so Triviales nicht klar ist, wie sehr das von Vorteil ist, anstatt es einfach in den eigenen Code zu kopieren und nach Bedarf anzupassen.

Das einzige, was mich abschreckt, ist der zukünftige Baumeister hier. Aber ich denke, das liegt nur an dem Beispiel, das Sie gewählt haben?

Ich habe versucht, ein Beispiel anzusprechen, das

Könnte ich das zum Beispiel erstellen?

Sie möchten den AnimationController-Konstruktor in eine Closure verschieben, die aufgerufen wird, anstatt ihn jedes Mal aufzurufen, da initProperties während des Hot-Reloads aufgerufen wird, um die neuen Closures zu erhalten, die Sie jedoch normalerweise nicht erstellen möchten einen neuen Controller während des Hot-Reloads (zB würde er Animationen zurücksetzen). Aber ja, ansonsten scheint es in Ordnung zu sein. Sie könnten sogar ein AnimationControllerProperty , das die AnimationController Konstruktorargumente nimmt und das Richtige damit macht (zB die Dauer beim Hot-Reload aktualisieren, wenn sie sich geändert hat).

Imo wäre es schön, wenn ein Entwickler Animatoren verwenden könnte, mit den richtigen didUpdate und disponieren und debugFillProperties und das Ganze funktioniert, ohne jemals darüber nachdenken zu müssen (genau wie wir es jetzt tun, wenn wir TweenAnimationBuilder verwenden, was der Hauptgrund ist, den wir empfehlen alle unsere Entwickler verwenden es standardmäßig anstelle der manuellen Verwaltung von Animatoren).

Meine Sorge, dass Entwickler nicht darüber nachdenken, ist, dass, wenn Sie nicht daran denken, wann Dinge zugewiesen und entsorgt werden, Sie am Ende eher viele Dinge zuweisen, die Sie nicht immer brauchen, oder eine Logik ausführen, die dies nicht tut. Es muss nicht ausgeführt werden oder andere Dinge tun, die zu weniger effizientem Code führen. Das ist ein Grund, warum ich dies nur ungern zu einem empfohlenen Standardstil machen würde.

Sie könnten sogar eine AnimationControllerProperty erstellen, die die AnimationController-Konstruktorargumente übernimmt und mit ihnen das Richtige tut (zB die Dauer beim Hot-Reload aktualisieren, wenn sie sich geändert hat).

Danke @Hixie , das ist wirklich cool und ich denke, dass das Problem ganz gut angegangen wird.

Ich behaupte nicht, dass Entwickler niemals über diese Dinge nachdenken sollten, aber ich denke, der 99%ige Anwendungsfall dieser Dinge ist fast immer an das StatefulWidget gebunden, in dem sie verwendet werden, und alles andere als das führt Sie bereits in das Land der Zwischenentwickler.

Auch hier sehe ich nicht, wie sich dies grundlegend von der Empfehlung von TweenAnimationBuilder gegenüber dem rohen AnimatorController unterscheidet. Es ist im Grunde die Idee , dass , wenn Sie den Zustand ganz wollen in diesem anderen Staat enthalten ist / verwaltet (und das ist in der Regel , was Sie wollen), dann tut es auf diese Weise seiner einfache und robuster.

An dieser Stelle sollten wir einen Call organisieren und gemeinsam mit den verschiedenen Interessenten diskutieren.
Weil diese Diskussion nicht voranschreitet, da wir immer wieder dieselbe Frage beantworten.

Ich verstehe nicht, wie wir nach einer so langen Diskussion mit so vielen Argumenten immer noch argumentieren können, dass Builder im Vergleich zu StatefulWidget keine Fehler vermeiden oder dass Hooks nicht mehr wiederverwendbar sind als rohe StatefulWidgets.

Das ist besonders frustrierend, wenn man bedenkt, dass alle wichtigen deklarativen Frameworks (React, Vue, Swift UI, Jetpack Compose) dieses Problem auf die eine oder andere Weise lösen können.
Es scheint, als weigerte sich nur Flutter, dieses Problem zu berücksichtigen.

@esDotDev Der Hauptgrund IMHO, einen AnimationBuilder oder TweenAnimationBuilder oder ValueListenableBuilder zu verwenden, besteht darin, dass sie nur dann neu erstellt werden, wenn sich der Wert ändert, ohne den Rest ihres Host-Widgets neu zu erstellen. Es ist eine Sache der Leistung. Es geht nicht wirklich um Ausführlichkeit oder Code-Wiederverwendung. Ich meine, es ist auch in Ordnung, sie aus diesen Gründen zu verwenden, wenn Sie sie aus diesen Gründen nützlich finden, aber das ist zumindest für mich nicht der Hauptanwendungsfall. Es ist auch etwas, das Ihnen Property (oder Hooks) nicht gibt. Damit bauen Sie das _gesamte_ Widget neu auf, wenn sich etwas ändert, was der Leistung nicht gut tut.

@rrousselGit

Es scheint, als weigerte sich nur Flutter, dieses Problem zu berücksichtigen.

Ich habe an diesem Wochenende buchstäblich Stunden meiner Freizeit damit verbracht, ganz zu schweigen von vielen Stunden der Zeit von Google davor, dieses Problem zu betrachten, mögliche Lösungen zu beschreiben und zu versuchen, genau herauszuarbeiten, was wir zu lösen versuchen. Bitte verwechseln Sie mangelndes Verständnis darüber, was ein Problem ist, nicht mit der Weigerung, es in Betracht zu ziehen. Vor allem, wenn ich bereits das Beste beschrieben habe, was getan werden kann, um dies voranzutreiben (Erstellen einer Demo-App, die die gesamte Zustandslogik enthält, die "zu ausführlich oder zu schwierig" ist, um den Titel des Problems zu zitieren, um sie wiederzuverwenden). andere zu diesem Fehler als Aufgabe übernommen haben und an der Sie sich geweigert haben, daran teilzunehmen.

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.

Interessant. Für uns haben wir wirklich noch nie eine Leistungssteigerung durch das Einsparen kleiner Umbauten wie diese gemessen oder beobachtet. Bei einer großen App-Codebasis ist es viel wichtiger, den Code prägnant, lesbar und frei von Routinefehlern zu halten, die auftreten können, wenn Sie alle paar Wochen Hunderte von Klassendateien entfernen.

Unserer Erfahrung nach sind die Kosten für das Neuzeichnen der Pixel, die anscheinend für den gesamten Baum auftreten, wenn Sie Ihre RepaintBoundaries nicht gezielt definieren, ein weitaus wichtigerer Faktor für die reale Leistung als die Kosten für das teilweise Widget-Layout. Vor allem, wenn Sie in den 4k-Monitorbereich gelangen.

Aber dies ist ein gutes Beispiel dafür, wann Bauherren für so etwas tatsächlich Sinn machen. Wenn ich einen Unterkontext erstellen möchte, ist ein Builder sinnvoll und ein guter Weg dorthin.

Oftmals tun wir das nicht, und in diesem Fall fügt Builder nur Unordnung hinzu, aber wir akzeptieren es, weil die Alternative nur eine andere Art von Unordnung ist, und zumindest mit Builder sind die Dinge mehr oder weniger garantiert fehlerfrei. In Fällen, in denen die gesamte Ansicht neu aufgebaut wird oder es nicht unbedingt eine Ansicht neu erstellt (TextEditingController, FocusController), macht die Verwendung eines Builders wenig Sinn, und in allen Fällen ist das Rollen der Boilerplate von Hand grundsätzlich nicht DRY.

Es ist sicherlich sehr situationsspezifisch, da Leistungsprobleme oft auftreten. Ich denke, es macht Sinn, wenn Leute so etwas wie Hooks oder Property verwenden, wenn sie diesen Stil mögen. Das ist heute möglich und scheint nichts zusätzliches vom Framework zu benötigen (und wie Property zeigt, erfordert es wirklich nicht viel Code).

Nein, aber es ist ein bisschen so, als würde man die Community bitten, TweenAnimationBuilder und ValueListenableBuilder zu erstellen, und ihnen kein StatefulWidget bereitzustellen, auf dem sie aufbauen können.

Nicht, dass Sie fragen, aber einer der Hauptvorteile dieser Art von Architektur besteht darin, dass sie sich natürlich für winzige kleine Komponenten eignet, die leicht gemeinsam genutzt werden können. Wenn Sie ein kleines grundlegendes Stück an Ort und Stelle setzen ...

StatefulWidget ist im Vergleich zu Property eine Menge Code und nicht trivial (im Gegensatz zu Property, bei dem es sich hauptsächlich um Glue-Code handelt). Das heißt, wenn Property etwas ist, das für eine breite Wiederverwendung sinnvoll ist (im Gegensatz zur Erstellung maßgeschneiderter Versionen für jede Anwendung oder jedes Team basierend auf seinen genauen Bedürfnissen), dann würde ich jemanden, der sich für seine Verwendung einsetzt, ermutigen, ein Paket zu erstellen und es hochzuladen pub . Das gleiche gilt in der Tat für Hooks. Wenn es etwas ist, das der Community gefällt, wird es viel Verwendung finden, genau wie Provider. Es ist nicht klar, warum wir so etwas in das Framework selbst einfügen müssen.

Ich denke, weil dies von Natur aus erweiterbar ist. Provider ist es nicht, es ist nur ein einfaches Tool. Dies ist etwas, das wie StatefulWidget erweitert werden soll, jedoch für StatefulComponents. Dass es relativ trivial ist, sollte man ihm nicht unbedingt entgegenhalten?

Eine Anmerkung zu "diejenigen, die diesen Stil bevorzugen". Wenn Sie 3 Überschreibungen und 15-30 Zeilen speichern können, ist dies in den meisten Fällen ein Gewinn an Lesbarkeit. Objektiv gesehen imo. Es eliminiert auch objektiv 2 ganze Klassen von Fehlern (Vergessen, Dinge zu entsorgen, vergessen, Deps zu aktualisieren).

Vielen Dank für die tolle Diskussion, bin gespannt, wohin das führt, ich werde es auf jeden Fall hier lassen :)

Es tut mir leid, sagen zu müssen, dass dieser Thread mich desillusioniert macht, wieder ins Flattern zu geraten, was der Plan war, wenn ich ein anderes Projekt abschließe, an dem ich arbeite. Ich spüre auch die Frustration wegen

Das ist besonders frustrierend, wenn man bedenkt, dass alle wichtigen deklarativen Frameworks (React, Vue, Swift UI, Jetpack Compose) dieses Problem auf die eine oder andere Weise lösen können.

Ich stimme @rrousselGit darin zu, dass wir meiner Meinung nach keine Zeit damit verbringen sollten, Flatter-Beispiel-Apps zu
Außerdem können wir eine App nicht im Flattern schreiben, wenn wir nach einer Lösung suchen, da wir die Lösungen benötigen, um eine App zu schreiben. Da zumindest die Flatter-Leute in diesem Gespräch ziemlich klar waren, mögen sie keine Hooks. Und Flutter hat einfach keine andere Lösung für das Problem, wie im OP beschrieben. Wie soll es überhaupt geschrieben werden.

Fühlt (zumindest für mich), dass das nicht ernst genommen wird, tut mir leid @Hixie , ich meine, es wird nicht ernst genommen im Sinne von: We understand the problem and want to solve it . Wie andere deklarative Frameworks anscheinend getan haben.
Auf einer anderen, aber ähnlichen Note, Dinge wie diese:

Ist es einfacher zu schreiben?
Entwickler müssen lernen, Hooks zu schreiben, was ein neues Konzept ist, also schwieriger

Das macht mich traurig. Warum jemals etwas verbessern oder ändern? Du könntest dieses Argument immer vorbringen, egal was passiert. Auch wenn die neuen Lösungen nach dem Erlernen viel einfacher und angenehmer sind. Sie können Hooks in dieser Anweisung durch viele Dinge ersetzen. Meine Mutter hätte vor 30 Jahren einen solchen Satz über Mikrowellen verwenden können. Es funktioniert zB genauso, wenn Sie im Satz "hooks" durch "flattern" oder "dart" ersetzen.

Ist es einfacher zu schreiben?
Es ist das gleiche, also ist es genauso einfach zu schreiben, was einigermaßen einfach ist

Ich glaube nicht, dass @rrousselGit mit is it easier to write? (einer booleschen Antwortfrage) gemeint ist , dass die Antwort nicht false / undefined ist, wenn es gleich ist.

Ich kann mir nicht vorstellen, wie wir jemals irgendwo hinkommen werden, da wir uns nicht einmal einig sind, dass es ein Problem gibt, nur dass viele Leute dies als Problem empfinden. Z.B:

Es ist sicherlich etwas, das die Leute hervorgebracht haben. Damit habe ich keine viszerale Erfahrung. Es ist kein Problem, das ich beim Schreiben meiner eigenen Apps mit Flutter empfunden habe. Das bedeutet jedoch nicht, dass es für manche Leute kein echtes Problem ist.

Und obwohl viele viele Male viele Argumente geliefert haben, warum eine Lösung für das OP im Kern sein muss.
ZB muss es im Kern enthalten sein, damit Dritte es genauso einfach und natürlich verwenden können, wie sie heute Widgets verwenden und erstellen. Und viele andere Gründe. Das Mantra scheint zu sein, einfach in ein Paket packen. Aber es gibt bereits Pakete wie die Haken. Wenn das geklärt ist, warum dann nicht einfach den Thread schließen.

Ich hoffe sehr, dass Sie @rrousselGit sein Angebot annehmen und einen Anruf organisieren, vielleicht ist es einfacher, eine Echtzeit-Diskussion darüber zu führen, anstatt ständig hin und her zu schreiben. Wenn irgendwelche Leute aus den anderen Frameworks, die das im OP beschriebene Problem gelöst haben, wenn einer von ihnen wirklich nett ist, könnten sie vielleicht auch für eine Weile in den Anruf einsteigen und ihnen 5 Cent über Dinge mitteilen, die auftauchen. Fragen konnte man immer.

Wie auch immer, ich melde mich jetzt ab, da ich ein bisschen traurig werde, diesen Thread live zu verfolgen, da ich nicht sehe, dass er nirgendwo hinführt. Aber ich hoffe, dass dieser Thread so weit kommt, dass er zustimmt, dass ein Problem vorliegt, damit er sich auf mögliche Lösungen für das OP konzentrieren kann. Da es etwas sinnlos erscheint, Lösungen vorzuschlagen, wenn Sie das Problem nicht verstehen, mit dem die Leute konfrontiert sind, wie @Hixie wahrscheinlich zustimmt, meine ich, da die Leute mit den Problemen Ihnen sagen, warum die Lösung im Nachhinein nicht funktioniert.

Ich wünsche Ihnen wirklich viel Glück beim Beenden dieses Threads, indem Sie einfach sagen, dass Flattern dieses Problem im Kern nicht lösen sollte, obwohl die Leute es wollen. Oder indem Sie eine Lösung finden. 😘

LayoutBuilder ist ein ganz anderes Widget als die meisten Builder, FWIW. Keiner der bisher besprochenen Vorschläge würde für LayoutBuilder-ähnliche Probleme funktionieren, und keine der vor Ihrem Kommentar beschriebenen Anforderungen enthält LayoutBuilder. Wenn wir diese neue Funktion auch für den Umgang mit LayoutBuilder verwenden sollten, ist das wichtig zu wissen; Ich empfehle, mit @TimWhiting zusammenzuarbeiten , um sicherzustellen, dass die Beispiel-App, auf der wir Vorschläge

@Hixie Ja, wir brauchen definitiv ein paar Proben. Ich werde etwas vorbereiten (aber ich denke immer noch, dass Compileränderungen erforderlich sind, sodass das Beispiel möglicherweise unvollständig ist). Die allgemeine Idee ist - ein Syntaxzucker, der Builder glättet und sich nicht darum kümmert, wie der Builder implementiert wird.

Trotzdem habe ich den Eindruck, dass sich niemand im Flutter-Team tiefer mit SwiftUI befasst hat, ich denke, unsere Bedenken wären ansonsten leicht zu verstehen. Für die Zukunft des Frameworks ist es wichtig, dass Menschen, die von anderen Plattformen migrieren, eine möglichst reibungslose Fahrt haben. Daher sind ein gutes Verständnis anderer Plattformen und Kenntnisse der Vor- und Nachteile erforderlich. Und sehen, ob einige der Nachteile von Flutter behoben werden könnten. Offensichtlich hat Flutter viele gute Ideen von React übernommen und ich könnte dasselbe mit neueren Frameworks tun.

@emanuel-lundman

Fühlt (zumindest für mich), dass das nicht ernst genommen wird, tut mir leid @Hixie , ich meine, es wird nicht ernst genommen im Sinne von: We understand the problem and want to solve it . Wie andere deklarative Frameworks anscheinend getan haben.

Ich stimme voll und ganz zu, dass ich das Problem nicht verstehe. Deshalb beschäftige ich mich immer wieder mit diesem Thema und versuche, es zu verstehen. Aus diesem Grund habe ich vorgeschlagen, eine Demo-App zu erstellen, die das Problem kapselt. Ob es etwas ist, das wir am Ende beheben wollen, indem wir das Framework grundlegend ändern oder ein kleines Feature zum Framework hinzufügen, oder durch ein Paket oder gar nicht, hängt wirklich davon ab, was das Problem tatsächlich ist.

@szotp

Trotzdem habe ich den Eindruck, dass sich niemand im Flutter-Team tiefer mit SwiftUI befasst hat, ich denke, unsere Bedenken wären ansonsten leicht zu verstehen.

Ich habe Swift UI studiert. Es ist sicherlich weniger ausführlich, Swift-UI-Code zu schreiben als Flutter-Code, aber die Lesbarkeitskosten sind IMHO sehr hoch. Es gibt eine Menge "Magie" (im Sinne von Logik, die auf eine Weise funktioniert, die im Verbrauchercode nicht offensichtlich ist). Ich kann absolut glauben, dass es ein Stil ist, den manche Leute bevorzugen, aber ich glaube auch, dass eine der Stärken von Flutter darin besteht, dass er sehr wenig Magie hat. Das bedeutet, dass Sie manchmal mehr Code schreiben, aber es bedeutet auch, dass das Debuggen dieses Codes _viel_ einfacher ist.

Ich denke, es gibt Raum für viele Arten von Frameworks. MVC-Stil, React-Stil, super knapp magisch, ohne Magie, aber ausführlich... Einer der Vorteile der Flutter-Architektur besteht darin, dass der Portabilitätsaspekt vollständig vom Framework selbst getrennt ist, sodass es möglich ist, alle unsere Tools zu nutzen -- plattformübergreifende Unterstützung, Hot Reload usw. -- aber ein völlig neues Framework erstellen. (Es gibt bereits andere Flutter-Frameworks, zB flatter_sprites.) Ähnlich ist das Framework selbst geschichtet aufgebaut, sodass Sie beispielsweise unsere gesamte RenderObject-Logik wiederverwenden können, jedoch mit einem Ersatz für die Widgets-Ebene Widgets ist zu viel, jemand könnte ein alternatives Framework erstellen, das es ersetzt. Und natürlich gibt es das Paketsystem, mit dem Funktionen hinzugefügt werden können, ohne den vorhandenen Framework-Code zu verlieren.

Wie auch immer, der Punkt meiner Abschweifung hier ist nur, dass dies nicht alles oder nichts ist. Auch wenn wir langfristig keine Lösung finden, die Sie glücklich macht, heißt das nicht, dass Sie nicht weiterhin von den Teilen des Angebots profitieren können, die Ihnen gefallen.


Ich fordere Leute, die an diesem Problem interessiert sind, auf, mit @TimWhiting zusammenzuarbeiten , um eine App zu erstellen, die zeigt, warum Sie Code wiederverwenden möchten und wie es heute aussieht, wenn dies nicht möglich ist (https://github.com/TimWhiting/local_widget_state_approaches). Dies wird uns direkt helfen, Vorschläge zu erstellen, wie dieses Problem so angegangen werden kann, dass die Bedürfnisse _aller_ Leute, die hier kommentieren (einschließlich derer, die Hooks mögen und derer, die Hooks nicht mögen) angegangen werden.

Es kann gar nicht so schwer zu verstehen sein, warum "_a Syntax Sugar, der Builder abflacht und sich nicht darum kümmert, wie der Builder implementiert ist._" von Entwicklern als erstklassiges Feature gewünscht wird. Wir haben die Probleme mit den alternativen Ansätzen immer wieder umrissen.

Kurz gesagt, Builder lösen das Problem der Wiederverwendbarkeit, sind aber schwer zu lesen und zu verfassen. Das "Problem" ist einfach, dass wir eine Builder-ähnliche Funktionalität wünschen, die deutlich leichter zu lesen ist.

Keine App kann dies deutlicher zeigen, wenn Sie einfach nicht der Meinung sind, dass 3 verschachtelte Builder schwer zu lesen sind oder dass Builder im Allgemeinen nicht wirklich einem Zweck der Wiederverwendung von Code dienen. Wichtiger ist nur zu hören, dass viele von uns tatsächlich gerne auf Verschachtelungen verzichten und es nicht mögen, Code in unserer gesamten Anwendung zu duplizieren, und so stecken wir zwischen zwei nicht idealen Optionen fest.

Ich habe an diesem Wochenende buchstäblich Stunden meiner Freizeit damit verbracht, ganz zu schweigen von vielen Stunden der Zeit von Google davor, dieses Problem zu betrachten, mögliche Lösungen zu beschreiben und zu versuchen, genau herauszuarbeiten, was wir zu lösen versuchen

Dafür bin ich dankbar

Bitte verwechseln Sie mangelndes Verständnis darüber, was ein Problem ist, nicht mit der Weigerung, es in Betracht zu ziehen

Mir geht es gut mit Unverständnis, aber die aktuelle Situation scheint aussichtslos.
Wir diskutieren noch immer über Punkte, die zu Beginn der Diskussion angesprochen wurden.

Aus meiner Sicht habe ich das Gefühl, dass ich Stunden damit verbracht habe, detaillierte Kommentare zu schreiben, die die verschiedenen Probleme aufzeigen und Fragen beantworten, aber meine Kommentare wurden abgewiesen und dieselbe Frage wurde erneut gestellt.

Im Zentrum der Diskussion steht beispielsweise die mangelnde Lesbarkeit der aktuellen Syntax.
Ich habe mehrere Analysen des Lesbarkeitsproblems durchgeführt, um dies zu untermauern:

Diese Analysen haben eine signifikante Anzahl von 👍 und andere Völker scheinen zuzustimmen

Und dennoch gibt es laut Ihrer letzten Antwort kein Lesbarkeitsproblem: https://github.com/flutter/flutter/issues/51752#issuecomment -671009593

Sie haben auch vorgeschlagen:

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);
}

zu wissen, dass das nicht lesbar ist

Aus diesen beiden Kommentaren können wir folgendes schließen:

  • Wir sind nicht der Meinung, dass es ein Lesbarkeitsproblem gibt
  • es ist noch unklar, ob die Lesbarkeit Teil des Umfangs dieser Ausgabe ist oder nicht

Das ist entmutigend zu hören, wenn man bedenkt, dass der einzige Zweck von Hooks darin besteht, die Syntax von Buildern zu verbessern – die auf dem Höhepunkt der Wiederverwendbarkeit sind, aber eine schlechte Lesbarkeit/Schreibbarkeit haben
Wenn wir uns in dieser grundlegenden Tatsache nicht einig sind, bin ich mir nicht sicher, was wir tun können.

@Hixie danke, das hilft sehr, deinen Standpunkt zu verstehen. Ich stimme zu, dass sie mit der Codemagie vielleicht über Bord gegangen sind, aber ich bin mir sicher, dass sie zumindest ein paar Dinge richtig gemacht haben.

Und ich mag Flutters geschichtete Architektur sehr. Ich möchte auch weiterhin Widgets verwenden. Vielleicht besteht die Antwort also darin, nur die Erweiterbarkeit von Dart & Flutter zu verbessern, was für mich lauten würde:

Machen Sie die Code-Generierung nahtloser - es ist möglicherweise möglich, SwiftUI-Magie in Dart zu implementieren, aber das übliche erforderliche Setup ist einfach zu groß und zu langsam.

Wenn die Verwendung der Codegenerierung so einfach wäre wie das Importieren von Paketen und das Einfügen einiger Anmerkungen, dann würden Leute, die das besprochene Problem haben, einfach das tun und aufhören, sich zu beschweren. Der Rest würde weiterhin die guten alten StatefulWidgets direkt verwenden.

EDIT: Ich denke, flutter generate war ein Schritt in eine gute Richtung, schade, dass es entfernt wurde.

Ich denke, das wäre eine sehr interessante Frage für die nächste Flutter-Entwicklerumfrage.

Es wäre ein guter Anfang. Teilen Sie dieses Problem in verschiedene Teile/Fragen auf und sehen Sie, ob dies ein echtes Problem ist, das die Flutter-Entwickler lösen möchten.

Sobald das klar ist, wird dieses Gespräch flüssiger und bereichernder

Aus meiner Sicht habe ich das Gefühl, dass ich Stunden damit verbracht habe, detaillierte Kommentare zu schreiben, die die verschiedenen Probleme aufzeigen und Fragen beantworten, aber meine Kommentare wurden abgewiesen und dieselbe Frage wurde erneut gestellt.

Wenn ich die gleichen Fragen stelle, liegt es daran, dass ich die Antworten nicht verstehe.

Um zum Beispiel zu Ihrem früheren Kommentar zurückzukehren (https://github.com/flutter/flutter/issues/51752#issuecomment-670959424):

Das diskutierte Problem ist:

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

Dieser Code ist nicht lesbar.

Ich sehe wirklich nicht, was daran nicht lesbar ist. Es erklärt genau, was passiert. Es gibt vier Widgets, drei der Widgets haben Builder-Methoden, eines hat nur einen String. Ich persönlich würde die Typen nicht weglassen, ich denke, das macht es schwieriger zu lesen, weil ich nicht sagen kann, was die Variablen alle sind, aber es ist kein großes Problem.

Warum ist das nicht lesbar?

Um es klar zu sagen, Sie finden es offensichtlich unleserlich, ich versuche nicht zu sagen, dass Sie falsch liegen. Ich verstehe einfach nicht warum.

Wir könnten das Lesbarkeitsproblem beheben, indem wir ein neues Schlüsselwort einführen, das die Syntax in Folgendes ändert:

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');
}

Dieser Code ist deutlich lesbarer, hat nichts mit Hooks zu tun und leidet nicht unter seinen Einschränkungen.

Es ist sicherlich weniger ausführlich. Ich bin mir nicht sicher, ob es besser lesbar ist, zumindest für mich. Es gibt mehr Konzepte (jetzt haben wir sowohl Widgets als auch diese "Schlüsselwort"-Funktion); mehr Konzepte bedeuten mehr kognitive Belastung. (Es ist auch potenziell weniger effizient, je nachdem, wie sehr diese Objekte unabhängig sind; z. B. wenn sich die Animation immer häufiger ändert als der Wert listenable und stream, bauen wir jetzt den ValueListenableBuilder und den StreamBuilder neu auf, obwohl sie normalerweise nicht ausgelöst würden ; auch die Initialisierungslogik muss nun bei jedem Build eingegeben und übersprungen werden.)

Sie haben gesagt, dass die Ausführlichkeit nicht das Problem ist, also ist es nicht der Grund, warum es besser lesbar ist (obwohl ich darüber auch verwirrt bin, da Sie "zu ausführlich" in den Titel der Ausgabe geschrieben haben und in die ursprüngliche Beschreibung des Problems). Sie haben erwähnt, dass Sie weniger Einzüge wünschen, aber Sie haben auch die Version der Verwendung von Buildern ohne Einrückung als unlesbar beschrieben, also ist vermutlich nicht die Einrückung im Original das Problem.

Sie sagen, dass Builder der Höhepunkt der Wiederverwendbarkeit sind und Sie nur eine alternative Syntax wünschen, aber die von Ihnen vorgeschlagenen Vorschläge sind nicht wie Builder (sie erstellen keine Widgets oder Elemente), also ist es nicht speziell der Builder-Aspekt, den Sie sind auf der Suche nach.

Sie haben eine Lösung, die Ihnen gefällt (Hooks), die, soweit ich das beurteilen kann, großartig funktioniert, aber Sie möchten etwas am Framework ändern, damit die Leute Hooks verwenden? Was ich auch nicht verstehe, denn wenn die Leute Hooks nicht genug mögen, um es als Paket zu verwenden, ist es wahrscheinlich auch keine gute Lösung für das Framework (im Allgemeinen bewegen wir uns mehr in Richtung der Verwendung von Paketen, sogar Features das Flutter-Team schafft, was es wert ist).

Ich verstehe, dass der Wunsch nach einer einfacheren Wiederverwendung von Code besteht. Ich weiß nur nicht, was das bedeutet.

Wie lässt sich die Lesbarkeit der folgenden mit den beiden obigen Versionen vergleichen?

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 Wenn es zu viel Reibung mit unserer aktuellen Codegen-Lösung gibt, zögern Sie bitte nicht, einen Fehler zu melden und dort nach Verbesserungen zu fragen.

@jamesblasco Ich glaube nicht, dass es hier ein echtes Problem gibt, das die Leute lösen wollen. Die Frage für mich ist genau, was dieses Problem ist, damit wir eine Lösung entwerfen können.

Ich könnte die Bedenken bezüglich der Hook-Fehler oder des Wunsches, in den Code aufgenommen zu werden, beantworten, aber ich denke nicht, dass wir uns jetzt darauf konzentrieren sollten.

Wir sollten uns zunächst darauf einigen, was das Problem ist. Wenn wir dies nicht tun, sehe ich keine Möglichkeit, uns auf andere Themen zu einigen.

Ich sehe wirklich nicht, was daran nicht lesbar ist. Es erklärt genau, was passiert. Es gibt vier Widgets, drei der Widgets haben Builder-Methoden, eines hat nur einen String. Ich persönlich würde die Typen nicht weglassen, ich denke, das macht es schwieriger zu lesen, weil ich nicht sagen kann, was die Variablen alle sind, aber es ist kein großes Problem.

Ich denke, ein großer Teil des Problems besteht darin, dass sich die Art und Weise, wie Sie codieren, drastisch von der Art und Weise unterscheidet, wie die meisten Leute codieren.

Zum Beispiel Flutter und das von Ihnen angegebene App-Beispiel:

  • Verwenden Sie nicht dartfmt
  • immer_specify_types verwenden

Allein mit diesen beiden Punkten wäre ich überrascht, wenn das mehr als 1% der Community ausmachen würde.

Daher unterscheidet sich das, was Sie als lesbar einschätzen, wahrscheinlich stark von dem, was die meisten Leute für lesbar halten.

Ich sehe wirklich nicht, was daran nicht lesbar ist. Es erklärt genau, was passiert. Es gibt vier Widgets, drei der Widgets haben Builder-Methoden, eines hat nur einen String. Ich persönlich würde die Typen nicht weglassen, ich denke, das macht es schwieriger zu lesen, weil ich nicht sagen kann, was die Variablen alle sind, aber es ist kein großes Problem.

Warum ist das nicht lesbar?

Meine Empfehlung wäre, zu analysieren, wohin Ihr Auge bei der Suche nach einem bestimmten Objekt schaut und wie viele Schritte es braucht, um dorthin zu gelangen.

Machen wir ein Experiment:
Ich gebe Ihnen zwei Widget-Bäume. Eine mit einer linearen Syntax, die andere mit einer verschachtelten Syntax.
Ich werde Ihnen auch spezifische Dinge geben, nach denen Sie in diesem Ausschnitt suchen müssen.

Ist es einfacher, die Antwort auf die Verwendung der linearen Syntax oder der verschachtelten Syntax zu finden?

Die Fragen:

  • Was ist das Nicht-Builder-Widget, das von dieser Build-Methode zurückgegeben wird?
  • Wer erstellt die Variable bar ?
  • Wie viele Bauherren haben wir?

Builder verwenden:

 Widget-Build (Kontext) {
 Rückgabe ValueListenableBuilder(
 valueListenable: someValueListenable,
 Builder: (Kontext, foo, _) {
 StreamBuilder zurückgeben(
 stream: someStream,
 Baumeister: (Kontext, baz) {
 zurück TweenAnimationBuilder(
 Tween: Tween(...),
 Builder: (Kontext, Balken) {
 Rückgabecontainer();
 },
 );
 },
 );
 },
 );
 }

Verwenden einer linearen Syntax:

 Widget-Build (Kontext) {
 final foo = Schlüsselwort ValueListenableBuilder(valueListenable: someValueListenable);
 Schlussbalken = Schlüsselwort StreamBuilder(stream: someStream);
 final baz = Schlüsselwort TweenAnimationBuilder(tween: Tween(...));

Bild zurückgeben(); }


In meinem Fall fällt es mir schwer, den verschachtelten Code zu durchsuchen, um die Antwort zu finden.
Auf der anderen Seite ist das Finden der Antwort mit dem linearen Baum augenblicklich

Sie haben erwähnt, dass Sie weniger Einzüge wünschen, aber Sie haben auch die Version der Verwendung von Buildern ohne Einrückung als unlesbar beschrieben. Vermutlich ist also nicht die Einrückung im Original das Problem.

War die Aufteilung von StreamBuilder in mehrere Variablen ein ernsthafter Vorschlag?
Nach meinem Verständnis war dies ein sarkastischer Vorschlag, um zu argumentieren. War es nicht? Glauben Sie wirklich, dass dieses Muster zu lesbarerem Code führen würde, selbst bei großen Widgets?

Abgesehen davon, dass das Beispiel nicht funktioniert, macht es mir nichts aus, es aufzuschlüsseln, um zu erklären, warum es nicht lesbar ist. Wäre das wertvoll?

```Pfeil
Widget-Build (Kontext) {
Rückkehr
ValueListenableBuilder(valueListenable: someValueListenable, builder: (context, value, _) =>
StreamBuilder(stream: someStream, builder: (context, value2) =>
TweenAnimationBuilder(tween: Tween(...), builder: (context, value3) =>
Text('$wert $wert2 $wert3'),
)));
}

Das sieht besser aus.
Aber das setzt voraus, dass die Leute Dartfmt nicht verwenden

Mit dartfmt haben wir:

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

was sich fast nicht vom Originalcode unterscheidet.

Sie sagen, dass Builder der Höhepunkt der Wiederverwendbarkeit sind und Sie nur eine alternative Syntax wünschen, aber die von Ihnen vorgeschlagenen Vorschläge sind nicht wie Builder (sie erstellen keine Widgets oder Elemente), also ist es nicht speziell der Builder-Aspekt, den Sie sind auf der Suche nach.

Das ist ein Implementierungsdetail.
Es gibt keinen besonderen Grund, ein Element zu haben oder nicht.
Tatsächlich kann es interessant sein, ein Element zu haben, sodass wir LayoutBuilder und möglicherweise GestureDetector einschließen könnten.

Ich denke, es hat eine niedrige Priorität. Aber in der React-Community habe ich unter den verschiedenen Hook-Bibliotheken Folgendes gesehen:

  • useIsHovered – gibt einen booleschen Wert zurück, der angibt, ob das Widget im Schwebezustand ist
  • useSize – (sollte wahrscheinlich useContraints in Flutter sein), was die Größe der zugehörigen Benutzeroberfläche angibt.

(Es ist auch potenziell weniger effizient, je nachdem, wie sehr diese Objekte unabhängig sind; z. B. wenn sich die Animation immer häufiger ändert als der Wert listenable und stream, bauen wir jetzt den ValueListenableBuilder und den StreamBuilder neu auf, obwohl sie normalerweise nicht ausgelöst würden ; auch die Initialisierungslogik muss nun bei jedem Build eingegeben und übersprungen werden.)

Das hängt davon ab, wie die Lösung gelöst wird.

Wenn wir eine Sprachkorrektur vornehmen, wäre dieses Problem überhaupt kein Problem.

Das könnten wir machen:

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');
}

"kompiliert" zu:

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

Wenn wir Hooks verwenden, enthält flatter_hooks ein HookBuilder Widget, damit wir bei Bedarf immer noch Dinge aufteilen können.
Ebenso bräuchte es geeignete Benchmarks, um festzustellen, ob es sich wirklich um ein Problem handelt, insbesondere in dem hier gemachten Beispiel.

Bei Hooks bauen wir nur ein Element nach.
Mit Builders wird der Wiederaufbau auf mehrere Elemente aufgeteilt. Das fügt auch etwas Overhead hinzu, auch wenn es klein ist.

Es ist nicht unmöglich, dass es schneller ist, alle Haken neu zu bewerten. Es scheint, dass dies die Schlussfolgerung des React-Teams war, zu dem sie bei der Entwicklung von Haken gekommen sind.
Dies gilt jedoch möglicherweise nicht für Flutter.

Warum ist das nicht lesbar?

Wegen der Verschachtelung - Verschachtelung macht es schwieriger, schnell zu durchsuchen und zu wissen, welche Teile Sie ignorieren können und welche für das Verständnis des Geschehens unerlässlich sind. Der Code ist von Natur aus "sequentiell", aber die Verschachtelung verbirgt dies. Das Verschachteln macht es auch schwierig, damit zu arbeiten - stellen Sie sich vor, Sie möchten zwei Dinge neu anordnen - oder ein neues Element zwischen zwei einfügen - trivial in wirklich sequenziellem Code, aber schwer, wenn Sie mit Verschachtelung arbeiten müssen.

Dies ist sehr ähnlich wie async/await Sugar im Vergleich zum Arbeiten mit roher Future API, darunter das Konzept der Dame-Fortsetzung (und sogar Argumente dafür und dagegen sind sehr ähnlich) - ja Future API kann direkt verwendet werden und verbirgt nichts, aber Lesbarkeit und Wartbarkeit ist sicherlich nicht gut - async/await ist dort ein Gewinner.

Meine Empfehlung wäre, zu analysieren, wohin Ihr Auge bei der Suche nach einem bestimmten Objekt schaut und wie viele Schritte es braucht, um dorthin zu gelangen.

Ich programmiere jetzt seit 25 Jahren in über 10 verschiedenen Sprachen und das ist einfach die schlechteste Art zu bewerten, was einen Code lesbar macht. Die Lesbarkeit des Quellcodes ist schwierig, aber es geht mehr darum, wie gut er Programmierkonzepte und -logik ausdrückt, als "wohin meine Augen schauen" oder wie viele Codezeilen er verwendet.

Oder besser gesagt, es scheint mir, dass Sie sich zu sehr auf die Lesbarkeit und weniger auf die Wartbarkeit konzentrieren .

Ihre Beispiele sind weniger lesbar, weil die __Absicht__ des Codes weniger offensichtlich ist und verschiedene Bedenken an derselben Stelle versteckt werden, macht es schwieriger, ihn zu verwalten.


final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);

Wie hoch wäre der Wert überhaupt? Ein Widget? Eine String-Variable? Ich meine, es wird in einem verwendet
return Text('$value $value2 $value3');

Grundsätzlich möchten Sie, dass durch Verweisen auf Variable A in der Build-Methode von Widget B B neu erstellt wird, wenn sich der Wert von A ändert. Das ist buchstäblich das, was mobx macht, und zwar genau mit der richtigen Menge an Magie / Boilerplate.


final value2 = keyword StreamBuilder(stream: someStream);

Was soll das zurückgeben? Ein Widget? Ein Strom? Ein String-Wert?

Auch hier sieht es wie ein String-Wert aus. Sie möchten also in der Lage sein, in einer Build-Methode einfach auf einen Stream zu verweisen, dieses Widget jedes Mal neu zu erstellen, wenn der Stream einen Wert ausgibt, und Zugriff auf den ausgegebenen Wert haben und den Stream erstellen/aktualisieren/verwerfen, wenn das Widget erstellt/aktualisiert wird /zerstört? In einer einzigen Codezeile? Innerhalb der Build-Methode?

Ja, mit mobx können Sie Ihre Build-Methoden genau wie Ihr "leserliches" Beispiel aussehen lassen (außer Sie verweisen auf Observables). Sie müssen immer noch den eigentlichen Code schreiben, der die ganze Arbeit erledigt, genau wie Sie es mit Hooks tun. Der eigentliche Code umfasst etwa 10 Zeilen und ist in jedem Widget wiederverwendbar.


final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

Eine Klasse namens "TweenAnimationBuilder", die einen String zurückgibt?! Ich gehe nicht einmal in die Nähe, warum das eine schreckliche Idee ist.

Es gibt keinen Unterschied in Einrückung/Lesbarkeit zwischen:

Future<double> future;

AsyncSnapshot<double> value = keyword FutureBuilder<double>(future: future);

und:

Future<double> future;

double value = await future;

Beide machen genau dasselbe: Auf ein Objekt hören und seinen Wert auspacken.

Ich sehe wirklich nicht, was daran nicht lesbar ist. Es erklärt genau, was passiert. Es gibt vier Widgets, drei der Widgets haben Builder-Methoden, eines hat nur einen String. Ich persönlich würde die Typen nicht weglassen, ich denke, das macht es schwieriger zu lesen, weil ich nicht sagen kann, was die Variablen alle sind, aber es ist kein großes Problem.

Das gleiche Argument könnte auf Promise/Future-Ketten angewendet werden.

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);

Man könnte sagen, die erste Schreibweise macht deutlich, dass Versprechen unter der Haube entstehen und wie genau die Kette gebildet wird. Ich frage mich, ob Sie das abonnieren oder ob Sie der Meinung sind, dass Promises sich grundlegend von Buildern unterscheiden, da sie eine Syntax verdienen.

Eine Klasse namens "TweenAnimationBuilder", die einen String zurückgibt?! Ich gehe nicht einmal in die Nähe, warum das eine schreckliche Idee ist.

Sie können dasselbe Argument für Versprechen/Zukunft anstellen und sagen, dass await die Tatsache verschleiert, dass es ein Versprechen zurückgibt.

Ich sollte anmerken, dass die Idee, Dinge per Syntax zu "entpacken", nicht neu ist. Ja, in den Mainstream-Sprachen kam es über async/await, aber zum Beispiel hat F# Berechnungsausdrücke , ähnlich der Notation do in einigen Hardcore-FP-Sprachen. Dort hat es viel mehr Leistung und ist verallgemeinert, um mit allen Wrappern zu arbeiten, die bestimmte Gesetze erfüllen. Ich schlage nicht vor, Monads zu Dart hinzuzufügen, aber ich denke, es lohnt sich darauf hinzuweisen, dass es definitiv einen Präzedenzfall für eine typsichere Syntax zum "Entpacken" von Dingen gibt, die nicht unbedingt asynchronen Aufrufen entsprechen.

Wenn ich einen Schritt zurück trete, denke ich, dass eine Sache, mit der viele Leute hier kämpfen (mich eingeschlossen), diese Frage nach der Lesbarkeit ist. Wie @rrousselGit erwähnt hat, gibt es in diesem Thread viele Beispiele für Lesbarkeitsprobleme mit dem aktuellen Builder basierten Ansatz. Für viele von uns scheint es selbstverständlich zu sein, dass dies:

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

ist deutlich weniger lesbar:

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');
}

Aber es ist eindeutig nicht selbstverständlich, da @Hixie und @Rudiksz nicht überzeugt sind (oder aktiv dagegen sind), dass das zweite besser lesbar ist als das erste.

Hier ist also meine Aufschlüsselung (für jeden kleinen Betrag), warum der zweite Codeblock besser lesbar ist als der erste:

1. Der erste Codeblock ist deutlich stärker eingerückt als der zweite

Meiner Erfahrung nach entspricht Einrückung typischerweise Asynchronität, Verzweigung oder Rückrufen, die alle mehr kognitive Belastung erfordern als nicht eingerückter, linearer Code. Der erste Codeblock hat mehrere Einrückungsschichten, und daher brauche ich nicht ganz trivial, um durchzuarbeiten, was hier passiert und was letztendlich gerendert wird (ein einzelnes Text ). Vielleicht können andere Leute diese Einrückung in ihren Köpfen besser durcharbeiten.


Im zweiten Codeblock gibt es keine Einrückung, die das Problem lindert.

2. Der erste Codeblock erfordert mehr Syntax, um seine Absicht auszudrücken

Im ersten Codeblock gibt es drei return Anweisungen, drei Builder-Anweisungen, drei Lambda-Header, drei Kontexte und schließlich drei Werte. Letztendlich sind uns diese drei Werte wichtig – der Rest ist der Standard, um uns dorthin zu bringen. Ich finde dies tatsächlich der schwierigste Teil dieses Codeblocks. Es passiert so viel, und die Teile, die mir wirklich wichtig sind (die Werte, die von den Erbauern zurückgegeben werden), sind so klein, dass ich den größten Teil meiner mentalen Energie damit verbringe, die Boilerplate zu grübeln, anstatt mich auf die Teile zu konzentrieren, die ich tatsächlich brauche ( wieder die Werte).


Im zweiten Codeblock gibt es eine enorme Reduzierung der Boilerplate, damit ich mich auf den Teil konzentrieren kann, der mir wichtig ist - wieder die Werte.

3. Der erste Codeblock verbirgt den wichtigsten Teil der Methode build im tiefsten Teil der Verschachtelung

Mir ist klar, dass alle Teile dieser build Methode wichtig sind, aber ich habe festgestellt, dass ich beim Lesen dieses deklarativen UI-Codes normalerweise nach dem suche, was angezeigt wird der Benutzer, in diesem Fall das Text Widget, das in den tiefsten verschachtelten Builder eingebettet ist. Anstatt im Vordergrund zu stehen, ist dieses Text Widget in mehreren Ebenen von Einrückung, Syntax und Absicht vergraben. Wenn Sie ein Column oder ein Row in eine dieser Ebenen werfen, wird sie noch tiefer verschachtelt, und an diesem Punkt haben Sie nicht einmal den Vorteil, nur den Abschnitt mit der höchsten Einrückung nachzuverfolgen .


Im zweiten Codeblock befindet sich das tatsächliche renderbare Widget , das zurückgegeben wird, am Ende der Funktion, was sofort ersichtlich ist. Darüber hinaus habe ich festgestellt, dass Sie, wenn Sie so etwas wie das vorgeschlagene Syntax-OP haben, darauf zählen können, dass das visuelle Widget immer am Ende der Funktion steht, was den Code viel vorhersehbarer und einfacher zu lesen macht.

Bezüglich der Verschachtelung gibt es einen Unterschied zwischen der Verschachtelung, die einen _tree_ ausdrückt, und der Verschachtelung, die eine _sequence_ ausdrückt .

Bei normaler View -> Text Verschachtelung und dergleichen ist die Verschachtelung wichtig, da sie die Eltern-Kind-Beziehungen auf dem Bildschirm darstellt. Für Features wie Context (nicht sicher, ob Flutter es hat) repräsentiert es den Umfang von Kontexten. Die Verschachtelung selbst hat also in diesen Fällen eine wichtige semantische Bedeutung und kann nicht außer Acht gelassen werden. Sie können nicht einfach die Plätze von Eltern und Kind tauschen und erwarten, dass das Ergebnis dasselbe ist.

Bei der Verschachtelung von Buildern (auch bekannt als "Render Props" in React) oder der Verschachtelung von Promises soll die Verschachtelung jedoch eine Abfolge von Transformationen / Erweiterungen kommunizieren. Der Baum selbst ist nicht so wichtig – wenn Sie beispielsweise unabhängige ABuilder -> BBuilder -> CBuilder verschachteln, vermitteln ihre Eltern-Kind-Beziehungen keine zusätzliche Bedeutung.

Solange alle drei Werte im unteren Bereich verfügbar sind, ist deren Baumstruktur nicht wirklich relevant. Es ist konzeptionell "flach", und die Verschachtelung ist nur ein Artefakt der Syntax. Natürlich können sie die Werte des anderen verwenden (in diesem Fall ist ihre Reihenfolge wichtig), aber dies ist auch bei sequentiellen Funktionsaufrufen der Fall, und das kann ohne jede Verschachtelung erfolgen.

Deshalb ist async/await überzeugend. Es entfernt zusätzliche Informationen (Eltern-Kind-Beziehung von Versprechen), die einen Mechanismus auf niedriger Ebene beschreiben, und lässt Sie sich stattdessen auf die übergeordnete Absicht (Beschreiben einer Sequenz) konzentrieren.

Ein Baum ist eine flexiblere Struktur als eine Liste. Aber wenn jedes Elternteil nur ein Kind hat, wird es zu einem pathologischen Baum – im Wesentlichen zu einer Liste. Async/Await und Hooks erkennen, dass wir Syntax für etwas verschwenden, das keine Informationen übermittelt, und entfernen sie.

Das ist eigentlich interessant, weil ich vorhin gesagt habe "hier geht es nicht um die Boilerplate" und jetzt scheint es, als würde ich mir selbst widersprechen. Ich denke, hier gibt es zwei Dinge.

An sich Builders (oder zumindest Render - Stützen in React) sind die Lösung (AFAIK) in die „wiederverwendbarer Logik“ -Problem. Sie sind nur nicht sehr ergonomisch, wenn Sie viele davon verwenden. Sie werden natürlich durch die Einrückung entmutigt, mehr als 4 oder 5 davon in derselben Komponente zu verwenden. Jeder nächste Level ist ein Hit für die Lesbarkeit.

Der Teil, der mir ungelöst erscheint, besteht darin, die Verschachtelungskosten des Lesers zu reduzieren. Und dieses Argument ist genau das gleiche Argument wie für async / await .

Es ist aus folgenden Gründen nicht so lesbar:

  • Übermäßige Verwendung von Leerzeichen lässt sich nicht gut skalieren. Wir haben nur eine begrenzte Anzahl von Zeilen auf unserem Monitor, und das erzwungene Scrollen verringert die Lesbarkeit und erhöht die kongitive Last. Stellen Sie sich vor, wir haben bereits einen 60-zeiligen Widget-Baum, und Sie haben mir gerade zusätzliche 15 für Bauherren aufgezwungen, nicht ideal.
  • Es verschwendet Hz-Platz, auf den wir beschränkt sind, was zu zusätzlichem Umbruch führt, was noch mehr Zeilenplatz verschwendet.
  • Es schiebt den Blattknoten, auch bekannt als Inhalt, weiter von der linken Seite und in den Baum, wo er mit einem Blick schwerer zu erkennen ist
  • Es ist deutlich schwieriger, die „Key Player“ oder „Non-Boilerplate“ auf einen Blick zu identifizieren. Ich muss "das Wichtige finden", bevor meine Argumentation überhaupt beginnen kann.

Eine andere Möglichkeit, dies zu betrachten, besteht darin, einfach den Nicht-Boilerplate-Code hervorzuheben, und ob er gruppiert ist, damit Ihr Auge sich leicht daran erfreuen kann, oder überall verstreut ist, damit Ihr Auge herumscannen muss:

Mit der Hervorhebung ist dies sehr einfach zu begründen. Ohne sie muss ich die gesamte Ausführlichkeit lesen, bevor ich herausfinden kann, wer was und wo verwendet:
image

Vergleichen Sie nun damit, die Hervorhebung ist im Grunde überflüssig, weil mein Auge nirgendwo anders hingehen kann:
image

Es ist erwähnenswert, dass es wahrscheinlich eine Meinungsverschiedenheit zwischen Lesbarkeit und Grokability gibt. @Hixie verbringt seine Zeit wahrscheinlich in monolithischen Klassendateien, in denen er ständig massive Bäume lesen und verstehen muss, während Ihr typischer App-Entwickler viel mehr damit beschäftigt ist, Hunderte kleinerer Klassen zu erstellen, und wenn Sie viele kleine Klassen verwalten, ist die Grok-Fähigkeit der Schlüssel . Es ist nicht so sehr, dass der Code lesbar ist, wenn Sie langsamer werden und ihn lesen, aber ich kann auf einen Blick erkennen, was er tut, damit ich einspringen und etwas optimieren oder reparieren kann.

Als Referenz ist das Äquivalent von Context in React InheritedWidgets/Provider

Der einzige Unterschied zwischen ihnen besteht darin, dass wir in React before Hooks das Builder-Muster verwenden müssen, um ein Context/Inheritedwidget zu verwenden

Während Flutter eine Möglichkeit hat, den Wiederaufbau mit einem einfachen Funktionsaufruf zu binden.
Es sind also keine Hooks erforderlich, um Bäume mit InheritedWidgets zu glätten – was das Problem von Builders umgeht

Das ist wahrscheinlich einer der Gründe, warum die Diskussion schwieriger ist, da wir Builder seltener brauchen.

Aber es ist erwähnenswert, dass die Einführung einer hakenartigen Lösung beides lösen würde https://github.com/flutter/flutter/issues/30062
und https://github.com/flutter/flutter/issues/12992

Es scheint auch, dass @Hixie eher daran links nach rechts mit tiefer Verschachtelung betrachtet werden, ähnlich wie HTML, mit dem @Hixie vermutlich

Die meisten Entwickler sind jedoch nicht oder kommen aus mehr Sprachen von oben nach unten , bei denen die Logik wiederum von oben nach unten gelesen wird und nicht in verschachtelten Bäumen; es befindet sich am untersten Punkt des Codeblocks. Daher ist das, was für ihn lesbar ist, bei vielen anderen Entwicklern nicht unbedingt so, weshalb Sie hier möglicherweise die Dichotomie der Meinungen zur Lesbarkeit sehen.

Eine andere Sichtweise ist, wie viel Code mein Gehirn benötigt, um visuell zu redigieren. Für mich stellt dies genau die "Grunzarbeit" dar, die mein Gehirn leisten muss, bevor ich analysieren kann, was vom Baum zurückgegeben wird:
image

Einfach ausgedrückt, hat die Builder-Version eine 4x höhere vertikale Grundfläche, während sie buchstäblich keine zusätzlichen Informationen oder Kontext hinzufügt UND den Code weitaus spärlicher/verstreuter verpackt. Meiner Meinung nach ist das ein offener und geschlossener Fall, es ist allein aus diesem Grund objektiv weniger lesbar, und das berücksichtigt noch nicht einmal die zusätzliche kognitive Belastung um Einrückungen und das Aufreihen von geschweiften Klammern, die wir alle in Flattern behandelt haben.

Stellen Sie sich mein Auge als eine hungrige CPU vor, die für die Verarbeitung optimiert ist? :)

Bei normaler View -> Text Verschachtelung und dergleichen ist die Verschachtelung wichtig, da sie _die Eltern-Kind-Beziehungen_ auf dem Bildschirm darstellt. Für Features wie Context (nicht sicher, ob Flutter es hat) repräsentiert es den Umfang von Kontexten. Die Verschachtelung selbst hat also in diesen Fällen eine wichtige semantische Bedeutung und kann nicht außer Acht gelassen werden. Sie können nicht einfach die Plätze von Eltern und Kind tauschen und erwarten, dass das Ergebnis dasselbe ist.

Stimme voll und ganz zu und das habe ich schon erwähnt. Semantisch macht es keinen Sinn, zusätzliche Kontextebenen in einer visuellen Anzeigestruktur zu erstellen, da ich zusätzliche nicht-visuelle Controller mit Status verwende. Wenn Sie 5 Animatoren verwenden, ist Ihr Widget jetzt 5 Ebenen tief? Allein auf diesem hohen Niveau riecht der aktuelle Ansatz ein bisschen.

Hier fallen mir zwei Probleme auf.

  1. Ich vermute, es gibt einige Meinungsverschiedenheiten darüber, wie schwierig / explizit es sein sollte, wenn eine teure Ressource verwendet wird. Die Philosophie von Flutter ist, dass es schwieriger / expliziter sein sollte, damit der Entwickler ernsthaft darüber nachdenkt, wann und wie er sie verwendet. Streams, Animationen, Layout-Builder usw. stellen nicht triviale Kosten dar, die ineffizient verwendet werden könnten, wenn sie zu einfach sind.

  2. Build ist synchronisiert, aber die interessantesten Dinge, mit denen Sie als App-Entwickler zu tun haben, sind asynchron. Natürlich können wir den Build nicht asynchron machen. Wir haben diese Annehmlichkeiten wie Stream/Animation/FutureBuilder entwickelt, aber sie funktionieren nicht immer gut genug für die Anforderungen eines Entwicklers. Es ist wahrscheinlich aufschlussreich, dass wir Stream oder FutureBuilder im Framework nicht oft verwenden.

Ich glaube nicht, dass die Lösung darin besteht, Entwicklern zu sagen, dass sie immer benutzerdefinierte Renderobjekte schreiben sollen, wenn sie mit asynchronen Operationen arbeiten. Aber in den Beispielen, die ich in diesem Fehler sehe, gibt es Mischungen aus asynchroner und synchronisierter Arbeit, die wir nicht einfach abwarten können. Build muss bei jedem Aufruf etwas produzieren.

fwiw hat das React-Team die Wiederverwendung des Lesbarkeitsproblems als 1 Motivation angesprochen:
Hooks Motivation: Es ist schwierig, zustandsorientierte Logik zwischen Komponenten wiederzuverwenden
React bietet keine Möglichkeit, wiederverwendbares Verhalten an eine Komponente zu „anhängen“ ... Sie kennen vielleicht Muster ... die versuchen, dieses Problem zu lösen. Diese Muster erfordern jedoch, dass Sie Ihre Komponenten bei der Verwendung neu strukturieren, was umständlich sein kann und die Nachvollziehbarkeit des Codes erschwert .

Dies ist sehr ähnlich wie Flutter uns derzeit keine Möglichkeit bietet, nativ einen Zustand zu komponieren. Es ist auch ein Echo, was passiert, wenn wir Builder verwenden, was unseren Layout-Baum modifiziert und es umständlicher macht, damit zu arbeiten und "schwerer zu folgen", besagter Baum.

@dnfield Wenn build jedes Mal aufgerufen werden muss, können wir die Hooks vielleicht nicht in der Methode build , damit build immer synchronisiert ist, dh sie in die Klasse einfügen, wo initState und dispose sind. Gibt es Probleme damit, von denen, die Hooks schreiben?

Sie können dasselbe Argument für Versprechen/Zukunft anstellen und sagen, dass await die Tatsache verschleiert, dass es ein Versprechen zurückgibt.

Nein, tust du nicht. Await ist buchstäblich nur syntaktischer Zucker für eine einzelne Funktion. Ob Sie die ausführlichen Futures oder die deklarative Syntax verwenden, die __Intent__ des Codes ist gleich.

Die Forderungen hier sind, Quellcode, der völlig unterschiedliche Anliegen behandelt, unter einen Hut zu bringen, alle möglichen Verhaltensweisen hinter einem einzigen Schlüsselwort zu verbergen und zu behaupten, dass er irgendwie die kognitive Belastung reduziert.

Das ist völlig falsch, denn jedes Mal, wenn ich dieses Schlüsselwort verwende, muss ich mich fragen, ob das Ergebnis asynchrone Operationen durchführt, unnötige Neuaufbauten auslöst, langlebige Objekte initialisiert, Netzwerkaufrufe durchführt, Dateien von der Festplatte liest oder einfach einen statischen Wert zurückgibt . All dies sind sehr unterschiedliche Situationen und ich müsste mit dem Geschmack des Hakens vertraut sein, den ich verwende.

Aus der Diskussion verstehe ich, dass sich die meisten Entwickler hier nicht gerne mit solchen Details beschäftigen und eine einfache Entwicklung wünschen, indem sie einfach diese "Hooks" verwenden können, ohne sich um die Implementierungsdetails kümmern zu müssen.
Die Verwendung dieser sogenannten "Hooks", ohne ihre volle Bedeutung zu verstehen, führt zu ineffizientem und schlechtem Code und wird dazu führen, dass sich die Leute selbst in den Fuß schießen - daher löst es nicht einmal das Problem des "Schutzes von Anfängerentwicklern".

Wenn Ihre Anwendungsfälle einfach sind, können Sie wohl oder übel Hooks verwenden. Sie können Builder nach Belieben verwenden und verschachteln, aber da Ihre App komplex wird und Sie Schwierigkeiten haben, Code wiederzuverwenden, denke ich, dass es angebracht ist, Ihrem eigenen Code und Ihrer Architektur mehr Aufmerksamkeit zu schenken. Wenn ich eine App für potenziell Millionen von Benutzern erstellen würde, würde ich sehr zögern, "Magie" zu verwenden, die wichtige Details von mir abstrahiert. Im Moment finde ich Flutters API genau richtig, einfach für sehr einfache Anwendungsfälle und dennoch flexibel, um es jedem zu ermöglichen, jede Art komplexer Logik auf sehr effiziente Weise zu implementieren.

@Rudiksz Auch hier zwingt dich niemand, zu Hooks zu wechseln. Der aktuelle Stil wird weiterhin vorhanden sein. Was argumentieren Sie angesichts dieses Wissens?

Außerdem können die Leute immer noch effizienten Code schreiben, wenn sie sehen, dass mehrere Hooks ihre App irgendwie blockieren. Sie sehen es, wenn sie ein Profil erstellen oder die App einfach ausführen, ähnlich wie beim aktuellen Stil.

@Rudiksz Auch hier zwingt dich niemand, zu Hooks zu wechseln. Der aktuelle Stil wird weiterhin vorhanden sein. Was argumentieren Sie angesichts dieses Wissens?

Oh je, das gleiche Argument gilt auch für die Leute, die sich über Probleme mit dem Framework beschweren. Niemand zwingt sie, das Hooks-Paket nicht zu verwenden.

Ich werde hier sehr unverblümt sein.
Bei diesem Thema geht es wirklich nicht um Hooks, Statefulwidget und wer was verwendet, sondern darum, jahrzehntelange Best Practices umzukehren, damit manche Leute 5 Zeilen Code weniger schreiben können.

Deine Argumentation funktioniert nicht wirklich. Der Grund, warum dieses Problem erstellt wurde, war, dass das Paket flatter_hooks nicht alles tut, was möglich wäre, wenn etwas im Framework vorhanden ist, während das aktuelle Modell dies tut, da es bereits nativ im Framework vorhanden ist. Das Argument besteht darin, Features von flatter_hooks nativ in das Framework zu verschieben. Ihr Argument besagt, dass ich alles, was ich mit dem aktuellen Modell tun kann, auch mit dem Hooks-Paket tun kann, was falsch ist, wie es von anderen in dieser Diskussion scheint. Wenn sie wahr wären, dann würde es funktionieren, was auch bedeuten würde, dass Hooks nativ im Framework wären, und daher, da Hooks und Nicht-Hooks gleichwertig wären, können Sie das aktuelle Modell genauso gut wie die Hooks-basierten verwenden Modell, was ich argumentiert habe.

Ich bin mir nicht sicher, woher Ihre Best Practices kommen, da ich weiß, dass es eine Best Practice ist, Code leicht lesbar zu halten, und dass übermäßige Verschachtelung ein Anti-Muster ist. Auf welche Best Practices beziehen Sie sich genau?

fwiw hat das React-Team die Wiederverwendung des Lesbarkeitsproblems als 1 Motivation angesprochen:
Hooks Motivation: Es ist schwierig, zustandsorientierte Logik zwischen Komponenten wiederzuverwenden
React bietet keine Möglichkeit, wiederverwendbares Verhalten an eine Komponente zu „anhängen“ ... Sie kennen vielleicht Muster ... die versuchen, dieses Problem zu lösen. Diese Muster erfordern jedoch, dass Sie Ihre Komponenten bei der Verwendung neu strukturieren, was umständlich sein kann und die Nachvollziehbarkeit des Codes erschwert .

Ich höre alle davon schwärmen, dass Flutter so viel erstaunlicher ist als React. Vielleicht liegt es daran, dass es nicht alles so macht wie React? Sie können nicht beides haben, Sie können nicht sagen, dass Flutter React meilenweit voraus ist und auch verlangen, dass es alles genau so macht, wie React es tut.

Welche Lösung Flutter auch immer für ein bestimmtes Problem wählt, sollte für sich allein stehen. Ich kenne React nicht, aber anscheinend verpasse ich ein wirklich erstaunliches Stück Technologie. :/

Ich glaube nicht, dass irgendjemand argumentiert, dass Flutter alles wie React machen sollte.

Tatsache ist jedoch, dass Flutters Widget-Layer stark von React inspiriert ist. Das steht in der offiziellen Dokumentation.
Als Konsequenz haben Widgets sowohl die gleichen Vorteile als auch die gleichen Probleme wie React Components.

Das bedeutet auch, dass React im Umgang mit diesen Themen mehr Erfahrung hat als Flutter.
Es stand ihnen länger gegenüber und versteht sie besser.

Daher sollte es nicht überraschen, dass die Lösungen für Flutter-Probleme den Lösungen für React-Probleme ähnlich sind.

Die Benutzer-API von @Rudiksz Flutter ist dem klassenbasierten Modell von React sehr ähnlich, auch wenn die interne API unterschiedlich sein kann (ich weiß nicht, ob sie sich unterscheiden, ich stoße nicht wirklich auf die interne API). Ich ermutige Sie, React mit Hooks auszuprobieren, um zu sehen, wie es ist, wie ich bereits sagte, dass es eine Dichotomie von Meinungen zu geben scheint, die fast ausschließlich auf denen basiert, die Hook-ähnliche Konstrukte in anderen Frameworks verwendet haben und nicht verwendet haben.

Angesichts ihrer Ähnlichkeit sollte es nicht überraschen, dass die Lösungen für Probleme, wie oben erwähnt, ähnlich aussehen.

Bitte versuchen wir unser Bestes, um nicht miteinander zu kämpfen.

Das einzige, wozu uns der Kampf führen wird, ist, diese Diskussion zu beenden und keine Lösung zu finden.

Ich höre alle davon schwärmen, dass Flutter so viel erstaunlicher ist als React. Vielleicht liegt es daran, dass es nicht alles so macht wie React? Sie können nicht beides haben, Sie können nicht sagen, dass Flutter React meilenweit voraus ist und auch verlangen, dass es alles genau so macht, wie React es tut.

Der Hinweis, dass das React-Team ähnliche Motivationen hatte, als es Hooks entwickelte, bestätigt die Bedenken, die wir hier zum Ausdruck bringen. Es bestätigt sicherlich, dass es ein Problem bei der Wiederverwendung und Kombination von gemeinsamer zustandsbehafteter Logik innerhalb dieser Art von komponentenbasiertem Framework gibt, und bestätigt auch in gewissem Maße die Diskussion über Lesbarkeit, Verschachtelung und das allgemeine Problem mit "Unordnung" in Ihren Ansichten.

Ich schwärme von nichts, ich habe noch nie in React gearbeitet und ich liebe Flutter. Ich kann das Problem hier einfach erkennen.

@Rudiksz Wir können nicht sicher sein, ob es in der Praxis performant ist, bis wir es in die Praxis

@Hixie Dies ist ein Beispiel für eine Reise, die ein HookWidget als auch mit StatefulWidget zu implementieren.

__Hook-Widget__

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);
  }
}

__zustandsorientiertes Widget__

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);
  }
}

bisher nichts interessantes. Beide Lösungen sind ziemlich akzeptabel, unkompliziert und performant.
jetzt wollen wir UserNickname in einem ListView . Wie Sie sehen können, gibt fetchNicknames eine Karte mit Spitznamen zurück, nicht nur einen Spitznamen. also jedes mal anzurufen ist überflüssig. einige Lösungen, die wir hier anwenden können:

  • Verschieben Sie die aufrufende fetchNicknames() Logik in das übergeordnete Widget und speichern Sie das Ergebnis.
  • einen Cache-Manager verwenden.

erste Lösung ist akzeptabel, hat aber 2 Probleme.
1 - es macht UserNickname nutzlos, da es jetzt nur noch ein Text-Widget ist und wenn Sie es woanders verwenden möchten, müssen Sie wiederholen, was Sie im übergeordneten Widget gemacht haben (das die ListView ) . die Logik zum Anzeigen des Spitznamens gehört zu den UserNickname aber wir müssen sie separat verschieben.
2 - wir können fetchNicknames() in vielen anderen Unterbäumen verwenden und es ist besser, es für die gesamte App zu cachen, nicht nur für einen Teil der Anwendung.

Stellen Sie sich vor, wir wählen den Cache-Manager und stellen eine Klasse CacheManager mit InheritedWidgets oder Provider .

nach dem Hinzufügen der Unterstützung für das Caching:

__Hook-Widget__

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);
  }
}

__zustandsorientiertes Widget__

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);
  }
}

Wir haben einen Socket-Server, der Clients benachrichtigt, wenn sich Spitznamen ändern.

__Hook-Widget__

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);
  }
}

__zustandsorientiertes Widget__

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);
  }
}

Bisher sind beide Umsetzungen akzeptabel und gut. IMO ist die Boilerplate im Statful überhaupt kein Problem. Das Problem tritt auf, wenn wir ein Widget wie UserInfo benötigen, das sowohl den Spitznamen als auch den Avatar des Benutzers enthält. Außerdem können wir das UserNickname Widget nicht verwenden, da wir es in einem Satz wie "Willkommen [Benutzername]" anzeigen müssen.

__Hook-Widget__

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)],
    );
  }
}

aber für das __stateful-Widget__ können wir nicht einfach die Logik verwenden, die wir geschrieben haben. Wir müssen die Logik in eine Klasse verschieben (wie ein Property , das Sie vorgeschlagen haben) und trotzdem müssen wir den Widget-Klebstoff mit der Eigenschaftsklasse erneut in das neue Widget schreiben.

Wenn Sie die Änderungen in den ersten 3 Beispielen sehen, haben wir das Widget selbst überhaupt nicht geändert, da die einzigen notwendigen Änderungen in der Zustandslogik waren und die einzige Stelle, die sich geändert hat, die gesamte Zustandslogik war.
Dies gab uns eine saubere (meinende), zusammensetzbare und vollständig wiederverwendbare Zustandslogik, die wir überall verwenden können.

IMHO das einzige Problem ist das Aufrufen von useUserNickname ist beängstigend, weil eine einzelne Funktion so viel tun kann.
Aber in meiner jahrelangen Erfahrung in Reagieren und Verwenden von flutter_hooks in 2 Apps, die in Produktion sind (die Hooks stark verwenden), beweist, dass ich kein gutes Zustandsmanagement habe (ich habe auch MobX und andere Zustandsmanagementlösungen ausprobiert, aber der Kleber im Widget ist immer da) ist viel beängstigender. Ich muss nicht für jeden Bildschirm in einer Frontend-App 5-seitige Dokumente schreiben, die ich möglicherweise in einigen Monaten nach der ersten Veröffentlichung um einige kleine Funktionen hinzufügen muss, damit sie verstehen, wie eine Seite meiner App funktioniert. App ruft den Server zu oft auf? Einfache Aufgabe Ich gehe zum zugehörigen Hook und ändere ihn und die ganze App wird repariert, weil die ganze App diesen Hook verwendet. Wir können ähnliche Dinge in den Apps haben, ohne Hooks mit einer guten Abstraktion zu verwenden, aber was ich sage, ist, dass Hooks eine so gute Abstraktion sind.

Ich bin mir ziemlich sicher, dass @gaearon es besser

Wenn Sie das obige Beispiel sehen, ist keine der obigen Methoden (Stateful- und Hook-Widget) performanter als die andere. Aber der Punkt ist, dass einer von ihnen die Leute ermutigt, den performanten Code zu schreiben.

Es ist auch möglich, nur den Teilbaum zu aktualisieren, den wir aktualisieren müssen, wie StreamBuilder wenn es zu viele Updates (zB Animationen) gibt mit:

1 - Erstellen Sie einfach ein neues Widget, das sowohl für HookWidget als auch für StatefulWidget/StatelessWidget absolut praktikabel ist
2 - Verwenden Sie etwas Ähnliches wie HookWidgetBuilder im flutter_hooks Paket, da die Daten der übergeordneten und untergeordneten Widgets sehr eng gekoppelt sind.

Randnotiz: Ich schätze @Hixie und @rrousselGit sehr, dass sie dieses Thema diskutiert und so viel Energie in diese Ausgabe

Ich denke mir etwas ziemlich Cooles / Elegantes ein, das auf dem Ausgangspunkt von apples:apples als Hooks, die so fremd aussehen.

Stellen Sie sich vor, wir haben ein StatefulWidget mit dieser Signatur:

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();
}

Wenn wir den Zustand mit Vanilla-Animator-Controllern implementieren, erhalten wir etwa Folgendes:

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();
  }
}

Wenn wir es mit einer StatefulProperty erstellen, erhalten wir ungefähr Folgendes:

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),
    );
  }
}

Einige Hinweise zu den Unterschieden hier:

  1. Von oben ist eine 20 Zeilen, die andere 45. Eine ist 1315 Zeichen, die andere 825. In dieser Klasse sind nur 3 Zeilen und 200 Zeichen wichtig (was im Build passiert), also ist dies bereits eine massive Verbesserung des Signals :Rauschverhältnis (dh ist mein Auge auf die wichtigen Bits gerichtet)
  2. Die Vanilla-Optionen haben mehrere Punkte, an denen Fehler erstellt werden können. Vergessen Sie, zu entsorgen, oder vergessen Sie, didChange zu verarbeiten, oder machen Sie einen Fehler in didChange, und Sie haben einen Fehler in Ihrer Codebasis. Dies wird noch schlimmer, wenn mehrere Arten von Controllern verwendet werden. Dann haben Sie einzelne Funktionen, die Objekte aller Art zerlegen, die nicht so schön und sequentiell benannt werden. Das wird unordentlich und es ist ziemlich einfach, Fehler zu machen oder Eingaben zu verpassen.
  3. Die Vanilla-Option bietet keine Methode zur Wiederverwendung allgemeiner Muster oder Logik, wie playOnInit, daher muss ich diese Logik duplizieren oder eine benutzerdefinierte Funktion in einer einzelnen Klasse erstellen, die einen Animator verwenden möchte.
  4. Es besteht keine Notwendigkeit, SingleTickerProviderMixin hier zu verstehen, was 'magisch' ist und monatelang verschleiert hat, was ein Ticker für mich ist (im Nachhinein hätte ich die Klasse einfach lesen sollen, aber jedes Tutorial sagt nur: Füge dieses magische Mixin hinzu). Hier können Sie sich direkt den Quellcode für StatefulAnimationProperty ansehen und sehen, wie die Animationscontroller einen Tickeranbieter direkt und im Kontext verwenden.

Sie müssen stattdessen verstehen, was StatefulPropertyManager macht, aber entscheidend ist, dass dies einmal gelernt und auf Objekte aller Art angewendet wird. Nur diskrete "StatefulObjects" zu haben, die all diese Dinge kennen (genau wie ein Builder!), ist viel sauberer und skaliert besser.

Der Code für StatefulAnimationProperty würde ungefähr so ​​aussehen:

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();
  }
}

Schließlich ist es erwähnenswert, dass die Lesbarkeit durch die Verwendung von Erweiterungen noch verbessert werden kann, sodass wir Folgendes haben könnten:

  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);
  }

[Bearbeiten] Um meinen eigenen Standpunkt zu verdeutlichen, hat mein Vanilla-Beispiel einen Fehler. Ich habe vergessen, jedem Animator im didUpdateWidget die richtige Dauer zu übergeben. Wie lange hätten wir gebraucht, um diesen Fehler in freier Wildbahn zu finden, wenn es niemand beim Code-Review bemerkt hätte?? Hat es jemand beim Lesen nicht bemerkt? Wenn man es belässt, ist es ein perfektes Beispiel dafür, was in der realen Welt passiert.

Hier ist eine Vogelperspektive mit rot markiertem Boilerplate:
image

Dies wäre nicht so schlimm, wenn es reines Boilerplate wäre und der Compiler Sie anbrüllte, wenn es fehlte. Aber es ist alles optional! Und wenn es weggelassen wird, werden Fehler erzeugt. Dies ist also eigentlich eine sehr schlechte Praxis und überhaupt nicht TROCKEN. Hier kommen Builder ins Spiel, die jedoch nur für einfache Anwendungsfälle geeignet sind.

Was ich daran sehr interessant finde, ist, wie hundert Zeilen und eine einfache Mischung in State eine Menge existierender Klassen überflüssig machen. Es besteht praktisch keine Notwendigkeit mehr, zum Beispiel die TickerProviderMixins zu verwenden. TweenAnimationBuilder muss fast nie verwendet werden, es sei denn, Sie _möchten_ tatsächlich einen Unterkontext erstellen. Viele traditionelle Schwachpunkte wie die Verwaltung von Fokus-Controllern und TextInput-Controllern werden erheblich erleichtert. Die Nutzung von Streams wird viel attraktiver und weniger umständlich. Über die Codebasis hinweg könnte die Verwendung von Buildern im Allgemeinen reduziert werden, was zu leichter zu durchsuchenden Bäumen führen wird.

Macht es auch _extrem_ einfach, Ihre eigenen benutzerdefinierten Zustandsobjekte zu erstellen, wie das zuvor aufgeführte FetchUser-Beispiel, für das derzeit im Wesentlichen ein Builder erforderlich ist.

Ich denke, das wäre eine sehr interessante Frage für die nächste Flutter-Entwicklerumfrage.

Es wäre ein guter Anfang. Teilen Sie dieses Problem in verschiedene Teile/Fragen auf und sehen Sie, ob dies ein echtes Problem ist, das die Flutter-Entwickler lösen möchten.

Sobald das klar ist, wird dieses Gespräch flüssiger und bereichernder

Die Emoji-Reaktionen unter jedem Kommentar geben eine klare Vorstellung davon, ob die Community dies als Problem ansieht oder nicht. Die Meinung von Entwicklern, die mehr als 250 lange Kommentare zu diesem Thema lesen, bedeutet imho viel.

@esDotDev Das ähnelt einigen der Ideen, mit denen ich gespielt habe, obwohl ich Ihre Idee mag, nur die Immobilie selbst als Ticker-Anbieter zu haben, hatte ich das nicht in Betracht gezogen. Eine Sache, die Ihrer Implementierung fehlt und die wir meiner Meinung nach hinzufügen müssen, ist die Behandlung von TickerMode (was der Sinn von TickerProviderStateMixin ist).

Die Hauptsache, mit der ich zu kämpfen habe, ist, dies auf effiziente Weise zu tun. ValueListenableBuilder verwendet beispielsweise ein untergeordnetes Argument, mit dem die Leistung messbar verbessert werden kann. Ich sehe keine Möglichkeit, dies mit dem Property-Ansatz zu tun.

@Hixie
Ich verstehe, dass Effizienzverluste bei solchen Ansätzen unvermeidlich zu sein scheinen. Aber ich mag die Flutter-Denkweise der Optimierung, nachdem Sie Ihren Code profiliert haben. Es gibt viele Anwendungen, die von der Klarheit und Prägnanz des Property-Ansatzes profitieren würden. Die Option, Ihren Code zu profilieren und in Builder umzuwandeln oder einen Teil des Widgets in ein eigenes Widget zu trennen, ist immer vorhanden.

Die Dokumentation müsste lediglich die Best Practices widerspiegeln und die Kompromisse deutlich machen.

Die Hauptsache, mit der ich zu kämpfen habe, ist, dies auf effiziente Weise zu tun. ValueListenableBuilder verwendet beispielsweise ein untergeordnetes Argument, mit dem die Leistung messbar verbessert werden kann. Ich sehe keine Möglichkeit, dies mit dem Property-Ansatz zu tun.

Hm, ich denke, der ganze Sinn der Eigenschaften ist für nicht-visuelle Objekte. Wenn etwas einen Kontext-Slot im Baum haben möchte, sollte dieses Ding ein Builder sein (eigentlich sind dies die einzigen Dinge, die jetzt Builder sein sollten, denke ich?)

Wir hätten also eine StatefulValueListenableProperty, die wir meistens verwenden, wenn wir nur die gesamte Ansicht binden möchten. Wir haben dann auch einen ValueListenableBuilder für den Fall, dass wir möchten, dass ein Unterabschnitt unseres Baums neu erstellt wird.

Dadurch wird auch das Verschachtelungsproblem behoben, da die Verwendung eines Builders als Blattknoten die Lesbarkeit bei weitem nicht so stört, wie das Verschachteln von 2 oder 3 oben in Ihrem Widget-Baum.

@TimWhiting Ein großer Teil der Designphilosophie von Flutter besteht darin, Menschen zur richtigen Wahl zu führen. Ich möchte es vermeiden, die Leute dazu zu ermutigen, einem Stil zu folgen, von dem sie sich dann entfernen müssten, um eine bessere Leistung zu erzielen. Es kann sein, dass es nicht möglich ist, alle Bedürfnisse auf einmal zu erfüllen, aber wir sollten es auf jeden Fall versuchen.

@Hixie
Wie wäre es mit so etwas für Bauherren?

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()),
    );
  }
}

Können Sie näher erläutern? Ich bin mir nicht sicher, ob ich den Vorschlag verstehe.

Ich denke, er sagt, dass die StatefulProperty eine optionale Build-Methode für Eigenschaften bereitstellen könnte, die eine visuelle Komponente haben:

return Column(
   children: [
      TopContent(),
      _valueProperty.build(SomeChildWidget()),
   ]
)

Was ziemlich imo ist,

Ja, ich weiß nicht, ob das funktionieren würde, aber die build-Methode würde ein Kind wie ein normaler Builder annehmen, außer dass die anderen Eigenschaften des Builders von der Eigenschaft festgelegt werden.
Wenn Sie den Kontext vom Builder benötigen, akzeptiert die build-Methode ein Builder-Argument, das den Kontext bereitstellt.

Unter der Haube könnte die Methode einfach einen normalen Builder mit den angegebenen Eigenschaften erstellen und das untergeordnete Argument an den normalen Builder übergeben und diesen zurückgeben.

Angenommen, Sie hätten diesen 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,
        );
      }
    ),
  );
}

...wie würdest du das umrechnen?

@esDotDev Ich mag Ihre Idee, nur die Eigenschaft selbst als Ticker-Anbieter zu haben, das hatte ich nicht in Betracht gezogen.

Einer der stärksten Aspekte dieses Hook-Stil-Ansatzes besteht darin, dass Sie die zustandsbehaftete Logik _vollständig_ kapseln können, was auch immer das sein mag. In diesem Fall lautet die vollständige Liste für AC:

  1. Erstellen Sie die AC
  2. Geben Sie ihm einen Ticker
  3. Widget-Änderungen bearbeiten
  4. Bereinigung von Wechselstrom und Laufschrift durchführen
  5. Ansicht bei Häkchen neu aufbauen

Derzeit sind die Dinge aufgeteilt, wobei Entwickler 1,3,4 manuell und wiederholt bearbeiten (oder nicht behandeln), und der semi-magische SingleTickerProviderMixin, der sich um die 2 und 5 kümmert (wobei wir 'dies' als vsync übergeben, was mich monatelang verwirrt hat! ). Und SingleTickerProviderMixin selbst ist eindeutig ein Lösungsversuch für diese Art von Problem, warum sonst nicht einfach den ganzen Weg gehen und uns TickerProvider für jede Klasse implementieren lassen, es wäre viel klarer.

Angenommen, Sie hätten diesen 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,
        );
      }
    ),
  );
}

...wie würdest du das umrechnen?

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
Danke für das Beispiel. Ich habe mein Bestes gegeben. Ich habe vielleicht etwas übersehen.

Wichtig zu beachten ist, dass der Builder das Kind zwischenspeichert. Die Frage wäre, wann muss das Kind tatsächlich wieder aufgebaut werden? Ich glaube, das war die Frage, die du stellen wolltest..

@Hixie hast du gesehen https://github.com/flutter/flutter/issues/51752#issuecomment -671104377
Ich denke, es gibt einige wirklich gute Punkte.
Ich baue heute etwas mit viel ValueListenableBuilder und kann nur sagen, dass es nicht schön zu lesen ist.

@Hixie
Danke für das Beispiel. Ich habe mein Bestes gegeben. Ich habe vielleicht etwas übersehen.

Ich glaube nicht, dass dies funktioniert, weil die Eigenschaft an den Zustand gebunden ist, in dem sie definiert ist, sodass ExpensiveParent hier immer neu erstellt wird. Dann denke ich, dass das Zwischenspeichern des Kindes auch problematisch ist, da im Builder-Beispiel das Kind nur neu erstellt werden soll, wenn der Elternzustand erstellt wird, aber bei dieser Methode weiß die Eigenschaft nicht, wann der Cache ungültig gemacht werden soll (aber vielleicht dies ist lösbar?)

Aber tbh, dies ist der perfekte Anwendungsfall für Builder, wenn Sie einen neuen Kontext einführen möchten. Ich denke, es ist ziemlich elegant, nur den Kontext von StatefulProperties (reiner Zustand) und StatefulWidgets (Mix aus Zustand und Layout) zu haben.

Jedes Mal, wenn Sie absichtlich einen Unterkontext erstellen, werden Sie dies per Definition weiter unten in Ihrem Baum tun, was dazu beiträgt, einen der Hauptnachteile für Builder zu bekämpfen (erzwungene Verschachtelung über den gesamten Baum).

@escamoteur (und @sahandevs , die diesen Kommentar geschrieben haben) ja, das habe ich früher studiert. Ich denke, es hilft sicherlich, die Art von Logik zu zeigen, die die Leute entfernen möchten. Ich denke jedoch, dass das Beispiel selbst insofern etwas fragwürdig ist, als ich erwarten würde, dass sich die meiste Logik (zB alles rund um das Caching) in der Geschäftslogik des App-Zustands befindet und nicht in der Nähe der Widgets. Ich kann auch keine gute Möglichkeit sehen, die Syntax so kurz zu halten, wie in diesem Kommentar vorgeschlagen, ohne das Hot Reload zu unterbrechen (z ).

Trotzdem denke ich, dass die @esDotDev und @TimWhiting sehr interessant ist und diese Probleme lösen könnte. Es ist nicht so kurz wie Hooks, aber es ist zuverlässiger. Ich denke, es wäre absolut sinnvoll, so etwas zu verpacken, es könnte sogar ein Flutter-Favorit sein, wenn es gut funktioniert. Ich bin mir nicht sicher, ob es als zentrales Framework-Feature sinnvoll ist, da die Verbesserung nicht _so_ wesentlich ist, wenn man die Komplexität der Gebäudeeigenschaften und die Auswirkungen auf die Leistung berücksichtigt und wie verschiedene Leute unterschiedliche Stile bevorzugen würden. Letztendlich sollte es für verschiedene Leute in Ordnung sein, unterschiedliche Stile zu verwenden, aber wir möchten nicht, dass das Kernframework mehrere Stile hat, das ist für neue Entwickler nur irreführend.

Es lässt sich auch argumentieren, dass der richtige Weg, Flutter zu lernen, darin besteht, zuerst Widgets zu verstehen und dann die Werkzeuge zu lernen, die sie abstrahieren (Hooks oder was auch immer), anstatt direkt zur abstrakten Syntax zu springen. Andernfalls fehlt Ihnen eine Schlüsselkomponente der Systemfunktion, die Sie beim Schreiben von performanten Codes wahrscheinlich in die Irre führen wird.

Ich kann auch keine gute Möglichkeit sehen, die Syntax so kurz zu halten, wie in diesem Kommentar vorgeschlagen, ohne das Hot Reload zu unterbrechen (z ).

Haken funktionieren ohne Probleme mit Hot-Reload.
Der erste Hook mit einem nicht übereinstimmenden runtimeType bewirkt, dass alle nachfolgenden Hooks zerstört werden.

Dies unterstützt das Hinzufügen, Entfernen und Neuordnen.

Ich denke, es gibt ein Argument, dass eine vollständige Abstraktion einer teilweisen Abstraktion vorzuziehen ist, die derzeit existiert.

Wenn ich verstehen möchte, wie Animator im Kontext einer Eigenschaft funktioniert, ignoriere ich es entweder vollständig oder springe ein, und alles ist in Ordnung, in sich geschlossen und stimmig.

Wenn ich verstehen möchte, wie AnimatorController im Kontext eines StatefulWidget funktioniert, muss ich (zwangsweise) die grundlegenden Lebenszyklus-Hooks verstehen, aber dann davon verschont, zu verstehen, wie der zugrunde liegende Tick-Mechanismus funktioniert. Das ist in gewisser Weise das Schlimmste aus beiden Welten. Nicht genug Magie, um es "einfach funktionieren" zu lassen, aber gerade genug, um neue Benutzer zu verwirren und sie zu zwingen, einfach einem Mixin (was an sich ein neues Konzept für die meisten ist) und einer magischen vsync-Eigenschaft blind zu vertrauen.

Ich bin mir nicht sicher, ob es andere Beispiele in der Codebasis gibt, aber dies würde auf jede Situation zutreffen, in der einige Helfer-Mixins für StatefulWidget bereitgestellt wurden, aber es gibt noch einige andere Bootstrappings, die immer ausgeführt werden müssen. Entwickler lernen das Bootstraping (den langweiligen Teil) und ignorieren das Mixin (das interessante/komplexe Bit).

Trotzdem denke ich, dass die @esDotDev und @TimWhiting sehr interessant ist und diese Probleme lösen könnte. Es ist nicht so kurz wie Hooks, aber es ist zuverlässiger

Wie ist das zuverlässiger?

Wir können immer noch keine Eigenschaften bedingt oder außerhalb ihres Lebenszyklus erstellen/aktualisieren, da wir in einen schlechten Zustand geraten könnten. Wenn beispielsweise eine Eigenschaft bedingt aufgerufen wird, wird die Eigenschaft nicht verworfen, wenn die Bedingung falsch ist.
Und alle Eigenschaften werden nach wie vor bei jedem Umbau neu bewertet.

Es verursacht jedoch mehrere Probleme, z. B. dass Benutzer gezwungen werden, ! nach NNBD überall zu verwenden, oder Benutzern möglicherweise den Zugriff auf eine Eigenschaft erlauben, bevor sie aktualisiert wird.

Was ist beispielsweise, wenn jemand eine Eigenschaft in didUpdateWidget liest?

  • Wurde initProperties vor dem Lebenszyklus ausgeführt? Das bedeutet jedoch, dass wir die Eigenschaften möglicherweise mehrmals pro Build aktualisieren müssen.
  • Wurde initProperties nach didUpdateWidget ausgeführt? Dann kann die Verwendung von Eigenschaften in didUpdateWidget zu einem veralteten Zustand führen

Am Ende haben wir also alle Probleme mit Hooks, aber:

  • wir können keine Eigenschaften innerhalb von `StatelessWidget verwenden. Die Lesbarkeit von StreamBuilder/ValueListenableBuilder/... ist also nach wie vor ein Problem – was das Hauptanliegen war.
  • Es gibt zahlreiche Randfälle
  • es ist schwieriger, benutzerdefinierte Eigenschaften zu erstellen (wir können nicht einfach eine Reihe von Eigenschaften in eine Funktion extrahieren)
  • es ist schwieriger, Umbauten zu optimieren

Am Ende unterscheidet sich das gegebene Beispiel im Verhalten nicht von:

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()),
    );
  }
}

Aber diese Syntax unterstützt noch viel mehr Dinge, wie zum Beispiel:

Vorzeitige Rückgabe:

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));

    ...
  }
}

die value2 entsorgen würde, wenn condition auf false wechselt

Extrahieren von Builder-Bündeln in eine Funktion:

Widget build(context) {
  final foo = keyword FooBuilder();
  final bar = keyword BarBuilder();

  return Text('$foo $bar');
}

kann geändert werden in:

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);
}

Umbauten optimieren

Der Parameter child ist noch möglich:

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()
  );
}

Als Teil der Sprache könnten wir dafür sogar Syntaxzucker haben:

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword TweenAnimationBuilder();
      final value2 = keyword ValueListenableBuilder();

      return Text();
    },
  );
}

Bonus: Als Sprachfeature werden bedingte Anrufe unterstützt

Als Teil der Sprache können wir ein solches Szenario unterstützen:

Widget build(context) {
  String label;

  if (condition) {
    label = keyword LabelBuilder();
  } else {
    label = keyword AnotherBuilder();
  }

  final value2 = keyword WhateverBuilder();

  return ...
}

Es ist nicht sehr nützlich, wird aber unterstützt – da die Syntax kompiliert ist, kann sie jede Verwendung von keyword indem sie sich auf Metadaten stützt, die sonst nicht verfügbar sind.

In Bezug auf die Lesbarkeit von Buildern ist hier das vorherige Beispiel, das jedoch mit Buildern erstellt wurde. Es erfüllt alle Anforderungen an Zuverlässigkeit und Code-Verwendung, aber sehen Sie, was es mit meinem armen Widget-Baum gemacht hat :'(

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),
                      );
                    });
              });
        });
  }
}

Es ist viel viel schwieriger (zumindest für mein Auge), den Code zu erkennen, auf den es ankommt. Außerdem musste ich fwiw dreimal von vorne anfangen, als ich dies schrieb, da ich ständig verwirrt war, welche Klammer wo hingehört, wo meine Semikolans hingehört usw. Verschachtelte Builder machen einfach keinen Spaß, darin zu schreiben oder zu arbeiten. Ein falsches Semikolon und Dartfmt scheitern und zerquetschen das Ganze vollständig.

Wie ist das zuverlässiger?

Dies ist ein perfektes Beispiel dafür, warum dieses _sollte_ ein Kern-Plugin-Imo sein. Die hierfür erforderlichen Domänenkenntnisse sind _deep_. Ich habe das Scripting-Wissen, um ein einfaches Caching-System wie dieses zu implementieren, ich habe nicht einmal annähernd das Domänenwissen, um jeden Grenzfall zu kennen, der auftreten kann, oder schlechte Zustände, in die wir geraten können. Abgesehen von Remi glaube ich, dass es außerhalb des Flutter-Teams etwa 4 Entwickler auf der Welt gibt, die sich damit auskennen! (offensichtlich übertrieben).

Das Problem der Unterstützung von Stateless Widgets ist gut. Einerseits verstehe ich es, StatefulWidgets sind seltsam ausführlich. Auf der anderen Seite sprechen wir hier wirklich von reiner Ausführlichkeit. Es gibt keine Fehler, die passieren können, wenn 2 Klassen definiert werden müssen, es gibt keine Möglichkeit, es zu vermasseln, der Compiler lässt Sie nicht zu, es gibt nie etwas Interessantes, das ich im StatelessWidget tun möchte. Ich bin also nicht davon überzeugt, dass dies ein wichtiges Thema ist ... Wäre SICHER schön zu haben, aber es sind die letzten 5% imo, nicht etwas, an dem man hängen bleiben kann.

Auf der anderen Seite ... diese Syntax von remi mit Schlüsselwortunterstützung ist absolut schön und wahnsinnig flexibel / mächtig. Und wenn Sie StatelessWidget kostenlos unterstützen, dann ist das nur extra 🔥

Die Unterstützung von StatelessWidget ist IMO eine große Sache. Optional, aber trotzdem sehr cool.

Obwohl ich zustimme, dass dies nicht kritisch ist, streiten sich die Leute bereits darum, Funktionen anstelle von StatelessWidget zu verwenden.
Die Anforderung, dass Benutzer ein StatefulWidget verwenden, um Builder zu verwenden (da die meisten Builder wahrscheinlich ein Property-Äquivalent haben), würde den Konflikt nur verschärfen.

Nicht nur das, sondern in einer Welt, in der wir in Dart (https://github.com/dart-lang/language/issues/418) Funktionen höherer Ordnung erstellen können, könnten wir Klassen ganz loswerden:

<strong i="9">@StatelessWidget</strong>
Widget Example(BuildContext context, {Key key, String param}) {
  final value = keyword StreamBuilder();

  return Text('$value');
}

dann verwendet als:

Widget build(context) {
  // BuildContext and Key are automatically injected
  return Example(param: 'hello');
}

Dies wird von Functional_widget unterstützt – einem Code-Generator, in dem Sie eine Funktion schreiben und eine Klasse für Sie generieren –, der auch HookWidget .

Der Unterschied besteht darin, dass die Unterstützung für Funktionen höherer Ordnung in Dart die Codegenerierung zur Unterstützung einer solchen Syntax überflüssig machen würde.

Ich vermute , was

Aber Ihr Vorschlag mit dem Schlüsselwort auch nicht. Ich denke, die Argumente für das neue Keyword sind ziemlich stark:

  • Flexibler und zusammensetzbarer als das Aufpfropfen auf State
  • Noch prägnantere Syntax
  • Funktioniert in Stateless, was eine sehr schöne Option ist

Was ich daran nicht mag, ist, dass wir uns über die Kosten für das mehrfache Festlegen von Eigenschaften für ein einfaches Objekt/Build sorgen, aber dann eine Lösung befürworten, die im Grunde eine Million Kontextebenen und eine Menge Layoutkosten schafft. Verstehe ich das falsch?

Der andere Nachteil ist diese Idee der Magie. Aber wenn Sie etwas Magisches tun möchten, ist ein neues Schlüsselwort meiner Meinung nach ein effektiver Weg, um dies zu erreichen, da es es einfach macht, die Community hervorzuheben und anzurufen und zu erklären, was es ist und wie es funktioniert. Es wäre im Grunde alles, worüber jemand für das nächste Jahr in Flutter spricht, und ich bin sicher, wir würden eine Explosion von coolen Plugins sehen, die daraus hervorgehen.

Ich vermute , was

Aber auch Hooks haben kein solches Problem, da sie statisch analysierbar sind und wir daher bei Missbrauch einen Kompilierungsfehler haben können.

Das ist kein Problem

Ebenso, wenn benutzerdefinierte Fehler ein No-Go sind, leidet Property, wie ich bereits erwähnt habe, unter genau dem gleichen Problem.
Wir können nicht vernünftig schreiben:

Property property;

<strong i="12">@override</strong>
void initProperties() {
  if (condition) {
    property = init(property, MyProperty());
  }
}

da das Umschalten von condition von true auf false die Eigenschaft nicht verwirft.

Wir können es auch nicht wirklich in einer Schleife nennen. Es macht nicht wirklich Sinn, da es sich um einen einmaligen Auftrag handelt. Was ist der Anwendungsfall, wenn die Eigenschaft in einer Schleife ausgeführt wird?

Und die Tatsache, dass wir Eigenschaften in beliebiger Reihenfolge lesen können, klingt gefährlich
Wir könnten zum Beispiel schreiben:

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),
>                       );
>                     });
>               });
>         });
>   }
> }

Das ist so ein seltsames Beispiel. Sind Sie sicher, dass AnimatedContainer dies noch nicht kann?

Natürlich. Das Beispiel hier ist, 3 Animationen in einem Widget zu verwenden, um "X" zu machen. Das X ist im Beispiel absichtlich vereinfacht, um die Menge an Boilerplate hervorzuheben.

Konzentrieren Sie sich nicht darauf, wie ich sie verwende. In einem realen Beispiel wäre der Widget-"Kern" hundert Zeilen oder so, die animierten Eigenschaften wären nicht so einfach und wir hätten mehrere Handler und andere Funktionen definiert. Angenommen, ich mache etwas, das nicht von einem der impliziten Widgets verarbeitet wird (nicht schwer, da sie außer AnimatedContainer extrem Einzweckzwecken dienen).

Der Punkt ist, dass Builder beim Erstellen von so etwas nicht gut funktionieren, da sie Sie zunächst in ein Lesbarkeits- (und Beschreibbarkeits-) Loch stecken. Als solche sind sie gut für einfache Anwendungsfälle geeignet, sie "komponieren" nicht. Gut. Compose ist die Zusammensetzung von 2 oder mehr Dingen.

Konzentrieren Sie sich nicht darauf, wie ich sie verwende. In einem realen Beispiel...

...und zurück zum Anfang. Warum bringen Sie kein echtes Beispiel?

Sie benötigen ein reales Beispiel für die Verwendung komplexer Animationen?
https://github.com/gskinnerTeam/flutter_vignetten

Das Anzeigen einer beliebigen komplexen Animation würde das Beispiel nur verschleiern. Es genügt zu sagen, dass es viele Anwendungsfälle für die Verwendung mehrerer Animatoren (oder jedes anderen zustandsbehafteten Objekts, das Sie sich vorstellen können) in einem Widget gibt

Natürlich. Das Beispiel hier ist, 3 Animationen in einem Widget zu verwenden, um "X" zu machen. Das X ist im Beispiel absichtlich vereinfacht, um die Menge an Boilerplate hervorzuheben.

das Widget "Kern" wäre hundert Zeilen oder so

In einem anderen Beitrag haben Sie ein Beispiel mit Boilerplate gepostet, das den "Kern" verdeckt, aber jetzt sagen Sie uns, dass der Kern Hunderte von Zeilen umfassen würde? In Wirklichkeit wäre die Boilerplate also winzig im Vergleich zum Kern? Sie können nicht beides haben.
Sie ändern ständig Ihre Argumente.

Konzentrieren Sie sich nicht darauf, wie ich sie verwende. In einem realen Beispiel...

...und zurück zum Anfang. Warum bringen Sie kein echtes Beispiel?

Wahrscheinlich, weil es viel Zeit in Anspruch nimmt, ein echtes Beispiel zu erstellen, wenn man nur mit verschiedenen Ideen herumspielt. Der Leser soll sich vorstellen, wie es in einer realen Situation verwendet werden könnte, und nicht erwähnen, dass es Möglichkeiten gibt, es zu umgehen. Natürlich kann man einen animierten Container verwenden, aber was wäre, wenn dies nicht möglich wäre? Was wäre, wenn es zu komplex wäre, nur einen animierten Container zu erstellen.

Nun, da die Autoren keine wirklich realen Beispiele verwenden, die entweder als gut oder schlecht gezeigt werden können, habe ich keine Meinung dazu, ich kommentiere nur die Tendenz in diesem Thread, Verbesserungsvorschläge für Probleme zu machen, die dies nicht tun das Problem vollständig lösen. Dies scheint eine Hauptursache für Verwirrung zwischen Hooks-Befürwortern und -Gegnern zu sein, da jeder bis zu einem gewissen Grad an einem anderen vorbeiredet nicht gezeigt und ein Befürworter kann nicht sagen, dass man sich nur ein reales Szenario vorstellen sollte.

Ich glaube, ich sagte, es wäre albern, eine Klasse mit 100 Zeilen zu haben, von der die Hälfte aus Boilerplate besteht. Genau das beschreibe ich hier. Der Kern, auch wenn er groß ist, sollte nicht durch eine Reihe von Geräuschen verschleiert werden, was bei Verwendung mehrerer Builder sicherlich der Fall ist.

Und der Grund ist die Scan-Fähigkeit, Lesbarkeit und Wartung über eine große Codebasis hinweg. Es ist nicht das Schreiben der Zeilen, obwohl das Schreiben in Baumeistern ein Produktivitätsverlust ist, da die Tendenz besteht, in die Hölle mit geschweiften Klammern zu geraten.

In einem anderen Beitrag haben Sie ein Beispiel mit Boilerplate gepostet, das den "Kern" verdeckt, aber jetzt sagen Sie uns, dass der Kern Hunderte von Zeilen umfassen würde? In Wirklichkeit wäre die Boilerplate also winzig im Vergleich zum Kern? Sie können nicht beides haben.
Sie ändern ständig Ihre Argumente.

Auch bei diesem Thema geht es nicht um Boilerplate, sondern um Lesbarkeit und Wiederverwendbarkeit.
Es spielt keine Rolle, ob wir 100 Zeilen haben.
Wichtig ist, wie lesbar/wartungsfähig/wiederverwendbar diese Zeilen sind.

Selbst wenn es um Boilerplate ginge, warum sollte ich als Benutzer ein solches Boilerplate auf jeden Fall tolerieren, wenn eine hinreichend äquivalente Ausdrucksweise dafür vorhanden ist? Beim Programmieren geht es darum, Abstraktionen zu erstellen und Arbeit zu automatisieren. Ich sehe keinen Sinn darin, immer wieder dasselbe in verschiedenen Klassen und Dateien zu wiederholen.

Sie benötigen ein reales Beispiel für die Verwendung komplexer Animationen?
https://github.com/gskinnerTeam/flutter_vignetten

Sicherlich können Sie nicht erwarten, dass ich Ihr ganzes Projekt durchforste. Welche Datei soll ich mir genau anschauen?

Das Anzeigen einer beliebigen komplexen Animation würde das Beispiel nur verschleiern.

Das genaue Gegenteil. Eine beliebige komplexe Animation zeigt , die von einer bestehenden Lösung gelöst werden kann , wäre, und das ist , was Hixie fragt immer wieder, glaube ich.

Scannen Sie einfach die Gifs und stellen Sie sich vor, wie Sie einige dieser Dinge erstellen könnten. Dieses Repo besteht eigentlich aus 17 eigenständigen Apps. Sie können auch nicht erwarten, dass ich Ihnen irgendeine beliebige Animation schreibe, nur um Ihnen zu beweisen, dass komplexe Animationen existieren können. Ich baue sie seit 20 Jahren, angefangen mit Flash, jedes einzelne anders als das letzte. Und es ist sowieso nicht spezifisch für Animationen, sie sind nur die einfachste bekannteste API, um einen größeren Punkt zu veranschaulichen.

Wie Sie wissen, wenn Sie einen Animator verwenden, müssen Sie jedes Mal 6 Dinge tun, aber es braucht auch Lebenszyklus-Hooks? Ok, also erweitere das jetzt auf ALLES, das 6 Schritte hat, die du jedes Mal tun musst ... Und du musst es an 2 Stellen verwenden. Oder Sie müssen 3 davon gleichzeitig verwenden. Es ist so offensichtlich ein Problem, ich weiß nicht, was ich noch hinzufügen kann, um es zu erklären.

Beim Programmieren geht es darum, Abstraktionen zu erstellen und Arbeit zu automatisieren. Ich sehe keinen Sinn darin, immer wieder dasselbe in verschiedenen Klassen und Dateien zu wiederholen.

__Alle__? Performance, Wartbarkeit sind also nicht relevant?

Es kommt ein Punkt, an dem das "Automatisieren" einer Arbeit und das "Ausführen" einer Arbeit gleich sind.

Leute, es ist in Ordnung, wenn Sie keine Zeit oder Lust haben, echte Beispiele zu erstellen, aber bitte, wenn Sie nicht daran interessiert sind, Beispiele zu erstellen, um das Problem zu erklären, sollten Sie auch nicht erwarten, dass sich die Leute gezwungen fühlen, das Problem zu lösen ( was viel mehr Arbeit bedeutet, als Beispiele zu erstellen, um das Problem aufzuzeigen). Niemand hier muss etwas für irgendjemanden tun, es ist ein Open-Source-Projekt, bei dem wir alle versuchen, uns gegenseitig zu helfen.

@TimWhiting würde es Ihnen etwas https://github.com/TimWhiting/local_widget_state_approaches-Repository aufzunehmen ? Einige Personen können ohne eine entsprechende Lizenz (idealerweise BSD, MIT oder ähnliches) nicht beitragen.

warum sollte ich, der benutzer, solche klauseln auf jeden fall tolerieren, wenn eine hinreichend äquivalente art und weise das gleiche ausdrückt?

Ja, Wartbarkeit und Performance sind selbstverständlich. Ich meine, wenn es eine gleichwertige Lösung gibt, sollten wir diejenige wählen, die weniger Boilerplate hat, leichter zu lesen ist, wiederverwendbar ist und so weiter. Das soll nicht heißen, dass Hooks die Antwort sind, da ich beispielsweise ihre Leistung nicht gemessen habe, aber sie sind meiner Erfahrung nach wartungsfreundlicher. Ich bin mir immer noch unsicher bezüglich Ihrer Argumentation, wie es sich auf Ihre Arbeit auswirkt, wenn ein hakenartiges Konstrukt in den Flutter-Kern eingefügt wird.

Scannen Sie einfach die Gifs und stellen Sie sich vor, wie Sie einige dieser Dinge erstellen könnten.

Ich habe die Gifs gescannt. Ich würde keine Builder-Widgets verwenden.
Viele der Animationen sind so komplex, dass ich, wenn ich wüsste, dass Sie sie mit den höherstufigen Buildern implementiert haben, Ihr Paket wahrscheinlich nicht verwenden würde.

Jedenfalls scheint diese Diskussion mit mehr persönlichen Meinungsverschiedenheiten aus dem Ruder zu laufen. Wir sollten uns auf die Hauptaufgabe konzentrieren. Ich bin mir nicht sicher, wie Hook-Befürworter kleinere Beispiele zeigen können, wenn Gegner, wie ich bereits sagte, Verbesserungen finden, die das gestellte Problem nicht wirklich lösen. Ich denke, wir sollten vorerst zum Repository von

Eine beliebige komplexe Animation zu zeigen, die von keiner bestehenden Lösung gelöst werden kann, wäre das Beispiel, und das ist es, was Hixie immer wieder fragt, glaube ich.

Beispiele für etwas zu zeigen, was heute nicht möglich ist, würde den Rahmen dieser Ausgabe sprengen.
In diesem Thema geht es darum, die Syntax dessen zu verbessern, was bereits machbar ist, und nicht, einige Dinge freizugeben, die heute nicht möglich sind.

Jede Anfrage, etwas bereitzustellen, was heute nicht möglich ist, gehört nicht zum Thema.

@TimWhiting würde es Ihnen etwas https://github.com/TimWhiting/local_widget_state_approaches-Repository aufzunehmen ? Einige Personen können ohne eine entsprechende Lizenz (idealerweise BSD, MIT oder ähnliches) nicht beitragen.

Fertig. Tut mir leid, ich hatte nicht viel Zeit, um an den Beispielen zu arbeiten, aber dazu werde ich wahrscheinlich irgendwann in dieser Woche kommen.

Eine beliebige komplexe Animation zu zeigen, die von keiner bestehenden Lösung gelöst werden kann, wäre das Beispiel, und das ist es, was Hixie immer wieder fragt, glaube ich.

Beispiele für etwas zu zeigen, was heute nicht möglich ist, würde den Rahmen dieser Ausgabe sprengen.
In diesem Thema geht es darum, die Syntax dessen zu verbessern, was bereits machbar ist, und nicht, einige Dinge freizugeben, die heute nicht möglich sind.

Jede Anfrage, etwas bereitzustellen, was heute nicht möglich ist, gehört nicht zum Thema.

Lassen Sie mich umformulieren, was ich gesagt habe.

Einen willkürlichen komplexen Anwendungsfall mit Animationen, Zuständen usw. zu zeigen , die schwer zu schreiben sind und die erheblich verbessert werden können, beeinträchtigen, wäre das Beispiel, und das ist es, was Hixie immer wieder fragt, glaube ich.

Ich verstehe den Drang nach weniger Boilerplate, mehr Wiederverwendbarkeit, mehr Magie. Ich mag es auch, weniger Code schreiben zu müssen, und die Sprache / das Framework, die mehr Arbeit leisten, ist ziemlich appetitlich.
Bisher konnte keine der hier vorgestellten Beispiele/Lösungskombinationen den Code drastisch verbessern. Das heißt, wenn es uns nicht nur darum geht, wie viele Codezeilen wir schreiben müssen.

Leute, es ist in Ordnung, wenn Sie keine Zeit oder Lust haben, echte Beispiele zu erstellen, aber bitte, wenn Sie nicht daran interessiert sind, Beispiele zu erstellen, um das Problem zu erklären, sollten Sie auch nicht erwarten, dass sich die Leute gezwungen fühlen, das Problem zu lösen ( was viel mehr Arbeit bedeutet, als Beispiele zu erstellen, um das Problem aufzuzeigen). Niemand hier muss etwas für irgendjemanden tun, es ist ein Open-Source-Projekt, bei dem wir alle versuchen, uns gegenseitig zu helfen.

@TimWhiting würde es Ihnen etwas https://github.com/TimWhiting/local_widget_state_approaches-Repository aufzunehmen ? Einige Personen können ohne eine entsprechende Lizenz (idealerweise BSD, MIT oder ähnliches) nicht beitragen.

Ich habe ungefähr 6 Stunden damit verbracht, diese verschiedenen Beispiele und Codeschnipsel zu erstellen. Aber ich sehe wirklich keinen Sinn darin, konkrete Beispiele für komplexe Animationen bereitzustellen, nur um zu beweisen, dass sie existieren können.

Die Anforderung besteht darin, dies im Grunde in etwas zu verwandeln, das von AnimatedContainer nicht verarbeitet werden kann:

Container(margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30), color: Colors.red.withOpacity(value1));

Das ist so trivial, dass man dem Thema fast absichtlich stumpfsinnig gegenübersteht. Ist es so schwer vorstellbar, dass ich ein paar pulsierende Schaltflächen habe, einige Partikel, die sich bewegen, vielleicht ein paar Textfelder, die beim Skalieren eingeblendet werden, oder einige Karten, die sich umdrehen? Vielleicht baue ich eine Soundbar mit 15 unabhängigen Bars, vielleicht schiebe ich ein Menü ein, benötige aber auch die Möglichkeit, einzelne Elemente wieder herauszuschieben. Und weiter und weiter und weiter. Und das ist nur für Animationen. Sie gilt für jeden Anwendungsfall, der im Kontext eines Widgets mühsam ist.

Ich denke, ich habe ausgezeichnete kanonische Beispiele für das Problem mit beiden Buildern und der Wiederverwendung von Vanilla State bereitgestellt:
https://github.com/flutter/flutter/issues/51752#issuecomment -671566814
https://github.com/flutter/flutter/issues/51752#issuecomment -671489384

Sie müssen sich einfach viele dieser Instanzen vorstellen (wählen Sie Ihr Gift), die weit und breit über ein Projekt mit mehr als 1000 Klassendateien verteilt sind, und Sie erhalten ein perfektes Bild von den Lesbarkeits- und Wartbarkeitsproblemen, die wir zu vermeiden versuchen.

Würden die von @esDotDev bereitgestellten Beispielbilder, die zeigen, wie die Verschachtelung den Code schwerer lesbar macht, für Sie, @Rudiksz , nicht ausreichen? Was fehlt ihnen? Ich nehme an, es gibt dort keine Leistungsmetriken, aber @rrousselGit ist sicher, dass sie nicht weniger leistungsstark sind als Builder.

@esDotDev Ich denke, es geht darum, ein singuläres kanonisches Beispiel zu haben, mit dem alle

Das macht für mich Sinn, aber ich verstehe nicht, warum es größer als eine einzelne Seite sein muss.

Die Verwaltung mehrerer Animationen ist das perfekte Beispiel für etwas, das häufig vorkommt, eine Menge Boilerplates erfordert, fehleranfällig ist und derzeit keine gute Lösung bietet. Wenn das nicht den Sinn macht, wenn die Leute sagen, dass sie nicht einmal verstehen, wie komplex Animationen sein können, dann wird natürlich jeder Anwendungsfall für den Kontext des Anwendungsfalls niedergeschlagen und nicht für das Architekturproblem, das wir haben versuche zu veranschaulichen.

Sicher, das Repository von @TimWhiting hat keine vollständige App, es hat einzelne Seiten als Beispiele, wie Sie sagen, wenn Sie ein kanonisches Beispiel für eine Animation für dieses Repository erstellen können, aus dem andere ihre Lösung implementieren könnten, würde das funktionieren.

Ich glaube auch nicht, dass wir eine riesige App oder so brauchen, aber es sollte eine ausreichende Komplexität ähnlich der von TodoMVC geben. Im Grunde muss es so ausreichen, dass Ihre Gegner nicht sagen können "Nun, ich könnte das so und so besser".

@Hixie Die Anfrage nach echten Apps zum Vergleich von Ansätzen ist fehlerhaft.

Es gibt zwei Mängel:

  • Wir sind uns bei dem Problem noch nicht einig, wie du selbst gesagt hast, verstehst du es nicht
  • Beispiele können wir nicht unter realen Produktionsbedingungen umsetzen, da wir fehlende Teile haben.

Wir können beispielsweise keine Anwendung schreiben mit:

final snapshot = keyword StreamBuilder();

da dies nicht implementiert ist.

Wir können auch die Leistung nicht beurteilen, da hier ein POC- und Produktionscode verglichen wird.

Wir können auch nicht beurteilen, ob etwas wie "Hooks können nicht bedingt aufgerufen werden" fehleranfällig ist, da es keine Compiler-Integration gibt, um auf Fehler bei Missbrauch hinzuweisen.

Die Leistung von Designs zu beurteilen, die API-Nutzbarkeit zu bewerten, Dinge zu implementieren, bevor wir Implementierungen haben ... all das ist Teil des API-Designs. Willkommen in meiner Arbeit. :-) (Flatter-Trivia: Wussten Sie, dass die ersten paar tausend Zeilen von RenderObject und RenderBox et al implementiert wurden, bevor wir dart:ui erstellt haben?)

Das ändert nichts daran, dass Sie das Unmögliche verlangen.

Einige der hier gemachten Vorschläge sind Teil der Sprache oder des Analysators. Das kann die Gemeinde nicht umsetzen.

Ich bin mir nicht so sicher, andere Frameworks und Sprachen entwickeln ständig APIs, ich denke nicht, dass es hier zu anders ist, oder dass Flutter einige überwältigende Unterschiede oder Schwierigkeiten beim API-Design hat als andere Sprachen. Wie in, sie tun es ohne Compiler- oder Analysator-Unterstützung, sie sind nur Proofs of Concept.

Ich habe ein Beispiel für ein "komplexes" Animationsszenario zusammengestellt, das 3 Animationen gut nutzt und ziemlich voll mit Boilerplate und Cruft ist.

Es ist wichtig zu beachten, dass ich einfach jede Animation hätte ausführen können, die ein hartes Zurückspringen auf die Startposition (Eliminierung aller impliziten Widgets) oder eine Drehung auf der z-Achse oder eine Skalierung auf einer einzelnen Achse oder einen anderen Anwendungsfall erfordert, der nicht von IWs abgedeckt wird. Ich machte mir Sorgen, dass diese nicht ernst genommen werden könnten (obwohl meine Designer mir dieses Zeug den ganzen Tag lang aushändigen), also habe ich etwas "realeres" gebaut.

Hier ist also ein einfaches Gerüst, es hat 3 Paneele, die sich auf- und zuschieben lassen. Es verwendet 3 Animatoren mit diskreten Zuständen. In diesem Fall benötige ich nicht wirklich die volle Kontrolle über AnimatorController, TweenAnimationBuilder wäre ok, aber die resultierende Verschachtelung in meinem Baum wäre sehr unerwünscht. Ich kann die TABs nicht im Baum verschachteln, da die Panels Abhängigkeiten von den Werten des anderen haben. AnimatedContainer ist hier keine Option, da jedes Panel vom Bildschirm gleiten muss, sie "quetschen" nicht.
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));

Von den ungefähr 100 Zeilen in diesem Körper sind ungefähr 40% oder so reine Boilerplate. Insbesondere 15 Zeilen, in denen etwas fehlt oder sich vertippt, können schwer zu erkennende Fehler verursachen.

Wenn wir so etwas wie StatefulProperty verwenden, würde dies die Boilerplate auf etwa 15% reduzieren (spart etwa 25 Zeilen). Kritisch würde dies das Problem von hinterhältigen Fehlern und doppelter Geschäftslogik vollständig lösen, aber es ist immer noch ein wenig ausführlich, insbesondere da es StatefulWidget erfordert, das direkt ein 10-Zeilen-Hit ist.

Wenn wir so etwas wie 'keyword' verwenden, reduzieren wir die Boilerplate-Zeilen im Wesentlichen auf 0%. Der gesamte Fokus der Klasse könnte auf der (einzigartigen) Geschäftslogik und den visuellen Baumelementen liegen. Wir machen die Verwendung von StatefulWIdgets im Allgemeinen viel seltener, die überwiegende Mehrheit der Ansichten wird um 10 oder 20 % weniger ausführlich und fokussierter.

Es ist auch erwähnenswert, dass das obige Panel-Szenario real ist, und in der realen Welt ist dieser Ansatz offensichtlich wirklich nicht schön, also haben wir ihn nicht verwendet und Sie würden ihn nicht in der Codebasis sehen. Wir würden auch keine verschachtelten Builder verwenden, da diese eklig aussehen und Sie das auch nicht sehen werden.

Wir haben ein dediziertes SlidingPanel-Widget erstellt, das eine IsOpen-Eigenschaft übernimmt und sich selbst öffnet und schließt. Dies ist im Allgemeinen die Lösung in jedem einzelnen dieser Anwendungsfälle, bei denen Sie ein bestimmtes Verhalten benötigen, die zustandsorientierte Logik in ein ultraspezifisches Widget verschieben und dieses verwenden. Sie schreiben im Grunde Ihr eigenes ImplicitlyAnimatedWidget.

Dies funktioniert im Allgemeinen in Ordnung, aber es ist immer noch Zeit und Mühe, und der einzige Grund, warum es existiert, ist, dass die Verwendung von Animationen so schwierig ist (was existiert, weil die Wiederverwendung von zustandsbehafteten Komponenten im Allgemeinen so schwierig ist). In Unity oder AIR würde ich zum Beispiel keine dedizierte Klasse erstellen, um einfach ein Panel auf einer einzelnen Achse zu verschieben, es wäre nur eine einzige Codezeile zum Öffnen oder Schließen, es gäbe nichts für ein dediziertes Widget zu tun. In Flutter müssen wir ein dediziertes Widget erstellen, da es buchstäblich die einzige vernünftige Möglichkeit ist, das Bootstraping und den Teardown von AnimatorController zu kapseln (es sei denn, wir möchten mit TAB verschachteln, verschachteln, verschachteln).

Mein Hauptpunkt hier ist, dass diese Art von Dingen der Grund dafür ist, warum Beispiele aus der realen Welt so schwer zu finden sind. Als Entwickler können wir diese Dinge nicht zu sehr in unseren Codebasen existieren lassen, also umgehen wir sie mit weniger als idealen, aber effektiven Problemumgehungen. Wenn Sie sich dann die Codebasis ansehen, sehen Sie nur diese Workarounds in Kraft und alles scheint in Ordnung zu sein, aber es könnte nicht das sein, was das Team machen wollte, es könnte 20 % länger gedauert haben, um dorthin zu gelangen, es könnte insgesamt gewesen sein Mühe beim Debuggen, nichts davon ist offensichtlich, wenn man einen Blick auf den Code wirft.

Der Vollständigkeit halber ist hier der gleiche Anwendungsfall, der mit Buildern erstellt wurde. Die Zeilenanzahl ist stark reduziert, es gibt keine Chance für Fehler, keine Notwendigkeit, das Fremdkonzept RE TickerProviderMixin zu erlernen... ) macht die Geschäftslogik viel schwieriger zu lesen, als sie sein müsste.

```Pfeil
Klasse _SlidingPanelViewState erweitert State{
bool isLeftMenuOpen = true;
bool isRightMenuOpen = true;
bool isBtmMenuOpen = true;

@überschreiben
Widget-Build (BuildContext-Kontext) {
zurück TweenAnimationBuilder(
tween: Tween(begin: 0, end: isLeftMenuOpen ? 1 : 0),
Dauer: widget.slideDuration,
Builder: (_, leftAnimValue, __) {
zurück TweenAnimationBuilder(
tween: Tween(Beginn: 0, Ende: isRightMenuOpen ? 1 : 0),
Dauer: widget.slideDuration,
Builder: (_, rightAnimValue, __) {
zurück TweenAnimationBuilder(
tween: Tween(Beginn: 0, Ende: isBtmMenuOpen ? 1 : 0),
Dauer: widget.slideDuration,
Builder: (_, btmAnimValue, __) {
doppelte leftPanelSize = 320;
double leftPanelPos = -leftPanelSize * (1 - leftAnimValue);
doppelte rightPanelSize = 230;
double rightPanelPos = -rightPanelSize * (1 - rightAnimValue);
doppelte bottomPanelSize = 80;
double bottomPanelPos = -bottomPanelSize * (1 - btmAnimValue);
Stapel zurück (
Kinder: [
//Bg
Behälter (Farbe: Colors.white),
//Hauptinhaltsbereich
Positioniert (
oben: 0,
links: leftPanelPos + leftPanelSize,
unten: bottomPanelPos + bottomPanelSize,
rechts: rightPanelPos + rightPanelSize,
Kind: ChannelInfoView(),
),
//Linkes Panel
Positioniert (
oben: 0,
links: leftPanelPos,
unten: bottomPanelPos + bottomPanelSize,
Breite: leftPanelSize,
Kind: ChannelMenu(),
),
//Bodenplatte
Positioniert (
links: 0,
rechts: 0,
unten: bottomPanelPos,
Höhe: BottomPanelSize,
Kind: NotificationsBar(),
),
//Rechtes Panel
Positioniert (
oben: 0,
rechts: rightPanelPos,
unten: bottomPanelPos + bottomPanelSize,
Breite: rightPanelSize,
Kind: SettingsMenu(),
),
// Tasten
Reihe(
Kinder: [
Button("links", () => setState(() => isLeftMenuOpen = !isLeftMenuOpen)),
Button("btm", () => setState(() => isBtmMenuOpen = !isBtmMenuOpen)),
Button("rechts", () => setState(() => isRightMenuOpen = !isRightMenuOpen)),
],
)
],
);
},
);
},
);
},
);
}
}

Letzteres ist interessant ... Ich wollte ursprünglich vorschlagen, dass die Builder um die positionierten Widgets herum und nicht um den Stapel herum angeordnet sein sollten (und ich würde dies immer noch für das linke und rechte Panel vorschlagen), aber dann wurde mir klar, dass sich das untere auswirkt alle drei, und mir wurde klar, dass der Builder Ihnen nur ein Argument child , eigentlich nicht genug ist, weil Sie wirklich sowohl die Container als auch die Row konstant halten wollen baut. Ich nehme an, Sie können sie einfach über dem ersten Builder erstellen.

Mein Hauptpunkt hier ist, dass diese Art von Dingen der Grund dafür ist, warum Beispiele aus der realen Welt so schwer zu finden sind. Als Entwickler können wir diese Dinge nicht zu sehr in unseren Codebasen existieren lassen, also umgehen wir sie mit weniger als idealen, aber effektiven Problemumgehungen. Wenn Sie sich dann die Codebasis ansehen, sehen Sie nur diese Workarounds in Kraft und alles scheint in Ordnung zu sein, aber es könnte nicht das sein, was das Team machen wollte, es könnte 20 % länger gedauert haben, um dorthin zu gelangen, es könnte insgesamt gewesen sein Mühe beim Debuggen, nichts davon ist offensichtlich, wenn man einen Blick auf den Code wirft.

Fürs Protokoll, dies ist kein Zufall. Dies ist sehr konstruktionsbedingt. Wenn diese Widgets eigene Widgets sind, wird die Leistung verbessert. Wir haben uns sehr vorgenommen, dass die Leute Flutter so verwenden. Darauf habe ich mich oben bezogen, als ich erwähnte, dass "ein großer Teil der Designphilosophie von Flutter darin besteht, Menschen zur richtigen Wahl zu führen".

Ich wollte ursprünglich vorschlagen, dass sich die Builder um die positionierten Widgets und nicht um den Stapel herum befinden sollten (und ich würde dies immer noch für das linke und rechte Bedienfeld vorschlagen).

Ich habe es der Kürze halber weggelassen, aber normalerweise würde ich hier wahrscheinlich einen Inhaltscontainer haben wollen, der die Größen aus allen 3 Menüs verwendet, um seine eigene Position zu definieren. Das heißt, alle 3 müssen sich an der Spitze des Baums befinden, und wenn es einen Neuaufbau gibt, benötige ich die gesamte Ansicht zum Neuaufbau, ohne dass ich daran vorbeikomme. Dies ist im Grunde Ihr klassisches Gerüst im Desktop-Stil.

Natürlich könnten wir anfangen, den Baum auseinanderzureißen, immer eine Option, wir verwenden es oft für größere Widgets, aber ich habe noch nie gesehen, dass dies die Grokbarkeit auf einen Blick verbessert, es gibt einen zusätzlichen kognitiven Schritt, den der Leser tun muss Punkt. In der Sekunde, in der Sie den Baum auseinanderbrechen, folge ich plötzlich einer Brotkrumenspur von variablen Zuweisungen, um herauszufinden, was hier passiert wird und wie das Ganze zusammenpasst, wird verdeckt. Den Baum als verdaulichen Baum zu präsentieren ist aus meiner Erfahrung immer einfacher zu argumentieren.

Wahrscheinlich ein guter Anwendungsfall für ein dediziertes RenderObject.

Oder 3 einfach zu verwaltende AnimatorObjects, die sich in den Widget-Lebenszyklus einklinken können :D

Wenn diese Widgets eigene Widgets sind, wird die Leistung verbessert.

Da wir hier konkret werden: Da es sich in diesem Fall um ein Gerüst handelt, ist jede Unteransicht bereits ein eigenes Widget, dieser Typ ist nur dafür verantwortlich, Kinder anzulegen und zu übersetzen. Die Childen wären BottomMenu(), ChannelMenu(), SettingsView(), MainContent() etc

In diesem Fall wickeln wir eine Reihe in sich abgeschlossener Widgets mit einer weiteren Schicht in sich abgeschlossener Widgets ein, um einfach die Boilerplate zu verwalten, um sie zu verschieben. Ich glaube nicht, dass das ein Leistungsgewinn ist? In diesem Fall werden wir in das hineingedrängt, was der Rahmen _denkt_, was wir tun wollen, und nicht in das, was wir tatsächlich tun wollen, nämlich eine ebenso performante Sicht prägnanter und kohärenter zu schreiben.

[Bearbeiten] Ich werde die Beispiele aktualisieren, um diesen Kontext hinzuzufügen

Der Grund, warum ich ein dediziertes RenderObject vorschlage, ist, dass es das Layout verbessern würde. Ich stimme zu, dass sich der Animationsaspekt in einem zustandsbehafteten Widget befinden würde, es würde einfach die drei 0..1 Doubles (Wert1, Wert2, Wert3) an das Renderobjekt weitergeben, anstatt die Stack-Mathematik zu verwenden. Aber das ist meistens ein Nebenschauplatz für diese Diskussion; An dem Punkt, an dem Sie dies tun, möchten Sie immer noch die von Hooks oder ähnlichem gebotene Vereinfachung in diesem zustandsbehafteten Widget vornehmen.

In einer relevanteren Anmerkung hatte ich einen Versuch , eine Demo für das Projekt von https://github.com/TimWhiting/local_widget_state_approaches/pull/1
Ich bin gespannt, wie eine Hooks-Version aussehen würde. Ich bin mir nicht sicher, ob ich eine gute Möglichkeit sehe, es einfacher zu machen, insbesondere die Leistungsmerkmale beizubehalten (oder sie zu verbessern; es gibt dort einen Kommentar, der eine Stelle anzeigt, an der es derzeit nicht optimal ist).

(Ich bin auch sehr gespannt, ob dies die Art ist, bei der wir, wenn wir einen Weg zur Vereinfachung finden würden, hier fertig wären, oder ob kritische Dinge fehlen, die wir lösen müssten, bevor wir fertig sind.)

Ist das Restaurierungsmaterial für dieses Beispiel entscheidend? Es fällt mir deswegen schwer, zu folgen, und ich weiß nicht wirklich, was RestorationMixin überhaupt macht. Ich nehme an, es wird... etwas dauern, um das für mich zu verstehen. Ich bin mir sicher, dass Remi die Hooks-Version in 4 Sekunden flach herausdrehen wird :)

Die Verwendung der Wiederherstellungs-API mit HookWidget anstelle von StatefulHookWidget wird derzeit nicht unterstützt.

Idealerweise sollten wir uns ändern können

final value = useState(42);

hinein:

final value = useRestorableInt(42);

Aber es braucht einiges Nachdenken, da die aktuelle Wiederherstellungs-API nicht wirklich mit Hooks im Hinterkopf entwickelt wurde.

Als Randnotiz kommen React-Hooks mit einer "Schlüssel" -Funktion, die normalerweise so verwendet wird:

int userId;

Future<User> user = useMemo(() => fetchUser(id), [id]);

wobei dieser Code bedeutet "das Ergebnis des Rückrufs zwischenspeichern und den Rückruf neu auswerten, wenn sich etwas im Array ändert"

Flutter_hooks haben eine 1-zu-1-Reimplementierung davon vorgenommen (da es nur eine Portierung ist), aber ich denke nicht, dass wir das für einen Flutter-optimierten Code tun möchten.

Wir würden wahrscheinlich wollen:

int userId;

Future<User> user = useMemo1(id, (id) => fetchUser(id));

was dasselbe tun würde, aber den Speicherdruck beseitigen, indem eine Listenzuordnung vermieden und eine Funktion zum Abreißen verwendet wird

Es ist zu diesem Zeitpunkt nicht kritisch, aber erwähnenswert, wenn wir flutter_hooks für Beispiele verwenden möchten.

@Hixie Ich habe dein Animationsbeispiel auf Hooks portiert

Es war ein interessantes Beispiel, ein großes Lob dafür, dass Sie darüber nachgedacht haben!
Es ist ein gutes Beispiel dafür, dass standardmäßig die Implementierung von "aktiv" und "dauer" überall vorhanden ist (und dabei voneinander abhängen).
Bis zu dem Punkt, an dem es zahlreiche "if (active)" / "controller.repeat"-Aufrufe gibt

Bei Hooks hingegen wird die gesamte Logik deklarativ behandelt und an einer einzigen Stelle ohne Duplikate konzentriert.

Das Beispiel zeigt auch, wie Hooks verwendet werden können, um Objekte einfach zwischenzuspeichern – wodurch das Problem des zu häufigen Neuaufbaus von ExpensiveWidget behoben wurde.
Wir nutzen die Vorteile von const-Konstruktoren, aber es funktioniert mit dynamischen Parametern.

Wir bekommen auch ein besseres Hot-Reload. Wir können die Timer.periodic-Dauer für die Hintergrundfarbe ändern und die Änderungen sofort sehen.

@rrousselGit hast du einen Link? Ich habe im Repository von @TimWhiting nichts Neues

Die Verwendung der Wiederherstellungs-API mit HookWidget anstelle von StatefulHookWidget wird derzeit nicht unterstützt.

Welche Lösung wir auch immer finden, wir müssen sicherstellen, dass sie nicht jedes letzte Mixin kennen muss. Wenn jemand unsere Lösung in Kombination mit einem anderen Paket verwenden möchte, das ein Mixin wie TickerProviderStateMixin oder RestorationMixin einführt, sollte dies möglich sein.

https://github.com/TimWhiting/local_widget_state_approaches/pull/3

Zugegeben, aber ich mache mir darüber keine Sorgen. useAnimationController erfordert nicht, dass sich Benutzer beispielsweise um SingleTickerProvider kümmern.

AutomaritKeepAlive könnte von der gleichen Behandlung profitieren.
Eines der Dinge, an die ich gedacht habe, ist ein "useKeepAlive(bool)"-Hook

Das vermeidet sowohl das Mixin als auch das "super.build(context)" (letzteres ist ziemlich verwirrend)

Ein weiterer interessanter Punkt sind die Änderungen, die beim Refactoring erforderlich sind.

Zum Beispiel können wir den Unterschied zwischen den Änderungen vergleichen, die erforderlich sind, um TickerMode für den Rohansatz im Vergleich zu Hooks zu implementieren:

Einige andere Dinge sind im Diff verwechselt, aber wir können daraus sehen, dass:

  • StatefulWidget erforderlich, um die Logik in einen völlig anderen Lebenszyklus zu verschieben
  • Hakenänderungen sind rein additiv. Vorhandene Zeilen wurden nicht bearbeitet/verschoben.

Imo ist dies sehr wichtig und ein wichtiger Gewinn aus dieser Art von eigenständigen Zustandsobjekten. Alles basierend auf verschachtelten Kontexten in einem Baum zu haben, ist grundsätzlich schwieriger und komplizierter zu refaktorisieren und zu ändern, was im Laufe eines Projekts einen unsichtbaren, aber definitiven Einfluss auf die Endqualität der Codebasis hat.

Einverstanden!
Das macht auch Code-Reviews viel einfacher zu lesen:

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'),
+        );
+      }
    );
  },
);

Im zweiten Diff ist nicht klar, dass Container unverändert bleibt und sich nur die Text geändert haben

@rrousselGit Glauben Sie, dass es sinnvoll ist, eine Version zu

Eine andere Sache, die Sie beachten sollten, ist, dass dies alles wirklich treffen würde, wenn Sie mehrere Widgets hätten, die alle diese "Farbschritt"-Logik teilen müssten, die resultierende Farbe jedoch auf völlig unterschiedliche Weise verwenden. Mit dem Ansatz im Hooks-Stil bündeln wir einfach jede Logik, die sinnvoll ist, um sie wiederzuverwenden, und wir verwenden sie einfach. Es gibt keine architektonischen Ecken, in die wir gezwungen werden, es ist wirklich agnostisch und flexibel.

Beim Stateful-Ansatz sind wir gezwungen,

  • Kopieren und Einfügen der Logik (sehr nicht wartbar)
  • mit einem Builder (nicht super lesbar, insbesondere bei der Verwendung von Verschachtelungen)
  • Mischen (komponiert nicht gut, sehr leicht, dass verschiedene Mixins in ihrem gemeinsamen Zustand in Konflikt geraten)

Das Wichtigste, was ich denke, ist, dass Sie bei letzterem sofort ein Architekturproblem haben, wo soll ich das in meinen Baum einfügen, wie kann ich das am besten kapseln, sollte es ein Builder oder vielleicht ein benutzerdefiniertes Widget sein? Bei ersterem besteht die einzige Entscheidung darin, in welcher Datei dieser Teil wiederverwendeter Logik gespeichert werden soll. Dies hat keinerlei Auswirkungen auf Ihren Baum. Dies ist architektonisch sehr schön, wenn Sie einige dieser logischen Kapselungen zusammen verwenden, sie in Ihrer Hierarchie nach oben und unten verschieben oder zu einem Geschwister-Widget wechseln möchten usw.

Ich bin auf keinen Fall ein erfahrener Entwickler. Aber dieser neue Stil, Logik wiederzuverwenden und an einer Stelle zu schreiben, ist wirklich praktisch.

Ich benutze Vue.js schon lange nicht mehr, aber sie haben sogar eine eigene API (inspiriert von Hooks) für ihre nächste Version, ein Blick könnte sich lohnen.

Und CMIIW, ich denke, die Regeln von Reaction-Hooks (keine Bedingung verwenden) gelten nicht für die Kompositions-API. Sie müssen also keinen Linter verwenden, um die Regeln durchzusetzen.

Auch hier verstärkt der Motivationsteil das OP stark:

MOTIVATION
Der Code komplexer Komponenten wird immer schwieriger, da die Funktionen im Laufe der Zeit wachsen. Dies geschieht insbesondere dann, wenn Entwickler Code lesen, den sie nicht selbst geschrieben haben.
[Es gab a] Es fehlte ein sauberer und kostenloser Mechanismus zum Extrahieren und Wiederverwenden von Logik zwischen mehreren Komponenten.

Die Stichworte sind dort "sauber" und "kostenlos". Mixins sind kostenlos, aber nicht sauber. Builder sind sauber, aber sie sind weder im Sinne der Lesbarkeit noch im Sinne der Widget-Architektur kostenlos, da sie schwieriger in der Struktur zu bewegen und in Bezug auf die Hierarchie zu argumentieren sind.

Ich denke auch, dass dies in der Lesbarkeitsdiskussion wichtig ist: "_passiert besonders, wenn Entwickler Code lesen, den sie nicht geschrieben haben_". Natürlich kann _Ihr_ verschachtelter Builder leicht zu lesen sein, Sie wissen, was da ist und können ihn bequem überspringen, es liest den Code von jemand anderem, wie Sie es bei jedem größeren Projekt tun, oder Ihren eigenen Code von vor Wochen/Monaten, wenn es ganz ruhig wird ärgerlich / schwer zu analysieren und diese Dinge umzugestalten.

Einige andere besonders relevante Abschnitte.

Warum es nicht ausreicht, nur Komponenten zu haben:

Das Erstellen von ... Komponenten ermöglicht es uns, wiederholbare Teile der Schnittstelle in Verbindung mit ihrer Funktionalität in wiederverwendbare Codestücke zu extrahieren. Dies allein kann unsere Anwendung in Bezug auf Wartbarkeit und Flexibilität ziemlich weit bringen. Unsere gemeinsame Erfahrung hat jedoch gezeigt, dass dies allein möglicherweise nicht ausreicht, insbesondere wenn Ihre Anwendung richtig groß wird – denken Sie an mehrere Hundert Komponenten. Bei solch großen Anwendungen wird die gemeinsame Nutzung und Wiederverwendung von Code besonders wichtig.

Warum es ein Gewinn ist, logische Fragmentierung zu reduzieren und Dinge stärker zu kapseln:

Fragmentierung macht es schwierig, eine komplexe Komponente zu verstehen und zu warten. Die Trennung von Optionen verschleiert die zugrunde liegenden logischen Bedenken. Darüber hinaus müssen wir bei der Arbeit an einem einzigen logischen Problem ständig in Optionsblöcken für den entsprechenden Code "springen". Es wäre viel schöner, wenn wir Code zusammenstellen könnten, der sich auf dasselbe logische Anliegen bezieht.

Ich bin gespannt, ob es andere Vorschläge für kanonische Beispiele gibt, die wir verbessern möchten, außer dem, den ich eingereicht habe.
Wenn es der Ausgangspunkt ist, den die Leute verwenden möchten, ist das großartig. Ich würde jedoch sagen, dass das derzeit größte Problem die Ausführlichkeit ist; In diesem Beispiel muss nicht viel Code wiederverwendet werden. Es ist mir also nicht klar, ob es eine gute Darstellung des Problems ist, wie es im OP beschrieben ist.

Ich habe länger darüber nachgedacht, wie ich die Eigenschaften ausdrücken kann, die ich persönlich in einer Lösung suchen würde, und es hat mir eines der großen Probleme klargemacht, die ich beim aktuellen Hooks-Vorschlag sehe, was ein Grund dafür ist, dass ich es nicht tun würde wollen es in das Flutter-Framework einbinden: Locality and Encapsulation bzw. deren Fehlen. Das Design von Hooks verwendet globale Zustände (zB die Statik, um zu verfolgen, welches Widget gerade erstellt wird). IMHO ist dies ein Designmerkmal, das wir vermeiden sollten. Im Allgemeinen versuchen wir, APIs eigenständig zu machen. Wenn Sie also eine Funktion mit einem Parameter aufrufen, sollten Sie sicher sein, dass sie mit Werten außerhalb dieses Parameters nichts anfangen kann (deshalb übergeben wir den BuildContext herum , anstatt das Äquivalent von useContext ). Ich sage nicht, dass dies eine Eigenschaft ist, die jeder unbedingt haben möchte; und natürlich können die Leute Hooks benutzen, wenn das kein Problem für sie ist. Nur, dass ich es vermeiden möchte, mehr in Flutter zu tun. Jedes Mal, wenn wir einen globalen Zustand (zB bei den Bindungen) hatten, haben wir es am Ende bereut.

Hooks könnten wahrscheinlich Methoden für den Kontext sein (ich glaube, sie waren in der frühen Version), aber ehrlich gesagt sehe ich keinen großen Wert darin, sie so zusammenzuführen, wie sie es derzeit sind. Merge müsste einige Vorteile haben, um Pakete zu trennen, wie erhöhte Leistung oder Hook-spezifische Debugging-Tools. Andernfalls würden sie nur zur Verwirrung beitragen, zum Beispiel hätten Sie 3 offizielle Möglichkeiten, ein Listenable zu hören: AnimatedBuilder, StatefulWidget & useListenable.

Für mich besteht der Weg also darin, die Codegenerierung zu verbessern - ich habe einige Änderungen vorgeschlagen: https://github.com/flutter/flutter/issues/63323

Wenn diese Vorschläge tatsächlich umgesetzt würden, könnten Leute, die magische SwiftUI-ähnliche Lösungen in ihrer App haben wollten, einfach ein Paket erstellen und niemanden sonst stören.

Die Diskussion über die Gültigkeit von Hooks ist zum jetzigen Zeitpunkt nicht am Thema, da wir uns meines Wissens immer noch nicht über das Problem einig sind.

Wie bereits mehrfach in dieser Ausgabe erwähnt, gibt es viele andere Lösungen, von denen mehrere Sprachfeatures sind.
Hooks sind lediglich eine Portierung einer bestehenden Lösung einer anderen Technologie, die in ihrer einfachsten Form relativ billig zu implementieren ist.

Diese Funktion könnte einen völlig anderen Weg einschlagen als Hooks, wie zum Beispiel SwiftUI oder Jetpack Compose; der Vorschlag "named mixin" oder der Syntax-Zucker für den Vorschlag von Builders.

Ich bestehe darauf, dass dieses Problem im Kern nach einer Vereinfachung von Mustern wie StreamBuilder verlangt:

  • StreamBuilder hat aufgrund seiner Verschachtelung eine schlechte Lesbarkeit/Schreibbarkeit
  • Mixins und Funktionen sind keine mögliche Alternative zu StreamBuilder
  • Kopieren und Einfügen der Implementierung von StreamBuilder in allen StatefulWidgets überall ist nicht sinnvoll

In allen Kommentaren wurden bisher Alternativen von StreamBuilder erwähnt, sowohl für unterschiedliche Verhaltensweisen (Einwegobjekt erstellen, HTTP-Anfragen stellen, ...) als auch unterschiedliche Syntaxen vorschlagen.

Ich bin mir nicht sicher, was ich sonst noch sagen soll, daher sehe ich nicht, wie wir weiter vorankommen können.
Was ist das, was Sie in dieser Aussage @Hixie nicht verstehen/nicht einverstanden sind?

@rrousselGit Könnten Sie eine Demo-App erstellen, die dies zeigt? Ich habe versucht, eine Demo-App zu erstellen, die zeigt, was ich als Problem verstanden habe, aber anscheinend habe ich das nicht richtig verstanden. (Ich bin mir nicht sicher, was der Unterschied zwischen "Mustern wie StreamBuilder" und dem ist, was ich in der Demo-App gemacht habe.)

  • StreamBuilder hat aufgrund seiner Verschachtelung eine schlechte Lesbarkeit/Schreibbarkeit

Sie haben bereits gesagt, dass die Ausführlichkeit nicht das Problem ist. Das Verschachteln ist nur ein weiterer Aspekt der Ausführlichkeit. Wenn Verschachtelung wirklich ein Problem ist, sollten wir nach Padding, Expanded, Flexibel, Center, SizeBox und allen anderen Widgets suchen, die ohne wirklichen Grund Verschachtelungen hinzufügen. Die Verschachtelung kann jedoch leicht gelöst werden, indem monolithische Widgets aufgeteilt werden.

Kopieren und Einfügen der Implementierung von StreamBuilder in allen StatefulWidgets überall ist nicht sinnvoll

Sie meinen, die Codezeilen zu kopieren und einzufügen, die die Streams erstellen und freigeben, die die StatefulWidgets erstellen und freigeben müssen? Ja, es ist absolut vernünftig.

Wenn Sie 10er haben oder Gott verbieten Hunderte von _verschiedenen_ benutzerdefinierten StatefulWidgets, die ihre eigenen Streams erstellen/entsorgen müssen - "use" in der Hook-Terminologie -, haben Sie größere Probleme als die ""logische" Wiederverwendung oder Verschachtelung. Ich würde mir Sorgen machen darüber, warum meine App überhaupt so viele verschiedene Streams erstellen muss.

Um fair zu sein, ich denke, es ist in Ordnung, wenn jemand ein bestimmtes Muster in seiner App für unangemessen hält. (Das bedeutet nicht unbedingt, dass das Framework dies nativ unterstützen muss, aber es wäre gut, zumindest einem Paket zu erlauben, es zu lösen.) Wenn jemand weder stream.listen noch StreamBuilder(stream) möchte

Um fair zu sein, ich denke, es ist in Ordnung, wenn jemand ein bestimmtes Muster in seiner App für unangemessen hält.

Ich bin zu 100% auf der gleichen Seite wie du.
Natürlich können die Leute in ihren Apps tun und lassen, was sie wollen. Was ich erreichen möchte ist, dass alle Probleme und Schwierigkeiten, die in diesem Thread beschrieben werden, auf schlechte Programmiergewohnheiten zurückzuführen sind und tatsächlich sehr wenig mit Dart oder Flutter zu tun haben. Das ist nur meine Meinung, aber ich würde sagen, wenn jemand eine App schreibt, die Dutzende von Streams überall erzeugt, sollte vielleicht sein App-Design überprüfen, bevor er das Framework um eine "Verbesserung" bittet.

Zum Beispiel die Hook-Implementierung, die es in das Beispiel-Repository geschafft hat.

  <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),
    );
  }

Ich habe ein schlechtes Gefühl dabei, also habe ich einige der Interna überprüft und einige Debug-Drucke hinzugefügt, um zu überprüfen, was vor sich geht.
In der Ausgabe unten können Sie sehen, dass der Listenable-Hook bei jedem Animations-Tick überprüft, ob er aktualisiert wurde. Wie in, wurde das Widget, das dieses Listenable "verwendet", aktualisiert? Wurde die Laufzeit geändert? Wurde die Instanz ersetzt?
Der auswendig gelernte Haken, ich weiß nicht einmal, was damit zu tun hat. Es ist wahrscheinlich dazu gedacht, ein Objekt zwischenzuspeichern, aber bei jedem einzelnen Build überprüft das Widget, ob sich das Objekt geändert hat? Was? Wieso den? Da es natürlich in einem zustandsbehafteten Widget verwendet wird und einige andere Widgets im Baum den Wert ändern können, müssen wir nach Änderungen fragen. Dies ist das wörtliche Abfrageverhalten, das das genaue Gegenteil von "reaktiver" Programmierung ist.

Was noch schlimmer ist, der "neue" und der "alte" Hook haben beide denselben Instanztyp, beide haben dieselben Werte, und dennoch durchläuft die Funktion die Werte, um zu überprüfen, ob sie sich geändert haben. Auf _jedem einzelnen Animations-Tick_.

Dies ist die Ausgabe, die ich erhalte, bis ins Unendliche.

/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

All diese Arbeit wird bei jedem einzelnen Animations-Tick erledigt. Wenn ich einen weiteren Haken wie "final color = useAnimation" hinzufüge(animationColor);", um auch die Farbe zu animieren, überprüft das Widget jetzt _zwei_mal, ob es aktualisiert wurde.

Ich sitze hier und sehe zu, wie ein Text hin und her animiert wird, ohne dass sich der App-Status oder eines der Widgets oder der Widget-Baum ändert, und trotzdem überprüfen die Hooks ständig, ob der Baum / die Widgets aktualisiert wurden. Es ist schlecht, wenn jedes Widget, das diese speziellen Hooks "benutzt", dieses Polling-Verhalten durchführt.

Die Handhabung der Initialisierungs- / Aktualisierungs- / Entsorgungslogik von Zustandsobjekten innerhalb der Build-Methode ist einfach schlechtes Design. Keine Verbesserung in Bezug auf Wiederverwendbarkeit, Hotreload oder kognitive Belastung rechtfertigt die Auswirkungen auf die Leistung.
Wieder meiner Meinung nach. Da Hooks ein Paket sind, kann jeder sie verwenden, wenn er der Meinung ist, dass die Gewinne den Overhead rechtfertigen.

Ich glaube auch nicht, dass jede Menge Sprachfeatures, Compiler-Magie oder Abstraktion solche unnötigen Prüfungen verhindern können, wenn wir versuchen, alles innerhalb des Build-Prozesses zu abstrahieren. So bleiben uns Alternativen wie das Erweitern des StatefulWidget. Etwas, das bereits getan werden kann und unzählige Male abgelehnt wurde.

@Hixie du hast die Frage nicht beantwortet. Was ist es, was Sie in der oben aufgeführten Aufzählungsliste nicht verstehen / womit Sie nicht einverstanden sind?

Ich kann kein Beispiel geben, ohne zu wissen, was ich demonstrieren soll.

@Rudiksz Sicherlich müsste jede Lösung, die wir tatsächlich in Betracht ziehen würden, profiliert und bewertet werden, um sicherzustellen, dass sie die Dinge nicht verschlimmert. Ein Teil der Art und Weise, wie ich die Demo-App erstellt habe, die ich @TimWhiting eingereicht habe, soll genau die Arten von Mustern abdecken, die leicht durcheinander gebracht werden können. (Und, wie bereits erwähnt, lässt es Raum für Verbesserungen, siehe TODO im Code.)

@rrousselGit Ich wollte nicht wirklich auf das Unkraut

  • Erstens würde ich generell vermeiden, Streams zu verwenden. ValueListenable ist IMHO ein viel besseres Muster für mehr oder weniger die gleichen Anwendungsfälle.
  • Ich glaube nicht, dass StreamBuilder besonders schwer zu lesen oder zu schreiben ist; aber, wie @satvikpendem früher kommentiert hat , ist meine Geschichte mit tief verschachtelten Bäumen in HTML, und ich starre jetzt seit 4 Jahren auf Flutter-Bäume (ich habe zuvor gesagt, dass Flutters Kernkompetenz darin besteht, wie man riesige Bäume effizient beschreitet). daher habe ich wahrscheinlich eine höhere Toleranz als die meisten Menschen, und meine Meinung hier ist nicht wirklich relevant.
  • Ob Mixins und Funktionen eine mögliche Alternative zu StreamBuilder sein könnten, ich denke, Hooks zeigt ziemlich gut, dass Sie definitiv Funktionen verwenden können, um Streams zu hören, und Mixins können eindeutig alles tun, was Klassen hier tun können, also verstehe ich nicht, warum sie das tun würden ist auch keine lösung.
  • Schließlich ist das in Bezug auf Copy-Paste-Implementierungen eine subjektive Angelegenheit. Ich kopiere die Logik nicht persönlich und füge sie in initState/didUpdateWidget/dispose/build ein, ich schreibe sie jedes Mal neu und es scheint größtenteils in Ordnung zu sein. Wenn es "außer Kontrolle" gerät, berücksichtige ich es in einem Widget wie StreamBuilder. Also meine Meinung ist hier wahrscheinlich nicht relevant.

Als allgemeine Regel ist es nicht relevant, ob ich das Problem habe, das Sie sehen oder nicht. Ihre Erfahrung ist unabhängig von meiner Meinung gültig. Ich arbeite gerne daran, Lösungen für Ihre Probleme zu finden, auch wenn ich diese Probleme nicht spüre. Der einzige Effekt, dass ich das Problem nicht selbst habe, ist, dass es für mich schwieriger ist, das Problem gut zu verstehen, wie wir in dieser Diskussion gesehen haben.

Ich möchte, dass Sie das Codierungsmuster demonstrieren, das

Das Problem ist, Sie fragen nach Beispielen für etwas, das für mich im Bereich "offensichtlich" liegt.

Es macht mir nichts aus, einige Beispiele zu nennen, aber ich habe keine Ahnung, was Sie erwarten, da ich nicht verstehe, was Sie nicht verstehen.

Ich habe schon alles gesagt, was ich zu sagen hatte.
Das einzige, was ich tun kann, ohne zu verstehen, was du nicht verstehst, ist, mich zu wiederholen.

Ich kann einige der Schnipsel hier laufen lassen, aber das ist gleichbedeutend damit, mich zu wiederholen.
Wenn das Snippet nicht nützlich wäre, verstehe ich nicht, warum die Möglichkeit, es auszuführen, etwas ändern würde.

Ob Mixins und Funktionen eine mögliche Alternative zu StreamBuilder sein könnten, ich denke, Hooks zeigt ziemlich gut, dass Sie definitiv Funktionen verwenden können, um Streams zu hören, und Mixins können eindeutig alles tun, was Klassen hier tun können, also verstehe ich nicht, warum sie das tun würden ist auch keine lösung.

Hooks sollten nicht als Funktionen betrachtet werden.
Sie sind ein neues Sprachkonstrukt ähnlich wie Iterable/Stream

Funktionen können nicht das tun, was Hooks tun – sie haben keinen Status oder die Fähigkeit, die Neuerstellung von Widgets zu veranlassen.

Das Problem mit Mixins wird im OP demonstriert. TL;DR: Namenskonflikte bei Variablen und es ist unmöglich, dasselbe Mixin mehrmals zu verwenden.

@rrousselGit Nun, da es Ihnen nichts ausmacht, einige Beispiele zu machen, und da die Beispiele, nach denen ich

Ich habe nicht gesagt, dass die Beispiele offensichtlich sind, aber das Problem ist.
Was ich meinte ist, ich kann keine neuen Beispiele erstellen. Alles, was ich zu sagen habe, steht bereits in diesem Thread:

Ich kann diesen Beispielen nichts hinzufügen.

Aber FWIW Ich arbeite an einer Open-Source-Wetter-App mit Riverpod. Ich werde es hier verlinken, wenn es fertig ist.


Ich habe eine Umfrage auf Twitter durchgeführt, in der einige Fragen zu Buildern im Zusammenhang mit den hier diskutierten Problemen gestellt wurden:

https://twitter.com/remi_rousselet/status/1295453683640078336

Die Umfrage steht noch aus, aber hier die aktuellen Zahlen:

Screenshot 2020-08-18 at 07 01 44

Die Tatsache, dass 86% von 200 Menschen sich eine Möglichkeit wünschen, Builder zu schreiben, die keine Verschachtelung beinhalten, spricht für sich.

Um es klar zu sagen, ich habe nie vorgeschlagen, dieses Problem nicht anzugehen. Wenn ich dachte, wir sollten es nicht ansprechen, wäre das Thema abgeschlossen.

Ich denke, ich werde versuchen, ein Beispiel zu erstellen, das die Snippets verwendet, mit denen Sie verknüpft sind.

Ich kann Ihnen helfen, Beispiele basierend auf den verlinkten Snippets zu erstellen, aber ich muss wissen, warum diese Snippets nicht gut genug waren
Ansonsten kann ich nur diese Schnipsel kompilieren, aber ich bezweifle, dass Sie das wollen.

Hier zum Beispiel ein Überblick über die zahlreichen ValueListenableBuilder+TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

Hier zum Beispiel ein Überblick über die zahlreichen ValueListenableBuilder+TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

FWIW, dieses eine Beispiel lässt sich leichter in mobx implementieren.
Es ist tatsächlich kürzer als Ihre Hooks-Implementierung.

Die Observablen von Mobx sind ValueNotifiers auf Steroiden und das Observer-Widget ist die Weiterentwicklung von Flutters ValueListenableBuilder - es kann mehr als einen ValueNotifier hören.
Als Drop-In-Ersatz für die Kombination ValueNotifier/ValueListenableBuilder bedeutet, dass Sie immer noch idiomatischen Flutter-Code schreiben, was eigentlich ein wichtiger Faktor ist.

Da es immer noch Flutters eingebauten Tween-Builder verwendet, müssen Sie keine neuen Widgets/Hooks lernen/implementieren (mit anderen Worten, es braucht keine neuen Funktionen) und es hat keine der Leistungseinbußen von Hooks.

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 ist so einfach wie...

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;
}

Hier ist eine weitere Implementierung, die nicht einmal Animationsbuilder benötigt. Die Build-Methode des Widgets ist so rein wie nur möglich, fast wie eine semantische HTML-Datei ... wie eine Vorlage.

https://gist.github.com/Rudiksz/cede1a5fe88e992b158ee3bf15858bd9

@Rudiksz Das Verhalten des Felds "total" ist in Ihrem Snippet
Ebenso bin ich mir nicht sicher, was dieses Beispiel der Variante ValueListenableBuilder hinzufügt.

Was Ihren letzten Kern betrifft, TickerProvider ist kaputt, da es TickerMode nicht unterstützt – noch werden Listener entfernt oder Controller entsorgt.

Und Mobx ist wahrscheinlich kein Thema. Wir diskutieren nicht, wie man Ambient State / ValueListenable vs Stores vs Streams implementiert, sondern wie man mit Local State / Nested Builders umgeht – was Mobx in keiner Weise löst

––––

Denken Sie auch daran, dass im Hooks-Beispiel useAnimatedInt in ein Paket extrahiert werden könnte/sollte und dass es kein Duplikat der Dauer/Kurve zwischen der einzelnen Textanimation und der Gesamtsumme gibt.

Was die Performances angeht, so bauen wir mit Hooks nur ein einzelnes Element um, während mit Builders 2-4 Builder neu aufgebaut werden.
Hooks könnte also durchaus schneller sein.

Das Verhalten des Felds "total" ist in Ihrem Snippet fehlerhaft. Es stimmt nicht mit dem Beispiel überein, in dem beide Zähler zusammen animieren können, die Animation jedoch zu unterschiedlichen Zeiten und mit unterschiedlichen Kurven beenden.

Sie haben offensichtlich nicht einmal versucht, das Beispiel auszuführen. Es verhält sich genau wie Ihr Code.

Was Ihren letzten Kern betrifft, ist TickerProvider defekt, da er den TickerMode nicht unterstützt.

Ich weiß nicht, was du damit meinst. Ich habe _Ihr_ Beispiel umgestaltet, das den TickerMode nicht verwendet. Sie ändern wieder die Anforderungen.

Was die Performances angeht, so bauen wir mit Hooks nur ein einzelnes Element um, während mit Builders 2-4 Builder neu aufgebaut werden. Hooks könnte also durchaus schneller sein.

Nein einfach nein. Ihre Hook-Widgets fragen ständig nach Änderungen bei jedem einzelnen Build. Builder, die auf valuelistenables basieren, sind "reaktiv".

Ebenso bin ich mir nicht sicher, was dieses Beispiel der ValueListenableBuilder-Variante hinzufügt .

Und Mobx ist wahrscheinlich kein Thema. Wir diskutieren nicht, wie man Ambient State / ValueListenable vs Stores vs Streams implementiert, sondern wie man mit Local State / Nested Builders umgeht – was Mobx in keiner Weise löst

Sie machen wohl Witze. Ich habe Ihr Beispiel genommen und mich mit verschachtelten ValueListenableBuilders * und Tween-Buildern befasst?! Ein Punkt, den Sie speziell angesprochen haben.
Aber dieser Satz hier beschreibt Ihre ganze Einstellung zu dieser Diskussion. Wenn es keine Hooks sind, ist es "off-topic", aber Sie sagen, dass es Ihnen egal ist, ob es Hooks sind, die als Lösung verwendet werden.
Gib mir eine Pause.

Sie haben offensichtlich nicht einmal versucht, das Beispiel auszuführen. Es verhält sich genau wie Ihr Code.

Es tut es nicht. Der erste Zähler wird über 5 Sekunden animiert, der zweite über 2 Sekunden – und beide verwenden auch eine andere Kurve.

Mit beiden Snippets, die ich gegeben habe, konnte man beide Zähler gleichzeitig inkrementieren, und während jedes einzelnen Frames der Animation wäre die "Gesamtzahl" korrekt. Auch wenn der zweite Zähler aufhört zu animieren, während der erste Zähler noch animiert

Auf der anderen Seite berücksichtigt Ihre Implementierung diesen Fall nicht, da sie die 2 TweenAnimationBuilder zu einem zusammengeführt hat.
Um es zu beheben, müssten wir schreiben:

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

Die beiden TweenAnimationBuilders sind notwendig, um zu respektieren, dass beide Zähler einzeln animieren können. Und die beiden Observer sind notwendig, weil die ersten Observer counters.secondCounter nicht beobachten können


Nein einfach nein. Ihre Hook-Widgets fragen ständig nach Änderungen bei jedem einzelnen Build. Builder, die auf valuelistenables basieren, sind "reaktiv".

Sie ignorieren, was Element tut, was zufällig dasselbe ist wie das, was Hooks tun: RuntimeType und Schlüssel vergleichen und entscheiden, ob ein neues Element erstellt oder das vorhandene aktualisiert wird

Ich habe Ihr Beispiel genommen und mich mit verschachtelten ValueListenableBuilders *und Tween-Buildern befasst

Angenommen, das Problem mit der Gesamtanzahl ist behoben, welche Verschachtelung wurde entfernt?

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

ist nicht anders als:

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

in Bezug auf die Verschachtelung.

Wenn Sie sich auf Ihr Wesentliches beziehen, dann bricht dieser Ansatz, wie ich bereits erwähnt habe, TickerProvider / TickerMode . Das vsync muss mit SingleTickerProviderClientStateMixin abgerufen werden, sonst unterstützt es die Muting-Logik nicht, was zu Leistungsproblemen führen kann.
Ich habe dies in einem Artikel von mir erklärt: https://dash-overflow.net/articles/why_vsync/

Und bei diesem Ansatz müssen wir die Tween-Logik an jedem Ort neu implementieren, der ursprünglich einen TweenAnimationBuilder haben möchte. Das führt zu einem signifikanten Duplikat, vor allem wenn man bedenkt, dass die Logik nicht so trivial ist

Mit beiden Snippets, die ich gegeben habe, konnte man beide Zähler gleichzeitig inkrementieren, und während jedes einzelnen Frames der Animation wäre die "Gesamtzahl" korrekt. Auch wenn der zweite Zähler aufhört zu animieren, während der erste Zähler noch animiert

Auf der anderen Seite berücksichtigt Ihre Implementierung diesen Fall nicht, da sie die 2 TweenAnimationBuilder zu einem zusammengeführt hat.

Ja, das ist ein Kompromiss, den ich eingehen wollte. Ich könnte mir leicht einen Fall vorstellen, in dem die Animation nur ein visuelles Feedback für die stattfindenden Änderungen wäre und die Genauigkeit nicht wichtig ist. Es hängt alles von den Anforderungen ab.

Ich vermutete jedoch, dass Sie Einwände erheben würden, daher die zweite Version, die genau dieses Problem löst, während der Code noch sauberer wird.

Wenn Sie sich auf Ihr Wesentliches beziehen, unterbricht dieser Ansatz, wie ich bereits erwähnt habe, TickerProvider/TickerMode. Das vsync muss mit SingleTickerProviderClientStateMixin abgerufen werden oder es unterstützt die Muting-Logik anderweitig nicht, was zu Leistungsproblemen führen kann.

Sie erstellen also den Tickerprovider im Widget und übergeben ihn an die Counter. Die Animationscontroller habe ich auch nicht entsorgt. Diese Details sind so trivial zu implementieren, dass ich nicht das Gefühl hatte, dass sie dem Beispiel etwas hinzufügen würden. Aber hier hacken wir auf ihnen herum.

Ich habe die Counter()-Klasse implementiert, um das zu tun, was Ihr Beispiel tut, und nicht mehr.

Und bei diesem Ansatz müssen wir die Tween-Logik an jedem Ort neu implementieren, der ursprünglich einen TweenAnimationBuilder haben möchte. Das führt zu einem signifikanten Duplikat, vor allem wenn man bedenkt, dass die Logik nicht so trivial ist

Was? warum? Erklären Sie bitte, warum ich nicht mehr als eine Instanz der Counter-Klasse erstellen und in verschiedenen Widgets verwenden konnte?

@Rudiksz Ich bin mir nicht sicher, ob Ihre Lösungen die dargelegten Probleme tatsächlich lösen. Du sagst

Ich habe die Counter()-Klasse implementiert, um das zu tun, was Ihr Beispiel tut, und nicht mehr.

und doch

Ja, das ist ein Kompromiss, den ich eingehen wollte. Ich könnte mir leicht einen Fall vorstellen, in dem die Animation nur ein visuelles Feedback für die stattfindenden Änderungen wäre und die Genauigkeit nicht wichtig ist. Es hängt alles von den Anforderungen ab.

Sie erstellen also den Tickerprovider im Widget und übergeben ihn an die Counter. Die Animationscontroller habe ich auch nicht entsorgt. Diese Details sind so trivial zu implementieren, dass ich nicht das Gefühl hatte, dass sie dem Beispiel etwas hinzufügen würden. Aber hier hacken wir auf ihnen herum.

Sie haben Code bereitgestellt, der nur scheinbar der Hook-Version von @rrousselGit entspricht, aber nicht wirklich äquivalent ist, da Sie Teile Erstellung des Repositorys von

Sie haben offensichtlich nicht einmal versucht, das Beispiel auszuführen. Es verhält sich genau wie Ihr Code.

Nein einfach nein.

Sie machen wohl Witze. Ich habe Ihr Beispiel genommen und mich mit verschachtelten ValueListenableBuilders *und Tween-Buildern befasst

Bitte sehen Sie von solchen Vorwürfen ab, sie dienen nur dazu, diesen Thread bösartig zu machen, ohne die zugrunde liegenden Probleme zu lösen. Sie können Ihre Punkte vorbringen, ohne anschuldigend, abwertend oder wütend auf die andere Partei zu sein, was die Wirkung der Kommentare ist, die ich in diesem Thread sehe. Ich sehe kein solches Verhalten von anderen Ihnen gegenüber. Ich bin mir auch nicht sicher, was die Emoji-Reaktion auf Ihren eigenen Beitrag bewirkt.

@rrousselGit

Ich kann Ihnen helfen, Beispiele basierend auf den verlinkten Snippets zu erstellen, aber ich muss wissen, warum diese Snippets nicht gut genug waren
Ansonsten kann ich nur diese Schnipsel kompilieren, aber ich bezweifle, dass Sie das wollen.

Der Grund, warum ich nach einer aufwändigeren App als nur nach einem Snippet frage, ist, dass Sie, als ich ein Beispiel gepostet habe, das eines Ihrer Snippets behandelt, gesagt haben, dass das nicht gut genug ist, weil es nicht auch einen anderen Fall behandelt, der es war nicht in Ihrem Snippet (zB hat didUpdateWidget nicht verarbeitet, oder es wurde nicht darauf geachtet, dass zwei dieser Snippets nebeneinander vorhanden sind oder andere völlig vernünftige Dinge, die Sie gerne behandeln möchten). Ich hoffe, mit einer ausgefeilteren App können wir es so ausführlich machen, dass es, sobald wir eine Lösung haben, keinen "Gotcha"-Moment gibt, in dem ein neues Problem auftaucht, das ebenfalls behandelt werden muss. Natürlich ist es immer noch möglich, dass wir alle etwas verpassen, das behandelt werden muss, aber die Idee ist, die Wahrscheinlichkeit zu minimieren.

In Bezug auf die letzten weniger als ganz einladenden Beiträge, bitte Leute, konzentrieren wir uns einfach darauf, Beispiele für das Repo von @TimWhiting zu

Dort können Sie Ihre Lösung einreichen, wenn Sie meinen, alle Anforderungen erfüllt zu haben.

Die Anforderungen wurden nachträglich geändert. Ich habe zwei Lösungen bereitgestellt. Einer, der einen Kompromiss macht (ein sehr vernünftiger) und einer, der genau das Verhalten des angegebenen Beispiels implementiert.

Sie haben Code bereitgestellt, der nur scheinbar der Hook-Version von @rrousselGit entspricht, aber nicht wirklich äquivalent ist.

Ich habe die "Hooks-Lösung" nicht implementiert, sondern das ValueListenableBuilder-Beispiel implementiert, das sich speziell auf das "Verschachtelungsproblem" konzentriert. Es macht nicht alles, was Hooks tun, ich habe einfach gezeigt, wie ein Punkt in der Aufzählungsliste der Beschwerden mit einer alternativen Lösung vereinfacht werden kann.

Wenn Sie externe Pakete in die Diskussion einbringen dürfen, dann bin ich es auch.

Wiederverwendbarkeit: Schauen Sie sich das Beispiel im Repo unten an
https://github.com/Rudiksz/cbl_example

Notiz:

  • Es ist als Demonstration dafür gedacht, wie Sie "Logik" außerhalb von Widgets kapseln und Widgets verwenden können, die schlank sind und fast wie HTML aussehen
  • es deckt nicht alles ab, was Haken abdecken. Das ist nicht der Punkt. Ich dachte, wir diskutieren Alternativen zum serienmäßigen Flutter-Framework und nicht das Hooks-Paket.
  • Es deckt nicht _jeden_ Anwendungsfall ab, den jedes Marketingteam finden kann
  • Das Counter-Objekt selbst ist jedoch ziemlich flexibel, es kann eigenständig verwendet werden (siehe den AppBar-Titel), als Teil eines komplexen Widgets, das Summen verschiedener Zähler berechnen, reaktiv gemacht werden oder auf Benutzereingaben reagieren muss.
  • Es liegt an den konsumierenden Widgets, die Menge, den Anfangswert, die Dauer und den Animationstyp der Zähler anzupassen, die sie verwenden möchten.
  • die Details der Art und Weise, wie Animationen behandelt werden, können Nachteile haben. Wenn das Flutter-Team sagt, dass die Verwendung von Animationscontrollern und Tweens außerhalb der Widgets das Framework irgendwie durchbricht, werde ich es mir noch einmal überlegen. Es gibt definitiv Dinge zu verbessern. Das Ersetzen des benutzerdefinierten Tickerproviders, den ich verwende, durch einen, der vom Mixin des konsumierenden Widgets erstellt wurde, ist trivial. Ich habe es nicht getan.
  • Dies ist nur eine weitere alternative Lösung zum Muster "Builder". Wenn Sie sagen, dass Sie Builder benötigen oder verwenden möchten, trifft dies nicht zu.
  • Tatsache ist jedoch, dass es Möglichkeiten gibt, Code ohne zusätzliche Funktionen zu vereinfachen. Wenn es Ihnen nicht gefällt, kaufen Sie es nicht. Ich befürworte nichts davon, um es in das Framework zu schaffen.

Bearbeiten: Dieses Beispiel hat einen Fehler, wenn Sie ein "Inkrement" initiieren, während die Änderung animiert ist, wird der Zähler zurückgesetzt und vom aktuellen Wert inkrementiert. Ich habe es nicht absichtlich repariert, weil ich die genauen Anforderungen nicht kenne, die Sie für diese "Zähler" haben könnten. Auch hier ist es trivial, die Inkrement-/Dekrement-Methoden zu ändern, um dieses Problem zu beheben.

Das ist alles.

@Hixie Soll ich Ihren Kommentar so interpretieren, dass mein Beispiel (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/hooks/animated_counter.dart) nicht gut genug ist?

Könnten wir auch einen Zoom/Google Meet-Call haben?

Ich bin mir auch nicht sicher, was die Emoji-Reaktion auf Ihren eigenen Beitrag bewirkt.

Nichts. Es ist für alles völlig irrelevant. Warum hast du es angesprochen?

@rrousselGit Nur du kannst wissen, ob es gut genug ist. Wenn wir einen Weg finden, dieses Beispiel so umzugestalten, dass es sauber und kurz ist und keinen doppelten Code enthält, werden Sie zufrieden sein? Oder gibt es Dinge, die wir Ihrer Meinung nach unterstützen sollten, die in diesem Beispiel nicht behandelt werden und die wir behandeln müssen, um diesen Fehler zu beheben?

Nur du kannst wissen, ob es gut genug ist

Das kann ich nicht beurteilen. Zunächst glaube ich nicht, dass wir das Problem mit einer endlichen Menge von Anwendungen erfassen können.

Es macht mir nichts aus, so zu arbeiten, wie Sie es wollen, aber ich brauche Anleitung, da ich nicht verstehe, wie wir damit vorankommen.

Ich denke, wir haben eine Menge Code-Schnipsel bereitgestellt, die das Problem aus unserer Sicht zeigen. Ich glaube wirklich nicht, dass durch weitere Codebeispiele klarer werden wird, wenn die gezeigten es nicht tun.

Wenn Sie beispielsweise mehrere verschachtelte Builder sehen, die schrecklich zu lesen sind, oder 50 Zeilen reiner Boilerplate, die viele Möglichkeiten für Fehler bieten, das Problem nicht stark genug demonstrieren, können Sie nirgendwo hingehen.

Es ist sehr seltsam, Mixins und Funktionen anzubieten, die hier eine Lösung sind, wenn die gesamte Anfrage gekapselt ist , also wiederverwendbar ist. Funktionen können den Status nicht beibehalten. Mixins sind nicht gekapselt. Dieser Vorschlag verfehlt den gesamten Sinn aller bereitgestellten Beispiele und Begründungen und zeigt immer noch ein tiefes Missverständnis der Frage.

Für mich denke ich, dass wir 2 Punkte in die Erde geschlagen haben, und ich denke, ich kann beides nicht ernsthaft bestreiten.

  1. Verschachtelte Builder sind von Natur aus schwer zu lesen
  2. Abgesehen von verschachtelten Buildern gibt es _keine Möglichkeit_, einen Zustand mit Widget-Lebenszyklus-Hooks zu kapseln und freizugeben.

Wie schon oft und sogar in Remis Umfrage gesagt, einfach ausgedrückt: Wir wollen die Fähigkeiten von Builder, ohne die Ausführlichkeit und verschachtelten Schließungen von Builder. Fasst das nicht ganz zusammen? Ich bin völlig verwirrt, warum ein weiteres Codebeispiel erforderlich ist, um hier fortzufahren.

Remis Umfrage zeigt uns, dass ~80% der Flutter-Entwickler eine Möglichkeit bevorzugen würden, verschachtelte Builder in ihrem Code nach Möglichkeit zu vermeiden. Das spricht imo wirklich für sich. Sie müssen es uns in diesem Thread nicht abnehmen, wenn die Stimmung in der Community so klar ist.

Aus meiner Sicht sind die Probleme klar, und sie werden noch deutlicher, wenn man sich konkurrierende Frameworks ansieht, die Absätze der Beschreibung der Begründung widmen. Vue, React, Flutter, sie sind alle Cousins, sie alle stammen von React ab und sie alle stehen vor diesem Problem mit dem Wiederverwendungszustand, der in den Widget-Lebenszyklus eingebunden werden muss. Sie alle beschreiben ausführlich, warum sie so etwas umgesetzt haben. Es ist alles in Ordnung. Es ist alles relevant.

@rrousselGit könnten Sie ein Beispiel für viele mehrere Hooks geben? Zum Beispiel erstelle ich eine Animation mit möglicherweise Dutzenden von AnimationControllern. Mit normalem Flutter kann ich Folgendes tun:

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();
}

Aber mit Hooks kann ich useAnimationController in einer Schleife aufrufen. Ich nehme an, dies ist ein triviales Beispiel, aber ich konnte nirgendwo wirklich die Lösung für diese Art von Anwendungsfall finden.

@satvikpendem

einige Beispiele aus meinen Apps, die sich in Produktion befinden (einige der Hooks wie das Senden einer Anfrage mit Paginierung können zu einem einzigen Hook zusammengeführt / umgestaltet werden, aber das ist hier irrelevant):

einfacher Datenabruf mit Paginierung:

    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 () {};
    }, []);

Formularlogik (Anmeldeformular mit Telefonnummernüberprüfung und Zeitgeber für erneutes Senden):

    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);
        });
      }
    };

für Animationen denke ich, dass @rrousselGit bereits genug Beispiele

Ich möchte nicht darüber sprechen, wie die zusammensetzbare Natur von Hooks das Refactoring von obigem Code viel einfacher, wiederverwendbar und sauberer machen kann, aber wenn Sie möchten, kann ich auch umgestaltete Versionen posten.

Wie schon oft und sogar in Remis Umfrage gesagt, einfach ausgedrückt: Wir wollen die Fähigkeiten von Builder, ohne die Ausführlichkeit und verschachtelten Schließungen von Builder. Fasst das nicht ganz zusammen? Ich bin völlig verwirrt, warum ein weiteres Codebeispiel erforderlich ist, um hier fortzufahren.

Ich habe buchstäblich Beispiele dafür bereitgestellt, wie Sie die Ausführlichkeit reduzieren und die Verschachtelung von Buildern vermeiden können, indem ich ein von Remi bereitgestelltes Beispiel verwendet habe.
Ich nahm seinen Code, steckte ihn in meine App, ließ ihn laufen und schrieb ihn neu. Das Endergebnis in Bezug auf die Funktionalität war fast identisch - so viel ich aus dem Ausführen des Codes entnehmen konnte, da der Code keine Anforderungen enthielt. Natürlich können wir die Randfälle und potenzielle Probleme diskutieren, aber stattdessen wurde es als Off-Topic bezeichnet.

Für einfache Anwendungsfälle verwende ich Builder, für komplexe Anwendungsfälle verwende ich keine Builder. Das Argument hier ist, dass es ohne die Verwendung von Buildern keine einfache Möglichkeit gibt, prägnanten, wiederverwendbaren Code zu schreiben. Implizit bedeutet dies auch, dass Builder ein Muss sind und die einzige Möglichkeit, Flutter-Apps zu entwickeln. Das ist nachweislich falsch.

Ich habe gerade einen funktionierenden Proof-of-Concept-Code gezeigt, der dies demonstriert. Es wurden keine Builder oder Hooks verwendet und es deckt nicht 100% der "unendlichen Reihe von Problemen" ab, die dieses spezielle Github-Problem lösen zu wollen scheint. Es wurde off-topic genannt.
Nebenbei bemerkt, es ist auch sehr effizient, sogar ohne Benchmark würde ich vermuten, dass es sogar die Builder-Widgets schlägt. Ich ändere gerne meine Meinung, wenn sich herausstellt, dass es falsch ist, und wenn ich Mobx jemals als Leistungsengpass empfinde, werde ich Mobx fallen lassen und sofort zu Vanilla-Buildern wechseln.

Hixie arbeitet für Google, er muss geduldig und höflich mit dir sein und kann dich nicht wegen deines mangelnden Engagements rügen. Das Beste, was er tun kann, ist, auf weitere Beispiele zu drängen.

Ich habe niemanden beschimpft oder persönliche Angriffe eingebracht. Ich habe immer nur auf die hier vorgetragenen Argumente reagiert, meine Meinung geteilt
(von denen ich weiß, dass sie unbeliebt und in der Minderheit sind) und sogar versucht haben, tatsächliche Gegenbeispiele mit Code zu präsentieren. Ich könnte mehr tun, ich bin bereit zu diskutieren, wo meine Beispiele zu kurz kommen und wie wir sie verbessern können, aber ja, als off-topic bezeichnet zu werden, ist irgendwie abstoßend.

Ich habe nichts zu verlieren, außer vielleicht gesperrt zu werden, also ist es mir egal, dich herauszurufen.
Es ist offensichtlich, dass Sie beide festgefahren sind, dass Hooks die einzige Lösung sind ("weil React es tut") für alle Probleme, die Sie erleben, und dass, es sei denn, eine Alternative erfüllt 100% der "unendlichen Menge von Problemen", die Sie sich vorstellen, Sie werden nicht einmal daran denken, sich zu engagieren.

Das ist nicht vernünftig und zeigt mangelndes Verlangen, sich wirklich zu engagieren.


Natürlich ist alles oben "nur meine Meinung".

Ich sehe die Nützlichkeit von Hooks in diesem Beispiel, aber ich glaube, ich verstehe nicht, wie es in meinem Fall funktionieren würde, in dem es so aussieht, als würden Sie viele Objekte gleichzeitig initialisieren wollen, in diesem Fall AnimationController s aber in Wirklichkeit kann es alles sein. Wie gehen Haken mit diesem Fall um?

Grundsätzlich gibt es eine Hakenmethode, dies zu drehen?

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

Hinein

var xs = []
for (int i = 0; i < 3; i++)
     xs[i] = useState(i);

Ohne gegen Hook-Regeln zu verstoßen? Weil ich das Äquivalent in normalem Flutter aufgelistet habe. Ich bin nicht allzu erfahren mit Hooks in Flutter, also behaltet es mit mir.

Ich möchte bei Bedarf einfach ein Array von Hook-Objekten (z. B. AnimationControllers) mit allen seinen initState erstellen und bereits instanziiert entsorgen, ich bin mir nur nicht sicher, wie es in Hooks funktioniert.

@satvikpendem denken Sie an Hooks wie Eigenschaften einer Klasse. Definieren Sie sie in einer Schleife oder benennen Sie sie manuell einzeln?

in Ihrem Beispiel so definieren

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

ist für diesen Anwendungsfall nützlich:

var isLoading = useState(1);
var selectedTab = useState(2);
var username = useState(3); // text field

sehen Sie, wie jedes useState mit einem benannten Teil Ihrer Zustandslogik zusammenhängt? (wie der useState von isLoading ist mit verbunden, wenn sich die App im Ladezustand befindet)

In Ihrem zweiten Snippet rufen Sie useState in einer Schleife auf. Sie denken an useState als Werthalter, der nicht Teil Ihrer Zustandslogik ist. Wird diese Liste benötigt, um eine Reihe von Elementen in einem ListView anzuzeigen? Wenn ja, sollten Sie sich jedes Element in der Liste als einen Zustand vorstellen, nicht einzeln.

final listData = useState([]);

Dies ist nur für useState und ich sehe einige Anwendungsfälle (die meiner Meinung nach sehr selten sind) für das Aufrufen einiger Hooks in einer Bedingung oder in einer Schleife. für diese Art von Hooks sollte es einen anderen Hook für die Handhabung von Datenlisten geben, anstatt einen. zum Beispiel:

var single = useTest("data");
var list = useTests(["data1", "data2"]);
// which is equivalent to
var single1 = useTest("data1");
var single2 = useTest("data2");

Ich verstehe, mit Hooks sieht es so aus, als müssten wir einen separaten Hook erstellen, um Fälle mit einem Array von Elementen zu behandeln, z. B. mehrere AnimationControllers.

Folgendes hatte ich anfangs, was nicht zu funktionieren scheint:

  final animationControllers = useState<List<AnimationController>>([]);

  animationControllers.value = List<AnimationController>.generate(
    50,
    (_) => useAnimationController(),
  );

aber ich nehme an, wenn ich meinen eigenen Hook schreibe, um mehrere Elemente zu verarbeiten, sollte dies funktionieren, oder?

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());
  }
}

Bedeutet dies, dass wir eine einzelne Version eines Hooks einfach nicht für eine Version mit mehreren Elementen verwenden können und stattdessen die Logik neu schreiben müssen? Oder gibt es einen besseren Weg, dies zu tun?

Wenn jemand auch ein Non-Hooks-Beispiel nennen möchte, das ich auch gerne wissen würde, habe ich mich über dieses Stück des Wiederverwendbarkeitspuzzles gewundert. Vielleicht gibt es eine Möglichkeit, dieses Verhalten in einer Klasse zu kapseln, die über ein eigenes AnimationController-Feld verfügt, aber wenn das in einer Schleife erstellt wird, würde auch der Hook die Regeln brechen. Vielleicht könnten wir uns überlegen, wie Vue es macht, das von Bedingungen und Schleifen für seine Hooks-Implementierung nicht beeinflusst wird.

@satvikpendem

Ich glaube nicht, dass meine Aussage für AnimationController oder useAnimationController gültig ist

denn Sie können zwar mehr als ein AnimationController aber Sie speichern sie nicht unbedingt in einem Array, um sie in der Klassenmethode zu verwenden. zum Beispiel:

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();
}, []);

(Sie erstellen keine Liste und verweisen darauf wie animation[0] )

Ehrlich gesagt, nach meiner Erfahrung in Reagieren und Flattern mit Hooks brauchte ich selten irgendwelche Hooks in einer Schleife. schon damals war die lösung unkompliziert und einfach zu implementieren. Jetzt denke ich darüber nach, es könnte definitiv besser gelöst werden, indem für jeden von ihnen eine Komponente (Widget) erstellt wird, was IMO die "sauberere" Lösung ist.

um Ihre Frage zu beantworten, ob es eine einfachere Möglichkeit gibt, mehrere AnimationController zu handhaben, ja, es gibt:

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]);

  • Sie können auch useState wenn AnimationController s dynamisch sind.

(es wird auch neu synchronisiert, wenn der Ticker geändert wird)

@rrousselGit könnten Sie ein Beispiel für viele mehrere Hooks geben? Zum Beispiel erstelle ich eine Animation mit möglicherweise Dutzenden von AnimationControllern. Mit normalem Flutter kann ich Folgendes tun:

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();
}

Aber mit Hooks kann ich useAnimationController in einer Schleife aufrufen. Ich nehme an, dies ist ein triviales Beispiel, aber ich konnte nirgendwo wirklich die Lösung für diese Art von Anwendungsfall finden.

Haken machen das anders.

Wir erstellen keine Controller-Liste mehr, sondern verschieben die Controller-Logik nach unten auf das Element:

Widget build(context) {
  return ListView(
    children: [
      for (var i = 0; i < 50; i++)
        HookBuilder(
          builder: (context) {
            final controller = useAnimationController();
          },
        ),
    ],
  );
}

Wir haben immer noch unsere 50 Animationscontroller erstellt, aber sie gehören einem anderen Widget.

Vielleicht könnten Sie ein Beispiel dafür nennen, warum Sie das brauchen, und wir könnten versuchen, es in Hooks umzuwandeln und es zu Tims Repo hinzuzufügen?

Hixie arbeitet für Google, er muss geduldig und höflich mit dir sein und kann dich nicht wegen deines mangelnden Engagements rügen. Das Beste, was er tun kann, ist, auf weitere Beispiele zu drängen.

@Hixie , wenn Sie sich so fühlen, sagen Sie es bitte (entweder hier oder kontaktieren Sie mich privat).

Ich habe buchstäblich Beispiele dafür bereitgestellt, wie Sie die Ausführlichkeit reduzieren und die Verschachtelung von Buildern vermeiden können, indem ich ein von Remi bereitgestelltes Beispiel verwendet habe.

Danke, aber mir ist unklar, wie Sie ein gemeinsames Muster aus diesem Code extrahieren würden, um diese Logik auf verschiedene Anwendungsfälle anzuwenden.

Im OP habe ich erwähnt, dass wir derzeit 3 ​​Möglichkeiten haben:

  • Verwenden Sie Builder und haben Sie verschachtelten Code
  • faktorisieren Sie den Code überhaupt nicht, der nicht auf komplexere Zustandslogik skaliert (ich würde argumentieren, dass StreamBuilder und sein AsyncSnapshot eine komplexe Zustandslogik sind).
  • Versuchen Sie, eine Architektur mit mixins/oop/... zu erstellen, erhalten Sie jedoch eine Lösung, die zu spezifisch für das Problem ist, dass jeder Anwendungsfall, der ein _winziges_ bisschen anders ist, ein Umschreiben erfordert.

Mir scheint, dass Sie die dritte Wahl verwendet haben (die in die gleiche Kategorie fällt wie die frühen Iterationen der Property oder addDispose Vorschläge).

Ich habe zuvor ein Bewertungsraster erstellt, um das Muster zu beurteilen:

Könnten Sie Ihre Variante darauf ausführen? Besonders der zweite Kommentar zur Implementierung aller Features von StreamBuilder ohne Code-Duplikat bei mehrfacher Verwendung.

Mein Plan zu diesem Zeitpunkt zu diesem Fehler ist:

  1. Nehmen Sie die Beispiele von https://github.com/flutter/flutter/issues/51752#issuecomment -675285066 und erstellen Sie eine App mit reinem Flutter, die diese verschiedenen Anwendungsfälle zusammen zeigt.
  2. Versuchen Sie, eine Lösung zu entwerfen, die die Wiederverwendung von Code für diese Beispiele ermöglicht, die die verschiedenen hier besprochenen Schlüsselanforderungen erfüllt und in unsere Designprinzipien passt.

Wenn jemand bei beiden helfen möchte, freue ich mich auf jeden Fall über Hilfe. Es ist unwahrscheinlich, dass ich so bald dazu komme, da ich zuerst am NNBD-Übergang arbeite.

@rrousselGit Sicher, ich Box es), und sie sollten sich unabhängig voneinander bewegen können (also muss es mindestens sein .) ein AnimationController für jedes Box ). Hier ist eine Version, die ich mit nur einem AnimationController erstellt habe, der von den mehreren Widgets geteilt wird, aber in Zukunft kann ich jedes Widget unabhängig animieren, zum Beispiel um komplizierte Transformationen durchzuführen, wie zum Beispiel einen CupertinoPicker mit seinem benutzerdefinierten Scrollrad-Effekt zu implementieren .

Es gibt drei Boxen in einem Stack, die sich nach oben und unten bewegen, wenn Sie auf einen FloatingActionButton klicken.

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,
          ),
        ),
      ),
      // ],
    );
  }
}

In diesem Fall bewegt sich jede Box im Gleichklang, aber man kann sich auch ein komplexeres Szenario vorstellen, wie zum Beispiel das Erstellen einer Visualisierung für eine Sortierfunktion oder das Verschieben von Elementen in einer animierten Liste, wobei das übergeordnete Widget die Daten darüber kennt, wo jedes Box sollte sein und es sollte in der Lage sein, jeden nach Belieben zu animieren.

Das Problem scheint zu sein, dass die AnimationControllers und die Box es, die sie verwenden, um ihre Bewegung zu steuern, nicht in derselben Klasse sind einen Builder, oder jeder Box seinen eigenen AnimationController.

Mit Hooks, da die Box es und das übergeordnete Widget nicht in derselben Klasse sind, wie würde ich eine Liste von AnimationControllers für den ersten Fall erstellen, in dem jedes Box in einem AnimationController übergeben wird? Dies scheint basierend auf Ihrer obigen Antwort mit HookBuilder nicht erforderlich zu sein, aber wenn ich dann, wie Sie sagen, den Zustand in das untergeordnete Widget verschiebe und jedes Box über useAnimationController einem eigenen AnimationController machen möchte? , stoße ich auf ein anderes Problem: Wie würde ich den erstellten AnimationController der übergeordneten Klasse zugänglich machen, damit er die unabhängigen Animationen für jedes Kind koordiniert und ausführt?

In Vue können Sie ein Ereignis über das Muster emit an das Elternteil zurücksenden Zustand? Es scheint, dass ich das nicht sollte, zumindest für ein einfaches Beispiel wie dieses. Danke für die Klärung meiner Verwirrung.

@satvikpendem Entschuldigung, ich war nicht klar. Könnten Sie zeigen, wie Sie es ohne Hooks machen würden, anstatt das Problem, bei dem Sie mit Hooks blockieren?

Ich möchte ein klares Verständnis davon haben, was Sie zu tun versuchen, und nicht, wo Sie stecken bleiben

Aber als schnelle Vermutung, ich denke, Sie suchen stattdessen nach der Intervallkurve und haben einen einzigen Animationscontroller.

@rrousselGit Klar, hier ist es

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,
      ),
    );
  }
}

Ich möchte eigentlich mehrere Animationscontroller, einen für jedes Widget, da sie sich unabhängig voneinander bewegen können, mit ihrer eigenen Dauer, Kurven usw. Beachten Sie, dass der obige Code einen Fehler zu haben scheint, den ich nicht herausfinden konnte, wo es sollte sauber animieren, aber im Grunde sollte es 3 Boxen auf und ab mit einem Knopfklick animieren. Wir können uns ein Szenario vorstellen, in dem ich, anstatt dass jeder dieselbe Kurve hat, jedem eine andere Kurve gebe oder ich 100 Boxen mache, jede mit einer längeren oder kürzeren Dauer als die vorherige, oder ich lasse die geraden nach oben gehen und die ungeraden gehen runter und so weiter.

Bei normalem Flutter können initState und dispose beide Loops haben, aber bei Hooks scheint es nicht so zu sein, also frage ich mich nur, wie man das bekämpfen kann. Außerdem möchte ich die Klasse Box in das übergeordnete Widget einfügen, da ich sie nicht eng kapseln möchte; Ich sollte in der Lage sein, die übergeordnete Logik beizubehalten, aber zum Beispiel Box durch Box2 auszutauschen.

Vielen Dank!
Ich habe Ihr Beispiel in das Repo von @TimWhiting verschoben , mit einem Hook-Äquivalent

TL;DR, mit Hooks (oder Buildern) denken wir deklarativ statt imperativ. Anstatt also eine Liste von Controllern in einem Widget zu haben, werden diese zwingend gesteuert – die den Controller zum Element bewegen und eine implizite Animation implementieren.

Danke @rrousselGit! Ich hatte eine Weile mit dieser Art von Implementierung zu kämpfen, nachdem ich angefangen hatte, Hooks zu verwenden, aber jetzt verstehe ich, wie es funktioniert. Ich habe gerade eine PR für eine Version mit einem anderen Ziel für jeden Animationscontroller geöffnet, da dies möglicherweise überzeugender ist, um zu verstehen, warum Hooks nützlich sind, wie ich oben gesagt habe:

Wir können uns ein Szenario vorstellen, in dem ich, anstatt dass jeder dieselbe Kurve hat, jedem eine andere Kurve gebe oder ich 100 Boxen mache, jede mit einer längeren oder kürzeren Dauer als die vorherige, oder ich lasse die geraden nach oben gehen und die ungeraden gehen runter und so weiter.

Ich hatte versucht, die deklarative Version zu erstellen, aber was ich wohl nicht verstanden habe, war die Lebenszyklusmethode didUpdateWidget/Hook aber dein Code hat es aufgeräumt.

Ich bin heute auf ein reales Beispiel in meiner Codebasis gestoßen, also dachte ich, ich könnte es genauso gut teilen.

In diesem Szenario arbeite ich also mit Firestore und habe einige Boilerplates, die ich mit jedem StreamBuilder ausführen möchte, also habe ich meinen eigenen benutzerdefinierten Builder erstellt. Ich muss auch mit einem ValueListenable arbeitendie es dem Benutzer ermöglicht, die Liste neu zu ordnen. Aus Kostengründen im Zusammenhang mit Firestore erfordert dies eine sehr spezifische Implementierung (jeder Artikel kann seine eigene Reihenfolge nicht speichern, stattdessen muss die Liste ihn als ein Feld mit verketteten IDs speichern). Dies liegt daran, dass Firestore für jeden Schreibvorgang Gebühren berechnet Sie können auf diese Weise möglicherweise viel Geld sparen. Am Ende liest man so etwas:

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);
                    },
                  );
                });
          },
        ),
      ),
    );

Es fühlt sich an, als ob es viel einfacher wäre, darüber nachzudenken, wenn ich es mehr schreiben könnte wie:

    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)
      ));

Dadurch geht die Optimierung bezüglich granularer Neuerstellungen verloren, aber das würde keine andere IRL ergeben, da sich alle visuellen Elemente am untersten Blattknoten befinden, alle Wrapper sind reiner Zustand.

Wie bei vielen realen Szenarien ist die Empfehlung "Nur kein X verwenden" realistisch, da Firebase nur eine Verbindungsmethode hat, nämlich Streams, und jedes Mal, wenn ich dieses Socket-ähnliche Verhalten möchte, habe ich keine andere Wahl, als es zu tun einen Stream verwenden. So ist das Leben.

Dadurch geht die Optimierung bezüglich granularer Neuerstellungen verloren, aber das würde keine andere IRL ergeben, da sich alle visuellen Elemente am untersten Blattknoten befinden, alle Wrapper sind reiner Zustand.

Es macht immer noch einen Unterschied. Ob ein Knoten visuell ist oder nicht, hat keinen Einfluss darauf, ob der Wiederaufbau etwas kostet.

Ich würde dieses Beispiel wahrscheinlich in verschiedene Widgets einteilen (die IDEs verfügen über Ein-Klick-Refactoring-Tools, um dies wirklich einfach zu machen). _buildItemList sollte wahrscheinlich ein Widget sein, ebenso wie der Teil, der bei FamilyStreamBuilder verwurzelt ist.

Wir verlieren den granularen Wiederaufbau nicht wirklich.
Tatsächlich verbessern Hooks diesen Aspekt, indem sie es ermöglichen, die Widget-Instanz einfach mit useMemoized .

Es gibt ein paar Beispiele in Tims Repo, die das tun.

Ich würde dieses Beispiel wahrscheinlich in verschiedene Widgets einteilen (die IDEs verfügen über Ein-Klick-Refactoring-Tools, um dies wirklich einfach zu machen). _buildItemList sollte wahrscheinlich ein Widget sein, ebenso wie der Teil, der bei FamilyStreamBuilder verwurzelt ist.

Die Sache ist die, ich möchte das nicht wirklich tun, weil ich aus dieser Sicht überhaupt keine Leistungsbedenken habe. Daher werde ich zu 100 % die Codelokalität und -kohärenz einer solchen Mikrooptimierung vorziehen. Diese Ansicht wird nur neu erstellt, wenn der Benutzer eine Aktion auslöst (~ durchschnittlich einmal alle 10 Sekunden) oder wenn sich die Backend-Daten ändern und er auf eine offene Liste starrt (kommt fast nie vor). Es ist auch nur eine einfache Ansicht, die in erster Linie eine Liste ist, und die Liste enthält eine Menge eigener Optimierungen, die intern durchgeführt werden. Mir ist klar, dass build() technisch jederzeit ausgelöst werden kann, aber in der Praxis sind zufällige Rebuilds ziemlich selten.

Imo ist es wesentlich einfacher, an dieser Ansicht zu arbeiten und sie zu debuggen, wenn all diese Logik in einem Widget gruppiert ist, hauptsächlich um mir das Leben zu erleichtern, wenn ich in Zukunft darauf zurückkomme :)

Eine andere zu beachtende Sache ist, dass die Verschachtelung mich im Grunde genommen aus der Build-Methode "verdrängt" hat, da ich auf keinen Fall damit beginnen konnte, meinen Baum innerhalb von 3 Verschlüssen und 16 Räumen im Loch zu konstruieren.

Und ja, man könnte sagen, es macht Sinn, dann einfach zu einem separaten Widget zu wechseln. Aber warum nicht einfach bei der Build-Methode bleiben? Wenn wir das Boilerplate auf das reduzieren könnten, was es wirklich _braucht_, dann besteht keine Notwendigkeit, die Lesbarkeit und den Wartungsaufwand zu haben, Dinge auf zwei Dateien aufzuteilen. (Angenommen, die Leistung ist kein Problem, was es oft einfach nicht ist)

Denken Sie daran, dass ich in diesem Szenario bereits ein benutzerdefiniertes Widget erstellt habe, um meinen Stream Builder zu handhaben. Jetzt müsste ich noch einen machen, um die Zusammensetzung dieser Builder zu handhaben? Scheint etwas übertrieben zu sein.

weil ich in dieser Ansicht überhaupt keine Leistungsbedenken habe

Oh, ich würde es nicht für die Leistung in Widgets umwandeln, die Builder sollten sich schon darum kümmern. Ich würde es aus Gründen der Lesbarkeit und Wiederverwendbarkeit umgestalten. Ich sage nicht, dass dies der "richtige" Weg ist, sondern nur, wie ich den Code strukturieren würde. Jedenfalls ist das weder hier noch dort.

Auf keinen Fall konnte ich anfangen, meinen Baum innerhalb von 3 Verschlüssen und 16 Räumen im Loch zu konstruieren

Vielleicht habe ich nur einen breiteren Monitor als du... :-/

Dann entfällt der Aufwand für Lesbarkeit und Wartung, die Dinge auf 2 Dateien aufzuteilen

Ich würde die Widgets in dieselbe Datei, FWIW, legen.

Wie auch immer, es ist ein gutes Beispiel, und ich glaube, Sie würden lieber ein Widget mit einer anderen Syntax verwenden, als mehrere Widgets zu verwenden.

Vielleicht habe ich nur einen breiteren Monitor als du... :-/

Ich habe ein Ultrawide :D, aber dartfmt begrenzt uns offensichtlich alle auf 80. Daher ist es wichtig, 16 zu verlieren. Das Hauptproblem ist das Ende meiner Aussage ist, dass },);});},),),); nicht wirklich Spaß macht, wenn etwas durcheinander geht. Ich muss jedes Mal sehr vorsichtig sein, wenn ich diese Hierarchie bearbeite, und gängige IDE-Helfer wie swap with parent funktionieren nicht mehr.

Ich würde die Widgets in dieselbe Datei, FWIW, legen.

100%, aber ich finde immer noch, dass es schwieriger ist, vertikal in einer einzigen Datei herumzuspringen. Natürlich unvermeidbar, aber wir versuchen, wenn möglich, zu reduzieren und „die Dinge zusammenzuhalten“.

Entscheidend ist jedoch, dass, selbst wenn ich die Hauptliste in ein eigenes Widget umgestalte (was meiner Meinung nach besser lesbar ist als eine verschachtelte Build-Methode), sie ohne die Verschachtelung im übergeordneten Widget immer noch viel lesbarer ist. Ich kann reinkommen, die gesamte Logik auf einen Blick verstehen, das _MyListView()-Widget klar sehen und direkt hineinspringen, sicher, dass ich den umgebenden Kontext verstehe. Ich kann auch relativ einfach zusätzliche Abhängigkeiten hinzufügen/entfernen, sodass es sehr gut skaliert.

Dartfmt begrenzt uns alle offensichtlich auf 80

Ich meine, das ist ein Grund, warum ich Dartfmt im Allgemeinen nicht verwende, und wenn ich es tue, stelle ich es auf 120 oder 180 Zeichen ein...

Ihre Erfahrung hier ist absolut gültig.

Ich eigentlich auch, 120 den ganzen Tag :) Aber pub.dev wertet Plugins, die nicht auf 80 formatiert sind, aktiv herunter, und ich habe den Eindruck, dass ich (wir) in der Minderheit bin, wenn wir diesen Wert ändern.

Nun, das ist absurd, das sollten wir beheben.

pub.dev wertet keine Plugins herunter, die dartfmt nicht respektieren. Es wird nur ein Kommentar auf der Notenseite angezeigt, aber der Punktestand ist unbeeinflusst
Aber es gibt wohl mehr Probleme mit dartfmt als nur die Zeilenlänge.

Eine zu große Zeilenlänge führt dazu, dass Dinge, die in mehreren Zeilen besser lesbar sind, in einer einzigen Zeile stehen, wie zum Beispiel:

object
  ..method()
  ..method2();

was werden kann:

object..method()..method2();

Ich sehe das?
image
Betreffendes Paket: https://pub.dev/packages/sized_context/score

Interessant – das war vorher definitiv nicht so, da der Provider dartfmt eine Zeit lang nicht verwendet hat.
Ich stehe korrigiert.

Yup, es ist definitiv ein neues Verhalten, als ich ursprünglich im letzten Frühjahr veröffentlicht habe, habe ich sichergestellt, dass ich alle Kästchen angekreuzt habe, und dartfmt war nicht erforderlich.

Nach all diesen Diskussionen hoffe ich, dass wir in Flattern native Unterstützung für Hook-ähnliche Lösungen sehen. entweder useHook oder use Hook oder alles, was das Flatter-Team fühlen kann, ist nicht wie React 😁🤷‍♂️

wir verwenden Hooks wie final controller = useAnimationController(duration: Duration(milliseconds: 800));
Ist es nicht besser, Darts neue Programmfunktion _Extension_ zu verwenden, die von kotlin/swift kopiert wurde, um schön diese Syntax zu verwenden?

etwas wie: final controller = AnimationController.use(duration: Duration(milliseconds: 800));
Wenn das Flatter-/Dart-Team bei diesem Ansatz beschließt, use Hook anstelle der derzeit verfügbaren Syntax useHook hinzuzufügen, denke ich, dass ein Annotation zu dieser Erweiterungsfunktion dazu führt, dass sie als verwendet wird
final controller = use AnimationController(duration: Duration(milliseconds: 800));

Es ist auch verständlich/sinnvoll, das Schlüsselwort use wie const und new :
new Something
const Something
use Something

Als Bonus zu dieser Empfehlung denke ich, dass endlich sogar Konstruktor-/Generatorfunktionen von dem vorgeschlagenen Annotation profitieren können. Dann konvertiert der Dart-Compiler mit einigen Anpassungen es, um das Schlüsselwort use .

So schön und Flatter-/Dart-spezifisches Feature 😉

Gehe ich richtig in der Annahme, dass die Beispiele in https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful jetzt repräsentativ für die Probleme sind, die von den Leuten gelöst werden sollen?

Ich bin mir nicht sicher, wie sich alle anderen fühlen, aber ich denke, dass die Probleme dort etwas vertreten sind (das heißt, ich bin mir nicht sicher, weil jemand auf etwas hinweisen könnte, das nicht dargestellt ist).

Ich habe eine Mittelweglösung in diesem Repository versucht. Es ist wie Hooks zusammensetzbar, aber nicht abhängig von der Reihenfolge der Funktionsaufrufe oder erlaubt keine Schleifen usw. Es verwendet StatefulWidgets direkt. Es beinhaltet ein Mixin sowie zustandsbehaftete Eigenschaften, die durch Schlüssel eindeutig identifiziert werden. Ich versuche nicht, dies als ultimative Lösung zu propagieren, sondern als Mittelweg zwischen den beiden Ansätzen.

Ich habe es den LifecycleMixin-Ansatz genannt, er ist dem hier besprochenen LateProperty-Ansatz sehr ähnlich, aber der Hauptunterschied besteht darin, dass mehr Lebenszyklen implementiert sind und er sich leicht komponieren lässt. (In Bezug auf die Lebenszyklen habe ich keine anderen Widget-Lebenszyklen als initState verwendet und viel disponiert, also habe ich es möglicherweise total vermasselt).

Ich mag diesen Ansatz, weil:

  1. Es hat sehr wenig Laufzeiteinbußen.
  2. Es gibt keine Logik / Funktionen, die den Status im Build-Pfad erstellen oder verwalten (Builds können rein sein - nur den Status abrufen).
  3. Das Lifecycle-Management ist klarer, wenn Rebuilds über einen Builder optimiert werden. (Aber Sie opfern nicht die Wiederverwendbarkeit und Zusammensetzbarkeit kleiner Zustandsteile).
  4. Da Sie die Erstellung von Zustandsbits wiederverwenden können, kann eine Bibliothek aus allgemeinen Zustandsbits bestehen, die auf bestimmte Weise erstellt und entsorgt werden sollten, sodass Ihr eigener Code weniger Boilerplate enthält.

Ich mag diesen Ansatz (im Vergleich zu Hooks) aus folgenden Gründen nicht:

  1. Ich weiß nicht, ob es alles abdeckt, was Haken können.
  2. Sie müssen Schlüssel verwenden, um die Eigenschaften eindeutig zu identifizieren. (Wenn Sie also die Logikteile zusammensetzen, die einen Zustand aufbauen, müssen Sie an den Schlüssel anhängen, um jeden Teil des Zustands eindeutig zu identifizieren - den Schlüssel zu einem erforderlichen Positionsparameter zu machen, hilft, aber ich würde gerne eine Lösung auf Sprachebene für den Zugriff auf a eindeutige ID für eine Variable).
  3. Es verwendet stark Erweiterungen zum Erstellen wiederverwendbarer Funktionen, um gemeinsame Zustandsbits zu erstellen. Und Erweiterungen können von den IDEs nicht automatisch importiert werden.
  4. Sie können sich selbst vermasseln, wenn Sie Lebenszyklen verschiedener Widgets mischen / zwischen Widgets darauf zugreifen, ohne sie explizit richtig zu verwalten.
  5. Die Builder-Syntax ist etwas seltsam, sodass der erstellte Zustand im Bereich der Build-Funktion liegt, die Build-Funktion jedoch rein bleibt.
  6. Ich habe noch nicht alle Beispiele implementiert, daher kann es einen Anwendungsfall geben, den ich nicht abdecken kann.

Einfaches Gegenbeispiel .
Beispiel für animierte Zähler

Rahmen
gemeinsame Bits der wiederverwendbaren Zustandslogik

Ich bin mir nicht sicher, wie viel Zeit ich habe, das Studium hält mich immer auf Trab, aber ich würde mich über Feedback freuen. @rrousselGit Wie nah ist das an Hooks, können Sie einige offensichtliche Löcher in der Wiederverwendbarkeit oder Zusammensetzbarkeit sehen?

Ich versuche nicht, für meine Lösung zu werben, sondern eine positive Diskussion auf einem Mittelweg anzuregen. Wenn wir uns darüber einigen können, was uns fehlt oder was uns diese Lösung bringt, werden wir meines Erachtens gute Fortschritte machen.

@TimWhiting Das Hauptproblem, das ich bei diesem Ansatz habe, ist der Mangel an Robustheit. Ein großer Treiber ist hier das Bedürfnis nach Verlässlichkeit der Bauherren, in prägnanter Form. Die magische ID und die Möglichkeit, im Lebenszyklus zu kollidieren, schaffen beide neue Vektoren für das Auftreten von Fehlern, und ich würde meinem Team weiterhin empfehlen, Builder zu verwenden, da wir, obwohl sie ziemlich unangenehm zu lesen sind, zumindest wissen, dass sie 100 sind % Bug-frei.

In Bezug auf Beispiele denke ich immer noch, dass das perfekte Beispiel einfach die Verwendung eines AnimationControllers mit einem an das Widget gebundenen Dauerwert ist. Hält es einfach und vertraut. Es gibt keinen Grund mehr esoterisch zu werden, es ist ein perfekter kleiner Anwendungsfall für wiederverwendbare Boilerplate, es braucht Lebenszyklus-Hooks und alle Lösungen könnten leicht nach ihrer Fähigkeit beurteilt werden, mehrere Animationen prägnant zu verwenden.

Alles andere ist nur eine Variation des gleichen Anwendungsfalls des 'Stateful Controller'. Ich möchte X in initState und Y im Dispose-Zustand ausführen und Z aktualisieren, wenn sich meine Abhängigkeiten ändern. Es spielt keine Rolle, was X, Y und Z sind.

Ich frage mich, ob @rrousselGit hier einen Einblick

Was den Wiederaufbau von Teilen des Baumes angeht, sind Builder natürlich sowieso für diese Aufgabe geeignet, wir sollten sie einfach machen lassen. Ein zustandsbehafteter Controller kann sowieso leicht in zustandslose Renderer eingehakt werden, wenn Sie dies wünschen (hallo an jede Übergangsklasse).

So wie wir es tun könnten:

var anim = get AnimationController();
return Column(
  _someExpensiveBuildMethod(),
  FadeTransition(opacity: anim, child: ...)
)

Wir könnten immer tun:

var foo = get ComplicatedThingController();
return Column(
  _someExpensiveBuildMethod(),
  ComplicatedThing(controller: foo, child: ...)
)

@esDotDev Ich stimme zu, die Schlüssel und die Builder-Syntax sind die Hauptnachteile des lifecycleMixin-Ansatzes. Ich weiß nicht, ob Sie das umgehen können, außer indem Sie einen Hooks-Stil-Ansatz mit den damit verbundenen Einschränkungen verwenden oder eine Sprachänderung vornehmen, um Variablendeklarationen mit Zustandsbits mit Lebenszyklen zu verknüpfen. Aus diesem Grund werde ich weiterhin Hooks verwenden und andere zustandsbehaftete Widgets verwenden lassen, es sei denn, es ergibt sich eine bessere Lösung. Ich denke jedoch, dass es eine interessante Alternative für diejenigen ist, die die Einschränkungen von Hooks nicht mögen, obwohl es eigene Einschränkungen hat.

Gehe ich richtig in der Annahme, dass die Beispiele in https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful jetzt repräsentativ für die Probleme sind, die von den Leuten gelöst werden sollen?

Ich bin mir ehrlich gesagt nicht sicher.
Ich würde Ja sagen_. Aber das hängt wirklich davon ab, wie Sie diese Beispiele interpretieren.

In diesem Thread haben wir uns in der Vergangenheit oft nicht verstanden, daher kann ich nicht garantieren, dass dies nicht noch einmal vorkommt.

Das ist zum Teil der Grund, warum ich die Verwendung von Codebeispielen nicht mag und vorgeschlagen habe, stattdessen eine Reihe von Regeln zu extrahieren.
Beispiele sind subjektiv und haben mehrere Lösungen, von denen einige das umfassendere Problem möglicherweise nicht lösen.

Ich frage mich, ob @rrousselGit hier einen Einblick

Ich finde es sehr homogen.

Obwohl useStream und Animationen wahrscheinlich am wenigsten verwendet werden:

  • useStream hat je nach Architektur normalerweise ein besseres Äquivalent. Könnte context.watch , useBloc , useProvider , ...
  • Nur wenige Leute nehmen sich die Zeit, Animationen zu erstellen. Das ist selten die Priorität, und TweenAnimationBuilder andere implizit animierte Widgets decken einen großen Teil des Bedarfs ab.
    Vielleicht würde sich das ändern, wenn ich meine useImplicitlyAnimatedInt Hooks in flatter_hooks hinzufüge.

@esDotDev Die Notwendigkeit von Schlüsseln/IDs im

Danke Remi, das überrascht mich, ich würde denken, dass die Leute Animation sehr häufig verwenden würden, um die große Sammlung von Transition-Widgets im Kern zu steuern, aber ich denke, die meisten Leute verwenden einfach die verschiedenen Implicits, da sie recht gut zu lesen sind und keine Verschachtelung haben .

Obwohl AnimatorController mit einer Reihe von impliziten und expliziten Widgets sehr gut bedient wird, halte ich es immer noch für ein großartiges Beispiel für ein "Ding, das den Zustand beibehalten und in die Widget-Parameter und den Lebenszyklus einbinden muss". Und dient als perfektes kleines Beispiel für das zu lösende Problem (die Tatsache ist in Flutter trotz eines Dutzend Widgets vollständig gelöst), dass wir alle diskutieren und uns auf die Architektur und nicht auf den Inhalt konzentrieren können.

Überlegen Sie beispielsweise, wie praktisch keine dieser impliziten oder expliziten Animationen wirklich existieren muss, wenn var anim = AnimationController.use(context, duration: widget.duration ?? _duration); ein erstklassiger Bürger wäre. Es macht sie überflüssig, da sie alle erstellt wurden, um das Kernproblem zu bewältigen: die einfache Zusammenstellung eines zustandsbehafteten Dings (AnimationController) im Kontext eines Widgets. TAB wird fast sinnlos, da Sie dasselbe mit AnimatedBuilder + AnimatorController.use() tun können.

Es zeigt wirklich die Notwendigkeit für den allgemeinen Anwendungsfall, wenn Sie sich die riesige Masse an Widgets ansehen, die um Animationen herum entstanden sind. Gerade weil es so umständlich/fehleranfällig ist, die Kern-Setup- / Teardown-Logik wiederzuverwenden, haben wir mehr als 15 Widgets, die alle sehr spezifische Dinge handhaben, aber die meisten von ihnen wiederholen die gleiche Animations-Boilerplate mit nur einer Handvoll in vielen Fällen eindeutige Codezeilen.

Es dient dazu zu zeigen, dass wir dies auch tun könnten, um unsere eigene zustandsbehaftete Logik wiederzuverwenden: ein Widget für jede einzelne Nutzungspermutation erstellen. Aber was für ein Ärger und Wartungskopfschmerz! Es ist so viel schöner, nur eine einfache Möglichkeit zu haben, kleine zustandsbehaftete Objekte mit Lifecycle-Hooks zu erstellen, und wenn wir dedizierte Widgets zum Rendern oder einen wiederverwendbaren Builder erstellen möchten, können wir diese einfach darüber legen.

Für das, was es wert ist, verwende ich etwas wie useAnimation eher in meiner App als die normalen Animations-Widgets. Dies liegt daran, dass ich eine SpringAnimation verwende, die beispielsweise mit Widgets wie AnimatedContainer nicht gut unterstützt wird; sie alle gehen von einer zeitbasierten Animation aus, mit curve und duration anstelle einer simulationsbasierten Animation, die ein Simulation Argument akzeptieren würde.

Ich habe eine Abstraktion über useAnimation aber mit Federn, also habe ich sie useSpringAnimation . Das Wrapper-Widget, mit dem ich diesen Hook verwendet habe, ähnelt einem AnimatedContainer aber es war viel einfacher zu erstellen , da ich den gesamten Animationscode wiederverwenden konnte, wie Sie useSpringAnimation aber das musste ich für mein Projekt nicht unbedingt. Dies zeigt einmal mehr die Leistungsfähigkeit der Wiederverwendung von Lebenszykluslogik, die Hooks bieten.

Überlegen Sie beispielsweise, wie, wenn var anim = AnimationController.use(context, duration: widget.duration ?? _duration); ein Bürger erster Klasse waren, muss praktisch keine dieser impliziten oder expliziten Animationen wirklich existieren. Es macht sie überflüssig, da sie alle erstellt wurden, um das Kernproblem zu bewältigen: die einfache Zusammenstellung eines zustandsbehafteten Dings (AnimationController) im Kontext eines Widgets. TAB wird fast sinnlos, da Sie dasselbe mit AnimatedBuilder + AnimatorController.use() tun können.

Wenn ich meine obigen Kommentare lese, scheint dies im Grunde genau das zu sein, was ich mit meinem Spring-Animations-Hook gemacht habe. Ich habe die Logik gekapselt und dann einfach AnimatedBuilder verwendet. Um sie implizit zu machen, damit, wenn ich die Requisite wie bei AnimatedContainer geändert habe, sie animiert würde, habe ich einfach die Methode didUpdateWidget (genannt didUpdateHook in flutter_hooks ) zu . hinzugefügt Führen Sie die Animation vom alten Wert zum neuen Wert aus.

Gehe ich richtig in der Annahme, dass die Beispiele in https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful jetzt repräsentativ für die Probleme sind, die von den Leuten gelöst werden sollen?

Ich bin mir ehrlich gesagt nicht sicher.
Ich würde Ja sagen_. Aber das hängt wirklich davon ab, wie Sie diese Beispiele interpretieren.

In diesem Thread haben wir uns in der Vergangenheit oft nicht verstanden, daher kann ich nicht garantieren, dass dies nicht noch einmal vorkommt.

Das ist zum Teil der Grund, warum ich die Verwendung von Codebeispielen nicht mag und vorgeschlagen habe, stattdessen eine Reihe von Regeln zu extrahieren.
Beispiele sind subjektiv und haben mehrere Lösungen, von denen einige das umfassendere Problem möglicherweise nicht lösen.

Ich würde auch sagen, dass wir alle Codebeispiele in diese Ausgabe aufnehmen sollten, die besprochen wurden. Ich denke, es gibt oben irgendwo eine Liste, die @rrousselGit erstellt hat. Ich könnte sie per PR zum local_state-Repository hinzufügen, aber sie sind nicht alle vollständige Codebeispiele, sodass sie möglicherweise nicht alle tatsächlich kompiliert und ausgeführt werden. Aber sie zeigen zumindest die potentiellen Probleme.

Ich könnte eine PR machen, die sie zum local_state Repository hinzufügt

Das wäre sehr nützlich.

Ich möchte darauf hinweisen, dass in diesem Thread die Wiederverwendung nicht definiert ist oder wie die Wiederverwendung aussieht. Ich denke, wir sollten dies schmerzlich genau definieren, damit das Gespräch nicht den Fokus verliert.

Wir haben nur gezeigt, was Wiederverwendung in Bezug auf Flutter _nicht_ ist.

Es gab eine ganze Reihe von Verwendungsbeispielen, und Hooks bieten eindeutig ein umfassendes Beispiel für die Wiederverwendung von Widget-Status. Ich bin mir nicht sicher, woher die Verwirrung kommt, da sie auf den ersten Blick einfach erscheint.

Wiederverwendung kann einfach wie folgt definiert werden: _Alles, was ein Builder-Widget tun kann._

Die Anfrage ist für ein zustandsbehaftetes Objekt, das in jedem Widget vorhanden sein kann, das:

  • Kapselt seinen eigenen Zustand
  • Kann sich selbst gemäß initState einrichten/abbauen/Anrufe entsorgen
  • Kann reagieren, wenn sich Abhängigkeiten im Widget ändern

Und das auf eine schöne, prägnante, einfach zuzubereitende, bausteinfreie Art und Weise, wie:
AnimationController anim = AnimationController.stateful(duration: widget.duration);
Wenn dies in Stateless- und Stateful-Widgets funktioniert. Wenn es neu aufgebaut wird, wenn sich widget.something ändert, wenn es seine eigenen init() und dispon() ausführen kann, dann haben Sie im Grunde einen Gewinner und ich bin sicher, jeder würde es zu schätzen wissen.

Die Hauptsache, mit der ich zu kämpfen habe, ist, dies auf effiziente Weise zu tun. ValueListenableBuilder verwendet beispielsweise ein untergeordnetes Argument, mit dem die Leistung messbar verbessert werden kann. Ich sehe keine Möglichkeit, dies mit dem Property-Ansatz zu tun.

Ich bin mir ziemlich sicher, dass dies kein Thema ist. Wir würden dies genauso tun, wie die XTransition Widgets jetzt funktionieren. Wenn ich einen komplexen Zustand habe, und ich wollte, dass er ein teures Kind hat, würde ich einfach ein kleines Wrapper-Widget dafür erstellen. So wie wir es machen könnten:
FadeTransition(opacity: anim, child: someChild)

Wir können das genauso einfach mit jedem Ding machen, das wir gerendert haben möchten, indem wir das 'Ding' an ein Widget übergeben, um es neu zu rendern.
MyThingRenderer(value: thing, child: someChild)

  • Dies _erfordert_ keine Verschachtelung wie der Builder, unterstützt es jedoch optional (.child könnte ein Build-fxn sein).
  • Es behält die Möglichkeit, direkt ohne ein Wrapping-Widget verwendet zu werden
  • Wir können jederzeit einen Builder erstellen und diese Syntax innerhalb des Builders verwenden, um ihn sauberer zu halten. Es öffnet auch die Tür zu mehreren Arten von Buildern, die um dasselbe Kernobjekt herum gebaut sind und nicht überall Code kopieren und einfügen müssen.

Einverstanden mit @esDotDev. Wie ich bereits erwähnt habe, wäre ein alternativer Titel dafür "Syntax Sugar for Builders".

Die Hauptsache, mit der ich zu kämpfen habe, ist, dies auf effiziente Weise zu tun. ValueListenableBuilder verwendet beispielsweise ein untergeordnetes Argument, mit dem die Leistung messbar verbessert werden kann. Ich sehe keine Möglichkeit, dies mit dem Property-Ansatz zu tun.

Ich bin mir ziemlich sicher, dass dies kein Thema ist. Wir würden dies genauso tun, wie die XTransition Widgets jetzt funktionieren. Wenn ich einen komplexen Zustand habe, und ich wollte, dass er ein teures Kind hat, würde ich einfach ein kleines Wrapper-Widget dafür erstellen. So wie wir es machen könnten:

Das ist nicht nötig.
Einer der Vorteile dieser Funktion ist, dass wir eine Zustandslogik haben können, die "die Widget-Instanz zwischenspeichert, wenn sich ihre Parameter nicht geändert haben".

Mit Hooks wäre das useMemo in React:

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

Mit diesem Code wird myWidget _nur_ neu erstellt, wenn sich value ändert. Auch wenn das Widget, das useMemo aufruft, aus anderen Gründen neu erstellt wird.

Das ähnelt einem const-Konstruktor für Widgets, lässt jedoch dynamische Parameter zu.

Es gibt ein Beispiel dafür in Tims Repo.

Die Anfrage ist für ein zustandsbehaftetes Objekt, das in jedem Widget vorhanden sein kann, das:

  • Kapselt seinen eigenen Zustand
  • Kann sich selbst gemäß initState einrichten/abbauen/Anrufe entsorgen
  • Kann reagieren, wenn sich Abhängigkeiten im Widget ändern

Ich denke, es fällt mir schwer zu erkennen, warum StatefulWidget die Arbeit nicht besser macht, als sie es tut. Deshalb habe ich die Frage gestellt, was wir hier in einer Lösung wirklich suchen. Als jemand, der flutter_hooks finde ich es lustiger, mit ihnen zu arbeiten als mit StatefulWidget , aber das ist nur, um Ausführlichkeit zu vermeiden - nicht weil ich in Begriffen von Hooks denke. Ich finde es tatsächlich schwierig, mit Hooks über UI-Updates nachzudenken, verglichen mit Widget s.

  • Kann reagieren, wenn sich Abhängigkeiten im Widget ändern

Du meinst eine Abhängigkeit, die im Widget erstellt/erworben wurde? Oder eine Abhängigkeit weit unterhalb des Widgets im Baum?

Ich leugne nicht, dass es ein Problem gibt, das in Flutter Ausführlichkeit / Verwirrung verursacht, ich zögere nur, mich darauf zu verlassen, dass jeder tatsächlich das gleiche mentale Modell von "Wiederverwendung" hat. Ich bin sehr dankbar für die Erklärung; und wenn Menschen unterschiedliche Modelle haben, schaffen sie unterschiedliche Lösungen.

Weil die Verwendung einer SW für einen bestimmten Anwendungsfall in Ordnung ist, aber nicht gut, um die wiederverwendbare Logik des Anwendungsfalls über viele SWs hinweg zu abstrahieren. Nehmen Sie das Setup/Teardown für Animation als Beispiel. Dies ist keine SW selbst, sondern etwas, das wir für sie verwenden möchten. Ohne erstklassige Unterstützung für das Teilen von gekapselten Zuständen müssen Sie am Ende einen Builder erstellen, z. B. TweenAnimationBuilder, oder eine Menge spezifischer Widgets, z es wie Sie wollen in einem Baum.

In Bezug auf die Widget-Abhängigkeit meine ich nur, wenn sich widget.foo ändert, erhält das zustandsbehaftete Ding die Möglichkeit, alle erforderlichen Aktualisierungen vorzunehmen. Im Fall von stateful AnimationController würde es prüfen, ob sich die Dauer geändert hat, und wenn ja, seine interne AnimatorController-Instanz aktualisieren. Dies erspart jedem Implementierer der Animation die Bearbeitung der Eigenschaftsänderung.

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

Mit diesem Code wird myWidget _nur_ neu erstellt, wenn sich value ändert. Auch wenn das Widget, das useMemo aufruft, aus anderen Gründen neu erstellt wird.

Ah, wie ich sehe, gibt Memoized selbst ein Widget zurück, und dann übergeben Sie [value] als Rebuild-Trigger, ordentlich!

Der Schlüssel zu AnimatedOpacity ist weder der übergeordnete noch der untergeordnete Neuaufbau. Tatsächlich wird beim Auslösen einer Animation mit AnimatedOpacity nach dem ersten Frame, in dem Sie die Animation auslösen, buchstäblich nichts neu aufgebaut. Wir überspringen die Build-Phase vollständig und machen alles im Render-Objekt (und im Render-Baum wird nur neu gezeichnet, nicht weitergeleitet, und tatsächlich verwendet es eine Ebene, sodass selbst die Farbe ziemlich minimal ist). Dies macht einen erheblichen Unterschied in Bezug auf die Leistung und den Akkuverbrauch aus. Jede Lösung, die wir hier finden, muss in der Lage sein, diese Art von Leistung aufrechtzuerhalten, wenn wir sie in das Kern-Framework einbauen wollen.

Leider hatte ich keine Zeit, die Beispiele in dieser Ausgabe im lokalen staatlichen Repo zu sammeln, mein Fehler. Ich werde es in naher Zukunft vielleicht nicht schaffen, also wenn jemand anderes das abholen möchte, wäre ich damit einverstanden.

In Bezug auf die Leistung von Hooks, die in der Build/Render-Methode definiert sind (was meiner Meinung nach jemand zuvor in dieser Ausgabe erwähnt hat), habe ich die React-Dokumentation durchgelesen und diese FAQ gesehen. Grundsätzlich wird gefragt, ob Hooks langsam sind, weil in jedem Rendering Funktionen erstellt werden, und sie sagen aus mehreren Gründen nein, von denen einer darin besteht, Funktionen mit einem Hook wie useMemo oder useCallback auswendig zu lernen.

https://reactjs.org/docs/hooks-faq.html#are -hooks-slow-because-of-creating-functions-in-render

Grundsätzlich wird gefragt, ob Hooks langsam sind, weil in jedem Rendering Funktionen erstellt werden, und sie sagen aus mehreren Gründen nein, von denen einer darin besteht, Funktionen mit einem Hook wie useMemo oder useCallback auswendig zu lernen.

Es geht nicht um die Kosten für die Herstellung von Verschlüssen, diese sind in der Tat relativ günstig. Es ist der Unterschied zwischen dem Ausführen eines beliebigen Codes und dem Ausführen von überhaupt keinem Code, der für die Leistung, die Flutter heute im optimalen Fall bietet, entscheidend ist. Wir haben viel Mühe darauf verwendet, Algorithmen zu entwickeln, die buchstäblich vermeiden, dass bestimmte Codepfade ausgeführt werden (z. B. die Build-Phase wird für AnimatedOpacity vollständig übersprungen oder die Art und Weise, wie wir es vermeiden, den Baum zu durchlaufen, um Updates durchzuführen, sondern stattdessen nur die betroffenen Knoten anvisieren).

Ich stimme zu. Ich kenne mich weder mit Flutter-Interna noch mit Hook-Interna aus, aber Sie haben Recht, dass Hooks (wenn sie es nicht bereits tun) herausfinden müssen, wann sie laufen sollten oder nicht, und die Leistung darf nicht zurückgehen.

Es ist der Unterschied zwischen dem Ausführen eines beliebigen Codes und dem Ausführen von überhaupt keinem Code, der der Schlüssel zu der Leistung ist, die Flutter heute im optimalen Fall bietet

Wie bereits mehrfach erwähnt, verbessern Hooks das.
Das animierte Beispiel in Tims Repo ist der Beweis dafür. Die Hooks-Variante wird dank useMemo seltener neu aufgebaut als die StatefulWidget-Variante

Da irgendwo in diesem Thread über Lösungen für dieses Problem diskutiert wird, bezeichne ich es auch als Vorschlag.

Ich würde mir wirklich wünschen, dass Hooks in das Flattern integriert werden, wie es bei React gemacht wurde. Ich betrachte den Zustand beim Flattern genauso wie früher, als ich das erste Mal reagierte. Da ich Hooks benutze, würde ich persönlich nie wieder zurückkehren.

Es ist so viel lesbarer IMO. Derzeit müssen Sie zwei Klassen mit einem zustandsbehafteten Widget im Gegensatz zu Hooks deklarieren, bei denen Sie nur usestate eingeben.

Es würde auch eine gewisse Vertrautheit mit Flattern bringen, die Entwickler oft nicht haben, wenn sie sich Flatter-Code ansehen. Offensichtlich ist es ein gefährlicher Weg, Flattern mit Reagieren zu vergleichen, aber ich denke wirklich, dass meine Entwicklererfahrung mit Hooks besser ist als meine Erfahrung ohne sie.

Ich hasse Flattern übrigens nicht, es ist eigentlich mein Lieblingsframework, aber ich denke, dies ist eine wirklich gute Gelegenheit, die Lesbarkeit und die Entwicklererfahrung zu verbessern.

Ich denke, es gibt definitiv eine Möglichkeit, die Namenskonventionen zu verbessern und sie flatterhafter zu machen.

Dinge wie UseMemoized und UseEffect klingen ziemlich fremd, und es klingt, als ob wir irgendwie den init()-Code im Build fxn nicht ausführen müssen.

Derzeit ist die Initialisierung mit Hooks so (glaube ich?):

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.
   );
}

Ich schätze die Kürze dieses Codes, aber er ist vom Standpunkt der Lesbarkeit und des "selbstdokumentierenden Codes" sicherlich weit weniger als ideal. Hier geht viel implizite Magie vor sich. Idealerweise haben wir etwas, das die Init/Dispose-Hooks explizit macht und sich nicht zum Build zwingt, wenn es mit einem Stateless Widget verwendet wird.

Dinge wie useMemoized und useEffect könnten vielleicht besser explizit genannt werden hook ComputedValue() und 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);
}

Ich mag das, bin mir jedoch nicht sicher, was ich von der Verwendung des Schlüsselworts hook halte, und ich glaube nicht, dass es das Problem der fremden Konzepte löst. Die Einführung neuer Keywords ist meiner Meinung nach nicht der beste Ansatz, withSideEffect oder withComputedValue ? Ich bin kein Sprachdesigner, also sind meine Worte wertlos.

Ich habe das Gefühl, dass die Hook-ähnliche Funktionalität in Flutter eine große Hilfe dabei sein wird, die Lernkurve für React-Entwickler zu glätten, die wirklich die Zielgruppe sind, wenn Unternehmen die Entscheidung zwischen ReactNative und Flutter treffen.

In Anlehnung an @lemusthelroy ist Flutter bei weitem mein Lieblingsframework und ich bin mehr als aufgeregt, die Richtungen zu sehen, die es nimmt. Aber ich glaube, dass funktionale Programmierkonzepte eine große Hilfe sein könnten, um das Framework in eine noch relativ unerforschte Richtung zu entwickeln. Ich denke, einige Leute lehnen die Idee ab, um sich von React zu distanzieren, was bedauerlich, aber verständlich ist.

Ja, diese Medaille hat zwei Seiten, denke ich. Ein neues Schlüsselwort ist ein wichtiges Ereignis, daher würde die Wissensverbreitung sehr schnell erfolgen, aber die andere Seite ist sicherlich, dass es jetzt für _jeder_jeder_ etwas Neues ist. Wenn es ohne geht, ist das auch cool! Ich bin mir nur nicht sicher, ob es so ist ... zumindest nicht so elegant.

Meinung: Die Neigung der Community, Hooks als de-facto-Lösung für dieses Problem zu benennen, entspringt einer Voreingenommenheit für Funktionen. Funktionen sind einfacher zu erstellen als Objekte, insbesondere in einer statisch typisierten Sprache. Ich denke, das mentale Modell von Widgets für viele Entwickler ist im Grunde nur die Methode build .

Ich denke, wenn Sie das Problem in den Grundlagen fassen, werden Sie eher eine Lösung entwerfen, die im Rest der Bibliothek gut funktioniert.

Was das Schlüsselwort hook in Bezug auf die Grundlagen betrifft; man könnte es als Deklaration und Definition einer Funktion aus einer Art Vorlage (einem Makro) betrachten, und das Präfix hook ruft eigentlich nur auf, dass die eingebaute Funktion einen internen Zustand hat (Statistik im c-Stil. )

Ich frage mich, ob es in Swift FunctionBuilders nicht eine Art Stand der Technik gibt.

Während wir träumen, erkläre ich meine Vermutung, was der notwendige Code wäre:

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);
}

Wobei Hook ein Hack auf Typsystemebene ist, der dabei hilft, statisch zu analysieren, ob der resultierende Hook in Übereinstimmung mit Hook-vertrauten Entwicklern als Hook-Gesetze aufgerufen wurde. Als solches kann der Hook-Typ als etwas dokumentiert werden, das einer Funktion sehr ähnlich ist, aber einen statischen internen veränderlichen Zustand hat.

Ich zucke ein bisschen zusammen, während ich dies schreibe, weil es aus sprachlicher Sicht so seltsam ist. Andererseits ist Dart die Sprache, die zum Schreiben von Benutzeroberflächen geboren wurde. Wenn es so eine Kuriosität irgendwo geben sollte, ist dies vielleicht der richtige Ort. Nur nicht diese Kuriosität im Besonderen.

Meinung: Die Neigung der Community, Hooks als de-facto-Lösung für dieses Problem zu benennen, entspringt einer Voreingenommenheit für Funktionen. Funktionen sind einfacher zu erstellen als Objekte, insbesondere in einer statisch typisierten Sprache. Ich denke, das mentale Modell von Widgets ist für viele Entwickler im Grunde nur die Build-Methode.

Ich bin mir nicht sicher, was du damit sagen willst. Der Hook-Ansatz, den ich auch mit meinem get_it_mixin verwende, macht den Widget-Baum nur einfacher zu lesen als mit einem Builder.

Interessanter Artikel über React Hooks

@nt4f04uNd Alle Ihre Punkte wurden zuvor angesprochen, einschließlich der Leistung, warum es ein

Ich schlage vor, dass Sie das gesamte Gespräch durchlesen, um die verschiedenen Punkte zu verstehen.

Das ist fair zu sagen, wenn man bedenkt, dass sie nicht den ganzen Thread gelesen haben, aber ich bin mir nicht sicher, ob es die Dinge klarer macht, den Rest des Threads zu lesen. Es gibt Leute, deren Priorität es ist, Widgets so zu belassen, wie sie sind, und eine andere Gruppe, die etwas ganz anderes machen oder Widgets modularer machen möchte.

Das mag stimmen, aber dieses Problem zeigt, dass es Probleme gibt, die mit Widgets in der jetzigen Form nicht gelöst werden können. Wenn wir die Probleme also lösen wollen, haben wir keine andere Wahl, als etwas Neues zu machen. Dies ist das gleiche Konzept wie Future s und später die Einführung der async/await Syntax. Letzteres macht Dinge möglich, die ohne neue Syntax einfach nicht möglich waren.

Die Leute _sind_ jedoch darauf hin, dass wir es zu einem Teil des Rahmens machen. React kann Javascript keine neue Syntax hinzufügen, da es nicht das einzige verfügbare Framework ist (nun, es kann durch Babel-Transformationen), aber Dart wurde speziell für die Arbeit mit Flutter entwickelt (zumindest Dart 2, nicht die Originalversion). viel mehr eine Fähigkeit, Hooks dazu zu bringen, mit der zugrunde liegenden Sprache zusammenzuarbeiten. React benötigt zum Beispiel Babel für JSX, und es muss einen Linter für useEffect Fehler verwenden, während wir es zu einem Kompilierzeitfehler machen könnten. Ein Paket zu haben, macht die Einführung viel schwieriger, da Sie sich vorstellen können, welche Traktion React-Hooks bekommen hätten (nicht), wenn es ein Paket von Drittanbietern gewesen wäre.

Es wäre kein Problem, wenn es neben den aktuellen Stateless- und Stateful-Widgets noch eine dritte Art von Widget geben könnte, nämlich HookWidget. Lassen Sie die Community entscheiden, welche Sie verwenden möchten. Es gibt bereits ein Paket von Remi, aber es hat zwangsläufig Einschränkungen. Ich habe es ausprobiert und es hat die Boilerplate erheblich reduziert, aber ich musste es aufgrund von Einschränkungen leider fallen lassen. Ich muss zustandsbehaftete Widgets erstellen, um nur die Init-Methode zu verwenden. Es könnte zusätzliche große Vorteile geben, wenn es mit der Sprachunterstützung Teil des Kernframeworks ist. Außerdem kann ein HookWidget es der Community ermöglichen, optimalere und leistungsfähigere Apps zu erstellen.

Ich muss zustandsbehaftete Widgets erstellen, um nur die Init-Methode zu verwenden.

Sie müssen dies nicht wirklich tun, useEffect() ist in der Lage, den initCall innerhalb des Builds auszuführen. Die Dokumentation macht sich keine Mühe, dies zu erklären, und geht grundsätzlich davon aus, dass Sie ein React-Entwickler sind, der bereits weiß, wie Hooks funktionieren.

Ich habe diese Methode verwendet, aber ich hatte einige andere Probleme mit den Einschränkungen des Pakets und ich erinnere mich nicht genau, was sie waren.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

aegis123 picture aegis123  ·  3Kommentare

drewwarren picture drewwarren  ·  3Kommentare

zoechi picture zoechi  ·  3Kommentare

ppolasek picture ppolasek  ·  3Kommentare

yjbanov picture yjbanov  ·  3Kommentare