Flutter: Reutilizar a lógica de estado é muito prolixo ou muito difícil

Criado em 2 mar. 2020  ·  420Comentários  ·  Fonte: flutter/flutter

.

Relacionado à discussão em torno dos ganchos # 25280

TL; DR: É difícil reutilizar a lógica State . Ou terminamos com um método build complexo e profundamente aninhado ou temos que copiar e colar a lógica em vários widgets.

Não é possível reutilizar tal lógica por meio de mixins ou funções.

Problema

Reutilizar uma lógica State em vários StatefulWidget é muito difícil, pois essa lógica depende de vários ciclos de vida.

Um exemplo típico seria a lógica de criação de TextEditingController (mas também AnimationController , animações implícitas e muito mais). Essa lógica consiste em várias etapas:

  • definindo uma variável em State .
    dart TextEditingController controller;
  • criando o controlador (geralmente dentro de initState), com potencialmente um valor padrão:
    dart <strong i="25">@override</strong> void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • descartou o controlador quando State foi descartado:
    dart <strong i="30">@override</strong> void dispose() { controller.dispose(); super.dispose(); }
  • fazendo o que quisermos com aquela variável dentro de build .
  • (opcional) expor essa propriedade em debugFillProperties :
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

Isso, em si, não é complexo. O problema começa quando queremos dimensionar essa abordagem.
Um aplicativo Flutter típico pode ter dezenas de campos de texto, o que significa que essa lógica é duplicada várias vezes.

Copiar e colar essa lógica em todos os lugares "funciona", mas cria uma fraqueza em nosso código:

  • pode ser fácil esquecer de reescrever uma das etapas (como esquecer de chamar dispose )
  • adiciona muito ruído ao código

A questão do Mixin

A primeira tentativa de fatorar essa lógica seria usar um mixin:

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

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

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

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

Então usei desta forma:

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

Mas isso tem falhas diferentes:

  • Um mixin pode ser usado apenas uma vez por aula. Se nosso StatefulWidget precisa de múltiplos TextEditingController , então não podemos mais usar a abordagem mixin.

  • O "estado" declarado pelo mixin pode entrar em conflito com outro mixin ou com o próprio State .
    Mais especificamente, se dois mixins declararem um membro usando o mesmo nome, haverá um conflito.
    Na pior das hipóteses, se os membros em conflito tiverem o mesmo tipo, isso falhará silenciosamente.

Isso torna os mixins não ideais e muito perigosos para serem uma solução verdadeira.

Usando o padrão "builder"

Outra solução pode ser usar o mesmo padrão de StreamBuilder & co.

Podemos fazer um widget TextEditingControllerBuilder , que gerencia esse controlador. Então, nosso método build pode usá-lo livremente.

Esse widget normalmente seria implementado desta forma:

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

Então usado como tal:

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

Isso resolve os problemas encontrados com mixins. Mas isso cria outros problemas.

  • O uso é muito prolixo. Isso significa efetivamente 4 linhas de código + dois níveis de indentação para uma única declaração de variável.
    Isso é ainda pior se quisermos usá-lo várias vezes. Embora possamos criar TextEditingControllerBuilder dentro de outro uma vez, isso diminui drasticamente a legibilidade do código:

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

    Esse é um código muito indentado apenas para declarar duas variáveis.

  • Isso adiciona alguma sobrecarga, pois temos uma instância extra de State e Element .

  • É difícil usar TextEditingController fora de build .
    Se quisermos que os ciclos de vida de State realizem alguma operação nesses controladores, precisaremos de GlobalKey para acessá-los. Por exemplo:

    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

Comentários muito úteis

Acrescentarei algumas reflexões da perspectiva do React.
Perdão se eles não são relevantes, mas eu queria explicar brevemente como pensamos sobre os Hooks.

Ganchos estão definitivamente "escondendo" coisas. Ou, dependendo de como você olha para ele, encapsule-os. Em particular, eles encapsulam o estado local e os efeitos (acho que nossos "efeitos" são as mesmas coisas que "descartáveis"). A "implicação" está no fato de que eles anexam automaticamente o tempo de vida ao componente dentro do qual são chamados.

Essa implicação não é inerente ao modelo. Você pode imaginar um argumento sendo explicitamente encadeado por todas as chamadas - do próprio Componente por meio de Ganchos customizados, até cada Gancho primitivo. Mas, na prática, descobrimos que era barulhento e não era realmente útil. Portanto, criamos o estado global implícito do componente atualmente em execução. Isso é semelhante a como throw em uma VM procura para cima pelo bloco catch mais próximo, em vez de você passar errorHandlerFrame no código.

Ok, então são funções com estado oculto implícito dentro delas, isso parece ruim? Mas no React, os componentes em geral também. Esse é o ponto principal dos componentes. São funções que têm um tempo de vida associado a elas (o que corresponde a uma posição na árvore da IU). A razão pela qual os componentes em si não são uma arma de fogo com relação ao estado é que você não os chama apenas a partir de um código aleatório. Você os chama de outros componentes. Portanto, sua vida útil faz sentido porque você permanece no contexto do código da IU.

No entanto, nem todos os problemas têm a forma de componentes. Os componentes combinam duas habilidades: estado + efeitos e um tempo de vida vinculado à posição da árvore. Mas descobrimos que a primeira habilidade é útil por si só. Assim como as funções são úteis em geral porque permitem encapsular código, faltava uma primitiva que nos permitisse encapsular (e reutilizar) pacotes de estado + efeitos sem necessariamente criar um novo nó na árvore. Isso é o que os ganchos são. Componentes = Ganchos + IU retornada.

Como mencionei, uma função arbitrária que esconde o estado contextual é assustadora. É por isso que impomos uma convenção por meio de um linter. Ganchos têm "cor" - se você usar um Gancho, sua função também é um Gancho. E o linter garante que apenas componentes ou outros ganchos podem usar ganchos. Isso remove o problema de funções arbitrárias que ocultam o estado contextual da interface do usuário, porque agora elas não estão mais implícitas do que os próprios componentes.

Conceitualmente, não vemos as chamadas de Hook como chamadas de função simples. Como useState() é mais use State() se tivéssemos a sintaxe. Seria um recurso de linguagem. Você pode modelar algo como Ganchos com efeitos algébricos em linguagens que possuem rastreamento de efeito. Nesse sentido, seriam funções regulares, mas o fato de "usarem" o Estado faria parte de sua assinatura de tipo. Então, você pode pensar no próprio React como um "manipulador" para esse efeito. De qualquer forma, isso é muito teórico, mas eu queria apontar para a técnica anterior em termos do modelo de programação.

Em termos práticos, existem algumas coisas aqui. Primeiro, é importante notar que os Hooks não são uma API "extra" para React. Eles são a API React para escrever componentes neste momento. Acho que concordaria que, como um recurso extra, eles não seriam muito atraentes. Então, eu não sei se eles realmente fazem sentido para o Flutter, que tem um paradigma geral indiscutivelmente diferente.

Quanto ao que eles permitem, acho que o principal recurso é a capacidade de encapsular estado + lógica eficaz e, em seguida, encadear como faria com a composição de função regular. Como as primitivas são projetadas para compor, você pode pegar alguma saída do Gancho como useState() , passá-la como uma entrada para um personalizado useGesture(state) , em seguida, passá-la como uma entrada para vários useSpring(gesture) chamadas que fornecem valores escalonados e assim por diante. Cada uma dessas peças é completamente inconsciente das outras e podem ser escritas por pessoas diferentes, mas elas são compostas bem juntas porque o estado e os efeitos são encapsulados e ficam "presos" ao Componente que os contém. Aqui está uma pequena demonstração de algo assim, e um artigo onde recapitulo brevemente o que são os Ganchos.

Quero enfatizar que não se trata de reduzir o clichê, mas da capacidade de compor pipelines de lógica encapsulada com estado. Observe que ele é totalmente reativo - ou seja, não é executado uma vez, mas reage a todas as alterações nas propriedades ao longo do tempo. Uma maneira de pensar neles é que são como plug-ins em um canal de sinal de áudio. Embora eu fique totalmente desconfiado sobre "funções que contêm memórias" na prática, não achamos que isso seja um problema, porque elas estão completamente isoladas. Na verdade, esse isolamento é sua principal característica. Caso contrário, iria desmoronar. Portanto, qualquer co-dependência deve ser expressa explicitamente, retornando e passando valores para a próxima coisa na cadeia. E o fato de que qualquer Gancho customizado pode adicionar ou remover estados ou efeitos sem quebrar (ou até mesmo afetar) seus consumidores é outro recurso importante da perspectiva da biblioteca de terceiros.

Não sei se isso foi útil, mas espero que dê alguma perspectiva sobre o modelo de programação.
Fico feliz em responder outras perguntas.

Todos 420 comentários

cc @dnfield @Hixie
Conforme solicitado, esses são os detalhes completos sobre quais são os problemas resolvidos por ganchos.

Preocupa-me que qualquer tentativa de tornar isso mais fácil dentro da estrutura, na verdade, esconda a complexidade na qual os usuários deveriam estar pensando.

Parece que parte disso poderia ser melhorado para os autores da biblioteca, se classes fortemente tipadas que precisam ser eliminadas com algum tipo de abstract class Disposable . Nesse caso, você deve ser capaz de escrever mais facilmente uma classe mais simples como esta se estiver inclinado a:

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

O que elimina algumas linhas de código repetidas. Você pode escrever uma classe abstrata semelhante para propriedades de depuração e até mesmo uma que combine as duas. Seu estado de inicialização pode ficar parecido com:

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

Estamos apenas perdendo o fornecimento de tais informações de digitação para aulas descartáveis?

Preocupa-me que qualquer tentativa de tornar isso mais fácil dentro da estrutura, na verdade, esconda a complexidade na qual os usuários deveriam estar pensando.

Os widgets escondem a complexidade na qual os usuários precisam pensar.
Não tenho certeza se isso é realmente um problema.

No final, cabe aos usuários fatorá-lo como quiserem.


O problema não é apenas sobre os descartáveis.

Isso esquece a parte de atualização do problema. A lógica de estágio também pode contar com ciclos de vida como didChangeDependencies e didUpdateWidget.

Alguns exemplos concretos:

  • SingleTickerProviderStateMixin que tem lógica dentro de didChangeDependencies .
  • AutomaticKeepAliveClientMixin, que depende de super.build(context)

Existem muitos exemplos na estrutura em que queremos reutilizar a lógica de estado:

  • StreamBuilder
  • TweenAnimationBuilder
    ...

Isso nada mais é do que uma forma de reutilizar o estado com um mecanismo de atualização.

Mas eles sofrem do mesmo problema que aqueles mencionados na parte do "construtor".

Isso causa muitos problemas.
Por exemplo, um dos problemas mais comuns no Stackoverflow são as pessoas tentando usar StreamBuilder para efeitos colaterais, como "empurrar uma rota na mudança".

E, em última análise, sua única solução é "ejetar" o StreamBuilder.
Isso involve:

  • convertendo o widget em stateful
  • escute manualmente o fluxo em initState + didUpdateWidget + didChangeDependencies
  • cancelar a assinatura anterior em didChangeDependencies / didUpdateWidget quando o fluxo muda
  • cancelar a assinatura à disposição

Isso é _diverso_ e não é efetivamente reutilizável.

Problema

Reutilizar uma lógica State em vários StatefulWidget é muito difícil, uma vez que essa lógica depende de vários ciclos de vida.

Um exemplo típico seria a lógica de criação de TextEditingController (mas também AnimationController , animações implícitas e muito mais). Essa lógica consiste em várias etapas:

  • definindo uma variável em State .
    dart TextEditingController controller;
  • criando o controlador (geralmente dentro de initState), com potencialmente um valor padrão:
    dart <strong i="19">@override</strong> void initState() { super.initState(); controller = TextEditingController(text: 'Hello world'); }
  • descartou o controlador quando State foi descartado:
    dart <strong i="24">@override</strong> void dispose() { controller.dispose(); super.dispose(); }
  • fazendo o que quisermos com aquela variável dentro de build .
  • (opcional) expor essa propriedade em debugFillProperties :
    dart void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('controller', controller)); }

Isso, em si, não é complexo. O problema começa quando queremos dimensionar essa abordagem.
Um aplicativo Flutter típico pode ter dezenas de campos de texto, o que significa que essa lógica é duplicada várias vezes.

Copiar e colar essa lógica em todos os lugares "funciona", mas cria uma fraqueza em nosso código:

  • pode ser fácil esquecer de reescrever uma das etapas (como esquecer de chamar dispose )
  • adiciona muito ruído ao código

Eu realmente tenho dificuldade em entender por que isso é um problema. Eu escrevi muitos aplicativos Flutter, mas realmente não parece ser um grande problema? Mesmo no pior caso, são quatro linhas para declarar uma propriedade, inicializá-la, descartá-la e relatá-la aos dados de depuração (e geralmente é menos, porque você geralmente pode declará-la na mesma linha que inicializa, aplicativos geralmente não precisa se preocupar em adicionar estado às propriedades de depuração, e muitos desses objetos não têm estado que precise ser descartado).

Eu concordo que um mixin por tipo de propriedade não funciona. Concordo que o padrão do construtor não é bom (ele literalmente usa o mesmo número de linhas que o pior cenário descrito acima).

Com o NNBD (especificamente com late final para que os inicializadores possam fazer referência a this ), seremos capazes de fazer algo assim:

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

Você o usaria assim:

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

Não parece realmente melhorar as coisas. Ainda são quatro linhas.

Os widgets escondem a complexidade na qual os usuários precisam pensar.

O que eles escondem?

O problema não é o número de linhas, mas quais são essas linhas.

StreamBuilder pode ter tantas linhas quanto stream.listen + setState + subscription.close .
Mas escrever StreamBuilder pode ser feito sem qualquer reflexão envolvida, por assim dizer.
Não há engano possível no processo. É apenas "passar o fluxo e construir widgets a partir dele".

Considerando que escrever o código manualmente envolve muito mais pensamentos:

  • O fluxo pode mudar com o tempo? Se nos esquecemos de cuidar disso, temos um bug.
  • Esquecemos de fechar a assinatura? Outro bug
  • Qual nome de variável devo usar para a assinatura? Esse nome pode não estar disponível
  • E quanto ao teste? Tenho que duplicar o teste? Com StreamBuilder , não há necessidade de escrever testes de unidade para ouvir o fluxo, isso seria redundante. Mas se escrevermos manualmente o tempo todo, é totalmente possível cometer um erro
  • Se ouvirmos dois fluxos ao mesmo tempo, agora temos várias variáveis ​​com nomes muito semelhantes poluindo nosso código, isso pode causar alguma confusão.

O que eles escondem?

  • FutureBuilder / StreamBuilder oculta o mecanismo de escuta e mantém o controle de qual é o instantâneo atual.
    A lógica de alternar entre dois Future é bastante complexa, considerando que não possui um subscription.close() .
  • AnimatedContainer oculta a lógica de fazer uma interpolação entre os valores anteriores e os novos.
  • Listview oculta a lógica de "montar um widget conforme ele aparece"

aplicativos geralmente não precisam se preocupar em adicionar estado às propriedades de depuração

Eles não querem, porque não querem lidar com a complexidade de manter o método debugFillProperties.
Mas se disséssemos aos desenvolvedores "Você gostaria que saísse da caixa, todos os seus parâmetros e propriedades de estado estivessem disponíveis no devtool do Flutter?" Tenho certeza que diriam sim

Muitas pessoas expressaram o desejo de um verdadeiro equivalente ao devtool do React. O devtool do Flutter ainda não chegou.
No React, podemos ver todo o estado de um widget + seus parâmetros e editá-lo, sem fazer nada.

Da mesma forma, as pessoas ficaram bastante surpresas quando eu disse a elas que ao usar provider + alguns outros pacotes meus, por padrão, todo o estado do aplicativo é visível para elas, sem ter que fazer nada ( módulo este bug irritante do devtool )

Tenho que admitir que não sou um grande fã do FutureBuilder, ele causa muitos bugs porque as pessoas não pensam em quando acionar o Future. Acho que seria razoável abandonarmos o apoio a ele. StreamBuilder está ok, eu acho, mas acho que os próprios Streams são muito complicados (como você mencionou em seu comentário acima), então ...

Por que alguém precisa pensar sobre a complexidade de criar Tweens?

ListView realmente não esconde a lógica de montar um widget conforme ele aparece; é uma grande parte da API.

O problema não é o número de linhas, mas quais são essas linhas.

Eu realmente não entendo a preocupação aqui. As linhas parecem muito simples clichês. Declare a coisa, inicialize a coisa, descarte a coisa. Se não for o número de linhas, qual é o problema?

Eu concordo com você que FutureBuilder é problemático.

É um pouco fora do assunto, mas eu sugeriria que, no desenvolvimento, o Flutter deve acionar um falso hot-reload a cada poucos segundos. Isso destacaria o uso indevido de FutureBuilder, chaves e muito mais.

Por que alguém precisa pensar sobre a complexidade de criar Tweens?

ListView realmente não esconde a lógica de montar um widget conforme ele aparece; é uma grande parte da API.

Nós concordamos com isso. Meu ponto é que não podemos criticar algo como os ganchos com "isso oculta a lógica", pois o que os ganchos fazem é estritamente equivalente ao que TweenAnimationBuilder / AnimatedContainer / ... faz.
A lógica não está escondida

No final, acho que as animações são uma boa comparação. As animações têm esse conceito de implícito versus explícito.
As animações implícitas são apreciadas por sua simplicidade, composição e legibilidade.
As animações explícitas são mais flexíveis, mas mais complexas.

Quando traduzimos este conceito para ouvir streams, StreamBuilder é uma _escuta implícita_, enquanto stream.listen é _explícito_.

Mais especificamente, com StreamBuilder você _não_ pode esquecer de lidar com o cenário em que o fluxo muda, ou esquecer de fechar a assinatura.
Você também pode combinar vários StreamBuilder juntos

stream.listen é um pouco mais avançado e mais sujeito a erros.

Os construtores são poderosos para simplificar o aplicativo.
Mas, como concordamos anteriormente, o padrão Builder não é ideal. É prolixo para escrever e usar.
Este problema, e o que os ganchos resolvem, é sobre uma sintaxe alternativa para * Builders

Por exemplo, flutter_hooks tem um equivalente estrito a FutureBuilder e StreamBuilder :

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

Na continuação, AnimatedContainer e semelhantes poderiam ser representados por useAnimatedSize / useAnimatedDecoractedBox / ... de modo que temos:

double opacity;

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

Meu ponto é que não podemos criticar algo como ganchos com "isso oculta a lógica",

Esse não é o argumento. O argumento é "isso oculta a lógica na qual os desenvolvedores deveriam estar pensando ".

Você tem um exemplo dessa lógica em que os desenvolvedores deveriam estar pensando?

Por exemplo, quem possui o TextEditingController (quem o cria, quem o descarta).

Como com este código?

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

O gancho o cria e o descarta.

Não tenho certeza do que está claro sobre isso.

Sim, exatamente. Não tenho ideia de qual é o ciclo de vida do controlador com esse código. Isso dura até o final do escopo lexical? A vida do Estado? Algo mais? Quem é o dono? Se eu passar para outra pessoa, ela pode assumir a propriedade? Nada disso é óbvio no próprio código.

Parece que seu argumento é causado mais por uma falta de entendimento sobre o que os ganchos fazem do que por um problema real.
Essas perguntas têm uma resposta claramente definida que é consistente com todos os ganchos:

Não tenho ideia de qual é o ciclo de vida do controlador com esse código

Nem você precisa pensar sobre isso. Não é mais responsabilidade do desenvolvedor.

Isso dura até o final do escopo lexical? A vida do estado

A vida do estado

Quem é o dono?

O gancho possui o controlador. É parte da API de useTextEditingController que possui o controlador.
Isso se aplica a useFocusNode , useScrollController , useAnimationController , ...

De certa forma, essas questões se aplicam a StreamBuilder :

  • Não precisamos pensar sobre os ciclos de vida do StreamSubscription
  • A assinatura dura toda a vida do Estado
  • o StreamBuilder possui o StreamSubscription

Em geral, você pode pensar em:

final value = useX(argument);

como um equivalente estrito a:

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

  },
);

Eles têm as mesmas regras e o mesmo comportamento.

Não é mais responsabilidade do desenvolvedor

Acho que, fundamentalmente, essa é a divergência aqui. Ter uma API semelhante a uma função que retorna um valor com um tempo de vida definido que não é claro é, IMHO, fundamentalmente muito diferente de uma API baseada na passagem desse valor para um fechamento.

Não tenho nenhum problema com alguém criando um pacote que usa esse estilo, mas é um estilo contrário ao tipo que eu gostaria de incluir na API de flutter principal.

@Hixie
Eu não acho que o que @rrousselGit estava dizendo é que eles são a mesma coisa, mas apenas que eles têm "as mesmas regras e o mesmo comportamento" em relação ao ciclo de vida? Correto?

Porém, eles não resolvem os mesmos problemas.

Talvez eu esteja errado aqui, mas no outono passado, quando experimentei o flutter, acredito que se eu precisasse de três desses construtores em um widget, teria havido muitos aninhamentos. Comparado a três ganchos (três linhas).
Também. Os ganchos são combináveis, portanto, se você precisar compartilhar a lógica de estado composta de vários ganchos, poderá fazer um novo gancho que use outros ganchos e alguma lógica extra e apenas usar um novo gancho.

Coisas como compartilhar lógica de estado facilmente entre widgets era algo que eu estava perdendo ao experimentar o flutter outono de 2019.

É claro que poderia haver muitas outras soluções possíveis. Talvez já tenha sido resolvido e eu simplesmente não o encontrei nos documentos.
Mas se não, há muito que pode ser feito para acelerar o desenvolvimento, se algo como ganchos ou outra solução para os mesmos problemas estiver disponível como um cidadão de primeira classe.

Definitivamente, não estou sugerindo o uso da abordagem do construtor, como o OP menciona, que apresenta todos os tipos de problemas. O que eu sugeriria é apenas usar initState / dispose. Eu realmente não entendo por que isso é um problema.

Estou curioso para saber como as pessoas se sentem sobre o código em https://github.com/flutter/flutter/issues/51752#issuecomment -664787791. Não acho que seja melhor do que initState / dispose, mas se as pessoas gostam de ganchos, também gostam disso? Os ganchos são melhores? Pior?

Os ganchos useAnimationController , não preciso mais pensar em initState e descarte. Isso remove a responsabilidade do desenvolvedor. Não preciso me preocupar se eliminei todos os controladores de animação que criei.

initState e dispose são adequados para uma única coisa, mas imagine ter que manter o controle de vários e díspares tipos de estado. Os ganchos compõem com base na unidade lógica de abstração ao invés de espalhá-los no ciclo de vida da classe.

Acho que o que você está perguntando é o equivalente a perguntar por que temos funções quando podemos cuidar manualmente dos efeitos todas as vezes. Eu concordo que não é exatamente o mesmo, mas em geral parece semelhante. Parece que você nunca usou ganchos antes, então os problemas não parecem muito aparentes para você, então eu o encorajaria a fazer um projeto de tamanho pequeno ou médio usando ganchos, com o pacote flutter_hooks talvez, e veja como se sente. Digo isso com todo o respeito, como um usuário do Flutter, deparei com esses problemas para os quais os ganchos fornecem soluções, assim como outros. Não tenho certeza de como convencê-lo de que esses problemas realmente existem para nós, deixe-nos saber se há uma maneira melhor.

Acrescentarei algumas reflexões da perspectiva do React.
Perdão se eles não são relevantes, mas eu queria explicar brevemente como pensamos sobre os Hooks.

Ganchos estão definitivamente "escondendo" coisas. Ou, dependendo de como você olha para ele, encapsule-os. Em particular, eles encapsulam o estado local e os efeitos (acho que nossos "efeitos" são as mesmas coisas que "descartáveis"). A "implicação" está no fato de que eles anexam automaticamente o tempo de vida ao componente dentro do qual são chamados.

Essa implicação não é inerente ao modelo. Você pode imaginar um argumento sendo explicitamente encadeado por todas as chamadas - do próprio Componente por meio de Ganchos customizados, até cada Gancho primitivo. Mas, na prática, descobrimos que era barulhento e não era realmente útil. Portanto, criamos o estado global implícito do componente atualmente em execução. Isso é semelhante a como throw em uma VM procura para cima pelo bloco catch mais próximo, em vez de você passar errorHandlerFrame no código.

Ok, então são funções com estado oculto implícito dentro delas, isso parece ruim? Mas no React, os componentes em geral também. Esse é o ponto principal dos componentes. São funções que têm um tempo de vida associado a elas (o que corresponde a uma posição na árvore da IU). A razão pela qual os componentes em si não são uma arma de fogo com relação ao estado é que você não os chama apenas a partir de um código aleatório. Você os chama de outros componentes. Portanto, sua vida útil faz sentido porque você permanece no contexto do código da IU.

No entanto, nem todos os problemas têm a forma de componentes. Os componentes combinam duas habilidades: estado + efeitos e um tempo de vida vinculado à posição da árvore. Mas descobrimos que a primeira habilidade é útil por si só. Assim como as funções são úteis em geral porque permitem encapsular código, faltava uma primitiva que nos permitisse encapsular (e reutilizar) pacotes de estado + efeitos sem necessariamente criar um novo nó na árvore. Isso é o que os ganchos são. Componentes = Ganchos + IU retornada.

Como mencionei, uma função arbitrária que esconde o estado contextual é assustadora. É por isso que impomos uma convenção por meio de um linter. Ganchos têm "cor" - se você usar um Gancho, sua função também é um Gancho. E o linter garante que apenas componentes ou outros ganchos podem usar ganchos. Isso remove o problema de funções arbitrárias que ocultam o estado contextual da interface do usuário, porque agora elas não estão mais implícitas do que os próprios componentes.

Conceitualmente, não vemos as chamadas de Hook como chamadas de função simples. Como useState() é mais use State() se tivéssemos a sintaxe. Seria um recurso de linguagem. Você pode modelar algo como Ganchos com efeitos algébricos em linguagens que possuem rastreamento de efeito. Nesse sentido, seriam funções regulares, mas o fato de "usarem" o Estado faria parte de sua assinatura de tipo. Então, você pode pensar no próprio React como um "manipulador" para esse efeito. De qualquer forma, isso é muito teórico, mas eu queria apontar para a técnica anterior em termos do modelo de programação.

Em termos práticos, existem algumas coisas aqui. Primeiro, é importante notar que os Hooks não são uma API "extra" para React. Eles são a API React para escrever componentes neste momento. Acho que concordaria que, como um recurso extra, eles não seriam muito atraentes. Então, eu não sei se eles realmente fazem sentido para o Flutter, que tem um paradigma geral indiscutivelmente diferente.

Quanto ao que eles permitem, acho que o principal recurso é a capacidade de encapsular estado + lógica eficaz e, em seguida, encadear como faria com a composição de função regular. Como as primitivas são projetadas para compor, você pode pegar alguma saída do Gancho como useState() , passá-la como uma entrada para um personalizado useGesture(state) , em seguida, passá-la como uma entrada para vários useSpring(gesture) chamadas que fornecem valores escalonados e assim por diante. Cada uma dessas peças é completamente inconsciente das outras e podem ser escritas por pessoas diferentes, mas elas são compostas bem juntas porque o estado e os efeitos são encapsulados e ficam "presos" ao Componente que os contém. Aqui está uma pequena demonstração de algo assim, e um artigo onde recapitulo brevemente o que são os Ganchos.

Quero enfatizar que não se trata de reduzir o clichê, mas da capacidade de compor pipelines de lógica encapsulada com estado. Observe que ele é totalmente reativo - ou seja, não é executado uma vez, mas reage a todas as alterações nas propriedades ao longo do tempo. Uma maneira de pensar neles é que são como plug-ins em um canal de sinal de áudio. Embora eu fique totalmente desconfiado sobre "funções que contêm memórias" na prática, não achamos que isso seja um problema, porque elas estão completamente isoladas. Na verdade, esse isolamento é sua principal característica. Caso contrário, iria desmoronar. Portanto, qualquer co-dependência deve ser expressa explicitamente, retornando e passando valores para a próxima coisa na cadeia. E o fato de que qualquer Gancho customizado pode adicionar ou remover estados ou efeitos sem quebrar (ou até mesmo afetar) seus consumidores é outro recurso importante da perspectiva da biblioteca de terceiros.

Não sei se isso foi útil, mas espero que dê alguma perspectiva sobre o modelo de programação.
Fico feliz em responder outras perguntas.

Definitivamente, não estou sugerindo o uso da abordagem do construtor, como o OP menciona, que apresenta todos os tipos de problemas. O que eu sugeriria é apenas usar initState / dispose. Eu realmente não entendo por que isso é um problema.

Estou curioso para saber como as pessoas se sentem sobre o código em # 51752 (comentário) . Não acho que seja melhor do que initState / dispose, mas se as pessoas gostam de ganchos, também gostam disso? Os ganchos são melhores? Pior?

A palavra-chave late torna as coisas melhores, mas ainda apresenta alguns problemas:

Tal Property pode ser útil para estados autocontidos ou que não dependem de parâmetros que podem mudar com o tempo. Mas pode ser difícil de usar quando em uma situação diferente.
Mais precisamente, falta a parte de "atualização".

Por exemplo, com StreamBuilder a transmissão ouvida pode mudar com o tempo. Mas não existe uma solução fácil para implementar tal coisa aqui, já que o objeto é inicializado apenas uma vez.

Da mesma forma, ganchos têm um equivalente a Key do Widget - o que pode fazer com que uma parte do estado seja destruída e recriada quando essa chave muda.

Um exemplo disso é useMemo , que é um gancho que armazena em cache uma instância do objeto.
Combinado com as chaves, podemos usar useMemo para obter a busca de dados implícita.
Por exemplo, nosso widget pode receber um ID de mensagem - que usamos para buscar os detalhes da mensagem. Mas esse ID de mensagem pode mudar com o tempo, então podemos precisar buscar novamente os detalhes.

Com useMemo , isso pode ser parecido com:

String messageId;

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

}

Nesta situação, mesmo se o método de construção for chamado novamente 10 vezes, desde que messageId não mude, a busca de dados não é executada novamente.
Mas quando o messageId muda, um novo Future é criado.


É importante notar que não acho que flutter_hooks em seu estado atual seja refinado para Dart. Minha implementação é mais um POC do que uma arquitetura completa.
Mas acredito que temos um problema com a capacidade de reutilização do código de StatefulWidgets.

Não me lembrava de onde, mas me lembro de sugerir que os ganchos no mundo ideal seriam um gerador de função personalizada, próximo a async* & sync* , que pode ser semelhante ao que Dan sugeriu com use State vez de useState

@gaearon

Quero enfatizar que não se trata de reduzir o clichê, mas da capacidade de compor pipelines de lógica encapsulada com estado.

Esse não é o problema que está sendo discutido aqui. Eu recomendo preencher um bug separado para falar sobre a incapacidade de fazer o que você descreve. (Isso soa como um problema muito diferente e, honestamente, mais convincente do que o descrito aqui.) Esse bug é especificamente sobre como parte da lógica é muito prolixa.

Não, ele está certo, é a minha formulação que pode ser confusa.
Como mencionei anteriormente, não se trata do número de linhas de código, mas das próprias linhas de código.

Trata-se de fatorar o estado.

Esse bug é extremamente claro sobre o problema de "Reutilizar a lógica de estado é muito detalhado / difícil" e sobre como há muito código em um estado quando você tem uma propriedade que precisa ter código para declará-la, em initState, em dispor e em debugFillProperties. Se o problema com o qual você se preocupa for algo diferente, recomendo preencher um novo bug que descreva esse problema.

Eu realmente recomendo fortemente esquecer os ganchos (ou qualquer solução) até que você entenda completamente o problema que deseja resolver. É apenas tendo uma compreensão clara do problema que você será capaz de articular um argumento convincente a favor de um novo recurso, porque devemos avaliar os recursos em relação aos problemas que eles resolvem.

Acho que você não entendeu bem o que eu disse naquela edição.

O problema não é clichê, mas capacidade de reutilização.

O padrão é uma consequência de um problema de reutilização, não a causa

O que esse problema descreve é:

Podemos querer reutilizar / compor a lógica de estado. Mas as opções disponíveis são mixins, Builders ou não reutilizá-lo - todos com seus próprios problemas.

Os problemas das opções existentes podem estar relacionados ao boilerplate, mas o problema que estamos tentando resolver não.
Embora a redução do clichê dos Construtores seja um caminho (que é o que os ganchos fazem), pode haver um caminho diferente.

Por exemplo, algo que eu queria sugerir por um tempo era adicionar métodos como:

context.onDidChangeDependencies(() {

});
context.onDispose(() {

});

Mas eles têm seus próprios problemas e não resolvem totalmente o problema, então não resolvi.

@rrousselGit , sinta-se à vontade para editar a declaração do problema original no topo aqui para refletir melhor o problema. Sinta-se à vontade para criar um documento de design: https://flutter.dev/docs/resources/design-docs que podemos iterar juntos (novamente, como @Hixie sugere, focando agora na exposição mais

Examinei o problema novamente algumas vezes. Com toda a honestidade, não entendo de onde vem o mal-entendido, então não tenho certeza do que melhorar.
O comentário original menciona repetidamente o desejo de reutilização / fatoração. As menções sobre o clichê não são "Flutter é prolixo", mas "Algumas lógicas não são reutilizáveis"

Não acho que a sugestão do documento de design seja justa. Leva muito tempo para escrever tal documento, e estou fazendo isso no meu tempo livre.
Estou pessoalmente satisfeito com os ganchos. Não estou escrevendo essas questões no meu interesse, mas para aumentar a conscientização sobre um problema que afeta um número significativo de pessoas.

Algumas semanas atrás, fui contratado para discutir a arquitetura de um aplicativo Flutter existente. Provavelmente foi exatamente o que é mencionado aqui:

  • Eles têm alguma lógica que precisa ser reutilizada em vários widgets (manipulação de estados de carregamento / marcação de "mensagens" como lidas quando alguns widgets se tornam visíveis / ...)
  • Eles tentaram usar mixins, o que causou grandes falhas de arquitetura.
  • Eles também tentaram manipular manualmente o "criar / atualizar / descartar" reescrevendo essa lógica em vários locais, mas isso causou bugs.
    Em alguns lugares, eles se esqueceram de fechar as assinaturas. Em outros, eles não lidaram com o cenário em que sua instância stream muda
  • marcando "mensagens" como lidas quando alguns widgets se tornam visíveis

É um caso interessante porque é semelhante a problemas que tive em um de meus próprios aplicativos, então observei como implementei o código lá e realmente não vejo muitos dos problemas descritos por esse bug, que podem é por isso que estou tendo problemas para entender o problema. Este é o código em questão:

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

Você tem exemplos de aplicativos reais que eu poderia estudar para ver o problema em ação?

(Aliás, em geral, eu recomendo fortemente não usar Streams. Acho que geralmente tornam as coisas piores.)

(Aliás, em geral, eu recomendo fortemente não usar Streams. Acho que geralmente tornam as coisas piores.)

(Eu concordo plenamente. Mas a comunidade atualmente tem a reação oposta. Talvez extrair ChangeNotifier / Listenable / ValueNotifier do Flutter em um pacote oficial ajude)

Você tem exemplos de aplicativos reais que eu poderia estudar para ver o problema em ação?

Infelizmente não. Só posso compartilhar a experiência que tive ao ajudar outras pessoas. Não tenho um aplicativo disponível.

É um caso interessante porque é semelhante a problemas que tive em um de meus próprios aplicativos, então observei como implementei o código lá e realmente não vejo muitos dos problemas descritos por esse bug, que podem é por isso que estou tendo problemas para entender o problema. Este é o código em questão:

Em sua implementação, a lógica não está ligada a nenhum ciclo de vida e colocada dentro de _build_, então meio que contorna o problema.
Pode fazer sentido nesse caso específico. Não tenho certeza se esse exemplo foi bom.

Um exemplo melhor pode ser puxar para atualizar.

Em um puxar para atualizar típico, queremos:

  • na primeira compilação, lidar com os estados de carregamento / erro
  • na atualização:

    • se a tela estava em estado de erro, mostra a tela de carregamento mais uma vez

    • se a atualização foi realizada durante o carregamento, cancele as solicitações HTTP pendentes

    • se a tela mostrasse alguns dados:

    • continue mostrando os dados enquanto o novo estado está carregando

    • se a atualização falhar, continue mostrando os dados obtidos anteriormente e mostre um snackbar com o erro

    • se o usuário abrir e entrar novamente na tela enquanto a atualização estiver pendente, mostre a tela de carregamento

    • certifique-se de que o RefreshIndicator diz visível enquanto a atualização está pendente

E queremos implementar esse recurso para todos os recursos e várias telas. Além disso, algumas telas podem desejar atualizar vários recursos de uma vez.

ChangeNotifier + provider + StatefulWidget terá muitas dificuldades para fatorar essa lógica.

Enquanto meus últimos experimentos (que são baseados na imutabilidade e dependem de flutter_hooks ) oferecem suporte a todo o espectro pronto para uso:

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

Essa lógica é totalmente independente. Ele pode ser reutilizado com qualquer recurso dentro de qualquer tela.

E se uma tela quiser atualizar vários recursos de uma vez, podemos fazer:

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

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

Eu recomendaria colocar toda essa lógica no estado do aplicativo, fora dos widgets, e apenas fazer com que o estado do aplicativo refletisse o estado atual do aplicativo. Puxar para atualizar não precisa de estado dentro do widget, ele apenas tem que informar ao estado do ambiente que uma atualização está pendente e então esperar que seu futuro seja concluído.

Não é responsabilidade do estado do ambiente determinar como processar um erro vs carregamento vs dados

Ter essa lógica no estado ambiente não remove todas as lógicas da IU
A IU ainda precisa determinar se deve mostrar o erro em tela inteira ou em uma lanchonete
Ele ainda precisa forçar a atualização de erros quando a página for recarregada

E isso é menos reutilizável.
Se a lógica de renderização estiver totalmente definida nos widgets, em vez de no estado ambiente, ela funcionará com _qualquer_ Futuree pode até mesmo ser incluído diretamente no Flutter.

Eu realmente não entendo o que você está defendendo em seu último comentário. Meu ponto é que você não precisa de mudanças na estrutura para fazer algo tão simples como o código do indicador de atualização acima, como é demonstrado pelo código que citei anteriormente.

Se tivermos muitos desses tipos de interações, não apenas para indicadores de atualização, mas para animações e outros, é melhor encapsulá-los onde estão mais próximos de serem necessários em vez de colocá-los no estado do aplicativo, porque o estado do aplicativo não precisa saber as especificações de cada interação no aplicativo se não for necessário em vários locais no aplicativo.

Não acho que estamos concordando sobre a complexidade do recurso e sua capacidade de reutilização.
Você tem um exemplo que mostre que tal recurso é fácil?

Vinculei à fonte de um aplicativo que escrevi acima. Certamente não é um código perfeito e pretendo reescrever partes dele para a próxima versão, mas não tive os problemas que você descreve nesta edição.

Mas você é um dos líderes de tecnologia do Flutter.
Mesmo diante de um problema, você teria habilidade suficiente para encontrar uma solução imediatamente.

Ainda assim, por outro lado, um número significativo de pessoas não entende o que há de errado com o seguinte código:

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

Este fato é comprovado pela popularidade de um Q / AI feito no StackOverflow .

O problema não é que seja impossível abstrair a lógica de estado de uma forma reutilizável e robusta (caso contrário, não há por que fazer essa questão).
O problema é que isso requer tempo e experiência para fazer isso.

Ao fornecer uma solução oficial, isso reduz a probabilidade de um aplicativo acabar impossibilitado de manter - o que aumenta a produtividade geral e a experiência do desenvolvedor.
Nem todo mundo poderia apresentar sua sugestão de propriedade . Se tal coisa fosse construída dentro do Flutter, seria documentada, teria visibilidade e, por fim, ajudaria pessoas que nunca teriam pensado nisso.

O problema é que realmente depende de como é seu aplicativo, de como é seu estado e assim por diante. Se a questão aqui for apenas "como você gerencia o estado do aplicativo", então a resposta não é nada como ganchos, é muita documentação falando sobre diferentes maneiras de fazer isso e recomendando diferentes técnicas para diferentes situações ... basicamente, este conjunto de documentos: https://flutter.dev/docs/development/data-and-backend/state-mgmt

Há estado efêmero e de aplicativo, mas parece haver outro caso de uso também: estado que diz respeito apenas a um único tipo de widget, mas que você deseja compartilhar entre esse tipo de widget.

Por exemplo, ScrollController pode invocar algum tipo de animação, mas não é necessariamente apropriado colocar isso no estado global do aplicativo, porque não são dados que precisam ser usados ​​em todo o aplicativo. No entanto, vários ScrollController s podem ter a mesma lógica e você deseja compartilhar essa lógica de ciclo de vida entre cada um deles. O estado ainda é de apenas ScrollController s, portanto, não o estado global do aplicativo, mas copiar e colar a lógica está sujeito a erros.

Além disso, você pode querer empacotar essa lógica para torná-la mais combinável para seus projetos futuros, mas também para outros. Se você observar o site useHooks , verá muitas peças de lógica que compartimentam certas ações comuns. Se você usar useAuth você o escreve uma vez e nunca precisa se preocupar se perdeu uma chamada de initState ou dispose , ou se a função assíncrona tem um then e catch . A função é escrita apenas uma vez, de modo que a margem de erro basicamente desaparece. Portanto, esse tipo de solução não só é mais combinável para várias partes do mesmo aplicativo e entre vários aplicativos, mas também é mais seguro para o programador final.

Não tenho objeções às pessoas que usam ganchos. Pelo que eu posso dizer, nada está impedindo isso. (Se algo _está_ impedindo isso, então, por favor, registre um bug sobre isso.)

Este bug não é sobre ganchos, é sobre "Reutilizar a lógica de estado é muito prolixo / difícil", e ainda estou lutando para entender por que isso requer alterações no Flutter. Houve muitos exemplos (incluindo ganchos) mostrando como é possível evitar o detalhamento estruturando o aplicativo de uma forma ou de outra, e já há muita documentação sobre isso.

Entendo, então você está perguntando por que, se existe algo como um pacote de ganchos que já foi construído sem alterações no Flutter, precisa haver uma solução original para ganchos? Suponho que @rrousselGit possa responder melhor a isso, mas a resposta provavelmente envolve um suporte melhor, um suporte mais integrado e mais pessoas os usando.

Posso concordar com você que, além disso, também estou confuso por que quaisquer mudanças fundamentais precisam ser feitas no Flutter para suportar ganchos, uma vez que aparentemente o pacote flutter_hooks já existe.

Ainda estou lutando para entender por que isso requer alterações no Flutter.

Dizer que esse problema está resolvido porque a comunidade fez um pacote é como dizer que o Dart não precisa de classes de dados + tipos de união porque eu fiz o Freezed .
Freezed pode ser bastante apreciado pela comunidade como uma solução para esses dois problemas, mas ainda podemos fazer melhor.

A equipe do Flutter tem muito mais influência do que a comunidade jamais terá. Você tem a capacidade de modificar a pilha inteira; pessoas que são especialistas em cada parte individual; e um salário para patrocinar o trabalho necessário.

Este problema precisa disso.
Lembre-se: um dos objetivos da equipe React é que os ganchos façam parte da linguagem, como no JSX.

Mesmo sem suporte a idiomas, ainda precisamos trabalhar no analisador; dartpad; flutter / devtools; e muitos ganchos para simplificar todas as coisas diferentes que o Flutter faz (como animações implícitas, formulários e muito mais).

Esse é um bom argumento, concordo, embora a filosofia geral do Flutter tenha um pequeno núcleo. Por esse motivo, temos adicionado cada vez mais novas funcionalidades como pacotes, mesmo quando se trata do Google, personagens cf e animações . Isso nos dá maior flexibilidade para aprender e mudar ao longo do tempo. Faríamos o mesmo para este espaço, a menos que haja uma razão técnica convincente para que um pacote seja insuficiente (e com métodos de extensão, isso é ainda menos provável do que nunca).

Colocar as coisas no âmago do Flutter é complicado. Um desafio é, como você sabe por experiência própria, que o estado é uma área que está evoluindo à medida que todos nós aprendemos mais sobre o que funciona bem em uma arquitetura de IU reativa. Dois anos atrás, se tivéssemos sido forçados a escolher um vencedor, poderíamos ter selecionado BLoC, mas é claro que seu pacote de provedor assumiu o controle e agora é nossa recomendação padrão.

Eu poderia confortavelmente conceber colaboradores empregados pelo Google apoiando flutter_hooks ou um pacote de ganchos semelhante que tivesse tração (embora tenhamos muitos outros trabalhos que estão competindo por nossa atenção , obviamente). Em particular, deveríamos. Se você está nos procurando para assumir o controle de você, essa é obviamente uma pergunta diferente.

Argumento interessante, @timsneath. A comunidade Rust também faz algo semelhante, porque uma vez introduzida na biblioteca central ou padrão de uma linguagem ou estrutura, é muito difícil retirá-la. No caso do Rust, é impossível, pois eles querem manter a compatibilidade com as versões anteriores para sempre. Portanto, eles esperam até que os pacotes cheguem e competem entre si até que apenas alguns vencedores apareçam, então eles dobram isso para o idioma.

Este poderia ser um caso semelhante com Flutter. Pode haver algo melhor do que os ganchos mais tarde, assim como o React teve que passar de classes para ganchos, mas ainda teve que manter as classes e as pessoas tiveram que migrar. Então, pode ser melhor ter soluções de gerenciamento de estado concorrentes antes de serem adicionadas ao núcleo. E talvez nós, a comunidade, devamos inovar em cima de ganchos ou tentar encontrar soluções ainda melhores.

Eu entendo essa preocupação, mas não se trata de uma solução de gerenciamento de estado.

Esse recurso está mais próximo de Inheritedwidget & StatefulWidget. É um primitivo de baixo nível, que poderia ser tão baixo quanto um recurso de linguagem.

Os ganchos podem ser independentes da estrutura, mas isso apenas por sorte.
Como mencionei antes, outro caminho para esse problema pode ser:

context.onDispose(() {

});

E ouvintes de eventos semelhantes.
Mas isso é impossível de implementar fora da estrutura.

Eu não sei o que a equipe iria propor.
Mas não podemos excluir a possibilidade de que tal solução teria que estar diretamente ao lado do Elemento

As extensões ajudam nisso?

(Talvez devêssemos falar sobre isso em um outro problema, no entanto. É meio fora do assunto aqui. Eu realmente preferiria se tivéssemos um problema por problema que as pessoas estão vendo, para que pudéssemos discutir as soluções no lugar certo. Não é claro como context.onDispose ajudaria com o detalhamento.)

Eu suspeito fortemente que existem algumas propostas de linguagem realmente boas que poderíamos apresentar relacionadas a isso.

Acho que seria útil falar sobre eles mais especificamente do que como eles podem habilitar um idioma específico de gerenciamento de estado. Poderíamos então considerar mais seriamente o que eles possibilitariam e quais compensações poderiam acarretar.

Em particular, poderíamos considerar como e se eles poderiam funcionar em tempos de execução de VM e JS

Não está claro como context.onDispose ajudaria com o detalhamento.)

Como mencionei antes, esse problema está mais relacionado à reutilização de código do que ao detalhamento. Mas se pudermos reutilizar mais código, isso deve reduzir implicitamente o detalhamento.

A forma context.onDispose está relacionado a este problema é, com a sintaxe atual que temos:

AnimationController controller;

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

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

O problema é:

  • isso está fortemente acoplado à definição de classe, portanto, não pode ser reutilizado
  • conforme o widget cresce, a relação entre a inicialização e o descarte se torna mais difícil de ler, pois há centenas de linhas de código no meio.

Com context.onDispose , poderíamos fazer:

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

A parte interessante é:

  • isso não está mais fortemente acoplado à definição de classe, portanto, pode ser extraído em uma função.
    Poderíamos teoricamente ter uma lógica semi-complexa:
    `` `dardo
    AnimationController someReusableLogic (BuildContext context) {
    controlador final = Controlador de animação (...);
    controlador.onDispor (controlador.dispor);
    controller.forward ();
    void listener () {}
    controller.addListener (ouvinte);
    context.onDispose (() => controller.removeListener (ouvinte));
    }
    ...

@sobrepor
void initState () {
controlador = someReusableLogic (contexto);
}
`` `

  • toda a lógica é agrupada. Mesmo que o widget cresça para 300, a lógica de controller ainda é facilmente legível.

O problema com esta abordagem é:

  • context.myLifecycle(() {...}) não é recarregável a quente
  • não está claro como fazer someReusableLogic ler as propriedades de StatefulWidget sem acoplar firmemente a função à definição do widget.
    Por exemplo, AnimationController 's Duration pode ser passado como um parâmetro do widget. Portanto, precisamos lidar com o cenário em que a duração muda.
  • não está claro como implementar uma função que retorna um objeto que pode mudar ao longo do tempo, sem ter que recorrer a um ValueNotifier e lidar com ouvintes

    • Isso é especialmente importante para estados computados.


Vou pensar em uma proposta de linguagem. Tenho algumas ideias, mas nada digno de falar agora.

Como mencionei antes, esse problema é mais sobre reutilização de código do que verbosidade

OK. Você pode registrar um novo bug que fale sobre isso especificamente? Este bug é literalmente chamado de "Reutilizar a lógica de estado é muito prolixo / difícil". Se o detalhamento não é o problema, _este_ não é o problema.

Com context.onDispose , poderíamos fazer:

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

Não sei por que context é relevante nisso (e onDispose viola nossas convenções de nomenclatura). Se você deseja apenas uma maneira de registrar os itens a serem executados durante o descarte, pode fazer isso facilmente hoje:

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

Chame assim:

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

Voce tambem pode fazer isso:

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

O problema com esta abordagem é:

  • context.myLifecycle(() {...}) não é recarregável a quente

Neste contexto, não parece importar, pois é apenas para coisas chamadas em initState? Estou esquecendo de algo?

  • não está claro como fazer someReusableLogic ler as propriedades de StatefulWidget sem acoplar firmemente a função à definição do widget.
    Por exemplo, AnimationController 's Duration pode ser passado como um parâmetro do widget. Portanto, precisamos lidar com o cenário em que a duração muda.

É muito simples adicionar uma fila didChangeWidget como a fila dispose:

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

Usado assim:

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)),
      ),
    );
  }
}
  • não está claro como implementar uma função que retorna um objeto que pode mudar ao longo do tempo, sem ter que recorrer a um ValueNotifier e lidar com ouvintes

    • Isso é especialmente importante para estados computados.

Não tenho certeza do que isso significa aqui, o que há de errado com ValueNotifier e, digamos, um ValueListenableBuilder?

Como mencionei antes, esse problema é mais sobre reutilização de código do que verbosidade

OK. Você pode registrar um novo bug que fale sobre isso especificamente? Este bug é literalmente chamado de "Reutilizar a lógica de estado é muito prolixo / difícil". Se o detalhamento não é o problema, então esse não é o problema.

Estou começando a ficar bastante desconfortável com essa discussão. Já respondi a este ponto antes:
O tópico desta edição é a reutilização e o detalhamento é discutido como consequência de um problema de reutilização; não como o tópico principal.

Há apenas um único ponto no comentário superior mencionando a verbosidade, e isso é com o StreamBuilder, visando principalmente os 2 níveis de indentações.

Não tenho certeza porque o contexto é relevante nisso [...]. Se você deseja apenas uma maneira de registrar os itens a serem executados durante o descarte, pode fazer isso facilmente hoje:

Quando mencionei context.onDispose , mencionei explicitamente que não acho que seja uma boa solução.
Expliquei porque você perguntou como isso se relaciona com a discussão.

Quanto ao motivo de context vez de StateHelper , é porque isso é mais flexível (como trabalhar com StatelessWidget)

context.myLifecycle (() {...}) não é recarregável a quente

Neste contexto, não parece importar, pois é apenas para coisas chamadas em initState? Estou esquecendo de algo?

Podemos mudar:

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

em:

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

Isso não aplicará as alterações ao retorno de chamada myLifecycle .

Mas se usássemos:

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

então o hot-reload funcionaria.

Não tenho certeza do que isso significa aqui, o que há de errado com ValueNotifier e, digamos, um ValueListenableBuilder?

Essa sintaxe foi projetada para evitar o uso de Builders, portanto, voltamos ao problema original.

Além disso, se realmente quisermos tornar nossa função combinável, em vez de sua sugestão ValueGetter + queueDidUpdateWidget , as funções terão que tomar ValueNotifier como parâmetro:

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

pois podemos querer obter isAnimating de algum lugar diferente de didUpdateWidget dependendo de qual widget está usando esta função.
Em um lugar, pode ser didUpdateWidget; em outro, pode ser didChangeDependencies; e em outro lugar pode estar dentro do retorno de chamada de stream.listen .

Mas então precisamos de uma maneira de converter esses cenários em ValueNotifier facilmente e fazer nossa função ouvir esse notificador.
Portanto, estamos tornando nossa vida significativamente mais difícil.
É mais confiável e fácil usar um ConditionalAnimatorBuilder que esse padrão, eu acho.

Quanto ao motivo de context vez de StateHelper , é porque isso é mais flexível (como trabalhar com StatelessWidget)

StatelessWidget é para, bem, widgets sem estado. A questão toda é que eles não criariam estado, descartariam coisas, reagiriam em didUpdateWidget, etc.

É a coisa do recarregamento a quente, sim. É por isso que usamos métodos em vez de colocar fechamentos em initState.

Sinto muito continuar dizendo isso e entendo que deve ser frustrante, mas ainda não entendo qual é o problema que estamos tentando resolver aqui. Achei que fosse verbosidade, de acordo com o resumo do bug original e uma grande parte da descrição original, mas entendo que não é isso. Então qual é o problema? Parece que existem muitos desejos mutuamente exclusivos aqui, espalhados pelos muitos comentários neste bug:

  • A declaração de como descartar algo deve ser feita no mesmo lugar que o aloca ...
  • ... e o local que o aloca precisa ser executado apenas uma vez, já que está alocando ...
  • ... e precisa funcionar com hot reload (que, por definição, não executa novamente o código que é executado apenas uma vez) ...
  • ... e precisa ser capaz de criar um estado que funcione com widgets sem estado (que por definição não têm estado) ...
  • ... e ele precisa permitir a conexão com coisas como didUpdateWidget e didChangeDependencies ...

Esta dança iterativa em que estamos envolvidos aqui não é uma maneira produtiva de fazer as coisas. Como tentei dizer antes, a melhor maneira de conseguir algo aqui é descrever o problema que você está enfrentando de uma forma que possamos entender, com todas as necessidades descritas em um lugar e explicadas com casos de uso. Recomendo não listar soluções, especialmente soluções que você sabe que não atendem às suas necessidades. Apenas certifique-se de que a necessidade que torna essas soluções inadequadas esteja listada na descrição.

Para ser honesto, fundamentalmente parece-me que você está pedindo um design de estrutura totalmente diferente. Isso está perfeitamente bem, mas não é Flutter. Se fizéssemos um framework diferente, seria, bem, um framework diferente, e ainda temos muito trabalho a fazer _este_ framework. Na verdade, muito do que você descreve é ​​muito semelhante a como o Jetpack Compose foi projetado. Não sou um grande fã desse design porque ele requer mágica do compilador, então depurar o que está acontecendo é realmente difícil, mas talvez seja mais fácil para você?

Parece que existem muitos desejos mutuamente exclusivos aqui, espalhados pelos muitos comentários neste bug:

Eles não são mutuamente exclusivos. Os ganchos fazem cada um desses. Não vou entrar em detalhes, pois não queremos nos concentrar em soluções, mas eles marcam todas as caixas.

Como tentei dizer antes, a melhor maneira de conseguir algo aqui é descrever o problema que você está enfrentando de uma forma que possamos entender, com todas as necessidades descritas em um lugar e explicadas com casos de uso.

Ainda não consigo entender como esse comentário principal falha em fazer isso.
Não está claro para mim o que não está claro para os outros.

Na verdade, muito do que você descreve é ​​muito semelhante a como o Jetpack Compose foi projetado. Não sou um grande fã desse design porque ele requer mágica do compilador, então depurar o que está acontecendo é realmente difícil, mas talvez seja mais fácil para você?

Não estou familiarizado com isso, mas com uma pesquisa rápida, diria _sim_.

Eles não são mutuamente exclusivos.

Todos os pontos que listei acima são parte do problema que estamos tentando resolver aqui?

mas eles marcam todas as caixas

Você pode listar as caixas?

Ainda não consigo entender como esse comentário principal falha em fazer isso.

Por exemplo, o OP diz explicitamente que o problema é sobre StatefulWidgets, mas um dos comentários recentes sobre esse problema disse que uma sugestão específica não era boa porque não funcionava com StatelessWidgets.

No OP você diz:

É difícil reutilizar a lógica State . Ou terminamos com um método build complexo e profundamente aninhado ou temos que copiar e colar a lógica em vários widgets.

Portanto, suponho que os requisitos incluem:

  • A solução não deve ser profundamente aninhada.
  • A solução não deve exigir muitos códigos semelhantes em locais que tentam adicionar estado.

O primeiro ponto (sobre aninhamento) parece bom. Definitivamente, não estou tentando sugerir que devemos fazer coisas que estão profundamente aninhadas. (Dito isso, podemos discordar sobre o que está profundamente aninhado; isso não está definido aqui. Outros comentários posteriores implicam que os construtores causam código profundamente aninhado, mas na minha experiência os construtores são muito bons, conforme mostrado no código que citei anteriormente.)

O segundo ponto parece ser um requisito de que não temos verbosidade. Mas então você explicou várias vezes que não se trata de verbosidade.

A próxima declaração do OP que descreve um problema é:

Reutilizar uma lógica State em vários StatefulWidget é muito difícil, pois essa lógica depende de vários ciclos de vida.

Honestamente, eu realmente não sei o que isso significa. "Difícil" para mim geralmente significa que algo envolve muita lógica complicada que é difícil de entender, mas alocar, descartar e reagir aos eventos do ciclo de vida é muito simples. A próxima declaração que dá um problema (aqui estou pulando o exemplo que é explicitamente descrito como "não complexo" e, portanto, presumivelmente não é uma descrição do problema) é:

O problema começa quando queremos dimensionar essa abordagem.

Isso me sugeriu que por "muito difícil" você quis dizer "muito prolixo" e que a dificuldade veio do fato de haver muitas ocorrências de código semelhante, uma vez que a única diferença entre o exemplo "não complexo" que você deu e o exemplo "muito difícil "O resultado de dimensionar o exemplo é literalmente apenas que o mesmo código acontece muitas vezes (ou seja, verbosidade, código clichê).

Isso é ainda confirmado pela próxima declaração que descreve um problema:

Copiar e colar essa lógica em todos os lugares "funciona", mas cria uma fraqueza em nosso código:

  • pode ser fácil esquecer de reescrever uma das etapas (como esquecer de chamar dispose )

Presumivelmente, é muito difícil porque o detalhamento torna mais fácil cometer um erro ao copiar e colar o código. Mas, novamente, quando tentei resolver esse problema, que eu descreveria como "verbosidade", você disse que o problema não era a verbosidade.

  • adiciona muito ruído ao código

Novamente, isso soa como apenas dizer verbosidade / clichê para mim, mas novamente você explicou que não é isso.

O resto do OP descreve apenas soluções das quais você não gosta, portanto, presumivelmente, não descreve o problema.

Isso explica como o OP falha em explicar o problema? Tudo no OP que realmente descreve um problema parece estar descrevendo a verbosidade, mas toda vez que sugiro que esse é o problema, você diz que não é e que há outro problema.

Acho que o mal-entendido se resume ao significado da palavra.
Por exemplo:

adiciona muito ruído ao código

Novamente, isso soa como apenas dizer verbosidade / clichê para mim, mas novamente você explicou que não é isso.

Este ponto não é sobre o número de controller.dispose() , mas o valor que essas linhas de código trazem para o leitor.
Essa linha deve estar sempre lá e é sempre a mesma. Como tal, seu valor para o leitor é quase nulo.

O que importa não é a presença dessa linha, mas sua ausência.

O problema é que quanto mais controller.dispose() tivermos, maior será a probabilidade de perdermos um problema real em nosso método de descarte.
Se tivermos 1 controlador e 0 descarte, é fácil pegar
Se tivermos 100 controladores e 99 descartados, será difícil encontrar o que está faltando.

Então nós temos:

Presumivelmente, é muito difícil porque o detalhamento torna mais fácil cometer um erro ao copiar e colar o código. Mas, novamente, quando tentei resolver esse problema, que eu descreveria como "verbosidade", você disse que o problema não era a verbosidade.

Como mencionei no ponto anterior, nem todas as linhas de códigos são iguais.

Se compararmos:

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

+    },
+ );

então, ambos os trechos têm o mesmo número de linhas e fazem a mesma coisa.
Mas ValueListenableBuilder é preferível.

A razão para isso é que não é o número de linhas que importa, mas quais são essas linhas.

O primeiro snippet tem:

  • 1 declaração de propriedade
  • Declaração de 1 método
  • 1 tarefa
  • 2 chamadas de método
  • todos os quais estão espalhados por 2 ciclos de vida diferentes. 3 se incluirmos construção

O segundo snippet tem:

  • 1 instanciação de classe
  • 1 função anônima
  • sem ciclo de vida. 1 se incluirmos construção

O que torna o ValueListenableBuilder _simpler_.

Há também o que essas linhas não dizem:
ValueListenableBuilder lida com valueListenable mudando ao longo do tempo.
Mesmo no cenário em que widget.valueNotifier não muda com o tempo enquanto falamos, não faz mal.
Um dia, essa afirmação pode mudar. Nesse caso, ValueListenableBuilder lida elegantemente com o novo comportamento, ao passo que, com o primeiro fragmento, agora temos um bug.

Portanto, não apenas ValueListenableBuilder é mais simples, mas também mais resiliente a alterações no código - para o mesmo número exato de linhas.


Com isso, acho que podemos concordar que ValueListenableBuilder é preferível.
A questão é então: "Por que não ter um equivalente a ValueListenableBuilder para cada lógica de estado reutilizável?"

Por exemplo, em vez de:

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

Nós teríamos:

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

  },
);

com o benefício adicional que muda para initialText pode ser recarregado a quente.

Este exemplo pode ser um pouco trivial, mas poderíamos usar esse princípio para lógicas de estado reutilizáveis ​​um pouco mais avançadas (como ModeratorBuilder ).

Isso é "bom" em pequenos trechos. Mas isso causa alguns problemas, pois queremos dimensionar a abordagem:

  • Os construtores voltam à questão do "barulho demais".

Por exemplo, já vi algumas pessoas gerenciarem seu modelo desta forma:

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

Mas então, um widget pode querer ouvir name , age e gender ao mesmo tempo.
O que significa que teríamos que fazer:

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

Obviamente, isso não é o ideal. Removemos a poluição dentro de initState / dispose para poluir nosso método build .

(vamos ignorar Listenable.merge por causa do exemplo. Não importa aqui; é mais sobre a composição)

Se usamos Builders extensivamente, é fácil nos ver neste cenário exato - e sem equivalente a Listenable.merge (não que eu goste deste construtor, para começar com 😛)

  • Escrever um construtor customizado é entediante

    Não existe uma solução fácil para criar um Construtor. Nenhuma ferramenta de refatoração nos ajudará aqui - não podemos simplesmente "extrair como Builder".
    Além disso, não é necessariamente intuitivo. Fazer um construtor personalizado não é a primeira coisa que as pessoas vão pensar - especialmente porque muitos vão ser contra o clichê (embora eu não seja).

    As pessoas estão mais propensas a criar uma solução de gerenciamento de estado personalizada e, potencialmente, acabar com um código ruim.

  • Manipular uma árvore de Construtores é entediante

    Digamos que queremos remover ValueListenableBuilder em nosso exemplo anterior ou adicionar um novo, isso não é fácil.
    Podemos passar alguns minutos contando () e {} para entender por que nosso código não compila.


Os ganchos existem para resolver os problemas do Builder que acabamos de mencionar.

Se refatorarmos o exemplo anterior para ganchos, teríamos:

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

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

É idêntico ao comportamento anterior, mas o código agora tem um recuo linear.
Que significa:

  • o código drasticamente mais legível
  • é mais fácil editar. Não precisamos temer () {}; para adicionar uma nova linha.

Esse é um dos principais provider gostou. Ele removeu muitos aninhamentos ao introduzir MultiProvider .

Da mesma forma, ao contrário da abordagem initState / dispose , nós nos beneficiamos do hot-reload.
Se adicionarmos um novo useValueListenable , a alteração será aplicada imediatamente.

E, claro, ainda temos a capacidade de extrair primitivos reutilizáveis:

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

e essa mudança pode ser automatizada com extract as function , o que funcionaria na maioria dos cenários.


Isso responde à sua pergunta?

Certo. O problema com algo assim é que ele simplesmente não tem informações suficientes para realmente fazer a coisa certa. Por exemplo:

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

... acabaria sendo cheio de erros de maneiras muito confusas.

Você pode contornar isso com a mágica do compilador (é assim que o Compose faz), mas para o Flutter, isso viola algumas de nossas decisões de design fundamentais. Você pode contornar isso com chaves, mas o desempenho sofre muito (uma vez que a pesquisa de variável acaba envolvendo pesquisas de mapa, hashes e assim por diante), o que para Flutter viola alguns de nossos objetivos fundamentais de design.

A solução Property que sugeri anteriormente, ou algo derivado dela, parece que evita a mágica do compilador enquanto ainda atinge os objetivos que você descreveu de ter todo o código em um só lugar. Eu realmente não entendo por que não funcionaria para isso. (Obviamente, ele seria estendido para também se conectar a didChangeDependencies e assim por diante para ser uma solução completa.) (Não colocaríamos isso na estrutura de base porque violaria nossos requisitos de desempenho.)

Exatamente devido aos bugs que podem ocorrer, como você disse, é a razão pela qual os ganchos não devem ser chamados condicionalmente. Veja o documento Rules of Hooks do ReactJS para mais detalhes. A essência básica é que, como em sua implementação eles são rastreados por ordem de chamada, usá-los condicionalmente interromperá essa ordem de chamada e, portanto, não permitirá que sejam rastreados corretamente. Para usar adequadamente o gancho, você os chama no nível superior em build sem qualquer lógica condicional. Na versão JS, você volta

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

O equivalente Dart pode ser semelhante, só é mais longo devido a não ter descompactado como o JS:

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

Se quiser lógica condicional, você pode decidir usar title no método de construção _após tê-los chamado no nível superior_, porque agora a ordem da chamada ainda está preservada. Muitas dessas questões que você levantou foram explicadas no documento de ganchos que vinculei acima.

Certo. E você pode fazer isso em um pacote. Só estou dizendo que esse tipo de requisito violaria nossa filosofia de design, e é por isso que não adicionaríamos isso ao Flutter a estrutura. (Especificamente, otimizamos para legibilidade e depuração; ter um código que parece funcionar, mas, por causa de uma condicional (que pode não ser óbvia no código) às vezes não funciona, não é algo que queremos encorajar ou habilitar no estrutura principal.)

O comportamento de depuração / condicional não é um problema. É por isso que um plug-in de analisador é importante. Esse plug-in:

  • avisa se uma função usa um gancho sem ser nomeada useMyFunction
  • avisar se um gancho for usado condicionalmente
  • avisa se um gancho é usado em um loop / retorno de chamada.

Isso cobre todos os erros potenciais. React provou que isso é viável.

Então, ficamos com os benefícios:

  • código mais legível (como mostrado anteriormente)
  • melhor recarregar a quente
  • código mais reutilizável / combinável
  • mais flexível - podemos criar estados computados facilmente.

Sobre os estados computados, os ganchos são bastante poderosos para armazenar em cache a instância de um objeto. Isso pode ser usado para reconstruir um widget apenas quando seu parâmetro muda.

Por exemplo, podemos ter:

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

Esse gancho useMemo permite otimizações fáceis de desempenho e manipulação de init + update declarativamente, o que também evita bugs.

Isso é algo que a proposta Property / context.onDispose perde.
Eles são difíceis de usar para estados declarativos sem acoplar firmemente a lógica a um ciclo de vida ou complexificar o código com ValueNotifier .

Mais sobre por que a proposta ValueGetter não é prática:

Podemos querer refatorar:

final int userId;

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

em:

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

Com ganchos, essa mudança funciona perfeitamente, já que useMemo não está vinculado a nenhum ciclo de vida.

Mas com Property + ValueGetter , teríamos que mudar a implementação de Property para fazer este trabalho - o que é indesejado, pois o código Property pode ser reutilizado em vários lugares. Portanto, perdemos a capacidade de reutilização mais uma vez.

FWIW, este snippet é equivalente a:

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

Suponho que teremos que encontrar uma solução que resolva os mesmos problemas mencionados por @rrousselGit, mas também tenha capacidade de leitura e depuração em mente. O Vue tem sua própria implementação que pode estar mais de acordo com o que você está procurando, onde condicionais ou ordem de chamada não causam bugs como no React.

Talvez a próxima etapa seja criar uma solução exclusiva para o Flutter, que é a versão dos ganchos desse framework, dadas as restrições do Flutter, assim como o Vue fez sua versão considerando as restrições do Vue. Eu uso os ganchos do React regularmente e diria que apenas ter um plugin de analisador às vezes pode não ser suficiente, provavelmente deve ser mais integrado à linguagem.

Em todo caso, acho que nunca chegaremos a um consenso. Parece que discordamos até no que é legível

Como um lembrete, estou compartilhando isso apenas porque sei que a comunidade tem alguns problemas com esse problema. Eu pessoalmente não me importo se Flutter não fizer nada sobre isso (embora eu ache isso meio triste), desde que tenhamos:

  • um sistema de plug-in de analisador adequado
  • a capacidade de usar pacotes dentro do dartpad

Se você quiser usar o plug-in de ganchos, o que eu recomendo fortemente, mas está enfrentando alguns problemas, recomendo preencher os problemas para esses problemas e preencher os PRs para corrigi-los. Estamos mais do que felizes em trabalhar com você nisso.

Aqui está uma nova versão da ideia anterior de Property . Ele lida com didUpdateWidget e eliminação (e pode facilmente ser feito para lidar com outras coisas como didChangeDependencies); suporta hot reload (você pode alterar o código que registra a propriedade e hot reload, e ele fará a coisa certa); é seguro para tipos, sem precisar de tipos explícitos (depende de inferência); ele tem tudo em um só lugar, exceto a declaração de propriedade e uso, e o desempenho deve ser razoavelmente bom (embora não tão bom quanto as formas mais detalhadas de fazer as coisas).

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

Veja como você o usaria:

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

Por conveniência, você pode criar subclasses de Property preparadas para coisas como AnimationControllers e assim por diante,

Você provavelmente pode fazer uma versão disso que funcione nos métodos State.build também ...

Compartilho algumas das dúvidas que @Hixie traz para a mesa. Por outro lado, vejo as vantagens claras que os Hooks têm e parece que vários desenvolvedores gostam disso.
Meu problema com a abordagem de pacote que @timsneath propôs é que o código que usa Hooks parece dramaticamente diferente do código sem. Se eles não os colocarem no cânone oficial, acabaremos com o código do Flutter que não é legível para as pessoas que estão apenas seguindo o cânone do Flutter.
Se os pacotes começarem a implementar coisas que deveriam ser a capacidade de resposta do framework, teremos muitos dialetos Flutter diferentes, o que torna difícil aprender novas bases de código. Então, para mim, provavelmente começaria a usar ganchos no momento em que fizesse parte do Flutter.
É muito parecido com a minha visão atual do pacote congelado. Eu amo a funcionalidade, mas a menos que Uniões e classes de dados não façam parte do Dart, não quero incluí-los em minha base de código porque tornaria mais difícil para as pessoas lerem meu código.

@escamoteur Só para eu entender, você está sugerindo que mudemos fundamentalmente a forma como os widgets funcionam? Ou você está sugerindo que deve haver algumas novas habilidades específicas? Dado como coisas como Ganchos e a proposta de propriedade acima são possíveis sem nenhuma alteração na estrutura principal, não está claro para mim o que você realmente gostaria que fosse alterado.

É ortogonal da conversa sobre qualquer alteração proposta em si, mas acho que o que ouvi de @escamoteur , @rrousselGit e outros aqui e em outros lugares é que estar _in_ no framework é percebido como uma forma importante de estabelecer a legitimidade de um determinado abordagem. Corrija-me se você discordar.

Eu entendo essa linha de pensamento - uma vez que há muito que resulta de estar no framework (por exemplo, DartPad não oferece suporte a pacotes de terceiros hoje, alguns clientes estão desconfiados sobre quantos pacotes eles dependem depois de serem gravados com NPM, parece mais 'oficial', é garantido que irá avançar com mudanças como segurança nula).

Mas também há custos significativos para ser incluído: em particular, ele ossifica uma abordagem e uma API. É por isso que ambos mantemos um padrão muito alto para o que adicionamos, especialmente quando não há acordo unânime (cf gerenciamento de estado), onde há probabilidade de evolução ou onde podemos facilmente adicionar algo como um pacote.

Eu me pergunto se precisamos documentar nossa filosofia de pacote primeiro, mas novamente, _onde_ ele vai é separado de uma discussão sobre _o que_ podemos querer mudar para melhorar a reutilização da lógica de estado.

Nossa política de pacotes está documentada aqui: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#deciding -where-to-put-code

Compreendo perfeitamente a abordagem do pacote inicial e concordo que é uma coisa importante.
Mas também acredito que alguns problemas precisam ser resolvidos no núcleo, não por pacotes.

É por isso que não estou argumentando que provider deve ser mesclado no Flutter, mas também acredito que esse problema descreve um problema que o Flutter deve resolver nativamente (não necessariamente com ganchos, é claro).

Com o Provider, o Flutter vem com um primitivo embutido para resolver este tipo de problema: InheritedWidgets.
O provedor apenas adiciona uma camada opinativa na parte superior para torná-la "" mais agradável "".

Ganchos são diferentes. Eles são os primitivos. Eles são uma solução de baixo nível não preconizada para um problema específico: Reutilizar a lógica em vários estados.
Eles não são o produto final, mas algo que se espera que as pessoas usem para construir pacotes personalizados (como eu fiz com hooks_riverpod )

Seria útil para mim (em termos de compreender os desejos aqui e as necessidades que os ganchos atendem e assim por diante) se alguém pudesse fornecer uma revisão detalhada de como a abordagem da propriedade que rabisquei acima se compara aos ganchos. (Meu objetivo com a ideia de propriedade é basicamente sobrepor a opinião sobre a estrutura para resolver o problema de como reutilizar a lógica em vários estados.)

Acho que a proposta da propriedade falha em resolver um objetivo-chave desse problema: a lógica de estado não deve se preocupar com a origem dos parâmetros e em que situação eles estão sendo atualizados.

Esta proposta aumenta a legibilidade até certo ponto, reagrupando toda a lógica em um só lugar; mas não consegue resolver o problema de reutilização

Mais especificamente, não podemos extrair:

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

de _ExampleState e reutilizá-lo em um widget diferente, pois a lógica está ligada a Example e initState + didUpdateWidget

Como seria com ganchos?

Eu concordo com @timsneath depois de ter visto algo semelhante na comunidade Rust. É muito difícil extrair algo do núcleo uma vez que esteja dentro. O padrão BLoC foi especificado antes do surgimento do provedor, mas agora o provedor é a versão recomendada. Talvez flutter_hooks possa ser a versão "abençoada" da mesma maneira. Digo isso porque no futuro pode haver melhorias em relação aos ganchos que as pessoas criarão. Reagir, tendo ganchos agora, não pode realmente mudá-los ou sair deles. Eles devem apoiá-los, assim como fazem com os componentes de classe, essencialmente para sempre, uma vez que estão no núcleo. Portanto, concordo com a filosofia do pacote.

O problema parece ser que a adoção será baixa e as pessoas usarão o que for mais conveniente para elas. Isso pode ser resolvido como eu disse, recomendando às pessoas que usem flutter_hooks. Isso também pode não ser um grande problema se olharmos analogamente para quantas soluções de gerenciamento de estado existem, mesmo se muitas pessoas usarem o provedor. Eu também experimentei alguns problemas e "pegadinhas" com ganchos em outras estruturas que devem ser abordadas a fim de criar uma solução superior para a lógica do ciclo de vida reutilizável e combinável.

Como seria com ganchos?

Sem usar quaisquer ganchos primitivos enviados por React / flutter_hooks, poderíamos ter:

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

Então usado:

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

Nesta situação, a lógica é completamente independente de Example e dos ciclos de vida de StatefulWidget .
Portanto, poderíamos reutilizá-lo em um widget diferente que gerencia seu userId diferente. Talvez esse outro widget seja um StatefulWidget que gerencia seu userId internamente. Em vez disso, talvez ele obtenha userId de um InheritedWidget.

Essa sintaxe deve deixar óbvio que os ganchos são como objetos State independentes com seus próprios ciclos de vida.

Como uma observação lateral, uma desvantagem da abordagem do pacote primeiro é: os autores de pacotes têm menos probabilidade de publicar pacotes contando com ganchos para resolver problemas.

Por exemplo, um problema comum que os usuários do Provedor enfrentam é que eles desejam descartar automaticamente o estado de um provedor quando ele não for mais usado.
O problema é que os usuários do Provider também gostam muito da sintaxe context.watch / context.select , em oposição à sintaxe detalhada Consumer(builder: ...) / Selector(builder:...) .
Mas não podemos ter essa ótima sintaxe _e_ resolver o problema mencionado anteriormente sem ganchos (ou https://github.com/flutter/flutter/pull/33213, que foi rejeitado).

O problema é:
O provedor não pode depender de flutter_hooks para resolver este problema.
Devido ao quão popular o Provider é, não seria razoável depender de ganchos.

Então, no final, optei por:

  • Provedor de bifurcação (sob o codinome de Riverpod )
  • Como consequência, perderá voluntariamente o "favorito do Flutter" / recomendação do Google
  • resolver este problema (e mais alguns)
  • adicione uma dependência de ganchos para oferecer uma sintaxe que as pessoas que gostam de context.watch gostariam.

Estou bastante satisfeito com o que descobri, pois acho que isso traz uma melhoria significativa em relação ao Provider (torna o InheritedWidgets seguro para compilação).
Mas o jeito de chegar lá me deixou um gosto ruim.

Existem basicamente três diferenças, tanto quanto posso dizer entre a versão de ganchos e a versão de propriedade:

  • A versão Hooks é muito mais código de apoio
  • A versão da propriedade é muito mais código clichê
  • A versão Hooks tem o problema de métodos de construção em que, se você chamar os ganchos na ordem errada, as coisas vão mal e não há realmente nenhuma maneira de ver isso imediatamente a partir do código.

O código clichê é realmente tão importante? Quer dizer, você pode facilmente reutilizar a propriedade agora, o código está todo em um só lugar. Portanto, agora é realmente _só_ um argumento de verbosidade.

Acho que uma boa solução não deve depender do conhecimento de outros pacotes. Não deve importar se está na estrutura ou não. Pessoas que não o usam não devem ser um problema. Se as pessoas não o usarem é um problema, então isso, IMHO, é uma bandeira vermelha para a API.

Quer dizer, você pode facilmente reutilizar a propriedade agora, o código está todo em um só lugar.

O código estar em um lugar não significa que seja reutilizável.
Você se importaria de fazer um widget secundário que reutilize o código atualmente localizado dentro de _ExampleState em um widget diferente?
Com uma diferença: esse novo widget deve gerenciar seu ID de usuário internamente dentro de seu estado, de modo que tenhamos:

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

Se as pessoas não o usarem é um problema, então isso, IMHO, é uma bandeira vermelha para a API.

Pessoas não usando algo porque não é oficial não significa que a API seja ruim.

É totalmente legítimo não querer adicionar dependências extras porque isso é um trabalho extra para manter (devido ao controle de versão, licença, depreciação e outras coisas).
Pelo que me lembro, o Flutter tem o requisito de ter o mínimo de dependências possível.

Mesmo com o próprio Provider, que é amplamente aceito e quase oficial agora, tenho visto pessoas dizerem "Eu prefiro usar o InheritedWidgets embutido para evitar adicionar uma dependência".

Você se importaria de fazer um widget secundário que reutilize o código atualmente localizado dentro de _ExampleState em um widget diferente?

O código em questão trata de obter um userId de um widget e passá-lo para um método fetchUser. O código para gerenciar o userId mudando localmente no mesmo objeto seria diferente. Isso parece estar bem? Não tenho certeza do problema que você está tentando resolver aqui.

Para o registro, eu não usaria Property para fazer o que você descreve, seria apenas parecido com:

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

Pessoas não usando algo porque não é oficial não significa que a API seja ruim.

Concordou.

O fato de as pessoas não usarem algo ruim é o que significa que a API é ruim. Quando você diz "Os autores de pacotes têm menos probabilidade de publicar pacotes contando com ganchos para resolver problemas", isso indica que os ganchos dependem de outras pessoas que os usem para serem úteis a você. Uma boa API, IMHO, não se torna ruim se ninguém mais a adota; ele deve se manter, mesmo que ninguém mais saiba sobre ele. Por exemplo, o Property exemplo acima não depende de outros pacotes que o utilizem para ser útil.

Mesmo com o próprio Provider, que é amplamente aceito e quase oficial agora, tenho visto pessoas dizerem "Eu prefiro usar o InheritedWidgets embutido para evitar adicionar uma dependência".

O que há de errado em pessoas preferirem usar InheritedWidget? Não quero impor uma solução às pessoas. Eles devem usar o que quiserem. Você está literalmente descrevendo um não-problema. A solução para as pessoas que preferem usar InheritedWidget é sair do caminho e permitir que usem InheritedWidget.

. Uma boa API, IMHO, não se torna ruim se ninguém mais a adota; ele deve se manter, mesmo que ninguém mais saiba sobre ele. Por exemplo, o exemplo de Propriedade acima não depende de outros pacotes que o utilizem para ser útil.

Existe um mal-entendido.

O problema não é sobre as pessoas não usarem ganchos em geral.
É sobre o Provedor não poder usar ganchos para consertar problemas porque os ganchos não são oficiais, mas o Provedor é.


O código para gerenciar o userId mudando localmente no mesmo objeto seria diferente. Isso parece estar bem? Não tenho certeza do problema que você está tentando resolver aqui.

Para o registro, eu não usaria Property para fazer o que você descreve, seria apenas parecido com:

Isso não responde à pergunta. Eu perguntei isso especificamente para comparar a reutilização de código entre ganchos e propriedade.

Com ganchos, poderíamos reutilizar FetchUser :

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

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

Com ganchos, poderíamos reutilizar FetchUser :

Não entendo por que isso é desejável. FetchUser não tem nenhum código interessante, é apenas um adaptador do Hooks para a função fetchUser . Por que não ligar diretamente para fetchUser ? O código que você está reutilizando não é um código interessante.

É sobre o Provedor não poder usar ganchos para consertar problemas porque os ganchos não são oficiais, mas o Provedor é.

IMHO, uma boa solução para o problema de reutilização de código não precisaria ser adotada pelo Provedor. Eles seriam conceitos inteiramente ortogonais. Isso é algo que o guia de estilo Flutter fala sob o título "evitar complicação".

Não entendo por que isso é desejável. FetchUser não tem nenhum código interessante, é apenas um adaptador dos Ganchos para a função fetchUser. Por que não chamar fetchUser diretamente? O código que você está reutilizando não é um código interessante.

Não importa. Estamos tentando demonstrar a capacidade de reutilização do código. fetchUser pode ser qualquer coisa - incluindo ChangeNotifier.addListener por exemplo.

Poderíamos ter uma implementação alternativa que não dependa de fetchUser e simplesmente fornecer uma API para fazer a busca implícita de dados:

int userId;

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

IMHO, uma boa solução para o problema de reutilização de código não precisaria ser adotada pelo Provedor. Eles seriam conceitos inteiramente ortogonais. Isso é algo que o guia de estilo Flutter fala sob o título "evitar complicação".

É por isso que mencionei que os ganchos são primitivos

Como metáfora:
package:animations depende de Animation . Mas isso não é um problema, porque este é o núcleo primitivo.
Seria diferente se package:animations estivesse usando um fork de Animation mantido pela comunidade

@escamoteur Só para eu entender, você está sugerindo que mudemos fundamentalmente a forma como os widgets funcionam? Ou você está sugerindo que deve haver algumas novas habilidades específicas? Dado como coisas como Ganchos e a proposta de propriedade acima são possíveis sem nenhuma alteração na estrutura principal, não está claro para mim o que você realmente gostaria que fosse alterado.

@Hixie não, meu ponto é que se os ganchos
Eu compartilho suas preocupações, mas por outro lado, um widget com ganchos parece muito elegante.
Não proibiria fazer as coisas como antes.

Não proibiria fazer as coisas como antes.

Acho que sim, não acho que seja uma boa ideia para a equipe do Flutter dizer "ei, agora recomendamos os ganchos, mas você ainda pode fazer as coisas como antes" as pessoas ficarão confusas sobre isso. Além disso, se a equipe do Flutter recomendar ganchos no futuro, eles também precisarão parar de publicar o código de flutter real como exemplos.

As pessoas sempre seguem a "forma oficial" de fazer as coisas e acho que não deveria haver duas formas oficiais de usar o Flutter.

Não importa. Estamos tentando demonstrar a capacidade de reutilização do código. fetchUser pode ser qualquer coisa - incluindo ChangeNotifier.addListener por exemplo.

Certo. É para isso que as funções são boas: abstrair código. Mas já temos funções. O código de propriedade acima, e o código _setUserId acima, mostram que você pode trazer todo o código que chama essas funções para um lugar sem precisar de nenhuma ajuda específica da estrutura. Por que precisamos de Ganchos para agrupar as chamadas a essas funções?

IMHO, uma boa solução para o problema de reutilização de código não precisaria ser adotada pelo Provedor. Eles seriam conceitos inteiramente ortogonais. Isso é algo que o guia de estilo Flutter fala sob o título "evitar complicação".

É por isso que mencionei que os ganchos são primitivos

Eles são uma conveniência, não são um primitivo. Se eles fossem primitivos, a pergunta "qual é o problema" seria muito mais fácil de responder. Você diria "aqui está uma coisa que eu quero fazer e não posso fazer".

Como metáfora:
package:animations depende de Animation . Mas isso não é um problema, porque este é o núcleo primitivo.
Seria diferente se package:animations estivesse usando um fork de Animation mantido pela comunidade

A hierarquia de classes Animation faz algo fundamental: apresenta tickers e uma maneira de controlá-los e assiná-los. Sem a hierarquia de classes Animation, você precisa inventar algo como a hierarquia de classes Animation para fazer animações. (O ideal é algo melhor. Não é nosso melhor trabalho.) Ganchos não apresenta um novo recurso fundamental. Ele apenas fornece uma maneira de escrever o mesmo código de maneira diferente. Pode ser que esse código seja mais simples ou fatorado de maneira diferente do que seria, mas não é um primitivo. Você não precisa de uma estrutura do tipo Hooks para escrever código que faz a mesma coisa que o código que usa Hooks faz.


Fundamentalmente, não acho que o problema descrito neste problema seja algo que a estrutura precise corrigir. Pessoas diferentes terão necessidades muito diferentes de como lidar com isso. Há muitas maneiras de consertá-lo, já discutimos várias neste bug; algumas das maneiras são muito simples e podem ser escritas em alguns minutos, portanto, dificilmente é um problema tão difícil de resolver a ponto de fornecer valor para nós possuirmos e mantermos a solução. Cada uma das propostas tem pontos fortes e fracos; os pontos fracos são, em cada caso, coisas que bloqueariam alguém para usá-los. Nem mesmo está realmente claro se todos concordam que o problema precisa ser consertado.

Ganchos _são_ primitivos
Aqui está um tópico de Dan: https://twitter.com/dan_abramov/status/1093698629708251136 explicando isso. Algumas formulações diferem, mas a lógica se aplica principalmente ao Flutter devido à semelhança entre a classe React Componentes e Flutter StatefulWidgets

Mais especificamente, você poderia pensar em flutter_hooks como mixins dinâmicos de estado.

Se eles fossem primitivos, a pergunta "qual é o problema" seria muito mais fácil de responder. Você diria "aqui está uma coisa que eu quero fazer e não posso fazer".

Está no OP:

É difícil reutilizar a lógica de estado. Ou terminamos com um método de construção complexo e profundamente aninhado ou temos que copiar e colar a lógica em vários widgets.
Não é possível reutilizar tal lógica por meio de mixins ou funções.

Pode ser que esse código seja mais simples ou fatorado de maneira diferente do que seria, mas não é um primitivo. Você não precisa de uma estrutura do tipo Hooks para escrever código que faz a mesma coisa que o código que usa Hooks faz.

Você não precisa de aulas para escrever um programa. Mas as classes permitem estruturar seu código e fatorá-lo de maneira significativa.
E as classes são primitivas.

A mesma coisa com os mixins, que também são primitivos

Ganchos são a mesma coisa.

Por que precisamos de Ganchos para agrupar as chamadas a essas funções?

Para quando precisarmos chamar essa lógica não em _um_ lugar, mas em _dois_ lugares.

Não é possível reutilizar tal lógica por meio de mixins ou funções.

Por favor, dê-me um exemplo concreto em que seja esse o caso. Até agora, todos os exemplos que estudamos são simples, sem ganchos.

Até agora neste tópico, não vi nenhuma outra solução além dos ganchos

Concedido eu não tenho feito muita agitação ultimamente, então posso estar faltando coisas nos exemplos de código de solução de propriedade acima, mas há alguma solução? Quais são as opções hoje que não exigem copiar e colar em vez de reutilizar?
Qual é a resposta para a pergunta @rrousselGit :

Você se importaria de fazer um widget secundário que reutilize o código atualmente localizado dentro de _ExampleState em um widget diferente?
Com uma diferença: esse novo widget deve gerenciar seu ID de usuário internamente dentro de seu estado

Se não for possível reutilizar uma lógica de estado tão fácil com a solução de propriedade acima, quais são as outras opções?
A resposta é simplesmente que não deve ser facilmente reutilizável na vibração? O que é totalmente bom, mas um pouco triste IMHO.

A propósito, o SwiftUI faz isso de uma maneira nova / inspiradora? Ou eles também não têm a mesma capacidade de reutilização da lógica de estado? Não usei o Swiftui sozinho. Talvez seja muito diferente?

Todos os construtores, basicamente. Os construtores são a única maneira de reutilizar o estado no momento.
Hooks torna Builders mais legíveis e fáceis de criar


Aqui está uma coleção de ganchos personalizados para mim ou alguns clientes feitos no mês passado para diferentes projetos:

  • useQuery - que é equivalente ao gancho ImplicitFetcher que dei anteriormente, mas faz uma consulta GraphQL em vez disso.
  • useOnResume que fornece um retorno de chamada para realizar a ação personalizada em AppLifecycleState.resumed sem ter que
    vá se dar ao trabalho de fazer um WidgetsBindingObserver
  • useDebouncedListener que escuta um ouvinte (geralmente TextField ou ScrollController), mas com um debounce no ouvinte
  • useAppLinkService que permite que os widgets executem alguma lógica em um evento personalizado semelhante a AppLifecycleState.resumed mas com regras de negócios
  • useShrinkUIForKeyboard por lidar suavemente com a aparência do teclado. Ele retorna um booleano que indica se a IU deve se adaptar ao preenchimento inferior ou não (que é baseado na escuta de um focusNode)
  • useFilter , que combina useDebouncedListener e useState (um gancho primitivo que declara uma única propriedade) para expor um filtro para uma barra de pesquisa.
  • useImplicitlyAnimated<Int/Double/Color/...> - equivalente a TweenAnimationBuilder como um gancho

Os aplicativos também usam muitos ganchos de baixo nível para lógicas diferentes.

Por exemplo, em vez de:

Whatever whatever;

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

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

Eles fazem:

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

Isso evita duplicar entre initState / didUpdateWidget / didChangeDependencies .

Eles também usam muito useProvider da Riverpod que, de outra forma, teria que ser um StreamBuilder / ValueListenableBuilder


A parte importante é que os widgets raramente usam "apenas um gancho".
Por exemplo, um widget pode fazer

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

É conciso e muito legível (assumindo que você tenha um conhecimento básico da API, é claro).
Toda a lógica pode ser lida de cima para baixo - não há saltos entre os métodos para entender o código.
E todos os ganchos usados ​​aqui são reutilizados em vários lugares na base de código

Se fizéssemos exatamente a mesma coisa sem ganchos, teríamos:

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

Isso é significativamente menos legível.

  • Temos 10 níveis de indentação - 12 se fizermos um FilterBuilder para reutilizar a lógica do filtro
  • A lógica do filtro não é reutilizável como está.

    • podemos esquecer de cancelar timer por engano

  • metade do método build não é útil para o leitor. Os Construtores nos distraem do que importa
  • Perdi uns bons 5 minutos tentando entender por que o código não compila devido à falta de um parêntese

Como um usuário de flutter_hooks eu mesmo, contribuirei com minha opinião. Antes de usar ganchos, eu estava feliz com o Flutter. Eu não vi a necessidade de algo assim. Depois de ler sobre isso e assistir a um vídeo no youtube sobre isso, ainda não estava convencido, parecia legal, mas eu precisava de alguma prática ou exemplos para realmente motivá-lo. Mas então percebi algo. Eu estava evitando widgets com estado a todo custo, havia apenas um monte de boilerplate envolvido e pulando a classe tentando encontrar coisas. Por causa disso, movi a maior parte do meu estado efêmero para uma solução de gerenciamento de estado junto com o restante do estado do aplicativo e apenas usei widgets sem estado. No entanto, isso faz com que a lógica de negócios dependa da Flutter rapidamente devido à confiança em obter Navigator ou BuildContext para acessar InheritedWidget s / Providers mais alto em a árvore. Não estou dizendo que foi uma boa abordagem de gestão do estado, eu sei que não. Mas fiz tudo o que podia para não ter que me preocupar com o gerenciamento de estado na IU.

Depois de usar ganchos por um tempo, me descobri muito mais produtivo, muito mais feliz usando o Flutter, colocando o estado efêmero no lugar certo (junto com a IU) em vez do estado do aplicativo.

Para mim, é como um coletor de lixo para estados / controladores efêmeros. Não preciso me lembrar de descartar todas as assinaturas na IU, embora ainda esteja muito ciente de que é isso que flutter_hooks faz por mim. Também torna muito mais fácil manter e refatorar meu código. Falando de escrever cerca de 10 aplicativos no ano passado para minha pesquisa de pós-graduação e diversão.

Como outros, não sei exatamente qual deve ser a principal motivação para incluí-lo no próprio Flutter SDK. No entanto, aqui estão duas reflexões sobre o assunto.

  1. Ocasionalmente, farei um gancho para facilitar o uso de um pacote que possui controladores que precisam ser inicializados / descartados. (Por exemplo golden_layout , ou zefyr ). Eu acredito que os outros usuários que usam flutter_hooks se beneficiariam com tal pacote. No entanto, não consigo justificar a publicação de um pacote que contém literalmente de 1 a 3 funções. A alternativa seria criar um pacote de pia de cozinha que contém muitos ganchos para vários pacotes que eu uso, eu posso então apenas usar uma dependência git, mas então qualquer um usando esses outros pacotes + flutter_hooks teria que depender do meu git em ordem para benefício (que é menos detectável e provavelmente contém dependências de pacotes com os quais eles não se importam), ou em um pacote que contém 3 funções ou eu publico um pacote garden-sink em pub.dev. Todas as ideias parecem ridículas e não muito detectáveis. Os outros usuários de flutter_hooks poderiam facilmente copiar e colar essas funções em seu código ou tentar descobrir a lógica eles mesmos, mas isso perderia totalmente o objetivo de compartilhar códigos / pacotes. As funções iriam muito melhor para os pacotes originais, e não em algum 'pacote de extensão'. Se flutter_hooks fizesse parte do framework, ou mesmo apenas um pacote usado ou exportado do framework como characters , então os autores do pacote original aceitariam muito mais provavelmente uma solicitação pull para gancho simples funções, e não teremos uma bagunça de 1-3 pacotes de funções.
    Se flutter_hooks não for adotado pelo Flutter, prevejo vários pacotes de funções de 1 a 3 bagunçando os resultados da pesquisa pub.dev. O fato de esses pacotes serem muito pequenos me faz realmente concordar com @rrousselGit que este é um primitivo. Se as 1228 estrelas no repositório flutter_hooks não são nenhuma indicação de que ele está resolvendo os problemas mencionados por @rrousselGit, eu não sei o que é.

  2. Eu estava assistindo a um vídeo do youtube sobre como contribuir para o repositório Flutter desde que estou interessado em ver o que posso fazer para ajudar. Enquanto eu assistia, a pessoa que criava o vídeo adicionou a nova propriedade com bastante facilidade, mas quase se esqueceu de cuidar da atualização de dispose , didUpdateWidget e debugFillProperties . Ver todas as complexidades de um widget com estado novamente e como é fácil perder algo me fez desconfiar deles novamente e não me deixou tão animado em contribuir para o repositório principal do Flutter. Não estou dizendo que isso me desencorajou completamente, ainda estou interessado em contribuir, mas parece que estaria criando um código clichê que é difícil de manter e revisar. Não se trata da complexidade de escrever o código, mas da complexidade de ler o código e verificar se você descartou e cuidou adequadamente do estado efêmero.

Desculpe pela resposta prolixa, no entanto, tenho examinado esse problema de vez em quando e estou um tanto perplexo com a resposta da equipe do Flutter. Parece que você não dedicou tempo para experimentar um aplicativo dos dois jeitos e ver a diferença por si mesmo. Eu entendo o desejo de não manter uma dependência adicional ou integrá-la demais ao framework. No entanto, a parte central da estrutura flutter_hook consiste em apenas 500 linhas de código bem documentado. Mais uma vez, desculpe se isso é tangencial à conversa e espero não estar ofendendo ninguém por dar meus 2 centavos e falar abertamente. Não falei antes porque achei que @rrousselGit estava apresentando pontos muito bons e sendo claro.

Desculpe pela resposta prolixa, no entanto, tenho examinado esse problema de vez em quando e estou um tanto perplexo com a resposta da equipe do Flutter. Parece que você não dedicou tempo para experimentar um aplicativo dos dois jeitos e ver a diferença por si mesmo.

Para ser justo, este é um segmento incrivelmente longo e o fundador do framework tem contribuído ativamente várias vezes ao dia, com várias soluções, solicitado feedback sobre elas e se engajado com elas, além de trabalhar para entender o que está sendo solicitado. Eu honestamente me esforço para pensar em um exemplo mais claro de um mantenedor sendo útil.

Eu gostaria que isso fosse um pouco mais de paciência com esse problema - não entendo os ganchos mais profundamente depois de ler este tópico, exceto que eles são outra maneira de vincular a vida útil dos descartáveis ​​a um estado. Não prefiro essa abordagem estilisticamente, e sinto que há algo fundamentalmente errado se a posição for 'apenas reserve um tempo para escrever um aplicativo totalmente novo no paradigma, então você entenderá por que ele precisa ser encaixado na estrutura! ' - como o engenheiro do React observou neste tópico, realmente não seria aconselhável para Flutter, e os benefícios descritos neste tópico são pequenos em comparação com o custo do tipo de religação que significa que você precisa de uma base de código totalmente nova para ver o benefício.

Eu honestamente me esforço para pensar em um exemplo mais claro de um mantenedor sendo útil.

Concordou. Agradeço Hixie por dedicar seu tempo para participar dessa discussão.

Eu não entendo os ganchos mais profundamente depois de ler este tópico

Para ser justo, esse problema é explicitamente tentar evitar falar especificamente sobre ganchos.
Trata-se mais de tentar explicar o problema do que da solução

Você acha que ele não consegue fazer isso?

Eu posso sentir os dois lados ( @rrousselGit e @Hixie) aqui e gostaria de deixar alguns comentários de um (meu) lado / perspectiva de uso do framework Flutter.

A abordagem flutter_hooks reduz bastante o clichê (apenas a partir dos exemplos mostrados aqui, já que podemos reutilizar essas configurações de estado) e reduz a complexidade por não ter que pensar ativamente em inicializar / descartar recursos. De modo geral, ele faz um bom trabalho melhorando e apoiando o fluxo / velocidade de desenvolvimento ... mesmo que não se encaixe tão bem no "núcleo" do Flutter em si (subjetivamente).

Como pelo menos> 95% do código que escrevo resulta no método de construção para ser apenas declarativo, sem variáveis ​​locais ou chamadas fora da subárvore do widget retornado, toda a parte lógica está dentro dessas funções de estado para inicializar, atribuir e descartar recursos e adicionar ouvintes (no meu caso, as reações do MobX) e coisas lógicas. Como essa também é a abordagem na maior parte do Flutter em si, parece muito nativo. Fazer isso também dá ao desenvolvedor a oportunidade de sempre ser explícito e aberto sobre o que você faz - me força a sempre converter esses widgets em StatefulWidget e escrever um código semelhante em initState / dispose, mas também sempre resulta em anotar exatamente o que você pretende fazer diretamente no Widget que está sendo utilizado. Para mim, pessoalmente, como @Hixie já mencionou a si mesmo, não me incomoda de forma alguma escrever esse tipo de código clichê e me permite, como desenvolvedor, decidir como lidar com isso em vez de depender de algo como flutter_hooks fazer isso por mim, resultando em não entender por que algo pode se comportar dessa forma. A extração de widgets em pequenas partes também garante que esse tipo de clichê esteja certo no caso de uso para o qual está sendo usado. Com flutter_hooks eu ainda precisaria pensar sobre quais tipos de estados valem a pena serem escritos para serem um gancho e, portanto, reutilizados - sabores diferentes podem resultar em vários ganchos de uso "único" ou nenhum gancho, já que eu poderia não reutilize as configurações com muita frequência, mas tende a escrever configurações mais personalizadas.

Não me interpretem mal, a abordagem em tais ganchos parece muito boa e útil, mas para mim parece um conceito muito fundamental que muda o conceito central de como lidar com isso. Parece muito bom como um pacote em si dar aos desenvolvedores a oportunidade de usar esse tipo de abordagem se eles não estiverem felizes com a forma de fazê-lo "nativamente", mas torná-lo parte do próprio framework Flutter seria, pelo menos, limpo / unificado, resulta em reescrever grandes partes do Flutter para fazer uso desse conceito (muito trabalho) ou usá-lo para coisas futuras / selecionadas (o que pode ser confuso ter essas abordagens mistas).

Se ele fosse integrado à própria estrutura do Flutter e fosse suportado / usado ativamente, eu obviamente entraria nisso. Como eu entendo e até gosto da abordagem atual e vejo as (possíveis) ações necessárias para implementar isso nativamente, posso entender a hesitação e / ou por que isso não deve ser feito e, em vez disso, mantê-lo como um pacote.

Corrija-me se eu estiver errado, mas este tópico é sobre os problemas de reutilização da lógica de estado em vários widgets de forma legível e combinável. Não especificamente ganchos. Acredito que este tópico foi aberto devido ao desejo de ter uma discussão em torno do problema com uma abordagem aberta de qual deveria ser a solução.

No entanto, os ganchos são mencionados por serem uma solução e acredito que @rrousselGit os tem usado aqui para tentar explicar o problema / problema que eles resolvem (já que são uma solução) para que outra solução talvez mais nativa para flutuar possa ser apresentada com e apresentado. Até agora, que eu saiba, não houve nenhuma outra solução apresentada neste tópico que resolva os problemas de reutilização?

Dito isso, não sei para onde o tópico está indo no momento.
Acho que o problema realmente existe. Ou estamos debatendo isso?
Se todos concordarmos que é difícil reutilizar a lógica de estado de uma forma combinável em vários widgets com o núcleo da vibração hoje, quais soluções existem que poderiam ser uma solução central? já que os construtores realmente são (para citar)

significativamente menos legível

A solução de propriedade não parece ser tão facilmente reutilizável ou é uma conclusão errada que tirei (?), Pois não houve resposta sobre como usá-la para:

fazendo um widget secundário que reutiliza o código atualmente localizado dentro de _ExampleState em um widget diferente?
Com uma diferença: esse novo widget deve gerenciar seu ID de usuário internamente dentro de seu estado

Eu estaria disposto a ajudar com um documento de design como @timsneath sugeriu. Eu acho que provavelmente é um formato melhor para explicar o problema com alguns exemplos de estudos de caso, bem como mencionar as diferentes soluções e explorar se podemos encontrar uma solução que se encaixe no flutter e onde ela está. Concordo que a discussão sobre o assunto está se perdendo um pouco.

Estou bastante cético quanto à ideia de fazer um documento de design no momento.
É evidente que, por enquanto, @Hixie é contra a solução desse problema diretamente no Flutter.

Para mim, parece que discordamos sobre a importância do problema e o papel do Google em resolvê-lo.
Se ambos os lados não concordarem com isso, não vejo como podemos ter uma discussão produtiva sobre como lidar com esse problema - qualquer que seja a solução.

Este tópico de discussão foi uma leitura muito interessante e estou feliz em ver que a troca de pontos de vista permaneceu civilizada. No entanto, estou um pouco surpreso com o impasse atual.

Quando se trata de ganchos, meu ponto de vista é que, embora o Flutter não precise necessariamente da solução de ganchos específica apresentada por @rrousselGit , ele também não está dizendo isso. O Flutter precisa de uma solução que ofereça benefícios semelhantes aos dos ganchos, exatamente por todas as razões que Remi e outros proponentes mencionam. @emanuel-lundman resumiu bem os argumentos acima e concordo com seus pontos de vista.

Na falta de quaisquer outras propostas viáveis ​​oferecendo os mesmos recursos e dado o fato de que os hooks têm um histórico comprovado no React, e que existe uma solução que poderia ser baseada no Flutter, eu não acho que seria uma má escolha para fazer isso. Eu não acho que o conceito de ganchos, como um primitivo que também está incluído no Flutter SDK (ou mesmo inferior), tiraria algo do Flutter. Na minha opinião, isso apenas o enriqueceria e tornaria ainda mais fácil desenvolver aplicativos agradáveis ​​e de fácil manutenção com o Flutter.

Embora o argumento de que os ganchos estejam disponíveis como um pacote para aqueles que desejam colher seus benefícios, seja um ponto válido, sinto que não é ideal para um gancho primitivo como os ganchos. Aqui está o porquê.

Muitas vezes, ao fazer pacotes reutilizáveis ​​internamente, discutimos se o pacote precisa ser "puro", no sentido de que pode depender apenas do SDK do Dart + Flutter, ou se permitimos alguns outros pacotes nele e, em caso afirmativo, quais uns. Even Provider está fora de questão para pacotes “puros”, mas muitas vezes permitido para pacotes de nível superior. Para um aplicativo, sempre há o mesmo debate, quais pacotes estão OK e quais não são. O provedor é verde, mas algo como Hooks ainda é um ponto de interrogação como um pacote.

Se ganchos como a solução fizessem parte do SDK, seria uma escolha óbvia usar os recursos que ele oferece. Embora eu queira usar Hooks e permitir que ele já entre agora como um pacote, também estou preocupado que ele crie um estilo de código Flutter e introduza conceitos que podem não ser familiares aos desenvolvedores de Flutter que não o usam. Parece uma bifurcação no caminho se seguirmos esse caminho sem suporte no SDK. Para projetos pessoais menores, é uma escolha fácil usar ganchos. Eu recomendo tentar junto com Riverpod.

(Acho que nosso conservadorismo de pacotes vem de ser queimado por pacotes e bagunça de dependências em outros gerenciadores de pacotes no passado, provavelmente não únicos.)

Não estou dizendo que os ganchos seriam a única maneira de resolver o problema atual, mesmo que seja a única solução comprovada que funciona até agora. Certamente poderia ser interessante e uma abordagem válida investigar opções em um nível mais genérico antes de se comprometer com uma solução. Para que isso aconteça, é necessário reconhecer que o Flutter SDK _atualmente tem uma falha quando se trata de uma lógica de estado reutilizável fácil_, que apesar das elaboradas explicações atualmente não parece haver.

Para mim, existem duas razões principais para não apenas colocar Ganchos na estrutura principal. A primeira é que a API contém armadilhas perigosas. Primeiramente, se você de alguma forma acabar chamando os ganchos na ordem errada, as coisas vão quebrar. Isso me parece um problema fatal. Eu entendo que com disciplina e seguindo a documentação você pode evitá-lo, mas IMHO uma boa solução para esse problema de reutilização de código não teria essa falha.

A segunda é que realmente não deveria haver razão para as pessoas não usarem apenas Hooks (ou outra biblioteca) para resolver este problema. Agora, com Ganchos especificamente, isso não funciona, como as pessoas discutiram, porque escrever o gancho é pesado o suficiente para que as pessoas desejassem que bibliotecas não relacionadas suportassem ganchos. Mas acho que uma boa solução para esse problema não precisaria disso. Uma boa solução seria independente e não precisaria que todas as outras bibliotecas soubessem dela.

Recentemente, adicionamos RestorableProperties à estrutura. Seria interessante ver se eles poderiam ser aproveitados aqui de alguma forma ...

Eu concordo com @Hixie sobre a API ter problemas ocultos que requerem um analisador ou linter para serem resolvidos. Acho que nós, como quem quer participar, devemos procurar várias soluções, talvez através da sugestão do doc de design, ou não, sobre o problema da gestão do ciclo de vida reutilizável. Idealmente, seria mais específico do Flutter e aproveitaria as APIs do Flutter, ao mesmo tempo em que resolveria os problemas que a API do gancho faz. Acho que a versão Vue é um bom modelo para começar, como mencionei antes, pois não depende da ordem de chamada de gancho. Alguém mais está interessado em investigar comigo?

@Hixie, mas você concorda com a questão de que não há uma boa maneira de reutilizar a lógica de estado de uma forma combinável entre widgets? É por isso que você começou a pensar em aproveitar as propriedades recuperáveis ​​de alguma forma?

Para mim, existem duas razões principais para não apenas colocar Ganchos na estrutura principal. A primeira é que a API contém armadilhas perigosas. Primeiramente, se você de alguma forma acabar chamando os ganchos na ordem errada, as coisas vão quebrar. Isso me parece um problema fatal. Eu entendo que com disciplina e seguindo a documentação você pode evitá-lo, mas IMHO uma boa solução para esse problema de reutilização de código não teria essa falha.

Por ter trabalhado com ganchos e trabalhado com outras pessoas que usam ganchos, isso realmente não é um grande problema, IMHO. E nem um pouco em comparação com todos os grandes ganhos (grandes ganhos em velocidade de desenvolvimento, capacidade de reutilização, composição e código facilmente legível) que eles trazem para a mesa.
Um gancho é um gancho, como uma classe é uma classe, não apenas uma função, e você não pode usá-lo condicionalmente. Você aprende rápido. E seu editor também pode ajudar com esse problema.

A segunda é que realmente não deveria haver razão para as pessoas não usarem apenas Hooks (ou outra biblioteca) para resolver este problema. Agora, com Ganchos especificamente, isso não funciona, como as pessoas discutiram, porque escrever o gancho é pesado o suficiente para que as pessoas desejassem que bibliotecas não relacionadas suportassem ganchos. Mas acho que uma boa solução para esse problema não precisaria disso. Uma boa solução seria independente e não precisaria que todas as outras bibliotecas soubessem dela.

Ganchos para escrever não são pesados.
Ainda é mais fácil do que as soluções disponíveis agora IMHO (para usar essa frase novamente 😉).
Talvez eu esteja interpretando mal o que você está escrevendo. Mas acho que ninguém disse isso?
Eu li isso como se as pessoas realmente apreciassem todos os ganhos que a solução de gancho traz para a mesa e gostariam de poder usá-la em qualquer lugar. Para colher todos os benefícios. Uma vez que um gancho é reutilizável, seria ótimo se os desenvolvedores de terceiros pudessem se sentir confiantes para codificar e enviar seus próprios ganchos sem exigir que todos escrevessem seus próprios invólucros. Aproveite o benefício da reutilização da lógica de estado.
Acho que @rrousselGit e @gaearon já explicaram a coisa primitiva. Então, não vou entrar nisso.
Talvez eu não tenha entendido esta afirmação porque não consigo ver se é um bom resumo do que as pessoas escreveram neste tópico. Eu sinto Muito.

Espero que haja um caminho a seguir. Mas acho que é hora de, pelo menos, concordar que isso é um problema e ir em frente, propondo soluções alternativas que sejam melhores, já que os ganchos não parecem estar uniformes.
Ou simplesmente decida pular a correção do problema no núcleo de flutter.

Quem decide o caminho a seguir?
Qual é o próximo passo?

Isso me parece um problema fatal. Eu entendo que com disciplina e seguindo a documentação você pode evitá-lo, mas IMHO uma boa solução para esse problema de reutilização de código não teria essa falha.

No React, resolvemos isso com uma análise linter - estática. Em nossa experiência, essa falha não foi importante, mesmo em uma grande base de código. Existem outros problemas que podemos considerar como falhas, mas eu só queria apontar que, embora a confiança na ordem de chamada persistente seja o que as pessoas intuitivamente acham que será um problema, o equilíbrio acaba sendo bem diferente na prática.

A verdadeira razão pela qual estou escrevendo este comentário é que o Flutter usa uma linguagem compilada. "Linting" não é opcional. Portanto, se houver um alinhamento entre a linguagem do host e a estrutura da IU, é definitivamente possível impor que o problema "condicional" nunca surja estaticamente. Mas isso só funciona quando a estrutura da IU pode motivar mudanças de linguagem (por exemplo, Compose + Kotlin).

@Hixie, mas você concorda com a questão de que não há uma boa maneira de reutilizar a lógica de estado de uma forma combinável entre widgets? É por isso que você começou a pensar em aproveitar as propriedades recuperáveis ​​de alguma forma?

Certamente é algo que as pessoas mencionaram. Não é algo com que eu tenha uma experiência visceral. Não é algo que eu achei que fosse um problema ao escrever meus próprios aplicativos com o Flutter. Isso não significa que não seja um problema real para algumas pessoas.

Uma vez que um gancho é reutilizável, seria ótimo se os desenvolvedores de terceiros pudessem se sentir confiantes para codificar e enviar seus próprios ganchos sem exigir que todos escrevessem seus próprios invólucros

Meu ponto é que uma boa solução aqui não exigiria que ninguém escrevesse invólucros.

Qual é o próximo passo?

Existem muitos próximos passos, por exemplo:

  • Se houver problemas específicos com o Flutter sobre os quais não falamos aqui, registre os problemas e descreva os problemas.
  • Se você tiver uma boa ideia de como resolver o problema desse problema de maneiras melhores do que os ganchos, crie um pacote que o faça.
  • Se há coisas que podem ser feitas para melhorar os Ganchos, faça-o.
  • Se houver problemas com o Flutter que impeçam os Hooks de atingir seu potencial máximo, registre-os como novos problemas.
    etc.

Este tópico de discussão foi uma leitura muito interessante e estou feliz em ver que a troca de pontos de vista permaneceu civilizada.

Eu odiaria ver como um fio rude se parece então. Há tão pouca empatia neste tópico que tem sido difícil de ler e seguir do lado de fora

Meu ponto é que uma boa solução aqui não exigiria que ninguém escrevesse invólucros.

Você não precisa escrever invólucros. Mas você pode querer colher os benefícios e a capacidade de reutilização em seu próprio código com os quais está acostumado. Você com certeza ainda pode usar as bibliotecas como estão. Se você escrever um material de embrulho de gancho (se possível), provavelmente não é porque você acha que é um fardo, mas que é melhor do que a alternativa.

Na verdade, esse é um bom motivo e um motivo mencionado pelo qual uma solução para o problema neste tópico seria ótima no núcleo. Uma solução de lógica de estado combinável reutilizável no núcleo significaria que as pessoas não teriam que escrever invólucros, uma vez que tal lógica reutilizável poderia ser enviada com segurança em todos os pacotes sem adicionar dependências.

Uma solução de lógica de estado combinável reutilizável no núcleo significaria que as pessoas não teriam que escrever invólucros, uma vez que tal lógica reutilizável poderia ser enviada com segurança em todos os pacotes sem adicionar dependências.

O que estou tentando mostrar é que uma boa solução IMHO não exigiria _nenhum_ para escrever essa lógica. Simplesmente não haveria nenhuma lógica redundante para reutilizar. Por exemplo, olhando para o exemplo "fetchUser" anterior, ninguém teria que escrever um gancho, ou seu equivalente, para chamar a função "fetchUser", você apenas chamaria a função "fetchUser" diretamente. Da mesma forma, "fetchUser" não precisaria saber nada sobre ganchos (ou o que quer que usemos) e os ganchos (ou o que quer que usemos) não precisaria saber nada sobre "fetchUser". Tudo isso mantendo a lógica que você escreve trivial, como é com ganchos.

As restrições atuais são causadas pelo fato de que os ganchos são um remendo no topo das limitações da linguagem.

Em alguns idiomas, ganchos são uma construção de linguagem, como:

state count = 0;

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

Isso seria uma variante das funções assíncronas / sincronizadas , que podem preservar algum estado nas chamadas.

Não requer mais um uso não condicional, pois, como parte da linguagem, podemos diferenciar cada variável por seu número de linha em vez de seu tipo.

Eu acrescentaria que as limitações dos ganchos são semelhantes às limitações --track-widget-creation.

Este sinalizador quebra a canonalização do construtor const para widgets. Mas isso não é um problema, pois os widgets são declarativos.

Nesse sentido, os ganchos são os mesmos. As limitações realmente não importam, pois são manipuladas declarativamente.
Não obteremos um livro muito específico sem ler os outros.

Talvez o exemplo do fetchuser não seja o ideal.
Mas o useStream, useAnimstion ou useStreamCintroller tornam a Árvore de Widget muito mais limpa e evita que você se esqueça de descartar ou de cuidar de dudChangeDependencues.
Portanto, o modo atual tem suas armadilhas em que você pode ser pego. Então eu acho que o problema potencial com a sequência de chamadas não é maior do que esses.
Não tenho certeza se começaria a escrever meus próprios ganchos, mas seria bom ter uma coleção de muitas vezes necessária pronta para ser usada dentro do framework.
Seria apenas uma forma alternativa de lidar com eles.

@Hixie , realmente sinto muito por não ser capaz de entender o que você está tentando descrever, eu culpo que seja tarde da noite aqui, mas provavelmente sou eu 😳 .. Mas na boa solução que você descreve, onde estariam os valores de estado, a lógica de negócios de estado e a lógica de evento de tempo de vida que a solução para o problema envolve / encapsula para ser facilmente combinável e compartilhada entre os widgets residem? Você poderia explicar um pouco o que uma boa solução faz e como você vê que ela funcionaria idealmente?

Só estou me intrometendo um pouco aqui, visto que há menções sobre civilidade nessa discussão. Pessoalmente, não sinto que alguém aqui esteja sendo rude.

Dito isso, acho que vale a pena notar que este é um assunto com o qual as pessoas se preocupam profundamente, por todos os lados.

  • @rrousselGit responde a perguntas de iniciantes sobre gerenciamento de estado no StackOverflow e no rastreador de problemas package:provider há anos. Estou seguindo apenas a última dessas mangueiras e não tenho nada além de respeito pela diligência e paciência de Remi.
  • @Hixie e outros na equipe do Flutter se preocupam profundamente com a API do Flutter, sua estabilidade, superfície, facilidade de manutenção e legibilidade. É graças a isso que a experiência de desenvolvedor do Flutter está onde está hoje.
  • Os desenvolvedores do Flutter se preocupam profundamente com o gerenciamento de estado, porque é isso que fazem uma parte significativa de seu tempo de desenvolvimento.

É claro que todas as partes nesta discussão têm um bom motivo para defender o que fazem. Também é compreensível que leve tempo para transmitir as mensagens.

Então, eu ficaria feliz se a discussão continuasse, seja aqui ou de alguma outra forma. Se eu puder ajudar de alguma forma, como com um documento formal, é só me avisar.

Se, por outro lado, as pessoas acham que a discussão aqui está saindo do controle, vamos fazer uma pausa e ver se há uma maneira melhor de nos comunicarmos.

(Separadamente, quero agradecer a

@ emanuel-lundman

Mas, na boa solução que você descreve, onde residem os valores de estado, a lógica de negócios de estado e a lógica de evento de vida útil que a solução para o problema envolve / encapsula para ser facilmente combinável e compartilhada entre os widgets? Você poderia explicar um pouco o que uma boa solução faz e como você vê que ela funcionaria idealmente?

Infelizmente não posso explicar porque não sei. :-)

@escamoteur

Talvez o exemplo do fetchuser não seja o ideal.
Mas o useStream, useAnimstion ou useStreamCintroller tornam a Árvore de Widget muito mais limpa e evita que você se esqueça de descartar ou de cuidar de dudChangeDependencues.

Uma das dificuldades nesta questão tem sido um "movimento das balizas" onde um problema é descrito, então quando é analisado, o problema é descartado como não sendo o problema real e um novo problema é descrito, e assim por diante. O que pode ser realmente útil seria apresentar alguns exemplos canônicos, por exemplo, um aplicativo de demonstração do Flutter que tenha um exemplo real do problema, um que não seja excessivamente simplificado para fins de exposição. Então poderíamos reimplementar isso usando Ganchos e outras propostas, e realmente avaliá-los uns contra os outros em termos concretos. (Eu mesmo faria isso, mas não entendo exatamente qual é o problema, então provavelmente é melhor se alguém que defende os Ganchos o fizer.)

O que pode ser realmente útil seria apresentar alguns exemplos canônicos, por exemplo, um aplicativo de demonstração do Flutter que tenha um exemplo real do problema, um que não seja excessivamente simplificado para fins de exposição

O que você acha do exemplo que dei aqui? https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

Este é um trecho de código do mundo real.

Acho que seria um ótimo começo. Podemos colocá-lo em um estado em que seja executado como um aplicativo independente, com uma versão que não usa ganchos e uma versão que usa?

Desculpe, eu quis dizer como um trecho de código, não como um aplicativo.

Acho que um dos problemas com a ideia do "aplicativo de demonstração Flutter" é que os exemplos feitos neste tópico são muito reais.
Eles não são simplificados demais.
O principal caso de uso de ganchos é fatorar microestados, como debounce, manipuladores de eventos, assinaturas ou efeitos colaterais implícitos - que são combinados para obter lógicas mais úteis.

Tenho alguns exemplos no Riverpod, como https://marvel.riverpod.dev/#/ onde o código-fonte está aqui: https://github.com/rrousselGit/river_pod/tree/master/examples/marvel/lib
Mas isso não vai ser muito diferente do que foi mencionado até agora.

@Hixie

Eu realmente tenho dificuldade em entender por que isso é um problema. Eu escrevi muitos aplicativos Flutter, mas realmente não parece ser um grande problema? Mesmo no pior caso, são quatro linhas para declarar uma propriedade, inicializá-la, descartá-la e relatá-la aos dados de depuração (e geralmente é menos, porque você geralmente pode declará-la na mesma linha que inicializa, aplicativos geralmente não precisa se preocupar em adicionar estado às propriedades de depuração, e muitos desses objetos não têm estado que precise ser descartado).

Eu estou no mesmo barco.
Eu admito, também não entendo realmente os problemas descritos aqui. Eu nem entendo o que é a "lógica de estado" a que as pessoas se referem, que precisa ser reutilizável.

Eu tenho muitos widgets de formulário com monitoração de estado, alguns com dezenas de campos de formulário, e eu mesmo tenho que gerenciar os controladores de texto e focusnodes. Eu os crio e os disponho nos métodos de ciclo de vida do statelesswidget. Embora seja um tanto entediante, não tenho nenhum widget que use a mesma quantidade de controladores / focusNodes ou para o mesmo caso de uso. A única coisa comum entre eles é o conceito geral de ter um estado e ser uma forma. Só porque é um padrão, não significa que o código seja repetido.
Quer dizer, em muitas partes do meu código eu tenho que percorrer arrays, eu não chamaria de fazer "for (var coisa nas coisas)" em todo o código de repetição do meu aplicativo.

Eu amo o poder do StatefulWidget que vem da simplicidade de sua API de ciclo de vida. Ele me permite escrever StatefulWidgets que fazem uma coisa e fazem isso de forma isolada do resto do aplicativo. O "estado" dos meus widgets é sempre privado para eles mesmos, portanto, reutilizar meus widgets não é um problema, nem a reutilização de código.

Tenho alguns problemas com os exemplos apresentados aqui, que estão de acordo com seus pontos:

  • criar vários widgets com estado com exatamente a mesma "lógica de estado" parece simplesmente errado e contra a ideia de ter widgets autocontidos. Mas, novamente, estou confuso quanto ao que as pessoas entendem por "lógica de estado" comum.
  • ganchos não parecem fazer nada que eu já não possa fazer usando o dardo simples e conceitos básicos de programação (como funções)
  • os problemas parecem estar relacionados ou causados ​​por um certo estilo de programação, estilo que parece favorecer o "estado global reutilizável".
  • abstrair algumas linhas de código cheira a "otimização prematura de código" e adiciona complexidade para resolver um problema que tem pouco ou nada a ver com a estrutura e tudo a ver com como as pessoas a usam.

Isso é significativamente menos legível.

  • Temos 10 níveis de indentação - 12 se fizermos um FilterBuilder para reutilizar a lógica do filtro
  • A lógica do filtro não é reutilizável como está.

    • podemos esquecer de cancelar timer por engano
  • metade do método build não é útil para o leitor. Os Construtores nos distraem do que importa
  • Perdi uns bons 5 minutos tentando entender por que o código não compila devido à falta de um parêntese

Seu exemplo é mais uma demonstração de como o Provider é prolixo e por que abusar de InheritedWidgets para tudo é uma coisa ruim, em vez de qualquer problema real com a API de ciclo de vida StatelessWidget e State do Flutter.

@rrousselGit Desculpe se não fui claro. A sugestão que eu estava fazendo acima era especificamente para criar aplicativos Flutter vanilla (usando StatefulWidget etc) que são realistas e mostram os problemas que você está descrevendo, para que possamos fazer propostas com base em um aplicativo completo real. Os exemplos concretos que discutimos aqui, como o exemplo "fetchUser", sempre terminaram com uma discussão do tipo "bem, você poderia lidar com esse caso como este e seria simples e não precisaria de ganchos" seguidos por "bem, isso é muito simplificado, no mundo real você precisa de ganchos". Então, meu ponto é, vamos criar um exemplo do mundo real que realmente _sobre_ precisa de Hooks, que não seja simplificado demais, que mostre a dificuldade em reutilizar o código, para que assim possamos ver se é possível evitar esses problemas sem usar nenhum código novo , ou se precisarmos de um novo código e, no último caso, se ele deve ter o formato de ganchos ou se pudermos encontrar uma solução ainda melhor.

Eu nem entendo o que é a "lógica de estado" a que as pessoas se referem, que precisa ser reutilizável.

É justo
Um paralelo à lógica de estado seria a lógica da IU e o que os Widgets trazem para a mesa.

Poderíamos remover tecnicamente a camada Widget. Nessa situação, o que sobraria são RenderObjects.

Por exemplo, poderíamos ter um contador minimalista:

var counter = 0;

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

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

Isso não é necessariamente complexo. Mas está sujeito a erros. Temos uma duplicata na renderização de counterLabel

Com widgets, temos:

class _CounterState exends State {
  int counter = 0;

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

A única coisa que isso fez foi fatorar a lógica Text , tornando-a declarativa.
É uma mudança minimalista. Mas em uma grande base de código, isso é uma simplificação significativa .

Os ganchos fazem exatamente a mesma coisa.
Mas em vez de Text , você obtém ganchos personalizados para lógicas de estado. Que inclui ouvintes, debouncing, fazer solicitações HTTP, ...


Seu exemplo é mais uma demonstração de como o Provider é prolixo e por que abusar de InheritedWidgets para tudo é uma coisa ruim, em vez de qualquer problema real com a API de ciclo de vida StatelessWidget e State do Flutter.

Isso não está relacionado ao provedor (este código, afinal, não usa o provedor).
Se houver alguma coisa, o provedor tem melhor porque tem context.watch vez de Consumer .

A solução padrão seria substituir Consumer por ValueListenableBuilder - o que leva exatamente ao mesmo problema.

Eu concordo @Hixie , acho que precisamos de duas comparações lado a lado para julgar a eficácia de apenas Flutter versus com ganchos. Isso também ajudaria a convencer os outros se os ganchos são melhores ou não, ou talvez outra solução seja ainda melhor se o aplicativo vanilla for construído com essa terceira solução. Este conceito de aplicativo vanilla já existe há um tempo, com coisas como TodoMVC mostrando a diferença entre vários frameworks de front-end, então não é necessariamente novo. Posso ajudar na criação desses aplicativos de exemplo.

@satvikpendem
Eu estaria disposto a ajudar.
Acho que o aplicativo de exemplo no repo flutter_hooks provavelmente mostra vários ganchos diferentes e o que eles tornam mais fácil / problemas que eles resolvem e seria um bom ponto de partida.

Também acho que poderíamos usar alguns dos exemplos e abordagens que são apresentados nesta edição.

Atualização: o código está aqui, https://github.com/TimWhiting/local_widget_state_approaches
Não tenho certeza do nome adequado para o repositório, então não presuma que esse é o problema que estamos tentando resolver. Eu fiz o aplicativo de contador básico em stateful e hooks. Não tenho muito tempo mais tarde esta noite, mas tentarei adicionar mais casos de uso que sejam mais ilustrativos de qual pode ser o problema. Quem quiser contribuir, solicite acesso.

Os exemplos concretos que discutimos aqui, como o exemplo "fetchUser", sempre terminaram com uma discussão do tipo "bem, você poderia lidar com esse caso como este e seria simples e não precisaria de ganchos" seguidos por "bem, isso é muito simplificado, no mundo real você precisa de ganchos".

Discordo. Eu não acho que tenha visto "você poderia lidar com esse caso assim" e concordei que o código resultante era melhor do que a variante de gancho.

Meu ponto o tempo todo foi que, embora possamos fazer as coisas de maneira diferente, o código resultante está sujeito a erros e / ou menos legível.
Isso se aplica a fetchUser também

Os ganchos fazem exatamente a mesma coisa.
Mas em vez de Text , você obtém ganchos personalizados para lógicas de estado. Que inclui ouvintes, debouncing, fazer solicitações HTTP, ...

Não, ainda não entendi o que essa lógica de estado comum deveria ser. Quer dizer, tenho muitos widgets que lêem de um banco de dados em seus métodos "initState / didUpdateDependency", mas não consigo encontrar dois widgets que fazem exatamente a mesma consulta, portanto, sua "lógica" não é a mesma.

Usando o exemplo de fazer uma solicitação HTTP. Supondo que eu tenha um "makeHTTPRequest (url, parâmetros)" em algum lugar da minha classe de serviço que alguns dos meus widgets precisam usar, por que eu usaria um gancho em vez de apenas chamar o método diretamente sempre que necessário? Como usar ganchos é diferente de chamadas de método simples neste caso?

Ouvintes. Eu não tenho widgets ouvindo as mesmas coisas. Cada um dos meus widgets é responsável por assinar tudo o que for necessário e garantir que eles cancelem a assinatura. Os ganchos podem ser um açúcar sintático para a maioria das coisas, mas como meus widgets não ouviriam a mesma combinação de objetos, os ganchos teriam que ser "parametrizados" de alguma forma. Novamente, como os ganchos são diferentes de uma função simples e antiga?


Isso não está relacionado ao provedor (este código, afinal, não usa o provedor).
Se houver alguma coisa, o provedor tem melhor porque tem context.watch vez de Consumer .

Huh? Seu contra-exemplo para o que o HookWidget "ChatScreen" deve resolver foi este:

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

Como isso não está relacionado ao provedor? Estou confuso. Não sou especialista em Provider, mas isso definitivamente parece código usando Provider.

Gostaria de insistir no fato de que esta questão não é sobre Estados complexos.
Trata-se de pequenos incrementos que podem ser aplicados a toda a base de código.

Se não concordarmos com o valor dos exemplos dados aqui, um aplicativo não trará nada para a conversa - já que não há nada que possamos fazer com ganchos que não possamos fazer com StatefulWidget.

Minha recomendação seria comparar micro-snippets lado a lado como ImplicitFetcher e _objetivamente_ determinar qual código é melhor usando métricas mensuráveis ​​e fazer isso para uma ampla variedade de pequenos snippets.


Como isso não está relacionado ao provedor? Estou confuso. Não sou especialista em Provider, mas isso definitivamente parece código usando Provider.

Este código não é do Provider, mas de um projeto diferente que não usa InheritedWidgets.
O provedor Consumer não tem um parâmetro provider .

E, como mencionei, você pode substituir Consumer -> ValueListenableBuilder / StreamBuilder / BlocBuilder / Observer / ...

em seus métodos "initState / didUpdateDependency", mas não consigo encontrar dois widgets que fazem exatamente a mesma consulta, portanto, sua "lógica" não é a mesma.

A lógica de estado que queremos reutilizar não é "fazer uma consulta", mas "fazer algo quando x mudar". O "fazer algo" pode mudar, mas o "quando x mudar" é comum

Exemplo concreto:
Podemos querer que um widget faça uma solicitação HTTP sempre que o ID que ele recebe for alterado.
Também queremos cancelar as solicitações pendentes usando CancelableOperation de package:async .

Agora, temos dois widgets que desejam fazer exatamente a mesma coisa, mas com uma solicitação HTTP diferente.
No final, temos:

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

A única diferença é que mudamos fetchUser por fetchMessage . A lógica é 100% igual. Mas não podemos reutilizá-lo, o que está sujeito a erros.

Com ganchos, poderíamos fatorar isso em um gancho useUnaryCancelableOperation .

O que significa que com os mesmos dois widgets, em vez disso, faria:

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

VS

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

Nesse cenário, toda a lógica relacionada a fazer a solicitação e cancelá-la é compartilhada. Ficamos apenas com uma diferença significativa, que é fetchUser vs fetchMessage .
Poderíamos até fazer um pacote com useUnaryCancelableOperation , e agora todos podem reutilizá-lo em seus aplicativos.

Se não concordarmos com o valor dos exemplos dados aqui, um aplicativo não trará nada para a conversa - já que não há nada que possamos fazer com ganchos que não possamos fazer com StatefulWidget.

Se for realmente o caso, acho que devemos fechar este bug, porque já discutimos os exemplos dados aqui e eles não têm sido convincentes. Eu realmente gostaria de entender a situação melhor, no entanto, e parecia, a partir de comentários anteriores neste bug, que os benefícios estão no nível do aplicativo, daí minha sugestão de estudarmos exemplos de aplicativos.

A única diferença é que mudamos fetchUser por fetchMessage . A lógica é 100% igual. Mas não podemos reutilizá-lo, o que está sujeito a erros.

O que está sujeito a erros e o que pode ser reutilizado? Implementar uma nova camada de abstração e hierarquia de classes apenas para que não tenhamos que implementar três métodos em uma classe e é um exagero.

Novamente, só porque algo é um padrão comum, não significa que você precisa criar um novo recurso para ele. Além disso, se você quiser reduzir o código repetitivo neste caso, você pode simplesmente estender a classe StatefulWidget * e substituir os métodos initstate / didUpdateWidget com os bits comuns.

Com ganchos, poderíamos fatorar isso em um gancho useUnaryCancelableOperation .

O que significa que com os mesmos dois widgets, em vez disso, faria:

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

VS

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

Nesse cenário, toda a lógica relacionada a fazer a solicitação e cancelá-la é compartilhada. Ficamos apenas com uma diferença significativa, que é fetchUser vs fetchMessage .
Poderíamos até fazer um pacote com useUnaryCancelableOperation , e agora todos podem reutilizá-lo em seus aplicativos.

Sinto muito, mas isso é grande, definitivo, não de mim. Além do fato de que salva apenas uma pequena quantidade de redundância de código, "fatorar" um código que conceitualmente pertence aos métodos de ciclo de vida "initState" e "atualizar", no método de construção é um grande não.

Espero que meus métodos de construção construam apenas o layout e nada mais. Configurar e eliminar dependências definitivamente não pertence ao método de construção, e estou muito feliz por ter que reescrever explicitamente o mesmo tipo de código para torná-lo explícito para mim e para os outros o que meu widget está fazendo. E não vamos colocar tudo no método de construção.

Se for realmente esse o caso, acho que devemos fechar este bug

@Hixie Por favor, não. As pessoas se preocupam com esse problema. Falei com você no reddit sobre a mesma coisa , mas no contexto do SwiftUI: https://github.com/szotp/SwiftUI_vs_Flutter

Não se trata de ganchos, mas de melhorar os widgets com estado. As pessoas simplesmente odeiam escrever clichês. Para desenvolvedores que escrevem código SwiftUI, que estão acostumados a RAII e copiar semânticas de Views, o gerenciamento manual de descartáveis ​​parece apenas fora de questão.

Portanto, encorajo a equipe do flutter a pelo menos ver isso como um problema e pensar em soluções / melhorias alternativas.

Espero que meus métodos de construção construam apenas o layout e nada mais. Configurar e eliminar dependências definitivamente não pertence ao método de construção,
Esse é um ponto importante. Os métodos de construção devem ser puros. Ainda assim, gostaria que pudéssemos ter as vantagens sem as dificuldades

Eu realmente não entendo a necessidade de mais exemplos aqui. Está claro em seu rosto.

O problema resolvido pelos ganchos é simples e óbvio, pois mantém o código SECO. Os benefícios disso são os óbvios, menos código == menos bugs, a manutenção é mais fácil, menos lugares para bugs se esconderem, a contagem de linhas mais baixa aumenta a legibilidade, os programadores juniores são melhor isolados etc.

Se você está falando de um caso de uso do mundo real, é um aplicativo onde você está configurando e derrubando 12 controladores de animador em 12 visualizações diferentes, a cada vez, deixando a porta aberta para perder uma chamada dispose () ou substituir alguns outro método de ciclo de vida. em seguida, aplique isso a dezenas de outras instâncias com estado e você verá facilmente centenas ou milhares de linhas de código inúteis.

Flutter está cheio desses casos em que precisamos nos repetir constantemente, configurando e destruindo o estado de pequenos objetos, que criam todos os tipos de oportunidades para bugs que não precisam existir, mas existem, porque atualmente não há uma abordagem elegante para compartilhando esta configuração / desmontagem / lógica de sincronização rotineira.

Você pode ver esse problema em literalmente _qualquer_ estado que tem uma fase de configuração e desmontagem ou tem algum gancho de ciclo de vida que sempre precisa ser conectado.

Eu mesmo acho que usar widgets é a melhor abordagem, raramente uso o AnimatorController, por exemplo, porque a configuração / desmontagem é tão irritante, prolixa e propensa a erros, em vez de usar TweenAnimationBuilder em todos os lugares que posso. Mas essa abordagem tem suas limitações à medida que você obtém um número maior de objetos com estado em uma determinada visualização, forçando aninhamento e detalhamento onde realmente nenhum deveria ser necessário.

@szotp Eu não ... Prefiro que estabeleçamos um ou mais aplicativos

Os aplicativos

@esDotDev Discutimos casos como este neste bug até agora, mas sempre que o fazemos, outras soluções além dos Hooks são descartadas porque não resolvem algum problema que não foi incluído no exemplo que a solução abordou. Conseqüentemente, descrições simples de problemas não parecem ser suficientes para capturar toda a extensão. Por exemplo, o caso dos "12 controladores de animador" talvez pudesse ser resolvido por um conjunto de controladores de animação e os recursos funcionais do Dart. TweenAnimationBuilder pode ser outra solução. Nenhum deles envolve Ganchos. Mas tenho certeza que se eu sugerir isso, alguém vai apontar algo que perdi e dizer "não funciona porque ..." e trazer à tona esse (novo, no contexto do exemplo) problema. Portanto, a necessidade de um aplicativo de linha de base que todos concordamos cobre toda a extensão do problema.

Se alguém quiser levar isso adiante, eu realmente acho que a melhor maneira de fazer isso é o que descrevi acima (https://github.com/flutter/flutter/issues/51752#issuecomment-670249755 e https://github.com / flutter / flutter / issues / 51752 # issuecomment-670232842). Isso nos dará um ponto de partida que todos concordamos que representa a extensão do problema que estamos tentando resolver; então, nós podemos projetar soluções que atendam esses problemas de formas que abordar todos os desejos (por exemplo @rrousselGit 's necessidade de reutilização, @Rudiksz' necessidade s de métodos de compilação limpa, etc), eo mais importante, podemos avaliar essas soluções na contexto dos aplicativos de linha de base.

Acho que todos poderíamos facilmente concordar com o problema:
_Não existe uma maneira elegante de compartilhar as tarefas de configuração / desmontagem associadas a coisas como Streams, AnimatorControllers etc. Isso leva a verbosidade desnecessária, aberturas para bugs e legibilidade reduzida._

Alguém não concorda com isso? Não podemos começar por aí e seguir em frente na busca de uma solução? Temos que concordar que esse é um problema central, que parece que ainda não concordamos.

Enquanto escrevo isso, porém, percebi que corresponde exatamente ao nome do problema, que está em aberto e deixa espaço para discussão:
" Reutilizar a lógica de estado é muito prolixo ou muito difícil "

Para mim, esse é um problema muito óbvio, e devemos passar rapidamente do estágio de debate e fazer um brainstorming sobre o que funcionaria, se não ganchos, então o quê. Precisamos de microestados reutilizáveis ​​... Tenho certeza de que podemos descobrir algo. Isso realmente limparia muitas visualizações de Flutter no final do dia e as tornaria mais robustas.

@Hixie Por favor, não. As pessoas se preocupam com esse problema. Falei com você no reddit sobre a mesma coisa , mas no contexto do SwiftUI: https://github.com/szotp/SwiftUI_vs_Flutter

Seu exemplo SwiftUI pode ser replicado em dart em algumas linhas de código, simplesmente estendendo a classe StatefulWidget.

Tenho StatefulWidgets que não se inscrevem em nenhum notificador e / ou fazem chamadas externas e, na verdade, a maioria deles é assim. Tenho cerca de 100 widgets personalizados (embora nem todos com estado) e talvez 15 deles tenham qualquer tipo de "lógica de estado comum", conforme descrito pelos exemplos aqui.

No longo prazo, escrever algumas linhas de código (também conhecido como clichê) é uma pequena troca para evitar sobrecarga desnecessária. E, novamente, essa questão de ter que implementar os métodos initState / didUpdate parece exagerada. Quando eu crio um widget que usa qualquer um dos padrões descritos aqui, talvez eu passe os primeiros 5-10 minutos "implementando" os métodos de ciclo de vida e depois alguns dias escrevendo e polindo o próprio widget, sem nunca tocar nos métodos de ciclo de vida. A quantidade de tempo que gasto escrevendo o chamado código padrão de configuração / desmontagem é minúscula em comparação com o código do meu aplicativo.

Como eu disse, o fato de StatefulWidgets fazerem tão poucas suposições sobre para que são usados ​​é o que os torna tão poderosos e eficientes.

Adicionar um novo tipo de widget ao Flutter que subclasse StatefulWidget (ou não) para este caso de uso específico seria bom, mas não vamos assá-lo no próprio StatefulWidget. Tenho muitos widgets que não precisam da sobrecarga que viria com um sistema de "ganchos" ou microestados.

@esDotDev Concordo que esse é um problema que algumas pessoas estão enfrentando; Eu até propus algumas soluções anteriormente neste problema (pesquise minhas várias versões de uma classe Property , pode estar enterrado agora, já que o GitHub não gosta de mostrar todos os comentários). A dificuldade é que essas propostas foram descartadas porque não resolveram problemas específicos (por exemplo, em um caso, não manipulou hot reload, em outro, não manipulou didUpdateWidget). Aí eu fiz mais propostas, mas essas foram novamente rejeitadas porque não cuidavam de outra coisa (esqueci o quê). É por isso que é importante ter algum tipo de linha de base que concordemos que represente o problema _entire_ para que possamos encontrar soluções para esse problema.

O objetivo nunca mudou. As críticas feitas são que as soluções sugeridas carecem de flexibilidade. Nenhum deles continua a trabalhar fora do snippet para o qual foram implementados.

É por isso que o título desta edição menciona "Difícil": Porque atualmente não há flexibilidade na maneira como resolvemos problemas atualmente.


Outra maneira de ver isso é:

Esta questão é basicamente argumentar que precisamos implementar uma camada de widget, para lógica de estado.
As soluções sugeridas são "Mas você pode fazer isso com RenderObjects".

_A longo prazo, escrever algumas linhas de código (também conhecido como clichê) é uma pequena troca para evitar sobrecarga desnecessária._

Junte lêndeas com esta afirmação:

  1. Não são realmente algumas linhas, se você pegar colchetes, espaçamento de linha @overides , etc. em conta, você pode estar olhando para 10-15 + linhas para um controlador de animador simples. Isso não é trivial na minha mente ... como muito além do trivial. 3 linhas para fazer isso me incomodam (no Unity é Thing.DOTween() ). 15 é ridículo.
  1. Não se trata de digitar, embora isso seja uma dor. É sobre a tolice de ter uma classe de 50 linhas, onde 30 linhas são clichês rotineiros. Sua ofuscação. É sobre o fato de que se você _não_ escreve o clichê, não há avisos nem nada, você acabou de adicionar um bug.
  2. Não vejo nenhuma sobrecarga que valha a pena discutir com algo como Hooks. Estamos falando de uma série de objetos, com um punhado de efeitos em cada um. No Dart, que é muito rápido. Este é um arenque vermelho imo.

@esDotDev

Para mim, esse é um problema muito óbvio, e devemos passar rapidamente do estágio de debate e fazer um brainstorming sobre o que funcionaria, se não ganchos, então o quê.

Ampliando widgets. Assim como o ValueNotifier estende o ChangeNotifier para simplificar um padrão de uso comum, todos podem escrever seus próprios sabores de StatelessWidgets para seus casos de uso específicos.

Sim, concordo que é uma abordagem eficaz, mas deixa a desejar. Se eu tiver um animador, posso apenas usar um TweenAnimationBuilder. Legal, ainda tem 5 linhas de código, mas tanto faz. funciona ... não é MUITO ruim. Mas se eu tiver 2 ou 3? Agora estou no inferno do aninhamento, se eu tenho um cpl de outros objetos com estado por algum motivo, bem, tudo meio que se tornou uma bagunça de recuo, ou estou criando algum widget muito específico que encapsula uma coleção aleatória de configuração, atualização e desmontagem lógica.

Ampliando widgets. Assim como o ValueNotifier estende o ChangeNotifier para simplificar um padrão de uso comum, todos podem escrever seus próprios sabores de StatelessWidgets para seus casos de uso específicos.

Você pode estender apenas uma classe base por vez. Isso não escala

Mixins são a próxima tentativa lógica. Mas, como o OP menciona, eles também não têm escala.

@esDotDev

ou estou criando algum widget muito específico que encapsula uma coleção aleatória de configuração, atualização e lógica de desmontagem.

Um tipo de widget que precisa configurar 3-4 tipos de AnimationControllers soa como um caso de uso muito específico e o suporte à coleta aleatória de lógica de configuração / desmontagem definitivamente não deve fazer parte de uma estrutura. Na verdade, é por isso que os métodos initState / didUpdateWidget são expostos em primeiro lugar, para que você possa fazer sua coleção aleatória de configuração de acordo com sua vontade.

Meu método initState mais longo tem 5 linhas de código, meus widgets não sofrem de aninhamento excessivo, então definitivamente temos necessidades e casos de uso diferentes. Ou um estilo de desenvolvimento diferente.

@esDotDev

3. Não vejo nenhuma sobrecarga que valha a pena discutir com algo como Hooks. Estamos falando de uma série de objetos, com um punhado de efeitos em cada um. No Dart, que é muito rápido. Este é um arenque vermelho imo.

Se a solução proposta for algo como o pacote flutter_hooks, isso é totalmente falso. Sim, conceitualmente é um array com funções, mas a implementação está longe de ser trivial ou eficiente.

Quer dizer, posso estar errado, mas parece que o HookElement verifica se deve se reconstruir em seu próprio método de construção ?!
Além disso, verificar se os ganchos devem ser inicializados, reinicializados ou descartados em cada construção de widget parece uma sobrecarga significativa. Simplesmente não parece certo, então espero estar errado.

Faria sentido tomar um dos exemplos de arquitetura de @brianegan como aplicativo base para uma comparação?

Se posso intervir aqui, não tenho certeza se isso já foi dito. Mas no React não pensamos realmente no ciclo de vida com ganchos, e isso pode parecer assustador se for como você está acostumado a construir componentes / widgets, mas aqui está porque o ciclo de vida não importa realmente.

Na maioria das vezes, quando você está construindo componentes / widgets com estado ou ações a serem tomadas com base em adereços, você quer que algo aconteça quando esse estado / adereços mudam (por exemplo, como eu vi mencionado neste tópico, você vai querer -obter os detalhes de um usuário quando o prop userId for alterado). Normalmente é muito mais natural pensar nisso como um "efeito" da alteração do userId, em vez de algo que acontece quando todos os adereços do widget mudam.

A mesma coisa para a limpeza, geralmente é muito mais natural pensar "Eu preciso limpar este estado / ouvinte / controlador quando este prop / estado mudar" em vez de "Eu preciso me lembrar de limpar o X quando todos os props / estado mudarem ou quando todo o componente é destruído ".

Eu não escrevo Flutter há algum tempo, então não estou tentando soar como se eu conhecesse o clima atual ou as limitações que essa abordagem teria na mentalidade atual do Flutter, estou aberto a opiniões divergentes. Só acho que muitas pessoas não familiarizadas com os ganchos React estão tendo a mesma confusão que eu tive quando fui apresentado a eles, porque minha mentalidade estava muito arraigada no paradigma do ciclo de vida.

@escamoteur @Rudiksz @Hixie houve um projeto GitHub criado por @TimWhiting para o qual fui convidado para onde estamos começando a criar esses exemplos. Cada pessoa / grupo pode criar como resolveria um problema predefinido. Eles não são aplicativos totalmente desenvolvidos, mais como páginas, mas podemos adicionar aplicativos também, se eles servirem para mostrar exemplos mais complexos. Então, podemos discutir problemas e criar uma API melhor. @TimWhiting pode convidar qualquer pessoa interessada, presumo.

https://github.com/TimWhiting/local_widget_state_approaches

Jetpack compose também é semelhante a ganchos, que foram comparados com react aqui .

@satvikpendem @TimWhiting Isso é ótimo! Obrigada.

@esDotDev
caso de uso muito específico e suporte a coleta aleatória de lógica de configuração / desmontagem definitivamente não deve fazer parte de uma estrutura.

Este é o prego que engancha acerta na cabeça. Cada tipo de objeto é responsável por sua própria configuração e desmontagem. Os animadores sabem como criar, atualizar e destruir a si mesmos, assim como os fluxos e assim por diante. Ganchos soluciona especificamente esse problema de 'coleções aleatórias' de andaimes de estado espalhados por toda a sua visualização. Isso permite que a maior parte do código de visualização se concentre na lógica de negócios e na formatação do layout, o que é uma vitória. Isso não força você a criar widgets personalizados, apenas para ocultar alguns clichês genéricos que são os mesmos em todos os projetos.

Meu método initState mais longo tem 5 linhas de código, meus widgets não sofrem de aninhamento excessivo, então definitivamente temos necessidades e casos de uso diferentes. Ou um estilo de desenvolvimento diferente.

Meu também. Mas é initState () + dispose () + didUpdateDependancies (), e perder um dos dois últimos pode causar bugs.

Acho que o exemplo canônico seria algo como: Escreva um modo de exibição que usa 1 controlador de fluxo e 2 controladores de animador.

Você tem 3 opções, tanto quanto posso ver:

  1. Adicione 30 linhas ou mais de clichê à sua classe e alguns mixins. O que não é apenas prolixo, mas bastante difícil de seguir inicialmente.
  2. Use 2 TweenAnimationBuilders e um StreamBuilder, por cerca de 15 níveis de indentação, antes mesmo de chegar ao seu código de visualização, e você ainda terá muitos clichês para o Stream.
  3. Adicione cerca de 6 linhas de código não indentado no topo de build (), para obter seus 3 subobjetos com estado e definir qualquer código init / destroy personalizado

Talvez haja uma 4ª opção que é SingleStreamBuilderDoubleAnimationWidget, mas isso é apenas uma coisa improvisada para desenvolvedores e bastante irritante em geral.

Também vale a pena notar que a carga cognitiva de 3 é significativamente menor do que as outras 2 para um novo desenvolvedor. A maioria dos novos desenvolvedores nem sabe que TweenAnimationBuilder existe, e simplesmente aprender o conceito de SingleTickerProvider é uma tarefa por si só. Apenas dizer, "Dê-me um animador, por favor", é uma abordagem mais fácil e robusta.

Vou tentar codificar algo hoje.

2. Use 2 TweenAnimationBuilders e um StreamBuilder, para cerca de 15 níveis de indentação, antes mesmo de chegar ao seu código de visualização, e você ainda terá muitos clichês para o Stream.

Direito. Mostre-nos um exemplo do mundo real de código que usa 15 níveis de indentação.

Como substituir 30 linhas de código por 6 linhas + centenas de linhas em uma biblioteca reduz a carga cognitiva? Sim, posso simplesmente ignorar a "mágica" que a biblioteca faz, mas não suas regras. Por exemplo, o pacote de ganchos informa em termos inequívocos que os ganchos devem ser usados ​​apenas em métodos de construção. Agora você tem uma restrição extra com a qual se preocupar.

Eu provavelmente tenho menos de 200 linhas de código que envolvem focusnodes, textcontrollers, singletickerproviders ou os vários métodos de ciclo de vida de meus statefulwidgets, em um projeto com 15k linhas de código. De que sobrecarga cognitiva você está falando?

@Rudiksz por favor, pare de ser passivo-agressivo.
Podemos discordar sem lutar.


As restrições dos ganchos são a menor das minhas preocupações.

Não estamos falando sobre ganchos especificamente, mas sobre o problema.
Se for necessário, podemos propor uma solução diferente.

O que importa é o problema que queremos resolver.

Além disso, os widgets só podem ser usados ​​dentro do build e incondicionalmente (ou estamos mudando a profundidade da árvore, o que é impossível)

Isso é idêntico às restrições de ganchos, mas não acho que as pessoas reclamaram disso.

Além disso, os widgets só podem ser usados ​​dentro do build e incondicionalmente (ou estamos mudando a profundidade da árvore, o que é impossível)

Isso é idêntico às restrições de ganchos, mas não acho que as pessoas reclamaram disso.

Não, não é idêntico. O problema que é apresentado aqui parece estar relacionado ao código que _prepara_ widgets _antes_ de serem (re) construídos. Preparando estado, dependências, fluxos, controladores, enfeites e manipulação de mudanças na estrutura da árvore. Nenhum deles deve estar no método de construção, mesmo se estiver oculto por trás de uma única chamada de função.
O ponto de entrada para essa lógica nunca deve estar no método de construção.

Forçar-me a colocar lógica de inicialização de qualquer tipo no método de construção não é o mesmo que "me forçar" a compor uma árvore de widget no método de construção. A razão para o método de construção é pegar um estado existente (conjunto de variáveis) e produzir uma árvore de widget que então é pintada.

Por outro lado, eu também seria contra me forçar a adicionar código que constrói widgets dentro dos métodos initState / didUpdateWidget.

Como estão agora, os métodos de ciclo de vida statefulwidget têm uma função muito clara e tornam muito fácil e direto separar o código com questões totalmente diferentes.

Conceitualmente, estou começando a entender os problemas que estão sendo descritos aqui, mas ainda não consigo ver isso como um problema real. Talvez alguns exemplos reais (que não sejam o aplicativo de contador) possam me ajudar a mudar de ideia.

Como uma observação lateral, Riverpod , meu último experimento, tem algumas ideias muito parecidas com ganchos, sem as restrições.

Por exemplo, ele resolve:

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

tendo:

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

Onde watch pode ser chamado condicionalmente:

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

Poderíamos até nos livrar de Consumer inteiramente tendo uma classe base StatelessWidget / StatefulWidget :

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

O principal problema é que isso é específico para um tipo de objeto e funciona com base no fato de que a instância do objeto tem um hashCode consistente nas reconstruções.

Portanto, ainda estamos longe da flexibilidade dos ganchos

@rrousselGit Acho que sem estender StatelessWidget / StatefulWidget classes e criar algo como ConsumerStatelessWidget, é possível ter algo como context.watch usando métodos de extensão em BuildContext classe e fazer com que o provedor forneça a função de observação com InheritedWidgets.

Esse é um assunto diferente. Mas tl; dr, não podemos confiar no InheritedWidgets como uma solução para esse problema: https://github.com/flutter/flutter/issues/30062

Para resolver esse problema, usar InheritedWidgets nos bloquearia por causa de https://github.com/flutter/flutter/issues/12992 e https://github.com/flutter/flutter/pull/33213

Conceitualmente, estou começando a entender os problemas que estão sendo descritos aqui, mas ainda não consigo ver isso como um problema real.

Comparando o Flutter com o SwiftUI, para mim é óbvio que existe um problema real, ou melhor - as coisas não estão tão boas quanto poderiam ser.

Pode ser difícil de ver, porque Flutter e outros trabalharam muito em torno disso: temos wrappers para cada caso específico: AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity, etc. StatefulWidget funciona muito bem para implementar esses pequenos utilitários reutilizáveis, mas é um nível muito baixo para componentes específicos de domínio não reutilizáveis, onde você pode ter uma infinidade de controladores de texto, animações ou qualquer coisa que a lógica de negócios exigir. A solução usual é morder a bala e escrever todo aquele clichê ou fazer uma árvore cuidadosamente construída de provedores e ouvintes. Nenhuma das abordagens é satisfatória.

É como @rrousselGit diz, antigamente (UIKit) éramos forçados a gerenciar manualmente nossos UIViews (o equivalente a RenderObjects) e lembrar de copiar os valores do modelo para a visualização e voltar, excluir visualizações não utilizadas, reciclar e assim por diante. Isso não era uma ciência de foguetes, e muitas pessoas não viram esse velho problema, mas acho que todos aqui concordariam que o Flutter claramente melhorou a situação.
Com o statefulness, o problema é muito semelhante: é um trabalho enfadonho e sujeito a erros que pode ser automatizado.

E, a propósito, não acho que ganchos resolvam isso de forma alguma. Acontece que os ganchos são a única abordagem possível sem alterar o interior da vibração.

StatefulWidget funciona muito bem para implementar esses pequenos utilitários reutilizáveis, mas é um nível muito baixo para componentes específicos de domínio não reutilizáveis, onde você pode ter uma infinidade de controladores de texto, animações ou qualquer coisa que a lógica de negócios exigir.

Estou confuso quando você diz que para construir seus componentes específicos de domínio não reutilizáveis, você precisa de um widget de alto nível. Normalmente é exatamente o oposto.

AnimatedBuilder, StreamBuilder, Consumer, AnimatedOpacity são todos widgets que implementam um determinado caso de uso. Quando eu preciso de um widget que tem uma lógica tão específica que não posso usar nenhum desses widgets de nível superior , é quando eu desço para uma API de nível inferior para que possa escrever meu próprio caso de uso específico. O chamado clichê está implementando como meu widget exclusivo gerencia sua combinação única de fluxos, chamadas de rede, controladores e outros enfeites.

Defender ganchos, comportamento semelhante a ganchos ou mesmo apenas "automação" é como dizer que precisamos de um widget de baixo nível que possa lidar com lógica de alto nível e não reutilizável que qualquer pessoa gostaria de ter sem ter que escrever o chamado código clichê.

Com o statefulness, o problema é muito semelhante: é um trabalho enfadonho e sujeito a erros que pode ser automatizado.

Novamente. Você deseja automatizar __ "componentes específicos de domínio não reutilizáveis, onde você pode ter uma infinidade de controladores de texto, animações ou qualquer outra lógica de negócios que chame" __ ?!

É como @rrousselGit diz, antigamente (UIKit) éramos forçados a gerenciar manualmente nossos UIViews (o equivalente a RenderObjects) e lembrar de copiar os valores do modelo para a visualização e voltar, excluir visualizações não utilizadas, reciclar e assim por diante. Isso não era uma ciência de foguetes, e muitas pessoas não viram esse velho problema, mas acho que todos aqui concordariam que o Flutter claramente melhorou a situação.

Eu desenvolvi iOS e Android de 6 a 7 anos atrás (na época em que o Android mudou para o design de material) e não me lembro de nenhum desses modos de exibição de gerenciamento e reciclagem ser um problema e o Flutter não parece melhor ou pior. Não posso falar sobre os assuntos atuais, parei quando o Swift e o Kotlin foram lançados.

O boilerplate que sou forçado a escrever em meu StatefulWidgets é cerca de 1% da minha base de código. É sujeito a erros? Cada linha de código é uma fonte potencial de bugs, então com certeza. É complicado? 200 linhas de código ouot de 15000? Eu realmente acho que não, mas essa é apenas minha opinião. Os controladores de texto / animação do Flutter, os focusnodes, todos têm problemas que podem ser melhorados, mas ser prolixo não é um deles.

Estou realmente curioso para ver o que as pessoas estão desenvolvendo que precisam de tanto clichê.

Ouvir alguns dos comentários aqui parece que gerenciar 5 linhas de código em vez de 1 é 5 vezes mais difícil. Não é.

No entanto, você não concordaria que, em vez de configurar initState e dispor para cada AnimationController, por exemplo, pode ser mais sujeito a erros do que fazer isso apenas uma vez e reutilizar essa lógica? Mesmo princípio de uso de funções, reutilização. Eu concordo que é problemático colocar ganchos na função de construção, porém, definitivamente há uma maneira melhor.

Realmente parece que a diferença entre aqueles que veem e não veem o problema aqui é que os primeiros usaram construções semelhantes a ganchos antes, como em React, Swift e Kotlin, e os últimos não, como trabalhar em Java puro ou Android. Acho que a única maneira de ficar convencido, pela minha experiência, é experimentar ganchos e ver se você consegue voltar ao modo padrão. Muitas vezes, muitas pessoas não conseguem, de novo, na minha experiência. Você sabe quando o usa.

Para esse fim, eu encorajo as pessoas que são céticas a usar flutter_hooks para um pequeno projeto e ver como ele se sai, depois refazê-lo da maneira padrão. Não é suficiente que simplesmente criemos versões do aplicativo para alguém ler como na sugestão de

Não é suficiente que simplesmente criemos versões do aplicativo para alguém ler como na sugestão de

Perdi dias tentando provedor, mais dias tentando bloco, não achei nenhum deles uma boa solução. Se isso funciona para você, ótimo.

Para que eu mesmo tente sua solução proposta para um problema que você está tendo, você precisa demonstrar suas vantagens. Eu olhei exemplos com ganchos de vibração e olhei sua implementação. Apenas não.

Qualquer que seja o código redutor clichê adicionado à estrutura, espero que Stateful / StatelessWidgets sejam deixados intactos. Não há muito mais que eu possa acrescentar a esta conversa.

Vamos começar de novo, em um mundo hipotético onde podemos mudar o dardo, sem falar em ganchos.

O problema debatido é:

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

Este código não é legível.

Poderíamos corrigir o problema de legibilidade introduzindo uma nova palavra-chave que altera a sintaxe para:

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

Este código é significativamente mais legível, não está relacionado a ganchos e não sofre de suas limitações.
O ganho de legibilidade não é muito sobre o número de linhas, mas a formatação e indentações.


Mas e quando o Builder não é o widget raiz?

Exemplo:

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

Poderíamos ter:

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

Mas como isso se relaciona com a questão da reutilização?

A razão pela qual isso está relacionado é que os Builders são tecnicamente uma maneira de reutilizar a lógica de estado. Mas o problema é que eles tornam o código pouco legível se planejamos usar _muitos_ construtores, como neste comentário https://github.com/flutter/flutter/issues/51752#issuecomment -669626522

Com essa nova sintaxe, corrigimos o problema de legibilidade. Como tal, podemos extrair mais coisas para os Builders.

Portanto, por exemplo, o useFilter mencionado neste comentário poderia ser:

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

Que podemos usar com a nova palavra-chave:

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

E quanto ao "extrair como função" de que falamos com ganchos, para criar ganchos / construtores personalizados?

Poderíamos fazer a mesma coisa com essa palavra-chave, extraindo uma combinação de Builders em uma função:

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

Obviamente, não se deu muita atenção a todas as implicações dessa sintaxe. Mas essa é a ideia básica.


Ganchos são esse recurso.
As limitações dos ganchos existem porque eles são implementados como um pacote, em vez de um recurso de linguagem.

E a palavra-chave é use , de modo que keyword StreamBuilder se torna use StreamBuilder , que é implementado em última instância como useStream

Este código é significativamente mais legível

Acho que isso é uma questão de opinião. Concordo que algumas pessoas pensam que as versões que você descreve como mais legíveis são melhores; Pessoalmente, prefiro as versões muito mais explícitas sem magia. Mas não tenho nenhuma objeção em tornar o segundo estilo possível.

Dito isso, a próxima etapa é trabalhar no aplicativo do @TimWhiting (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/stateful/counter.dart) para torná-lo algo que tenha todos os problemas que queremos resolver.

Pelo que vale a pena, https://github.com/flutter/flutter/issues/51752#issuecomment -670959424 é muito parecido com a inspiração para Hooks in React. O padrão Builder parece idêntico ao padrão Render Props que costumava ser predominante no React (mas conduzia a árvores com profundidade semelhante). Mais tarde, @trueadm sugeriu sintaxe de açúcar para Render Props, e mais tarde isso levou a Hooks (para remover sobrecarga de tempo de execução desnecessária).

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

Se legibilidade e indentação forem o problema, isso pode ser reescrito como

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

Se as funções não são o seu lugar, ou você precisa de capacidade de reutilização, extraia-as como 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');
      },
    );
  }
}

Não há nada em seu exemplo que justifique uma nova palavra-chave ou recurso.

Eu sei o que você vai dizer. A variável 'valor' deve ser passada por todos os widgets / chamadas de função, mas isso é apenas o resultado de como você arquitetou seu aplicativo. Eu divido meu código usando métodos de "construção" e widgets personalizados, dependendo do caso de uso, e nunca preciso passar a mesma variável para uma cadeia de três chamadas.

O código reutilizável é reutilizável quando depende o menos possível de efeitos colaterais externos (como InheritedWidgets ou estado (semi) global).

@Rudiksz Não acho que você está acrescentando nada à discussão aqui. Estamos cientes das estratégias para mitigar esses problemas porque as fazemos o dia todo. Se você acha que isso não é um problema, pode simplesmente continuar a usar as coisas como estão e isso não o afetará de forma alguma.

Claramente, existem muitas pessoas que veem isso como um ponto fundamental de dor, e apenas andar de um lado para o outro é inútil. Você não vai, por meio de vários argumentos, convencer as pessoas de que elas não querem o que querem ou mudar a opinião de ninguém aqui. Todos nesta discussão claramente têm centenas ou milhares de horas no Flutter em seu currículo, e não é esperado que todos concordemos sobre as coisas.

Acho que isso é uma questão de opinião. Concordo que algumas pessoas pensam que as versões que você descreve como mais legíveis são melhores; Pessoalmente, prefiro as versões muito mais explícitas sem magia. Mas não tenho nenhuma objeção em tornar o segundo estilo possível.

Se for uma questão de opinião, suponho que seja um tanto assimétrico em uma direção.

  1. Ambos têm magia. Não sei necessariamente o que qualquer um desses construtores está fazendo internamente. A versão não mágica é escrever o boilerplate real dentro desses construtores. Usar um mixin SingleAnimationTIckerProvider também é mágico para 95% dos desenvolvedores de Flutter.
  2. Um ofusca nomes de variáveis ​​muito importantes que serão usados ​​posteriormente na árvore, que são value1 e value2 , o outro os tem na frente e no centro no topo da construção. Esta é uma vitória clara de análise / manutenção.
  3. Um tem 6 níveis de recuo antes mesmo de a árvore do widget começar, o outro tem 0
  4. Um tem 5 linhas verticais, o outro tem 15
  5. Um mostra a parte do conteúdo real com destaque (Text ()) e a outra o oculta, aninhado, bem no fundo da árvore. Outra vitória clara na análise.

De um modo geral, pude ver que isso talvez fosse uma questão de gosto. Mas o Flutter tem um problema específico com a contagem de linha, indendação e relação sinal: ruído em geral. Embora eu absolutamente _adore_ a capacidade de formar declarativamente uma árvore no código Dart, isso pode levar a algum código extremamente ilegível / detalhado, especialmente quando você depende de várias camadas de construtores empacotados. Portanto, no contexto do Flutter em si, onde estamos continuamente lutando nessa batalha, esse tipo de otimização é um recurso matador, porque nos fornece uma ferramenta realmente excelente para combater esse problema um tanto difuso de verbosidade geral.

TL; DR - Qualquer coisa que reduza significativamente o recuo e a contagem de linha no Flutter, é duplamente valioso, devido às contagens de linha e recuo geralmente altos inerentes ao Flutter.

@Rudiksz Não acho que você está acrescentando nada à discussão aqui. Estamos cientes das estratégias para mitigar esses problemas porque as fazemos o dia todo. Se você acha que isso não é um problema, pode simplesmente continuar a usar as coisas como estão e isso não o afetará de forma alguma.

Exceto se for uma mudança na estrutura principal, isso me afeta, não?

Claramente, existem muitas pessoas que veem isso como um ponto fundamental de dor, e apenas andar de um lado para o outro é inútil. Você não vai, por meio de vários argumentos, convencer as pessoas de que elas não querem o que querem ou mudar a opinião de ninguém aqui. Todos nesta discussão claramente têm centenas ou milhares de horas no Flutter em seu currículo, e não é esperado que todos concordemos sobre as coisas.

Certo, esse cavalo já foi espancado até a morte inúmeras vezes, então não vou cair na armadilha de responder a mais comentários.

Os construtores são tecnicamente uma forma de reutilizar a lógica de estado. Mas o problema é que eles tornam o código pouco legível.

Isso afirma perfeitamente. Para pensar em termos de flutuação, precisamos de construtores de linha única.

Construtores que não requerem dezenas de guias e linhas, mas ainda permitem que algum clichê customizado seja conectado ao ciclo de vida do widget.

O mantra "tudo é um widget" especificamente não é uma coisa boa aqui. O código relevante em um construtor é geralmente apenas seus adereços de configuração e a coisa stateful que ele retorna que o construtor precisa. Cada quebra de linha e tabulação são basicamente inúteis.

Exceto se for uma mudança na estrutura principal, isso me afeta, não?

@Rudiksz Eu não acho que alguém está propondo que mudemos os widgets Stateful. Você sempre pode usá-los em sua forma atual, se desejar. Qualquer solução que possamos propor usará widgets com estado sem alterações ou outro tipo de widget inteiramente. Não estamos dizendo que widgets com estado são ruins, apenas que gostaríamos de outro tipo de widget que permite um estado de widget mais combinável. Pense nele como um widget Stateful que, em vez de um objeto de estado associado a ele, contém vários objetos de estado e uma função de construção que é separada, mas tem acesso a esses objetos de estado. Dessa forma, você pode reutilizar bits de estado comum (junto com a lógica de estado associada a esse estado) com seus initState e dispose já implementados para você. Estado essencialmente mais modular, que pode ser composto de diferentes maneiras em diferentes situações. Novamente, esta não é uma proposta concreta, mas talvez uma outra maneira de pensar a respeito. Talvez pudesse se transformar em uma solução que fosse mais flutter como, mas eu não sei.

Dito isso, a próxima etapa é trabalhar no aplicativo do @TimWhiting (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/stateful/counter.dart) para torná-lo algo que tenha todos os problemas que queremos resolver.

Isso é complicado porque esse problema é inerentemente um de uma morte por mil cortes. Ele apenas adiciona inchaço e diminui a legibilidade em toda a base de código. Seu impacto é sentido mais em widgets pequenos, onde a classe inteira tem menos de 100 linhas e metade dela é gasta no gerenciamento de um controlador de animador. Então, eu não sei o que ver 30 desses exemplos mostrará, que eu não.

É realmente a diferença entre isso:

<strong i="10">@override</strong>
  Widget build(BuildContext context) {
    final controller = get AnimationController(vsync: this, duration: widget.duration);
    //do stuff
  }

E isto:

  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
  }

Simplesmente não há melhor maneira de ilustrar do que isso. Você pode estender este caso de uso a qualquer objeto do tipo controlador. AnimatorController, FocusController e TextEditingController são provavelmente os mais comuns e irritantes de gerenciar no dia a dia. Agora imagine apenas 50 ou 100 deles espalhados por toda a minha base de código.

  • Você tem cerca de 1000-2000 linhas que podem simplesmente desaparecer.
  • Você provavelmente tem dezenas de bugs e RTEs (em vários pontos de desenvolvimento) que nunca precisaram existir porque alguma substituição ou outra estava faltando.
  • Você tem widgets que, quando olhados com olhos frios, são muito mais difíceis de grocar à primeira vista. Preciso ler cada uma dessas substituições, não posso simplesmente assumir que são clichês.

E você pode estender isso para controladores personalizados. Todo o conceito de usar controladores é menos atraente no Flutter porque você sabe que terá que inicializá-los, gerenciá-los e destruí-los assim, o que é irritante e sujeito a bugs. Isso o leva a evitar fazer seus próprios e, em vez disso, criar StatefulWidgets / Builders personalizados. Seria bom se os objetos do tipo controlador fossem apenas mais fáceis de usar e mais robustos, uma vez que os construtores têm problemas de legibilidade (ou, pelo menos, são significativamente mais prolixos e carregados de espaços em branco).

Isso é complicado

Sim, o design da API é complicado. Bem-vindo a minha vida.

Então, eu não sei o que ver 30 desses exemplos mostrará, que eu não.

Não são 30 exemplos que vão ajudar, é 1 exemplo que é elaborado o suficiente que não pode ser simplificado de maneiras que você diria "bem, com certeza, para este exemplo funciona, mas não para um exemplo _real_".

Não são 30 exemplos que irão ajudar, é 1 exemplo que é elaborado o suficiente que não pode ser simplificado de maneiras que você diria "bem, com certeza, para este exemplo funciona, mas não para um exemplo real".

Já disse algumas vezes, mas essa forma de julgar os ganchos é injusta.
Ganchos não têm como objetivo tornar possível algo que antes era impossível. Trata-se de fornecer uma API consistente para resolver esse tipo de problema.

Solicitar um aplicativo que mostra algo que não pode ser simplificado de outra forma é julgar um peixe por sua capacidade de subir em árvores.

Não estamos apenas tentando julgar ganchos, estamos tentando avaliar diferentes soluções para ver se há alguma que atenda às necessidades de todos.

(Estou curioso para saber como você avaliaria diferentes propostas aqui, se não realmente escrevendo aplicações em cada uma das propostas e comparando-as. Que métrica de avaliação você proporia?)

Uma maneira adequada de julgar a solução para esse problema não é um aplicativo (já que cada uso individual da API será descartado como os exemplos aqui).

O que devemos julgar a solução proposta é:

  • O código resultante é objetivamente melhor do que a sintaxe padrão?

    • Isso evita erros?

    • É mais legível?

    • É mais fácil escrever?

  • Quão reutilizável é o código produzido?
  • Quantos problemas podem ser resolvidos com esta API?

    • Perdemos alguns benefícios para alguns problemas específicos?

Quando avaliada nesta grade, a proposta Property / addDispose pode ter um bom "o código resultante é melhor?" pontuação, mas avaliada mal em relação à capacidade de reutilização e flexibilidade.

Não sei como responder a essas perguntas sem realmente ver cada proposta em uso real.

Porque?

Não precisei fazer aplicativos usando Property para saber que esta proposta terá dificuldade em produzir código verdadeiramente reutilizável e resolver muitos problemas.

Podemos pegar qualquer * Builder existente e tentar reimplementá-lo usando a solução proposta.
Também podemos tentar e reimplementar os ganchos listados neste tópico, ou alguns ganchos feitos na comunidade React (há várias compilações de ganchos disponíveis online).

Não precisei fazer aplicativos usando Property para saber que esta proposta terá dificuldade em produzir código verdadeiramente reutilizável e resolver muitos problemas.

Infelizmente, não compartilho seus instintos aqui (como demonstrado pelo fato de que eu pensei que Propriedade (https://github.com/flutter/flutter/issues/51752#issuecomment-667737471) funcionava muito bem até que você disse que tinha que lidar com valores tanto de um widget quanto de um estado local com a mesma API, que eu não sabia ser uma restrição até que você a trouxe à tona). Se eu fornecer uma versão da propriedade que resolva esse problema também, com certeza vai ficar tudo bem ou vai haver algum novo problema que não cobre? Sem um alvo que todos concordamos ser o alvo, não sei para o que estamos projetando soluções.

Podemos pegar qualquer * Builder existente e tentar reimplementá-lo usando a solução proposta.

Claramente, não _qualquer_. Por exemplo, você deu um no OP, e quando fiz minha primeira proposta de propriedade (https://github.com/flutter/flutter/issues/51752#issuecomment-664787791) você apontou problemas com ele que não foram ilustrados por o Construtor original.

Também podemos tentar e reimplementar os ganchos listados neste tópico, ou alguns ganchos feitos na comunidade React (há várias compilações de ganchos disponíveis online).

Não me importo por onde começar. Se você tem um exemplo particularmente bom que você acha que ilustra o problema e ele usa ganchos, ótimo, vamos adicioná-lo ao repositório do @TimWhiting . O objetivo é implementar a mesma coisa de várias maneiras diferentes, não me importo de onde as ideias vêm.

Não são 30 exemplos que vão ajudar, é 1 exemplo que é elaborado o suficiente que não pode ser simplificado de maneiras que você diria "bem, com certeza, para este exemplo funciona, mas não para um exemplo _real_".

Nunca haverá nada mais elaborado do que o simples desejo de usar um AnimatorController (ou qualquer outro componente com estado reutilizável que você possa imaginar) sem um construtor ilegível ou um monte de clichês de ciclo de vida propenso a bugs.

Não foi proposta nenhuma solução que contemplasse os benefícios de legibilidade e robustez solicitados, de forma geral.

Eu insisto no fato de que _qualquer_ construtor servirá, já que este problema poderia ser renomeado para "Precisamos de sintaxe de açúcar para Construtores" e levar à mesma discussão.

Todos os outros argumentos apresentados (como criar e descartar AnimationController ) são baseados em que eles também podem ser extraídos em um construtor:

Widget build(context) {
  return AnimationControllerBuilder(
    duration: Duration(seconds: 2),
    builder: (context, animationController) {

    }
  );
}

No final, acho que o exemplo perfeito é tentar reimplementar o StreamBuilder em sua totalidade e testá-lo em diferentes cenários:

  • o stream vem de Widget
  • // de um InheritedWidget
  • do estado local

e testar cada caso individual em relação a "o fluxo pode mudar com o tempo", então:

  • didUpdateWidget com um novo fluxo
  • o InheritedWidget atualizado
  • nós chamamos setState

No momento, isso não tem solução com Property ou onDispose

@esDotDev Você pode enumerar "os benefícios de legibilidade e robustez solicitados"? Se alguém fizer uma proposta que lida com o AnimationController com esses benefícios de legibilidade e robustez, então terminamos aqui?

@rrousselGit Não estou defendendo a Propriedade, como você disse antes, que ela não resolve seus problemas. Mas se alguém criasse uma solução que fizesse tudo o que o StreamBuilder faz, mas sem o recuo, seria isso? Você ficaria feliz?

Mas se alguém criasse uma solução que fizesse tudo o que o StreamBuilder faz, mas sem o recuo, seria isso? Você ficaria feliz?

Muito provavelmente sim

Precisamos comparar essa solução com outras soluções, é claro. Mas isso alcançaria o nível aceitável.

@esDotDev Você pode enumerar "os benefícios de legibilidade e robustez solicitados"?

Robustez porque pode encapsular totalmente o clichê em torno das dependências e do ciclo de vida. ou seja, Eu não deveria ter que dizer a fetchUser todas as vezes, que ele provavelmente deveria reconstruir quando o id muda, ele sabe fazer isso internamente. Não deveria ter que dizer a uma animação para se desfazer toda vez que seu widget pai for descartado, etc. (eu não entendo completamente se Property pode fazer isso tbh). Isso impede que os desenvolvedores cometam erros em tarefas rotineiras, em toda a base de código (um dos principais benefícios do uso de Builders atualmente)

Legibilidade é que podemos obter a coisa com estado com uma única linha de código não indentado, e a variável para a coisa é içada e claramente visível.

@esDotDev Se alguém fizer uma proposta que lida com AnimationController com esses benefícios de legibilidade e robustez, então terminamos aqui?

Se você quer dizer especificamente o AnimationController no. Se você quer dizer qualquer objeto do tipo AnimationController / FocusController / TextEdiitingController, então sim.

Ter uma API semelhante a uma função que retorna um valor com um tempo de vida definido que não é claro é

Acho que isso é um mal-entendido fundamental. O tempo de vida de um gancho é claro, porque eles são, por definição, subestados. Eles _sempre_ existem durante a vida do Estado que os "usa". Na verdade, não poderia ser mais claro. A sintaxe pode ser estranha e desconhecida, mas certamente não falta clareza.

Semelhante a como o tempo de vida de um TweenAnimationBuilder () também é claro. Ele irá embora quando seu pai for embora. Como um widget filho, esses são estados filhos. Eles são "componentes" de estado totalmente independentes, podemos montar e reutilizar com facilidade e explicitamente não gerenciamos seu tempo de vida porque queremos que seja naturalmente vinculado ao estado em que está declarado, um recurso, não um bug.

@esDotDev

etc

Você pode ser mais específico? (É por isso que sugeri criar um aplicativo de demonstração que abrangesse todas as bases. Continuo pensando que essa é a melhor maneira de fazer isso.) Existem recursos que importam além de apenas chamar o inicializador, a menos que a configuração tenha mudado e descartado automaticamente recursos alocados quando o elemento host é descartado?

Objeto semelhante a TextEdiitingController

Você pode ser mais específico? O TextEditingController é mais elaborado do que o AnimationController de alguma forma?


@rrousselGit

Mas se alguém criasse uma solução que fizesse tudo o que o StreamBuilder faz, mas sem o recuo, seria isso? Você ficaria feliz?

Muito provavelmente sim

Aqui está uma solução que faz tudo o que o StreamBuilder faz, sem qualquer recuo:

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

Estou supondo que isso viola alguma outra restrição, no entanto. É por isso que eu preferiria ter algo com que todos concordemos como uma descrição completa do problema antes de tentarmos resolvê-lo.

São simplesmente as mesmas restrições que os construtores têm @Hixie; ninguém está pedindo mais do que isso. Um construtor pode se conectar a widget. Seja o que for, um construtor pode gerenciar totalmente qualquer estado interno necessário no contexto da árvore de widgets. Isso é tudo que um gancho pode fazer, e tudo o que qualquer um está pedindo por um microestado ou como você quiser chamá-lo.

Você pode ser mais específico? O TextEditingController é mais elaborado do que o AnimationController de alguma forma?

Não, mas ele pode fazer coisas diferentes em init / dispose, ou se vinculará a propriedades diferentes, e eu gostaria de encapsular esse clichê específico.

@esDotDev Então você quer a mesma coisa que um construtor, mas sem o recuo e em uma linha (sem o próprio retorno de chamada do construtor, presumivelmente)? O exemplo que acabei de postar (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483) faz isso com os construtores hoje, então presumivelmente há restrições adicionais além disso?

(FWIW, eu não acho que construtores, ou algo como construtores, mas em uma linha, são uma boa solução, porque eles exigem que cada tipo de dados tenha seu próprio construtor criado para ele; não há uma boa maneira de apenas construir um na hora .)

(FWIW, eu não acho que construtores, ou algo como construtores, mas em uma linha, são uma boa solução, porque eles exigem que cada tipo de dados tenha seu próprio construtor criado para ele; não há uma boa maneira de apenas construir um na hora .)

Eu não entendo o que isso significa. Você poderia reformular isso? 🙏

Você deve criar um AnimationBuilder para animações e um StreamBuilder para streams e assim por diante. Em vez de apenas ter um único Builder e dizer "aqui está como você consegue um novo, eis como você o descarta, eis como você obtém os dados" etc, quando você cria seu StatefulWidget.

Estou supondo que isso viola alguma outra restrição, no entanto. É por isso que eu preferiria ter algo com que todos concordemos como uma descrição completa do problema antes de tentarmos resolvê-lo.

Eu acho que obviamente viola qualquer solicitação de código legível, que é o objetivo final aqui, caso contrário, todos nós apenas usaríamos um milhão de construtores especificamente tipados, aninhá-los para sempre e encerrar o dia.

Acho que o que está sendo solicitado é algo como (paciência, eu não uso muito o 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)]);
}

Este é todo o código. Nada mais haveria, já que o Stream é criado para nós, o stream é disposto para nós, não podemos dar um tiro no próprio pé E o código é muito mais legível.

Você deve criar um AnimationBuilder para animações e um StreamBuilder para streams e assim por diante.

Não vejo isso como um problema. Já temos RestorableInt vs RestorableString vs RestorableDouble

E os genéricos podem resolver isso:

GenericBuilder<Stream<int>>(
  create: (ref) {
    var controller = StreamController<int>();
    ref.onDispose(controller.close);
    return controller.stream;
  }
  builder: (context, Stream<int> stream) {

  }
)

Da mesma forma, Flutter ou Dart podem incluir uma interface Disposable se isso realmente for um problema.

@esDotDev

Acho que o que está sendo solicitado é algo como:

Isso violaria algumas das restrições bastante razoáveis ​​que outros listaram (por exemplo, @Rudiksz), a saber, garantir que nenhum código de inicialização aconteça durante a chamada para o método de construção.

@rrousselGit

Não vejo isso como um problema. Já temos RestorableInt vs RestorableString vs RestorableDouble

E temos AnimationBuilder e StreamBuilder e assim por diante, sim. Em ambos os casos, é lamentável.

GenericBuilder

Isso é semelhante ao que propus para a propriedade, mas se entendi suas preocupações, você considerou isso muito prolixo.

Anteriormente, você disse que se alguém criasse uma solução que fizesse tudo o que o StreamBuilder faz, mas sem o recuo, você provavelmente ficaria feliz. Você não comentou minha tentativa de fazer isso (https://github.com/flutter/flutter/issues/51752#issuecomment-671004483). Você está feliz com essa solução?

@esDotDev

Acho que o que está sendo solicitado é algo como:

Isso violaria algumas das restrições bastante razoáveis ​​que outros listaram (por exemplo, @Rudiksz), a saber, garantir que nenhum código de inicialização aconteça durante a chamada para o método de construção.

Não é importante que este código esteja em construção. A parte importante é que

  1. Não sou forçado a recuar minha árvore ou adicionar uma tonelada de linhas extras.
  2. O código de ciclo de vida específico para isso é encapsulado.

Isso seria incrível:

AsyncSnapshot<int> snapshot1 = createLifecycleState(widget.stream1);
AsyncSnapshot<int> snapshot2 = createLifecycleState(widget.stream2);
AniamtorController animator = createLifecycleState(duration: Duration(seconds: 1), (a)=>a.forward());

Widget build(context) {
   return Opacity(opacity: animator.value, child: Column(children: [Text(snapshot1.data), Text(snapshot2.data)]));
}

Ou não tão sucinto, mas ainda muito mais legível do que usar construtores e menos prolixo e sujeito a erros do que fazer isso diretamente:

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

Não entendo por que continuamos voltando à verbosidade.
Eu disse várias vezes explicitamente que esse não é o problema e que o problema é capacidade de reutilização versus legibilidade versus flexibilidade.

Até fiz uma grade para avaliar a solução https://github.com/flutter/flutter/issues/51752#issuecomment -671000137 e um caso de teste https://github.com/flutter/flutter/issues/51752#issuecomment - 671002248


Anteriormente, você disse que se alguém criasse uma solução que fizesse tudo o que o StreamBuilder faz, mas sem o recuo, você provavelmente ficaria feliz. Você não comentou sobre minha tentativa de fazer isso (# 51752 (comentário)). Você está feliz com essa solução?

Isso atinge o nível mínimo aceitável de flexibilidade.

Avaliando-o de acordo com https://github.com/flutter/flutter/issues/51752#issuecomment -671000137 dá:

  • O código resultante é objetivamente melhor do que a sintaxe padrão?

    • Isso evita erros?

      _A sintaxe padrão (StreamBuilder sem hacking) é menos sujeita a erros. Esta solução não evita erros, ela cria alguns_ ❌

    • É mais legível?

      _Claro que não é mais legível_ ❌

    • É mais fácil escrever?

      _Não é fácil escrever_ ❌

  • Quão reutilizável é o código produzido?
    _StreamBuilder não está vinculado ao widget / estado / ciclos de vida, então esta passagem_ ✅
  • Quantos problemas podem ser resolvidos com esta API?
    _Podemos fazer construtores personalizados e usar esse padrão. Portanto, esta passagem. ✅
  • Perdemos alguns benefícios para alguns problemas específicos?
    _Não, a sintaxe é relativamente consistente_. ✅
  1. Este recurso IMO pode se estender a todos os widgets do construtor, incluindo LayoutBuilder, por exemplo.
  2. Deve haver uma maneira de desabilitar a escuta, de modo que você possa criar controladores 10x e passá-los para as folhas para reconstrução, ou o flutter precisa saber de alguma forma qual parte da árvore usou o valor obtido pelo construtor.
  3. Usar isso não deve ser muito mais prolixo do que enganchar.
  4. O compilador deve ser estendido para lidar com isso corretamente.
  5. Auxiliares de depuração são necessários. Digamos que você coloque breakpoins em um de seus widgets que usa isso. Ao atingir um ponto de interrupção dentro de um método de construção, porque um dos construtores foi acionado, o IDE pode exibir informações extras para cada construtor que foi usado:
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)]);
}

Além disso, @Hixie

Isso violaria algumas das restrições bastante razoáveis ​​que outros listaram (por exemplo, @Rudiksz), a saber, garantir que nenhum código de inicialização aconteça durante a chamada para o método de construção.

Já estamos fazendo isso implicitamente usando * Builders. Precisamos apenas de um açúcar de sintaxe para desindentá-los. É muito parecido com async / await e futures, eu acho.

@esDotDev O que você descreve parece muito semelhante ao que propus anteriormente com Property (consulte, por exemplo, https://github.com/flutter/flutter/issues/51752#issuecomment-664787791 ou https://github.com/flutter/flutter/ questões / 51752 # issuecomment-667737471). Existe algo que impede que esse tipo de solução seja criado como um pacote? Ou seja, que mudança a estrutura central precisaria ter feito para permitir que você use esse tipo de recurso?

@rrousselGit Assim como Shawn, eu faria a mesma pergunta. Se a única diferença entre o recurso StreamBuilder atual e aquele que satisfaria suas necessidades for uma sintaxe diferente, o que é necessário da sintaxe principal para permitir o uso de tal recurso? Não seria suficiente apenas criar a sintaxe de sua preferência e usá-la?

O problema que tenho com sua grade é que se eu fosse aplicá-la às soluções até agora, obteria isso, o que suponho ser muito diferente do que você obteria:

StatefulWidget

  • O código resultante é objetivamente melhor do que a sintaxe padrão?

    • Isso evita erros?

      _É igual à sintaxe padrão, que não é particularmente propensa a erros._ 🔷

    • É mais legível?

      _É o mesmo, por isso é igualmente legível, o que é razoavelmente legível._ 🔷

    • É mais fácil escrever?

      _É a mesma coisa, por isso é igualmente fácil de escrever, o que é razoavelmente fácil._ 🔷

  • Quão reutilizável é o código produzido?
    _Há muito pouco código para reutilizar._ ✅
  • Quantos problemas podem ser resolvidos com esta API?
    _Esta é a linha de base._ 🔷
  • Perdemos alguns benefícios para alguns problemas específicos?
    _Não parece assim._ ✅

Variações na propriedade

  • O código resultante é objetivamente melhor do que a sintaxe padrão?

    • Isso evita erros?

      _Ele move o código para um lugar diferente, mas não reduz particularmente o número de erros._ 🔷

    • É mais legível?

      _Ele coloca o código de inicialização e código de limpeza e outro código de ciclo de vida no mesmo lugar, então é menos claro._ ❌

    • É mais fácil escrever?

      _Ele mistura código de inicialização e código de limpeza e outro código de ciclo de vida, então é mais difícil de escrever._ ❌

  • Quão reutilizável é o código produzido?
    _Exatamente tão reutilizável quanto StatefulWidget, apenas em lugares diferentes._ ✅
  • Quantos problemas podem ser resolvidos com esta API?
    _Este é um açúcar sintático para StatefulWidget, então nenhuma diferença._ 🔷
  • Perdemos alguns benefícios para alguns problemas específicos?
    _O desempenho e o uso de memória sofreriam um pouco._ ❌

Variações sobre construtores

  • O código resultante é objetivamente melhor do que a sintaxe padrão?

    • Isso evita erros?

      _É basicamente a solução StatefulWidget, mas fatorada; os erros devem ser aproximadamente equivalentes._ 🔷

    • É mais legível?

      _Os métodos de construção são mais complexos, o resto da lógica muda para um widget diferente, portanto quase o mesmo._ 🔷

    • É mais fácil escrever?

      _Mais difícil de escrever na primeira vez (criando o widget do construtor), um pouco mais fácil depois disso, então quase o mesmo._ 🔷

  • Quão reutilizável é o código produzido?
    _Exatamente tão reutilizável quanto StatefulWidget, apenas em lugares diferentes._ ✅
  • Quantos problemas podem ser resolvidos com esta API?
    _Este é um açúcar sintático para StatefulWidget, então quase sempre nenhuma diferença. Em alguns aspectos, é realmente melhor, por exemplo, reduz a quantidade de código que deve ser executado ao lidar com uma mudança de dependência._ ✅
  • Perdemos alguns benefícios para alguns problemas específicos?
    _Não parece assim._ ✅

Soluções tipo gancho

  • O código resultante é objetivamente melhor do que a sintaxe padrão?

    • Isso evita erros?

      _Incentiva padrões ruins (por exemplo, construção no método de compilação), arrisca bugs se usado acidentalmente com condicionais._ ❌

    • É mais legível?

      _Aumenta o número de conceitos que devem ser conhecidos para entender um método de construção._ ❌

    • É mais fácil escrever?

      _O desenvolvedor tem que aprender a escrever ganchos, que é um conceito novo, muito mais difícil._ ❌

  • Quão reutilizável é o código produzido?
    _Exatamente tão reutilizável quanto StatefulWidget, apenas em lugares diferentes._ ✅
  • Quantos problemas podem ser resolvidos com esta API?
    _Este é um açúcar sintático para StatefulWidget, então nenhuma diferença._ 🔷
  • Perdemos alguns benefícios para alguns problemas específicos?
    _Desempenho e uso de memória sofrem._ ❌

Não entendo por que continuamos voltando à verbosidade.
Eu disse várias vezes explicitamente que esse não é o problema e que o problema é capacidade de reutilização versus legibilidade versus flexibilidade.

Desculpe, não me lembrei de quem foi que disse que a Propriedade era muito prolixa. Você está certo, sua preocupação era apenas que havia um novo caso de uso que não tinha sido listado antes e que não tratava, embora eu ache que seria trivial estender Property para tratar esse caso de uso também (eu não não tentei, parece melhor esperar até que tenhamos um aplicativo de demonstração claro para que possamos resolver as coisas de uma vez por todas, em vez de ter que iterar repetidamente enquanto os requisitos são ajustados).

@szotp

  1. Este recurso IMO pode se estender a todos os widgets do construtor, incluindo LayoutBuilder, por exemplo.

LayoutBuilder é um widget muito diferente da maioria dos construtores, FWIW. Nenhuma das propostas que foram discutidas até agora funcionaria para problemas do tipo LayoutBuilder, e nenhum dos requisitos descritos antes de seu comentário inclui LayoutBuilder. Se deveríamos usar esse novo recurso para lidar com o LayoutBuilder, é importante saber; Recomendo trabalhar com @TimWhiting para garantir que o aplicativo de amostra no qual

  1. Deve haver uma maneira de desabilitar a escuta, de modo que você possa criar controladores 10x e passá-los para as folhas para reconstrução, ou o flutter precisa saber de alguma forma qual parte da árvore usou o valor obtido pelo construtor.

Não tenho certeza do que isso significa exatamente. Pelo que eu posso dizer, você pode fazer isso hoje com ouvintes e construtores (por exemplo, eu uso ValueListenableBuilder no aplicativo que mencionei anteriormente para fazer exatamente isso).

Isso violaria algumas das restrições bastante razoáveis ​​que outros listaram (por exemplo, @Rudiksz), a saber, garantir que nenhum código de inicialização aconteça durante a chamada para o método de construção.

Já estamos fazendo isso implicitamente usando * Builders.

Eu não acho que isso seja correto. Depende do construtor, mas alguns trabalham muito para separar initState, didChangeDependencies, didUpdateWidget e a lógica de construção, de modo que apenas a quantidade mínima de código precisa para executar cada construção com base no que mudou. Por exemplo, ValueListenableBuilder registra apenas os ouvintes quando criado pela primeira vez, seu construtor pode ser executado novamente sem o pai ou o initState do construtor executando novamente. Isso não é algo que os Hooks podem fazer.

@esDotDev O que você descreve parece muito semelhante ao que propus anteriormente com Propriedade (veja, por exemplo, # 51752 (comentário) ou # 51752 (comentário) ).

Se eu entendi direito, poderíamos fazer UserProperty que lida automaticamente com DidDependancyChange for UserId, ou AnimationProperty , ou qualquer outra propriedade necessária para lidar com o init / update / dispose para esse tipo? Então isso parece bom para mim. Os casos de uso mais comuns podem ser criados rapidamente.

A única coisa que me confunde é o futuro construtor aqui. Mas acho que isso se deve apenas ao exemplo que você escolheu?

Por exemplo, eu poderia criar isso?

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

Se sim, isso é totalmente LGTM! Em termos de adição ao framework, é um caso de se isso deve ser promovido a uma abordagem sintática de primeira classe (o que significa que se torna prática geral em um ano ou mais), ou se existe como um plugin que uma porcentagem de um único dígito de desenvolvedores usar.

É uma questão de saber se você deseja ser capaz de atualizar os exemplos detalhados e (ligeiramente?) Sujeitos a erros com uma sintaxe melhor e mais concisa. Ter que sincronizar propriedades manualmente e dispor () coisas manualmente leva a bugs e carga cognitiva.

Imo, seria bom se um desenvolvedor pudesse usar animadores, com didUpdate e dispose adequados e debugFillProperties e todas as obras, sem nunca ter que pensar duas vezes sobre isso (exatamente como fazemos quando usamos TweenAnimationBuilder agora, que é o principal motivo que recomendamos todos os nossos desenvolvedores costumam usá-lo em vez de gerenciar animadores manualmente).

Se sim, isso é totalmente LGTM! Em termos de adição ao framework, é um caso de se isso deve ser promovido a uma abordagem sintática de primeira classe (o que significa que se torna prática geral em um ano ou mais), ou se existe como um plugin que uma porcentagem de um único dígito de desenvolvedores usar.

Dado o quão trivial Property é, minha recomendação para alguém que gosta desse estilo seria apenas criar o seu próprio (talvez começando com meu código, se isso ajudar) e usá-lo diretamente em seu aplicativo como acharem adequado, ajustando-o para atender às suas necessidades. Ele poderia ser transformado em um pacote se muitas pessoas gostassem também, embora, novamente, para algo tão trivial, não esteja claro para mim o quanto isso é benéfico em vez de apenas copiá-lo para o código e ajustá-lo conforme necessário.

A única coisa que me confunde é o futuro construtor aqui. Mas acho que isso se deve apenas ao exemplo que você escolheu?

Eu estava tentando abordar um exemplo dado por @rrousselGit . Em princípio, pode ser adaptado para funcionar para qualquer coisa.

Por exemplo, eu poderia criar isso?

Você gostaria de mover o construtor AnimationController para um encerramento que seria chamado, em vez de chamá-lo todas as vezes, uma vez que initProperties é chamado durante o hot reload para obter os novos encerramentos, mas normalmente você não deseja criar um novo controlador durante o hot reload (por exemplo, ele redefiniria animações). Mas sim, além disso, parece bom. Você poderia até mesmo fazer um AnimationControllerProperty que pega os argumentos do construtor AnimationController e faz a coisa certa com eles (por exemplo, atualizando a duração no recarregamento a quente se ela mudou).

Imo, seria bom se um desenvolvedor pudesse usar animadores, com didUpdate e dispose adequados e debugFillProperties e todas as obras, sem nunca ter que pensar duas vezes sobre isso (exatamente como fazemos quando usamos TweenAnimationBuilder agora, que é o principal motivo que recomendamos todos os nossos desenvolvedores costumam usá-lo em vez de gerenciar animadores manualmente).

Minha preocupação em não ter os desenvolvedores pensando sobre isso é que, se você não pensar sobre quando as coisas são alocadas e dispostas, é mais provável que acabe alocando muitas coisas de que nem sempre precisa, ou executando uma lógica que não não precisa ser executado ou fazer outras coisas que levam a um código menos eficiente. Essa é uma das razões pelas quais eu ficaria relutante em tornar esse estilo padrão recomendado.

Você poderia até mesmo fazer uma AnimationControllerProperty que pega os argumentos do construtor AnimationController e faz a coisa certa com eles (por exemplo, atualizando a duração no recarregamento a quente se ela mudou).

Obrigado @Hixie, isso é muito legal e acho que resolve o problema muito bem.

Não estou sugerindo que os desenvolvedores nunca devem pensar sobre essas coisas, mas acho que o caso de uso de 99% dessas coisas quase sempre está vinculado ao StatefulWidget em que são usadas, e fazer qualquer outra coisa além disso já está levando você para a terra do desenvolvedor intermediário.

Novamente, não vejo como isso seja fundamentalmente diferente de recomendar TweenAnimationBuilder em vez de AnimatorController bruto. É basicamente a ideia de que SE você deseja que o estado totalmente contido / gerenciado dentro desse outro estado (e isso geralmente é o que você deseja), faça desta forma é mais simples e mais robusto.

Neste ponto, devemos organizar uma chamada e discuti-la junto com as diferentes partes interessadas.
Porque esta discussão não está progredindo, pois estamos respondendo à mesma pergunta continuamente.

Não entendo como, após uma discussão tão longa, com tantos argumentos apresentados, ainda podemos argumentar que os Builders não evitam erros em comparação com o StatefulWidget, ou que os ganchos não são mais reutilizáveis ​​do que os StatefulWidgets brutos.

É especialmente frustrante argumentar, considerando que todas as principais estruturas declarativas (React, Vue, Swift UI, Jetpack Compose) têm uma maneira ou outra de resolver esse problema nativamente.
Parece que apenas Flutter se recusa a considerar esse problema.

@esDotDev O principal motivo do IMHO para usar um AnimationBuilder ou TweenAnimationBuilder ou ValueListenableBuilder é que eles reconstroem apenas quando o valor muda, sem reconstruir o resto de seu widget de host . É uma questão de desempenho. Não se trata realmente de verbosidade ou reutilização de código. Quer dizer, não há problema em usá-los por esses motivos também, se você os achar úteis por esses motivos, mas esse não é o caso de uso principal, pelo menos para mim. Também é algo que a Propriedade (ou Ganchos) não fornece a você. Com eles, você acaba reconstruindo o widget _entire_ quando algo muda, o que não é bom para o desempenho.

@rrousselGit

Parece que apenas Flutter se recusa a considerar esse problema.

Eu gastei literalmente horas do meu próprio tempo neste fim de semana, sem mencionar muitas horas do tempo do Google antes disso, considerando este problema, descrevendo soluções possíveis e tentando extrair exatamente o que estamos tentando resolver. Por favor, não confunda falta de compreensão sobre o que é um problema com recusa em considerá-lo. Especialmente quando já descrevi a melhor coisa que pode ser feita para seguir em frente (criar um aplicativo de demonstração que tem toda a lógica de estado que é "muito prolixo ou muito difícil", para citar o título do problema, para reutilizar), que outros neste bug assumiram como uma tarefa, e você se recusou a participar.

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.

Interessante. Para nós, realmente nunca medimos ou observamos uma melhoria no desempenho ao salvar pequenas reconstruções como esta. É muito mais importante em relação a uma grande base de código de aplicativo manter o código sucinto, legível e livre de quaisquer erros de rotina que podem acontecer quando você está retirando centenas de arquivos de classe a cada duas semanas.

De nossa experiência, o custo de repintar os pixels, que parece acontecer com a árvore inteira, a menos que você esteja decidido a definir seus RepaintBoundaries, é um fator muito mais importante no desempenho do mundo real do que os custos parciais do layout do widget. Especialmente quando você atinge o alcance do monitor 4k.

Mas este é um bom exemplo de quando os construtores realmente fazem sentido para esse tipo de coisa. Se eu quiser criar um subcontexto, então um construtor faz sentido e é uma boa maneira de chegar lá.

Muitas vezes não o fazemos e, neste caso, o Builder está apenas adicionando desordem, mas aceitamos, porque a alternativa é apenas um tipo diferente de desordem e, pelo menos com o Builder, as coisas são mais ou menos garantidas sem bugs. Nos casos em que toda a visualização é reconstruída ou não há necessariamente reconstruções de visualização (TextEditingController, FocusController), o uso de um construtor faz pouco sentido e, em todos os casos, enrolar o clichê manualmente não é DRY.

Certamente é muito específico da situação, como os problemas de desempenho costumam ser. Acho que faz sentido que as pessoas usem algo como Ganchos ou Propriedade se gostarem desse estilo. Isso é possível hoje e não parece precisar de nada extra da estrutura (e como mostra a propriedade, realmente não requer muito código).

Não, mas é um pouco como pedir à comunidade para construir TweenAnimationBuilder e ValueListenableBuilder e não fornecer a eles um StatefulWidget para construir.

Não que você esteja perguntando, mas um dos principais benefícios desse tipo de arquitetura é que ela naturalmente se presta a pequenos componentes que podem ser facilmente compartilhados. Se você colocar uma pequena peça fundamental no lugar ...

StatefulWidget é um _lote_ de código, comparado a Property, e não é trivial (ao contrário de Property, que é principalmente código de colagem). Dito isso, se Propriedade é algo que faz sentido reutilizar amplamente (em vez de criar versões sob medida para cada aplicativo ou equipe com base em suas necessidades específicas), então eu encorajaria alguém que defende seu uso a fazer um pacote e enviá-lo para pub . O mesmo se aplica, de fato, a Hooks. Se for algo que a comunidade goste, terá muita utilidade, assim como o Provedor. Não está claro por que precisaríamos colocar algo assim no próprio framework.

Acho que porque isso é inerentemente extensível. Provedor não é, é apenas uma ferramenta simples. Isso é algo feito para ser estendido, assim como StatefulWidget, mas para StatefulComponents. O fato de ser relativamente trivial não deve necessariamente ser considerado contra ele?

Uma nota sobre "quem prefere este estilo". Se você puder salvar 3 substituições e 15-30 linhas, isso será uma vitória na legibilidade na maioria dos casos. Objetivamente falando imo. Ele também elimina objetivamente 2 classes inteiras de erros (esquecimento de descartar coisas, esquecimento de atualizar deps).

Muito obrigado pela discussão incrível, estou ansioso para ver onde isso vai dar, definitivamente vou deixá-lo aqui :)

Lamento dizer que este tópico me deixa desiludido por voltar à vibração, que era o plano ao terminar outro projeto em que estou trabalhando. Eu também sinto a frustração por causa de

É especialmente frustrante argumentar, considerando que todas as principais estruturas declarativas (React, Vue, Swift UI, Jetpack Compose) têm uma maneira ou outra de resolver esse problema nativamente.

Concordo com @rrousselGit no sentido de que não acho que devemos perder tempo criando aplicativos de exemplo flutuantes, uma vez que o problema foi descrito claramente repetidamente neste tópico. Não consigo ver como isso não iria apenas obter a mesma resposta. Porque serão as mesmas coisas que aqui foram apresentadas. Minha opinião sobre este segmento é que do ponto de vista de frameworks flutter é melhor para os desenvolvedores flutter repetir o mesmo código vitalício em vários widgets em vez de apenas escrevê-lo uma vez e terminar com ele.
Além disso, não podemos escrever um aplicativo em flutter se estivermos procurando uma solução, pois precisamos das soluções para escrever um aplicativo. Já que as pessoas agitadas nesta conversa pelo menos foram bastante claras, elas não gostam de ganchos. E o Flutter simplesmente não tem outra solução para o problema, conforme descrito no OP. Como deveria ser escrito.

Parece (pelo menos para mim) que isso não é levado a sério, sinto muito @Hixie , quero dizer, não é levado a sério no sentido de: We understand the problem and want to solve it . Como outras estruturas declarativas, aparentemente parecem ter feito.
Por outro lado, mas com a mesma nota, coisas como esta:

É mais fácil escrever?
O desenvolvedor precisa aprender a escrever ganchos, que é um conceito novo, muito mais difícil

Deixa-me triste. Por que melhorar ou mudar alguma coisa? você sempre pode usar esse argumento, não importa o quê. Mesmo que as novas soluções sejam muito mais fáceis e agradáveis ​​depois de aprendidas. Você pode substituir ganchos nessa instrução com muitas coisas. Minha mãe poderia ter usado esse tipo de frase sobre microondas há 30 anos. Por exemplo, funciona da mesma forma se você substituir "ganchos" na frase por "flutter" ou "dardo".

É mais fácil escrever?
É o mesmo, por isso é igualmente fácil de escrever, o que é razoavelmente fácil

Não acho que o que @rrousselGit quis dizer com is it easier to write? (uma pergunta com resposta booleana) foi que, se for a mesma, a resposta não é false / undefined .

Não vejo como chegaremos a algum lugar, já que nem mesmo concordamos que existe um problema, apenas que muitas pessoas acham isso um problema. Por exemplo:

Certamente é algo que as pessoas mencionaram. Não é algo com que eu tenha uma experiência visceral. Não é algo que eu achei que fosse um problema ao escrever meus próprios aplicativos com o Flutter. Isso não significa que não seja um problema real para algumas pessoas.

E embora muitos tenham fornecido muitos argumentos, muitas vezes, por que uma solução para o OP precisa estar no centro.
Por exemplo, ele precisa estar no centro para tornar terceiros capazes de usá-lo com a mesma facilidade e naturalidade com que usam e criam widgets hoje. E um múltiplo de outros motivos. O mantra parece ser, basta colocar em um pacote. Mas já existem pacotes como os ganchos. Se isso foi decidido, por que não encerrar o tópico.

Eu realmente espero que você aceite a oferta de

De qualquer forma, estou cancelando a inscrição agora, pois fico um pouco triste acompanhando este tópico ao vivo, pois não vejo que vai a lugar nenhum. Mas espero que este tópico chegue ao ponto de concordar que há um problema para que possa se concentrar em possíveis soluções para o OP. Já que parece um pouco fútil propor soluções se você não entende o problema que as pessoas estão enfrentando, como

Eu realmente desejo a você boa sorte para encerrar este tópico simplesmente dizendo que flutter não deve resolver este problema no núcleo, apesar das pessoas quererem. Ou encontrando uma solução. 😘

LayoutBuilder é um widget muito diferente da maioria dos construtores, FWIW. Nenhuma das propostas que foram discutidas até agora funcionaria para problemas do tipo LayoutBuilder, e nenhum dos requisitos descritos antes de seu comentário inclui LayoutBuilder. Se deveríamos usar esse novo recurso para lidar com o LayoutBuilder, é importante saber; Recomendo trabalhar com @TimWhiting para garantir que o aplicativo de amostra no qual

@Hixie Sim, definitivamente precisamos de algumas amostras. Vou preparar algo (mas ainda acho que são necessárias mudanças no compilador, então o exemplo pode estar incompleto). A ideia geral é - um açúcar de sintaxe que nivela os construtores e não se preocupa com a forma como o construtor é implementado.

Ainda assim, estou tendo a impressão de que ninguém na equipe do Flutter deu uma olhada mais profunda no SwiftUI, acho que nossas preocupações seriam fáceis de entender de outra forma. É importante para o futuro do framework que as pessoas que migram de outras plataformas tenham um percurso o mais tranquilo possível e, portanto, é necessário um bom entendimento de outras plataformas e conhecimento dos prós e contras. E ver se alguns dos contras do Flutter poderiam ser consertados. Obviamente, o Flutter tirou muitas boas ideias do React e eu poderia fazer o mesmo com frameworks mais novos.

@ emanuel-lundman

Parece (pelo menos para mim) que isso não é levado a sério, sinto muito @Hixie , quero dizer, não é levado a sério no sentido de: We understand the problem and want to solve it . Como outras estruturas declarativas, aparentemente parecem ter feito.

Concordo plenamente que não entendo o problema. É por isso que continuo me empenhando nesse assunto, tentando entendê-lo. É por isso que sugeri a criação de um aplicativo de demonstração que resume o problema. Se é algo que no final decidimos corrigir alterando fundamentalmente o framework, ou adicionando um pequeno recurso ao framework, ou por um pacote, ou não, realmente depende de qual é o problema realmente.

@szotp

Ainda assim, estou tendo a impressão de que ninguém na equipe do Flutter deu uma olhada mais profunda no SwiftUI, acho que nossas preocupações seriam fáceis de entender de outra forma.

Eu estudei Swift UI. Certamente é menos prolixo escrever código Swift UI do que código Flutter, mas o custo de legibilidade IMHO é muito alto. Há muita "mágica" (no sentido de lógica que funciona de maneiras que não são óbvias no código do consumidor). Posso acreditar totalmente que é um estilo que algumas pessoas preferem, mas também acredito que um dos pontos fortes do Flutter é que ele tem muito pouca magia. Isso significa que você escreve mais código às vezes, mas também significa que depurar esse código é _muito_ mais fácil.

Acho que há espaço para muitos estilos de frameworks. Estilo MVC, estilo React, mágico super conciso, livre de magia, mas prolixo ... Uma das vantagens da arquitetura do Flutter é que o aspecto de portabilidade é totalmente separado da própria estrutura, portanto, é possível aproveitar todas as nossas ferramentas - suporte multiplataforma, hot reload, etc - mas crie uma estrutura inteiramente nova. (Existem outros frameworks Flutter, por exemplo, flutter_sprites, já.) Da mesma forma, o próprio framework é projetado em camadas para que, por exemplo, você possa reutilizar toda a nossa lógica RenderObject, mas com uma substituição para a camada Widgets, portanto, se a verbosidade do Widgets é demais, alguém poderia criar uma estrutura alternativa que o substitua. E, claro, há o sistema de empacotamento para que os recursos possam ser adicionados sem perder nenhum código do framework existente.

De qualquer forma, o ponto da minha digressão aqui é apenas que isso não é tudo ou nada. Mesmo que, a longo prazo, não acabemos adotando uma solução que o faça feliz, isso não significa que você não possa continuar a se beneficiar das partes da oferta de que gosta.


Exorto as pessoas interessadas neste problema a trabalhar com @TimWhiting para criar um aplicativo que demonstra por que você deseja reutilizar o código e como ele se parece hoje, quando você não pode (https://github.com/TimWhiting/local_widget_state_approaches). Isso nos ajudará diretamente a criar propostas de como lidar com esse problema de uma forma que atenda às necessidades de _todas_ as pessoas que estão comentando aqui (incluindo aqueles que gostam de ganchos e aqueles que não gostam de ganchos).

Realmente não pode ser tão difícil entender por que "_um açúcar de sintaxe que nivela os construtores e não se preocupa com como o construtor é implementado._" é desejado pelos desenvolvedores como um recurso de primeira classe. Descrevemos os problemas com as abordagens alternativas repetidamente.

Resumindo, os construtores resolvem o problema da reutilização, mas são difíceis de ler e compor. O "problema" é simplesmente que gostaríamos de uma funcionalidade do tipo builder que seja significativamente mais fácil de ler.

Nenhum aplicativo pode mostrar isso de forma mais clara, se você fundamentalmente não concordar que três construtores aninhados são difíceis de ler ou que os construtores em geral não atendem realmente a um propósito de reutilização de código. Mais importante apenas ouvir que muitos de nós realmente gostam de reduzir o aninhamento e realmente não gostam de duplicar o código em todo o nosso aplicativo e, portanto, estamos presos entre 2 opções não ideais.

Eu gastei literalmente horas do meu próprio tempo neste fim de semana, sem mencionar muitas horas do tempo do Google antes disso, considerando este problema, descrevendo possíveis soluções e tentando extrair exatamente o que estamos tentando resolver

Eu sou grato por isso

Por favor, não confunda falta de compreensão sobre o que é um problema com recusa em considerá-lo

Estou bem com a falta de compreensão, mas a situação atual parece desesperadora.
Ainda estamos debatendo pontos que foram levantados bem no início da discussão.

Do meu ponto de vista, sinto que passei horas escrevendo comentários detalhados, mostrando os diferentes problemas e respondendo a perguntas, mas meus comentários foram rejeitados e a mesma pergunta foi feita novamente.

Por exemplo, a falta de legibilidade da sintaxe atual está no centro da discussão.
Fiz várias análises do problema de legibilidade para comprovar isso:

Essas análises têm um número significativo de 👍 e outras pessoas parecem concordar

E, no entanto, de acordo com sua resposta recente, não há problema de legibilidade: https://github.com/flutter/flutter/issues/51752#issuecomment -671009593

Você também sugeriu:

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

sabendo que isso não é legível

A partir desses dois comentários, podemos concluir que:

  • discordamos de que haja um problema de legibilidade
  • ainda não está claro se a legibilidade faz parte do escopo deste problema ou não

Isso é desanimador de ouvir, considerando que o único propósito dos ganchos é melhorar a sintaxe dos Builders - que estão no auge da capacidade de reutilização, mas têm baixa capacidade de leitura / gravação
Se não concordarmos com esse fato básico, não tenho certeza do que podemos fazer.

@Hixie obrigado, isso ajuda muito a entender seu ponto de vista. Eu concordo que eles podem ter exagerado com a magia do código, mas tenho certeza de que há pelo menos algumas coisas que eles acertaram.

E eu gosto muito da arquitetura em camadas do Flutter. Eu também gostaria de continuar usando widgets. Então, talvez a resposta seja apenas melhorar a extensibilidade do Dart & Flutter, o que para mim seria:

Torne a geração de código mais perfeita - pode ser possível implementar a mágica SwiftUI no Dart, mas a configuração usual necessária é muito grande e muito lenta.

Se usar a geração de código fosse tão simples quanto importar um pacote e adicionar algumas anotações, então as pessoas que têm o problema discutido fariam isso e parariam de reclamar. O resto continuaria usando o bom e velho StatefulWidgets diretamente.

EDITAR: Acho que flutter generate foi um passo na direção certa, pena que foi removido.

Acho que essa seria uma pergunta muito interessante para fazer na próxima Pesquisa de Desenvolvedores do Flutter.

Seria um bom começo. Divida este problema em diferentes partes / questões e veja se este é um problema real que os desenvolvedores do Flutter desejam resolver.

Assim que estiver claro, esta conversa será mais fluente e enriquecedora

Do meu ponto de vista, sinto que passei horas escrevendo comentários detalhados, mostrando os diferentes problemas e respondendo a perguntas, mas meus comentários foram rejeitados e a mesma pergunta foi feita novamente.

Se estou fazendo as mesmas perguntas, é porque não entendo as respostas.

Por exemplo, voltando ao seu comentário anterior (https://github.com/flutter/flutter/issues/51752#issuecomment-670959424):

O problema debatido é:

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

Este código não é legível.

Eu realmente não vejo o que não é legível sobre isso. Isso explica exatamente o que está acontecendo. Existem quatro widgets, três deles têm métodos construtores, um tem apenas uma string. Eu pessoalmente não omitiria os tipos, acho que isso torna mais difícil de ler porque não posso dizer quais são as variáveis, mas não é um grande problema.

Por que isso é ilegível?

Para ser claro, você certamente acha ilegível, não estou tentando dizer que você está errado. Eu simplesmente não entendo o porquê.

Poderíamos corrigir o problema de legibilidade introduzindo uma nova palavra-chave que altera a sintaxe para:

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

Este código é significativamente mais legível, não está relacionado a ganchos e não sofre de suas limitações.

Certamente é menos prolixo. Não tenho certeza se é mais legível, pelo menos para mim. Existem mais conceitos (agora temos widgets e esse recurso de "palavra-chave"); mais conceitos significa mais carga cognitiva. (Também é potencialmente menos eficiente, dependendo de quanto esses objetos são independentes; por exemplo, se a animação sempre muda com mais frequência do que o valor listenable e stream, agora estamos reconstruindo o ValueListenableBuilder e o StreamBuilder, embora normalmente eles não fossem acionados ; também a lógica do inicializador agora deve ser inserida e ignorada a cada construção.)

Você disse que o detalhamento não é o problema, portanto, sendo mais conciso, não é por isso que é mais legível, eu suponho (embora também esteja confuso sobre isso, pois você colocou "muito detalhado" no título do problema e em a descrição original do problema). Você mencionou querer menos recuo, mas descreveu a versão de usar construtores sem recuo como ilegível também, portanto, presumivelmente, não é o recuo no original que é o problema.

Você diz que os construtores são o pico da reutilização e que deseja apenas uma sintaxe alternativa, mas as propostas que você sugeriu não se parecem em nada com os construtores (eles não criam widgets ou elementos), portanto, não é especificamente o aspecto do construtor que você estão procurando.

Você tem uma solução que você gosta (Ganchos), que pelo que eu posso dizer funciona muito bem, mas você quer algo mudado no framework para que as pessoas usem Ganchos? O que eu também não entendo, porque se as pessoas não gostam de Hooks o suficiente para usá-los como um pacote, provavelmente também não é uma boa solução para o framework (em geral, estamos indo mais para o uso de pacotes, até mesmo recursos a equipe do Flutter cria, pelo que vale a pena).

Eu entendo que há um desejo de reutilização de código mais fácil. Só não sei o que isso significa.

Como o seguinte se compara em legibilidade às duas versões acima?

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 Se houver muito atrito em torno de nossa solução codegen atual, por favor, não hesite em registrar um bug pedindo melhorias.

@jamesblasco Não tenho dúvidas de que há um problema real aqui que as pessoas querem resolver. A questão para mim é exatamente qual é o problema, para que possamos projetar uma solução.

Eu poderia responder às dúvidas sobre as falhas dos ganchos ou o desejo de ser incluído no código, mas não acho que devemos nos concentrar nisso agora.

Devemos primeiro concordar sobre qual é o problema. Se não o fizermos, não vejo como poderíamos concordar em outros tópicos.

Eu realmente não vejo o que não é legível sobre isso. Isso explica exatamente o que está acontecendo. Existem quatro widgets, três deles têm métodos construtores, um tem apenas uma string. Eu pessoalmente não omitiria os tipos, acho que isso torna mais difícil de ler porque não posso dizer quais são as variáveis, mas não é um grande problema.

Acho que grande parte do problema aqui é que a maneira como você codifica é drasticamente diferente de como a maioria das pessoas codifica.

Por exemplo, Flutter e o exemplo de aplicativo que você deu a ambos:

  • não use dartfmt
  • use always_specify_types

Com apenas esses dois pontos, eu ficaria surpreso se representasse mais de 1% da comunidade.

Como tal, o que você avalia como legível é provavelmente muito diferente do que a maioria das pessoas pensa que é legível.

Eu realmente não vejo o que não é legível sobre isso. Isso explica exatamente o que está acontecendo. Existem quatro widgets, três deles têm métodos construtores, um tem apenas uma string. Eu pessoalmente não omitiria os tipos, acho que isso torna mais difícil de ler porque não posso dizer quais são as variáveis, mas não é um grande problema.

Por que isso é ilegível?

Minha recomendação seria analisar para onde seu olho está olhando ao procurar por uma coisa específica e quantos passos são necessários para chegar lá.

Vamos fazer uma experiência:
Vou te dar duas árvores de widget. Um usando uma sintaxe linear, o outro com uma sintaxe aninhada.
Também darei a você coisas específicas que você precisa procurar nesse trecho.

É mais fácil encontrar a resposta para usar a sintaxe linear ou a sintaxe aninhada?

As questões:

  • Qual é o widget não construtor retornado por este método de construção?
  • Quem cria a variável bar ?
  • Quantos construtores nós temos?

Usando construtores:

 Construção de widget (contexto) {
 return ValueListenableBuilder(
 valueListenable: someValueListenable,
 construtor: (contexto, foo, _) {
 retornar StreamBuilder(
 stream: someStream,
 construtor: (contexto, baz) {
 retornar TweenAnimationBuilder(
 tween: Tween (...),
 construtor: (contexto, barra) {
 return Container ();
 },
 );
 },
 );
 },
 );
 }

Usando uma sintaxe linear:

 Construção de widget (contexto) {
 foo final = palavra-chave ValueListenableBuilder (valueListenable: someValueListenable);
 barra final = palavra-chave StreamBuilder (stream: someStream);
 final baz = palavra-chave TweenAnimationBuilder (tween: Tween (...));

return Image (); }


No meu caso, tenho dificuldade em examinar o código aninhado para encontrar a resposta.
Por outro lado, encontrar a resposta com a árvore linear é instantâneo

Você mencionou querer menos recuo, mas descreveu a versão de usar construtores sem recuo como ilegível também, portanto, presumivelmente, não é o recuo no original que é o problema.

O StreamBuilder dividido em múltiplas variáveis ​​foi uma sugestão séria?
Pelo que entendi, essa foi uma sugestão sarcástica para argumentar. Não foi? Você realmente acha que esse padrão levaria a um código mais legível, mesmo em widgets grandes?

Ignorando o fato de que o exemplo não funciona, não me importo em dividi-lo para explicar por que não é legível. Isso seria valioso?

`` `dardo
Construção de widget (contexto) {
Retorna
ValueListenableBuilder (valueListenable: someValueListenable, builder: (context, value, _) =>
StreamBuilder (stream: someStream, builder: (context, value2) =>
TweenAnimationBuilder (tween: Tween (...), construtor: (contexto, valor3) =>
Texto ('$ valor $ valor2 $ valor3'),
)));
}

Isso parece melhor.
Mas isso presumindo que as pessoas não usam o dartfmt

Com o dartfmt, temos:

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

que quase não é diferente do código original.

Você diz que os construtores são o pico da reutilização e que deseja apenas uma sintaxe alternativa, mas as propostas que você sugeriu não se parecem em nada com os construtores (eles não criam widgets ou elementos), portanto, não é especificamente o aspecto do construtor que você estão procurando.

Esse é um detalhe de implementação.
Não há nenhuma razão particular para ter um elemento ou não.
Na verdade, pode ser interessante ter um Elemento, de modo que possamos incluir LayoutBuilder e potencialmente GestureDetector .

Acho que é de baixa prioridade. Mas na comunidade React, entre as diferentes bibliotecas de ganchos, eu vi:

  • useIsHovered - retorna um booleano que informa se o widget está passando
  • useSize - (provavelmente deve ser useContraints no Flutter) que fornece o tamanho da IU associada.

(Também é potencialmente menos eficiente, dependendo de quanto esses objetos são independentes; por exemplo, se a animação sempre muda com mais frequência do que o valor listenable e stream, agora estamos reconstruindo o ValueListenableBuilder e o StreamBuilder, embora normalmente eles não fossem acionados ; também a lógica do inicializador agora deve ser inserida e ignorada a cada construção.)

Isso depende de como a solução é resolvida.

Se buscarmos uma correção de idioma, esse problema não será um problema.

Nós poderíamos fazer isso:

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

"compila" em:

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

Se estivermos usando ganchos, flutter_hooks virá com um widget HookBuilder , para que ainda possamos dividir as coisas quando precisarmos.
Da mesma forma, seriam necessários benchmarks adequados para determinar se realmente é um problema, especialmente no exemplo feito aqui.

Com ganchos, estamos reconstruindo apenas um Elemento.
Com Builders, a reconstrução é dividida em vários elementos. Isso também adiciona alguma sobrecarga, mesmo que pequena.

Não é impossível que seja mais rápido reavaliar todos os ganchos. Parece que essa foi a conclusão que a equipe do React chegou ao projetar os ganchos.
Isso pode não se aplicar ao Flutter.

Por que isso é ilegível?

Por causa do aninhamento - o aninhamento torna mais difícil examinar rapidamente e saber quais partes você pode ignorar e quais são essenciais para a compreensão do que está acontecendo. O código é meio “sequencial” por natureza, mas o aninhamento esconde isso. O aninhamento também torna difícil trabalhar com ele - imagine que você deseja reordenar duas coisas - ou injetar algo novo entre dois - trivial em código verdadeiramente sequencial, mas difícil quando você precisa trabalhar com aninhamento.

Isso é muito semelhante ao async / await sugar vs trabalhar com a API Bruta do Futuro, conceito baseado em continuação da dame por baixo (e até mesmo os argumentos a favor e contra são muito semelhantes) - sim a API do Futuro pode ser usada diretamente e não esconde nada, mas facilidade de leitura e manutenção certamente não é bom - async / await é o vencedor.

Minha recomendação seria analisar para onde seu olho está olhando ao procurar por uma coisa específica e quantos passos são necessários para chegar lá.

Eu tenho programado por 25 anos em mais de 10 linguagens diferentes e essa é facilmente a pior maneira de avaliar o que torna um código legível. A legibilidade do código-fonte é complicada, mas é mais sobre como ele expressa bem os conceitos de programação e lógica, em vez de "para onde meus olhos estão olhando" ou quantas linhas de código ele usa.

Ou melhor, parece-me que vocês estão se concentrando muito na legibilidade e menos na manutenção .

Seus exemplos são menos legíveis, porque o __intent__ do código é menos evidente e diferentes preocupações sendo ocultadas no mesmo lugar tornam mais difícil de manter.


final value = keyword ValueListenableBuilder(valueListenable: someValueListenable);

Qual seria o valor mesmo? Um widget? Uma variável de string? Quero dizer, é usado dentro de um
return Text('$value $value2 $value3');

Basicamente, o que você deseja é que, ao fazer referência à variável A no método de construção do widget B, isso deve fazer com que B seja reconstruído sempre que o valor de A mudar? Isso é literalmente o que mobx faz, e faz exatamente com a quantidade certa de magia / clichê.


final value2 = keyword StreamBuilder(stream: someStream);

O que isso deve retornar? Um widget? Um fluxo? Um valor String?

Novamente, parece um valor de string. Então, você deseja ser capaz de simplesmente fazer referência a um fluxo em um método de construção, fazer com que o widget seja reconstruído sempre que o fluxo emitir um valor e ter acesso ao valor emitido e criar / atualizar / descartar o fluxo sempre que o widget for criado / atualizado /destruído? Em uma única linha de código? Dentro do método de construção?

Sim, com mobx você pode fazer com que seus métodos de construção sejam exatamente iguais ao seu exemplo "mais legível" (exceto que você faz referência a observáveis). Você ainda precisa escrever o código real que faz todo o trabalho, assim como faz com os ganchos. O código real tem cerca de 10 linhas e pode ser reutilizado em qualquer widget.


final value3 = keyword TweenAnimationBuilder(tween: Tween(...));

Uma classe chamada "TweenAnimationBuilder" retornando uma string ?! Não estou nem perto de por que isso é uma ideia terrível.

Não há diferença no recuo / legibilidade entre:

Future<double> future;

AsyncSnapshot<double> value = keyword FutureBuilder<double>(future: future);

e:

Future<double> future;

double value = await future;

Ambos fazem exatamente a mesma coisa: ouvir um objeto e desembrulhar seu valor.

Eu realmente não vejo o que não é legível sobre isso. Isso explica exatamente o que está acontecendo. Existem quatro widgets, três deles têm métodos construtores, um tem apenas uma string. Eu pessoalmente não omitiria os tipos, acho que isso torna mais difícil de ler porque não posso dizer quais são as variáveis, mas não é um grande problema.

O mesmo argumento pode ser aplicado às cadeias Promise / Future.

foo().then(x =>
  bar(x).then(y =>
    baz(y).then(z =>
      qux(z)
    )
  )
)

vs

let x = await foo();
let y = await bar(x);
let z = await baz(y);
await qux(z);

Pode-se dizer que a primeira forma de escrever deixa claro que as promessas estão sendo criadas sob o capô e como exatamente a corrente é formada. Eu me pergunto se você concorda com isso ou se considera Promises fundamentalmente diferentes de Builders no sentido de que merecem uma sintaxe.

Uma classe chamada "TweenAnimationBuilder" retornando uma string ?! Não estou nem perto de por que isso é uma ideia terrível.

Você pode fazer o mesmo argumento sobre Promessas / Futuros e dizer que await obscurece o fato de que retorna uma Promessa.

Devo observar que a ideia de "desembrulhar" coisas por meio da sintaxe não é nova. Sim, nas linguagens principais ele veio via async / await, mas, por exemplo, F # tem expressões computacionais , semelhantes à notação do em algumas linguagens FP hardcore. Lá, ele tem muito mais poder e é generalizado para funcionar com qualquer invólucro que satisfaça certas leis. Não estou sugerindo adicionar Mônadas ao Dart, mas acho que vale a pena mencionar que definitivamente há precedentes para sintaxe de tipo seguro para "desembrulhar" coisas que não correspondem necessariamente a chamadas assíncronas.

Dando um passo para trás, acho que uma coisa que muitas pessoas aqui estão lutando (inclusive eu) é essa questão sobre legibilidade. Como @rrousselGit mencionou, há muitos exemplos ao longo deste tópico de problemas de legibilidade com a abordagem atual baseada em Builder . Para muitos de nós, parece evidente que isto:

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

é significativamente menos legível do que isto:

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

Mas claramente não é evidente, uma vez que @Hixie e @Rudiksz não estão convencidos (ou se opõem ativamente) à ideia de que o segundo é mais legível do que o primeiro.

Portanto, aqui está minha análise (por qualquer pequena quantia que valha a pena) sobre por que o segundo bloco de código é mais legível do que o primeiro:

1. O primeiro bloco de código é significativamente mais recuado do que o segundo

Na minha experiência, recuo normalmente equivale a assincronicidade, ramificação ou retornos de chamada, todos os quais requerem mais carga cognitiva para pensar do que código linear não recuado. O primeiro bloco de código tem várias camadas de indentação e, como tal, levo uma quantidade não trivial de tempo para trabalhar o que está acontecendo aqui e o que está sendo renderizado (um único Text ). Talvez outras pessoas sejam melhores em trabalhar com esse recuo em suas mentes.


No segundo bloco de código, não há indentação que alivia o problema.

2. O primeiro bloco de código requer mais sintaxe para expressar sua intenção

No primeiro bloco de código, há três instruções return , três instruções do construtor, três cabeçalhos lambda, três contextos e, finalmente, três valores. Em última análise, o que nos preocupa são esses três valores - o resto é um clichê para nos levar até lá. Na verdade, acho que esta é a parte mais desafiadora deste bloco de código. Há tanta coisa acontecendo, e as partes que realmente me interessam (os valores que estão sendo devolvidos pelos construtores) são uma parte tão pequena que eu gasto a maior parte da minha energia mental grocando o clichê em vez de focar nas partes que eu realmente preciso ( novamente, os valores).


No segundo bloco de código, há uma grande redução do clichê para que eu possa me concentrar na parte que me interessa - novamente, os valores.

3. O primeiro bloco de código oculta a parte mais importante do método build na parte mais profunda do aninhamento

Eu reconheço que todas as partes desse método build são importantes, mas descobri que quando estou lendo este estilo de código de IU declarativo, o que estou geralmente procurando é o que é exibido para o usuário, que neste caso é o widget Text embutido no construtor aninhado mais profundo. Em vez de estar na frente e no centro, esse widget Text está enterrado em várias camadas de indentação, sintaxe e intenção. Se você jogar um Column ou Row em uma dessas camadas, ele se torna ainda mais aninhado e, nesse ponto, você nem mesmo tem o benefício de apenas traçar a seção mais recuada .


No segundo bloco de código, o Widget real renderizável que está sendo retornado está na parte inferior da função, o que é imediatamente aparente. Além disso, descobri que quando você tem algo como a sintaxe OP proposta, pode contar com o visual Widget sempre na parte inferior da função, o que torna o código muito mais previsível e fácil de ler.

Com relação ao aninhamento, há uma diferença entre o aninhamento expressando uma _árvore_ e o aninhamento expressando uma _sequência_ .

No caso de aninhamento View -> Text normal e outros semelhantes, o aninhamento é importante porque representa os relacionamentos pai-filho na tela. Para recursos como Contexto (não tenho certeza se Flutter tem), ele representa o escopo dos contextos. Portanto, o aninhamento em si tem um significado semântico importante nesses casos e não pode ser desconsiderado. Você não pode simplesmente trocar os lugares dos pais e dos filhos e esperar que o resultado seja o mesmo.

No entanto, com aninhamento de Builders (também conhecido como "Render Props" no React), ou aninhamento de Promises, o aninhamento deve comunicar uma sequência de transformações / aumentos . A árvore em si não é tão importante - por exemplo, ao aninhar ABuilder -> BBuilder -> CBuilder independentes, seus relacionamentos pai-filho não transmitem significado adicional.

Contanto que todos os três valores estejam disponíveis no escopo abaixo, sua estrutura em árvore não é realmente relevante. É conceitualmente "plano" e o aninhamento é apenas um artefato da sintaxe. Claro, eles podem usar os valores uns dos outros (nesse caso, sua ordem é importante), mas este é o caso com chamadas de função sequenciais também, e isso pode ser feito sem qualquer aninhamento.

É por isso que async/await é atraente. Ele remove informações adicionais (relacionamento pai-filho de Promises) que descrevem um mecanismo de baixo nível e, em vez disso, permite que você se concentre na intenção de alto nível (descrevendo uma sequência).

Uma árvore é uma estrutura mais flexível do que uma lista. Mas quando cada pai tem apenas um filho, torna-se uma árvore patológica - essencialmente, uma lista. Async / Await e Hooks reconhecem que estamos desperdiçando sintaxe em algo que não transmite informações e a remove.

Isso é realmente interessante porque eu disse anteriormente "isso não é sobre o clichê" e agora parece que estou me contradizendo. Acho que há duas coisas aqui.

Por si só, Builders (ou pelo menos Render Props in React) são a solução (AFAIK) para o problema da "lógica reutilizável". Só que eles não são muito ergonômicos se forem usados ​​muitos. Você é naturalmente desencorajado pelo recuo a usar mais de 4 ou 5 deles no mesmo componente. Cada próximo nível é um acerto de legibilidade.

Portanto, a parte que parece não resolvida para mim é reduzir o custo do leitor de aninhamento. E esse argumento é precisamente o mesmo argumento de async / await .

Não é tão legível pelos seguintes motivos:

  • O uso excessivo de espaços em branco não é bem dimensionado. Temos apenas algumas linhas em nosso monitor, e forçar a rolagem reduz a legibilidade e aumenta a carga congitiva. Imagine que já temos uma árvore de widgets de 60 linhas e você acabou de forçar 15 adicionais para mim para os construtores, o que não é o ideal.
  • Ele desperdiça espaço em Hz, o que nos limita a levar a um acondicionamento extra, o que desperdiça ainda mais espaço na linha.
  • Ele empurra o nó folha, também conhecido como o conteúdo, mais longe do lado esquerdo e na árvore onde é mais difícil identificá-lo com um relance
  • É significativamente mais difícil identificar os 'atores principais' ou 'não padronizados' à primeira vista. Tenho que "encontrar as coisas importantes" antes mesmo de meu raciocínio começar.

Outra maneira de ver isso é simplesmente destacar o código não padronizado e se ele está agrupado para que seus olhos se deleitem facilmente ou espalhados por toda parte para que seu olho tenha que examinar:

Com o realce, é muito fácil raciocinar sobre isso. Sem ele, preciso ler todo o detalhamento antes de descobrir quem está usando o quê e onde:
image

Agora compare com isso, o realce é basicamente redundante, porque não há nenhum outro lugar para onde ir:
image

Vale a pena notar que provavelmente há uma discordância em legibilidade vs grokability. @Hixie provavelmente gasta seu tempo em arquivos de classes monolíticas, onde ele deve constantemente ler e entender árvores enormes, enquanto seu desenvolvedor de aplicativos típico se preocupa muito mais com a construção de centenas de classes menores, e quando você gerencia muitas classes pequenas, a habilidade de grocar é a chave . Não é tanto que o código seja legível quando você o desacelera e o lê, mas posso dizer o que isso está fazendo rapidamente, para que eu possa entrar e ajustar ou consertar algo.

Para referência, o equivalente de Context no React é InheritedWidgets / Provider

A única diferença entre eles é que no React before hooks nós _tínhamos_ para usar o padrão Builder para consumir um Context / Inheritedwidget

Enquanto o Flutter tem uma maneira de vincular a reconstrução com uma chamada de função simples.
Portanto, não há necessidade de ganchos para achatar árvores usando InheritedWidgets - que meio que contorna o problema de Builders

Essa é provavelmente uma das razões pelas quais a discussão é mais difícil, já que precisamos dos Construtores com menos frequência.

Mas vale a pena mencionar que a introdução de uma solução semelhante a um gancho resolveria https://github.com/flutter/flutter/issues/30062
e https://github.com/flutter/flutter/issues/12992

Também parece que @Hixie está mais acostumado a ler árvores aninhadas profundas porque Flutter é basicamente todas as árvores, muito mais do que outras linguagens na minha opinião. Como um dos principais desenvolvedores do próprio Flutter, é claro, ele teria muito mais experiência com isso. Flutter essencialmente pode ser pensado como uma estrutura da esquerda para a direita , com aninhamento profundo, muito parecido com o HTML com o qual suponho que @Hixie teve experiência, tendo criado a especificação do HTML5. Ou seja, o ponto mais à direita do bloco de código é onde residem a lógica principal e o valor de retorno.

No entanto, a maioria dos desenvolvedores não é, ou vem de mais linguagens de cima para baixo , onde a lógica é, novamente, lida de cima para baixo, em vez de em árvores aninhadas; está no ponto mais inferior do bloco de código. Portanto, o que é legível para ele não é necessariamente o mesmo com muitos outros desenvolvedores, e é potencialmente por isso que você vê a dicotomia de opiniões sobre legibilidade aqui.

Outra maneira de ver isso é a quantidade de código que meu cérebro precisa para editar visualmente. Para mim, isso representa com precisão o "trabalho pesado" que meu cérebro deve fazer antes que eu possa analisar o que está sendo retornado da árvore:
image

Simplificando, a versão do construtor tem uma pegada vertical 4x mais alta enquanto não adiciona literalmente nenhuma informação ou contexto adicional, E empacota o código de uma maneira muito mais esparsa / dispersa. Em minha opinião, esse é um caso aberto e fechado, objetivamente menos legível apenas por esse motivo, e isso nem mesmo considerando a carga cognitiva adicional em torno da indentação e do alinhamento de colchetes, que todos nós tratamos em flutter.

Pense no meu olho como uma CPU com fome, que é mais otimizada para processamento? :)

No caso de aninhamento View -> Text normal e outros semelhantes, o aninhamento é importante porque representa _as relações pai-filho_ na tela. Para recursos como Contexto (não tenho certeza se Flutter tem), ele representa o escopo dos contextos. Portanto, o aninhamento em si tem um significado semântico importante nesses casos e não pode ser desconsiderado. Você não pode simplesmente trocar os lugares dos pais e dos filhos e esperar que o resultado seja o mesmo.

Concordo totalmente e eu mencionei isso antes. Semanticamente, não faz sentido criar camadas de contexto adicionais em uma árvore de exibição visual, porque estou usando controladores não visuais adicionais que têm estado. Usando 5 animadores, agora seu widget tem 5 camadas de profundidade? Só nesse nível alto, a abordagem atual meio que cheira mal.

Há dois problemas que estão me ocorrendo aqui.

  1. Suspeito que haja alguma discordância sobre o quão difícil / explícito deve ser ao usar algum recurso caro. A filosofia do Flutter é que eles devem ser mais difíceis / explícitos para que o desenvolvedor pense seriamente sobre quando e como usá-los. Streams, animações, criadores de layout, etc. representam custos não triviais que podem ser usados ​​de forma ineficiente se forem muito fáceis.

  2. Build é sincronizado, mas as coisas mais interessantes com as quais você lida como desenvolvedor de aplicativos são assíncronas. Claro que não podemos tornar a compilação assíncrona. Criamos essas conveniências como Stream / Animation / FutureBuilder, mas nem sempre funcionam bem o suficiente para as necessidades de um desenvolvedor. Provavelmente está dizendo que não usamos muito Stream ou FutureBuilder no framework.

Não acho que a solução seja dizer aos desenvolvedores para apenas sempre escreverem objetos de renderização personalizados ao trabalhar com operações assíncronas, é claro. Mas nos exemplos que estou vendo neste bug, existem combinações de trabalho assíncrono e de sincronização que não podemos simplesmente esperar. O Build deve produzir algo em cada chamada.

Fwiw, a equipe React abordou a reutilização do problema de legibilidade como a principal motivação:
Motivação dos ganchos: é difícil reutilizar a lógica com estado entre os componentes
O React não oferece uma maneira de “anexar” comportamento reutilizável a um componente ... você pode estar familiarizado com os padrões ... que tentam resolver isso. Mas esses padrões exigem que você reestruture seus componentes ao usá-los, o que pode ser complicado e tornar o código mais difícil de seguir .

Isso é muito semelhante a como o Flutter atualmente não nos oferece nenhuma maneira de 'compor o estado' nativamente. Isso também reflete o que acontece quando nós, construtores, estamos modificando nossa árvore de layout e tornando-a mais complicada de trabalhar e "mais difícil de seguir", disse a árvore.

@dnfield se build deve ser chamado a cada vez, talvez possamos fazer os ganchos que não estão no método build para que a compilação esteja sempre sincronizada, ou seja, coloque-os dentro da classe onde initState e dispose são. Existem problemas em fazer isso, daqueles que escrevem ganchos?

Você pode fazer o mesmo argumento sobre Promessas / Futuros e dizer que await obscurece o fato de que retorna uma Promessa.

Não, você não precisa. Await é literalmente apenas açúcar sintático para um único recurso. Se você usar Futures verbosos ou a sintaxe declarativa, o __intent__ do código é o mesmo.

As demandas aqui são mover o código-fonte que lida com questões totalmente diferentes sob o mesmo guarda-chuva, ocultar todos os tipos de comportamento diferente por trás de uma única palavra-chave e alegar que de alguma forma isso reduz a carga cognitiva.

Isso é totalmente falso, porque agora toda vez que uso essa palavra-chave, preciso me perguntar se o resultado fará qualquer operação assíncrona, acionará reconstruções desnecessárias, inicializará objetos de longa duração, fará chamadas de rede, lerá arquivos do disco ou simplesmente retornará um valor estático . Todas essas são situações muito diferentes e eu teria que estar familiarizado com o sabor do anzol que estou usando.

Eu entendo pela discussão que a maioria dos desenvolvedores aqui não gosta de se preocupar com esses tipos de detalhes e deseja um desenvolvimento fácil, apenas sendo capaz de usar esses "ganchos" sem ter que se preocupar com os detalhes de implementação.
Usar esses chamados "ganchos" à toa sem entender sua implicação total levará a um código ineficiente e ruim, e fará com que as pessoas atiram no próprio pé - portanto, nem mesmo resolve o problema de "proteger os desenvolvedores iniciantes".

Se seus casos de uso são simples, então sim, você pode usar ganchos à vontade. Você pode usar e aninhar construtores o quanto quiser, mas à medida que seu aplicativo se torna complexo e você encontra dificuldades para reutilizar o código, acho que é necessário prestar mais atenção ao seu próprio código e arquitetura. Se eu estivesse construindo um aplicativo para potencialmente milhões de usuários, hesitaria em usar a "mágica" que abstrai detalhes importantes de mim. No momento, acho que a API do Flutter é simples para casos de uso muito simples e ainda flexível para permitir que qualquer pessoa implemente qualquer tipo de lógica complexa de uma maneira muito eficiente.

@Rudiksz Novamente, ninguém está forçando você a passar para os ganchos. O estilo atual ainda estará lá. Qual é o seu argumento diante desse conhecimento?

E, de qualquer forma, as pessoas ainda podem escrever um código eficiente, uma vez que vejam que vários ganchos estão de alguma forma bloqueando seu aplicativo; eles verão quando fizerem o perfil ou apenas executarem o aplicativo, da mesma forma que você faria com o estilo atual.

@Rudiksz Novamente, ninguém está forçando você a passar para os ganchos. O estilo atual ainda estará lá. Qual é o seu argumento diante desse conhecimento?

Nossa, esse mesmo argumento se aplica também às pessoas reclamando de problemas com o framework. Ninguém os obriga a não usar o pacote de ganchos.

Vou ser muito direto aqui.
Este problema não é realmente sobre ganchos, statefulwidget e quem usa o quê, mas sim sobre reverter décadas de melhores práticas para que algumas pessoas possam escrever 5 linhas de código a menos.

Seu argumento realmente não funciona. A razão pela qual esse problema foi criado foi porque o pacote flutter_hooks não faz tudo o que poderia ser possível tendo algo no framework, enquanto o modelo atual faz, em virtude de já estar no framework nativamente. O argumento é mover recursos de flutter_hooks para a estrutura nativamente. Seu argumento postula que tudo o que posso fazer com o modelo atual, também posso fazer com o pacote de ganchos, o que não é verdade, parece que outros nesta discussão. Se eles fossem verdadeiros, então funcionaria, o que também significaria que os ganchos estavam no framework nativamente e, portanto, novamente, uma vez que ganchos e não ganchos seriam equivalentes, você pode usar o modelo atual tão bem quanto os ganchos baseados modelo, que é o que eu estava argumentando.

Não tenho certeza de onde vêm suas práticas recomendadas, pois sei que manter o código facilmente legível é uma prática recomendada e que o aninhamento excessivo é um antipadrão. A quais práticas recomendadas exatamente você está se referindo?

Fwiw, a equipe React abordou a reutilização do problema de legibilidade como a principal motivação:
Motivação dos ganchos: é difícil reutilizar a lógica com estado entre os componentes
O React não oferece uma maneira de “anexar” comportamento reutilizável a um componente ... você pode estar familiarizado com os padrões ... que tentam resolver isso. Mas esses padrões exigem que você reestruture seus componentes ao usá-los, o que pode ser complicado e tornar o código mais difícil de seguir .

Eu ouço todo mundo delirando sobre como Flutter é muito mais incrível do que React. Talvez seja porque ele não faz tudo da maneira que o React faz? Você não pode ter as duas coisas, não pode dizer que o Flutter está quilômetros à frente do React e também pedir que ele faça tudo exatamente como o React faz.

Qualquer solução que o Flutter decida usar para um determinado problema deve se basear em seus próprios méritos. Não estou familiarizado com o React, mas aparentemente estou perdendo uma peça de tecnologia realmente incrível. : /

Não acho que alguém esteja argumentando que Flutter deveria fazer tudo como React.

Mas o fato é que a camada de widget do Flutter é fortemente inspirada no React. Isso está declarado na documentação oficial.
E, como consequência, os widgets têm os mesmos benefícios e os mesmos problemas que os componentes React.

Isso também significa que o React tem mais experiência do que o Flutter para lidar com essas questões.
Enfrentou-os por mais tempo e os entendeu melhor.

Portanto, não deve ser surpresa que as soluções para os problemas de Flutter sejam semelhantes às soluções para os problemas de React.

A API de usuário do @Rudiksz Flutter é muito semelhante ao modelo baseado em classe do React, mesmo que a API interna possa ser diferente (não sei se eles diferem, realmente não corro muito com a API interna). Eu o encorajo a tentar React com ganchos para ver como é, como afirmei anteriormente que parece haver uma dicotomia de opiniões baseada quase exclusivamente naqueles que usaram e não usaram construções semelhantes a ganchos em outras estruturas.

Dada a sua semelhança, não deve ser surpresa que as soluções para os problemas sejam semelhantes, como dito acima.

Por favor, vamos tentar o nosso melhor para não brigarmos uns com os outros.

A única coisa a que lutar nos levará é acabar com essa discussão e não conseguir encontrar uma solução.

Eu ouço todo mundo delirando sobre como Flutter é muito mais incrível do que React. Talvez seja porque ele não faz tudo da maneira que o React faz? Você não pode ter as duas coisas, não pode dizer que o Flutter está quilômetros à frente do React e também pedir que ele faça tudo exatamente como o React faz.

Ressaltar que a equipe React teve motivações semelhantes quando surgiu com ganchos, valida as preocupações que estamos expressando aqui. Certamente valida que há um problema de reutilização e combinação de lógica stateful comum dentro deste tipo de estrutura baseada em componente e também, até certo ponto, valida a discussão sobre legibilidade, aninhamento e o problema geral com "desordem" em suas visualizações.

Sem delirar sobre nada, nunca trabalhei no React e adoro Flutter. Posso ver facilmente o problema aqui.

@Rudiksz , não podemos ter certeza se é um bom desempenho na prática até colocá-lo em prática. Não é muito fácil decidir agora.

@Hixie, este é um exemplo de jornada que um usuário comum pode ter para implementar um widget para mostrar o apelido do usuário a partir de userId com HookWidget e StatefulWidget .

__ widget do gancho__

String useUserNickname(Id userid) {
  final name = useState("");
  useEffect(async () {
    name.value = "Loading...";
    name.value = await fetchNicknames()[userId;
  }, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__ widget estadual__

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

até agora nada de interessante. ambas as soluções são bastante aceitáveis, diretas e eficientes.
agora queremos usar UserNickname dentro de um ListView . como você pode ver, fetchNicknames retorna um mapa de apelidos, não apenas um apelido. portanto, chamá-lo sempre é redundante. algumas soluções que podemos aplicar aqui:

  • mova a chamada fetchNicknames() lógica para o widget pai e salve o resultado.
  • usando um gerenciador de cache.

a primeira solução é aceitável, mas tem 2 problemas.
1 - torna UserNickname inútil porque agora é apenas um widget de texto e se você quiser usá-lo em outro lugar, você deve repetir o que fez no widget pai (que tem o ListView ) . a lógica para mostrar o apelido pertence a UserNickname mas temos que movê-lo separadamente.
2 - podemos usar fetchNicknames() em muitas outras subárvores e é melhor armazená-lo em cache para todo o aplicativo, não apenas uma parte do aplicativo.

então imagine que escolhemos o gerenciador de cache e fornecemos uma classe CacheManager com InheritedWidgets ou Provider .

depois de adicionar suporte para cache:

__ widget do gancho__

String useUserNickname(Id userid) {
  final context = useContext();
  final cache = Provider.of<CacheManager>(context);
  final name = useState("");
  useEffect(async () {
    name.value = "Loading...";
    var cachedValue = cache.get("nicknames");
    if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        cache.set("nicknames", result );
        cachedValue = result ;
    }
    final result = cachedValue[widget.userId];
    name.value = result ;
  }, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__ widget estadual__

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

temos um servidor de socket que notifica os clientes quando os apelidos mudam.

__ widget do gancho__

String useUserNickname(Id userid) {
  final context = useContext();
  final cache = Provider.of<CacheManager>(context);
  final notifications = Provider.of<ServiceNotifications>(context);
  final name = useState("");

  fetchData() async {
    name.value = "Loading...";
    var cachedValue = cache.get("nicknames");
    if (cachedValue == null || cachedValue[widget.userId] == null) {
        final result = await fetchNicknames();
        cache.set("nicknames", result );
        cachedValue = result ;
    }
    final result = cachedValue[widget.userId];
    name.value = result ;
   }

  useEffect(() {
     final sub = notifications.on("nicknameChanges", fetchData);
     return () => sub.unsubscribe();
   }, [])

  useEffect(fetchData, [userId]);
  return name.value;
}

class UserNickname extends HookWidget {

  final userId;

  Widget build(BuildContext context) {
    final nickname = useUserNickname(userId);
    return Text(nickname);
  }
}

__ widget estadual__

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

até agora, ambas as implementações são aceitáveis ​​e boas. IMO, o clichê no statful não é problema algum. o problema surge quando precisamos de um widget como UserInfo que tem o apelido e avatar do usuário. também não podemos usar o widget UserNickname porque precisamos mostrar uma frase como "Bem-vindo [nome de usuário]".

__ widget do gancho__

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

mas para o widget __stateful__ não podemos apenas usar a lógica que escrevemos. temos que mover a lógica para uma classe (como Property que você sugeriu) e ainda precisamos escrever a cola do widget com a classe de propriedade novamente no novo widget.

se você ver as mudanças nos primeiros 3 exemplos, não alteramos o widget em si porque as únicas mudanças necessárias foram na lógica de estado e o único lugar que mudou foi tudo, a lógica de estado.
isso nos deu uma lógica de estado limpa (opinativa), compostável e totalmente reutilizável que podemos usar em qualquer lugar.

IMHO, o único problema é chamar useUserNickname é assustador porque uma única função pode fazer isso.
mas em meus anos de experiência em reagir e usar flutter_hooks em 2 aplicativos que estão em produção rn (que estão usando muitos ganchos) prova que não ter um bom gerenciamento de estado (eu também tentei MobX e outras soluções de gerenciamento de estado, mas a cola no widget está sempre lá) é muito mais assustador. Não preciso escrever documentos de 5 páginas para cada tela em um aplicativo de front-end, pois posso precisar adicionar algum pequeno recurso alguns meses após o primeiro lançamento para que ele entenda como uma página do meu aplicativo funciona. O aplicativo chama muito o servidor? Tarefa fácil Eu pego o gancho relacionado e o altero e todo o aplicativo é corrigido porque o aplicativo inteiro usa esse gancho. podemos ter coisas semelhantes nos aplicativos sem usar ganchos com uma boa abstração, mas o que estou dizendo é que ganchos são uma boa abstração.

Tenho certeza que @gaearon pode

vendo o exemplo acima, nenhum dos métodos acima (stateful e widget de gancho) tem mais desempenho que o outro. mas o ponto é que um deles incentiva as pessoas a escreverem o código de desempenho.

também é possível atualizar apenas a subárvore que precisamos atualizar como StreamBuilder quando há muitas atualizações (por exemplo, animações) com:

1 - simplesmente criar um novo widget que seja uma opção totalmente viável para HookWidget e StatefulWidget/StatelessWidget
2 - usando algo semelhante a HookWidgetBuilder no pacote flutter_hooks porque os dados dos widgets pai e filho estão fortemente acoplados.

Observação lateral: eu realmente aprecio @Hixie e @rrousselGit por discutir este tópico e colocar tanta energia nesta edição. Estou realmente ansioso pelo resultado dessas conversas.

Estou pensando em algo muito legal / elegante, baseado no ponto de partida de @Hixie . Ainda não estou totalmente pronto para compartilhar, mas está me permitindo criar alguns exemplos de código bastante decentes, que acho que serão mais fáceis de comparar : maçãs em vez de ganchos que parecem tão estranhos.

Então, imagine que temos um StatefulWidget com esta assinatura:

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

Se implementássemos o estado usando controladores de animação vanilla, obteríamos algo como:

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

Se o criarmos usando uma StatefulProperty, produziremos algo mais parecido com isto:

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

Algumas notas sobre as diferenças aqui:

  1. Acima, um tem 20 linhas, o outro tem 45. Um tem 1315 caracteres e o outro tem 825. Apenas 3 linhas e 200 caracteres importam nesta classe (o que está acontecendo no build), então isso já é uma grande melhoria no sinal : relação de
  2. As opções básicas têm vários pontos onde podem ser criados bugs. Esqueça de descartar, ou esqueça de manipular didChange, ou cometa um erro em didChange, e você terá um bug em sua base de código. Isso fica pior quando vários tipos de controladores são usados. Então você tem funções únicas derrubando objetos de todos os tipos diferentes, que não serão nomeados como nice e sequencialmente como este. Isso fica confuso e é muito fácil cometer erros ou perder entradas.
  3. A opção vanilla não fornece nenhum método para reutilizar padrões ou lógicas comuns, como playOnInit, então eu tenho que duplicar essa lógica ou criar alguma função personalizada em uma única classe que deseja usar um Animator.
  4. Não há necessidade de entender SingleTickerProviderMixin aqui, que é 'mágico' e ofuscou o que um Ticker é para mim por meses (em retrospecto, eu deveria ter acabado de ler a aula, mas todos os tutoriais apenas dizem: Adicione este mixin mágico). Aqui você pode olhar diretamente para o código-fonte de StatefulAnimationProperty e ver como os controladores de animação usam um provedor de ticker diretamente e no contexto.

Em vez disso, você precisa entender o que StatefulPropertyManager faz, mas crucialmente isso aprendido uma vez e aplicado a objetos de qualquer tipo, SingleTickerProviderMixin é principalmente específico para o uso de controladores de animação, e cada controlador pode ter sua própria mixagem para facilitar o uso, o que fica confuso. Apenas ter "StatefulObjects" distintos que conheçam todas essas coisas (assim como um construtor faz!) É muito mais limpo e escalável.

O código para StatefulAnimationProperty seria semelhante a este:

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

Por fim, é importante notar que a legibilidade pode ser ainda melhor com o uso de extensões, para que possamos ter algo como:

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

[Editar] Como se fosse para defender minha opinião, meu exemplo básico tem um bug. Esqueci de passar a duração correta para cada animador no didUpdateWidget. Quanto tempo teríamos levado para encontrar esse bug na selva, se ninguém notasse na revisão do código? Alguém não percebeu durante a leitura? Deixar isso na causa é um exemplo perfeito do que acontece no mundo real.

Esta é uma visão panorâmica, com clichê marcado em vermelho:
image

Isso não seria tão ruim se fosse puro clichê, e o compilador gritasse com você se estivesse faltando. Mas é tudo opcional! E quando omitido, cria bugs. Portanto, esta é uma prática muito ruim e nada SECA. É aqui que entram os construtores, mas eles só são bons para casos de uso simplistas.

O que eu acho muito interessante sobre isso, é como uma centena de linhas e um simples mixin para State, torna um monte de classes existentes redundantes. Praticamente não há necessidade de usar o TickerProviderMixins, por exemplo. TweenAnimationBuilder quase nunca precisa ser usado, a menos que você realmente _queja_ criar um subcontexto. Muitos pontos de dor tradicionais, como gerenciar controladores de foco e controladores textInput, são substancialmente amenizados. Usar o Streams se torna muito mais atraente e menos pesado. Em toda a base de código, o uso de Builders pode ser reduzido em geral, o que levará a árvores mais facilmente grokable.

Também torna _extremamente_ fácil fazer seus próprios objetos de estado personalizados, como o exemplo FetchUser listado anteriormente, que atualmente requer basicamente um construtor.

Acho que essa seria uma pergunta muito interessante para fazer na próxima Pesquisa de Desenvolvedores do Flutter.

Seria um bom começo. Divida este problema em diferentes partes / questões e veja se este é um problema real que os desenvolvedores do Flutter desejam resolver.

Assim que estiver claro, esta conversa será mais fluente e enriquecedora

As reações dos Emojis sob cada comentário dão uma ideia clara sobre se a comunidade vê isso como um problema ou não. A opinião dos desenvolvedores que leram mais de 250 comentários longos para este problema significa muito.

@esDotDev Isso é semelhante a algumas das ideias com as quais estou brincando, embora eu goste da sua ideia de apenas ter a propriedade em si como o provedor do ticker, eu não tinha considerado isso. Uma coisa que está faltando em sua implementação e que acho que precisaríamos adicionar é o tratamento de TickerMode (que é o ponto de TickerProviderStateMixin).

A principal coisa com a qual estou lutando é como fazer isso de maneira eficiente. Por exemplo, ValueListenableBuilder aceita um argumento filho que pode ser usado para melhorar o desempenho de forma mensurável. Não vejo uma maneira de fazer isso com a abordagem de propriedade.

@Hixie
Eu entendo que as perdas de eficiência com abordagens como essa parecem ser inevitáveis. Mas eu gosto da mentalidade Flutter de otimizar depois de criar o perfil de seu código. Existem muitos aplicativos que se beneficiariam da clareza e concisão da abordagem de propriedade. A opção de criar o perfil de seu código e refatorar em construtores ou separar uma parte do widget em seu próprio widget está sempre lá.

A documentação precisaria apenas refletir as melhores práticas e deixar claras as compensações.

A principal coisa com a qual estou lutando é como fazer isso de maneira eficiente. Por exemplo, ValueListenableBuilder aceita um argumento filho que pode ser usado para melhorar o desempenho de forma mensurável. Não vejo uma maneira de fazer isso com a abordagem de propriedade.

Hm, acho que todo o objetivo das Propriedades é para objetos não visuais. Se algo deseja ter um slot de contexto na árvore, então essa coisa deve ser um construtor (na verdade, essas são as únicas coisas que agora devem ser construtores, eu acho?)

Portanto, teríamos uma StatefulValueListenableProperty que usamos na maioria das vezes quando queremos apenas vincular a exibição inteira. Então, também temos um ValueListenableBuilder na chance de querermos que alguma subseção de nossa árvore seja reconstruída.

Isso também aborda o problema de aninhamento, já que usar um construtor como um nó folha não é tão prejudicial para a legibilidade quanto aninhar 2 ou 3 no topo de sua árvore de widgets.

@TimWhiting Uma grande parte da filosofia de design do Flutter é guiar as pessoas na escolha certa. Eu gostaria de evitar encorajar as pessoas a seguir um estilo do qual elas teriam que se afastar para obter um melhor desempenho. Pode ser que não haja como atender a todas as necessidades de uma vez, mas definitivamente devemos tentar.

@Hixie
Que tal algo assim para construtores?

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

Você pode elaborar? Não tenho certeza se entendi a proposta.

Acho que ele está dizendo que a StatefulProperty pode fornecer um método de compilação opcional para propriedades que têm algum componente visual:

return Column(
   children: [
      TopContent(),
      _valueProperty.build(SomeChildWidget()),
   ]
)

O que é bastante 🔥 imo,

Sim, não sei se isso funcionaria, mas o método de construção levaria um filho como um construtor normal, exceto que as outras propriedades do construtor são definidas pela propriedade.
Se você precisar do contexto do construtor, o método de construção aceita um argumento do construtor que fornece o contexto.

Nos bastidores, o método pode apenas criar um construtor normal com as propriedades especificadas e passar o argumento filho para o construtor normal e retorná-lo.

Suponha que você tenha este código:

Widget build(BuildContext context) {
  return ExpensiveParent(
    child: ValueListenableBuilder(
      valueListenable: foo,
      child: ExpensiveChild(),
      builder: (BuildContext context, value, Widget child) {
        return SomethingInTheMiddle(
          value: value,
          child: child,
        );
      }
    ),
  );
}

... como você converteria isso?

@esDotDev Gosto da sua ideia de apenas ter a propriedade em si como o provedor do ticker, não havia considerado isso.

Sim, um dos aspectos mais fortes desta abordagem estilo gancho, é que você pode encapsular _fully_ a lógica stateful, seja ela qual for. Portanto, neste caso, a lista completa para AC é:

  1. Crie o AC
  2. Dê uma nota
  3. Lidar com alterações de widget
  4. Lidar com a limpeza de ac e ticker
  5. Reconstruir vista no tick

Atualmente as coisas estão divididas com os desenvolvedores lidando (ou não manipulando) 1,3,4 manualmente e repetidamente, e o semi-mágico SingleTickerProviderMixin cuidando do 2 e 5 (com a gente passando 'isso' como vsync, o que me confundiu por meses! ) E o próprio SingleTickerProviderMixin é claramente uma tentativa de correção para esse tipo de problema, caso contrário, por que não ir até o fim e fazer com que implementemos TickerProvider para cada classe, seria muito mais claro.

Suponha que você tenha este código:

Widget build(BuildContext context) {
  return ExpensiveParent(
    child: ValueListenableBuilder(
      valueListenable: foo,
      child: ExpensiveChild(),
      builder: (BuildContext context, value, Widget child) {
        return SomethingInTheMiddle(
          value: value,
          child: child,
        );
      }
    ),
  );
}

... como você converteria isso?

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
Obrigado pelo exemplo. Eu dei o meu melhor. Eu posso ter perdido alguma coisa.

É importante observar que o construtor armazena o filho em cache. A questão seria quando é necessário realmente reconstruir a criança? Acho que era essa a questão que você estava tentando levantar ..

@Hixie você viu https://github.com/flutter/flutter/issues/51752#issuecomment -671104377
Acho que existem alguns pontos realmente bons.
Eu construo hoje algo com muito ValueListenableBuilder e só posso dizer que não é agradável de ler.

@Hixie
Obrigado pelo exemplo. Eu dei o meu melhor. Eu posso ter perdido alguma coisa.

Não acho que isso funcione porque a propriedade se vincula ao estado em que está definida, então a ExpensiveParent está sempre sendo reconstruída aqui. Então eu acho que o cache do filho também é problemático, como no exemplo do Builder ele saberia reconstruir o filho apenas quando o estado pai for construído, mas neste método a propriedade não sabe quando invalidar o cache (mas talvez isso é solucionável?)

Mas tbh, este é o caso de uso perfeito para construtores, quando você deseja introduzir um novo contexto. Acho que é bastante elegante ter apenas o contexto de StatefulProperties (estado puro) e StatefulWidgets (mistura de estado e layout).

Sempre que você estiver criando intencionalmente um subcontexto, você, por definição, o fará mais abaixo em sua árvore, o que ajuda a combater uma das principais desvantagens para os construtores (aninhamento forçado em toda a árvore)

@escamoteur (e @sahandevs que escreveu esse comentário) sim, eu estava estudando isso antes. Acho que certamente ajuda a mostrar o tipo de lógica que as pessoas desejam remover. Eu acho, porém, que o exemplo em si é um pouco duvidoso porque eu esperaria que a maior parte da lógica (por exemplo, tudo em torno do cache) estivesse na lógica de negócios do estado do aplicativo, e longe dos widgets. Também não consigo ver uma boa maneira de obter a sintaxe tão breve quanto proposta naquele comentário sem interromper o hot reload (por exemplo, se você alterar o número de ganchos que está usando, não está claro como eles podem ser mantidos com estado durante um recarregamento )

Dito isso, acho que o trabalho @esDotDev e @TimWhiting mostrado acima é muito interessante e poderia resolver esses problemas. Não é tão breve quanto Hooks, mas é mais confiável. Eu acho que faria todo o sentido empacotar algo assim, poderia até ser um Favorito do Flutter se funcionar bem. Não tenho certeza se faz sentido como um recurso central da estrutura porque a melhoria não é _tão_ substancial, uma vez que você leva em consideração a complexidade em torno das propriedades de construção e o impacto no desempenho, e como diferentes pessoas preferem estilos diferentes. No final do dia, deve ser bom que pessoas diferentes usem estilos diferentes, mas não queremos que o framework principal tenha vários estilos, isso é apenas enganoso para novos desenvolvedores.

Também há um argumento a ser feito de que a maneira certa de aprender Flutter é primeiro entender os widgets e, em seguida, aprender as ferramentas que os abstraem (ganchos ou qualquer outra coisa), em vez de pular direto para a sintaxe abstrata. Caso contrário, você estará perdendo um componente-chave de como o sistema funciona, o que provavelmente o levará ao erro em termos de escrita de código de desempenho.

Também não consigo ver uma boa maneira de obter a sintaxe tão breve quanto proposta naquele comentário sem interromper o hot reload (por exemplo, se você alterar o número de ganchos que está usando, não está claro como eles podem ser mantidos com estado durante um recarregamento )

Os ganchos funcionam com recarga a quente sem problemas.
O primeiro gancho com um runtimeType não correspondente faz com que todos os ganchos subsequentes sejam destruídos.

Isso suporta adicionar, remover e reordenar.

Eu acho que há um argumento de que a abstração completa é preferível a parcial que existe atualmente.

Se eu quiser entender como o Animator funciona no contexto de uma propriedade, ou o ignoro totalmente ou pulo para dentro, e está tudo bem aí, independente e coerente.

Se eu quiser entender como o AnimatorController funciona no contexto de um StatefulWidget, preciso (sou forçado) a entender os ganchos básicos do ciclo de vida, mas não preciso entender como funciona o mecanismo de tique subjacente. Em certo sentido, isso é o pior dos dois mundos. Não mágica o suficiente para fazer 'apenas funcionar', mas apenas o suficiente para confundir novos usuários e forçá-los a confiar cegamente em algum mixin (que em si é um novo conceito para a maioria) e uma propriedade vsync mágica.

Não tenho certeza de outros exemplos na base de código, mas isso se aplicaria a qualquer situação em que alguns mixins auxiliares tenham sido fornecidos para StatefulWidget, mas ainda há algum outro bootstrap que deve sempre ser executado. Dev's irão aprender o bootstraping (a parte chata) e ignorar o Mixin (a parte interessante / complexa)

Dito isso, acho que o trabalho @esDotDev e @TimWhiting mostrado acima é muito interessante e poderia resolver esses problemas. Não é tão breve quanto Hooks, mas é mais confiável

Como isso é mais confiável?

Ainda não podemos criar / atualizar propriedades condicionalmente ou fora de seu ciclo de vida, pois podemos entrar em um estado incorreto. Por exemplo, chamar uma propriedade condicionalmente não descartará a propriedade quando a condição for falsa.
E todas as propriedades ainda são reavaliadas em cada reconstrução.

Mas isso causa vários problemas, como forçar os usuários a usar ! qualquer lugar após o NNBD ou permitir que os usuários acessem uma propriedade antes que ela seja atualizada.

Por exemplo, e se alguém ler uma propriedade dentro de didUpdateWidget ?

  • initProperties executado antes do ciclo de vida? Mas isso significa que podemos ter que atualizar as propriedades várias vezes por compilação.
  • initProperties foi executado após didUpdateWidget? Então, usar propriedades dentro de didUpdateWidget pode levar a um estado desatualizado

Portanto, no final, temos todos os problemas dos ganchos, mas:

  • não podemos usar Propriedades dentro de `StatelessWidget. Portanto, a legibilidade de StreamBuilder / ValueListenableBuilder / ... ainda é um problema - que era a principal preocupação.
  • existem vários casos extremos
  • é mais difícil criar propriedades personalizadas (não podemos simplesmente extrair um monte de propriedades em uma função)
  • é mais difícil otimizar reconstruções

No final, o exemplo dado não é diferente em comportamento de:

class Example extends StatelessWidget {
  <strong i="29">@override</strong>
  Widget build(context) {
    final value1 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));
    final value2 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));
    final value3 = keyword TweenAnimationBuilder(tween: Tween(begin: 0, end: 1));

    return Container(
     margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30),
     color: Colors.red.withOpacity(value1),
      child: _anim2(child: SomeChildWidget()),
    );
  }
}

Mas essa sintaxe oferece suporte a muito mais coisas, como:

Retornos iniciais:

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

    ...
  }
}

que eliminaria value2 quando condition muda para falso

Extração de pacotes de construtores em uma função:

Widget build(context) {
  final foo = keyword FooBuilder();
  final bar = keyword BarBuilder();

  return Text('$foo $bar');
}

pode ser alterado para:

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

Otimize reconstruções

O parâmetro child ainda é viável:

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

Como parte da linguagem, poderíamos até ter sintaxe para isso:

Widget build(context) {
  return Scaffold(
    body: {
      final value = keyword TweenAnimationBuilder();
      final value2 = keyword ValueListenableBuilder();

      return Text();
    },
  );
}

Bônus: como um recurso de linguagem, chamadas condicionais são suportadas

Como parte da linguagem, podemos apoiar esse cenário:

Widget build(context) {
  String label;

  if (condition) {
    label = keyword LabelBuilder();
  } else {
    label = keyword AnotherBuilder();
  }

  final value2 = keyword WhateverBuilder();

  return ...
}

Não é muito útil, mas tem suporte - uma vez que a sintaxe é compilada, é capaz de diferenciar cada uso de keyword confiando em metadados que não estão disponíveis de outra forma.

Com relação à legibilidade dos construtores, aqui está o exemplo anterior, mas feito com os construtores. Ele resolve todas as necessidades de confiabilidade e uso de código, mas veja o que ele fez com a minha pobre árvore de widgets: '(

class _ExampleSimpleBuilderState extends State<ExampleSimpleBuilder> {
  <strong i="6">@override</strong>
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
        tween: Tween(begin: 0, end: 1),
        duration: widget.duration1,
        builder: (_, value1, __) {
          return TweenAnimationBuilder<double>(
              tween: Tween(begin: 0, end: 1),
              duration: widget.duration2,
              builder: (_, value2, __) {
                return TweenAnimationBuilder<double>(
                    tween: Tween(begin: 0, end: 1),
                    duration: widget.duration3,
                    builder: (_, value3, __) {
                      return Container(
                        margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30),
                        color: Colors.red.withOpacity(value1),
                      );
                    });
              });
        });
  }
}

É muito mais difícil (pelo menos para os meus olhos) identificar o código que importa. Além disso, fwiw, tive que começar de novo umas 3 vezes ao escrever isso, já que ficava continuamente confuso a respeito de qual colchete pertencia a onde, onde minhas semicolanas deveriam ir, etc. Construtores aninhados simplesmente não são divertidos de escrever ou trabalhar dentro deles. Um ponto-e-vírgula errado e dartfmt esmagam completamente a coisa toda.

Como isso é mais confiável?

Este é um exemplo perfeito de por que este _deve_ ser um plugin central para mim. O conhecimento de domínio necessário aqui é _deep_. Eu tenho o conhecimento de script para implementar um sistema de cache simples como este, não tenho nem mesmo conhecimento próximo do domínio para saber todos os casos extremos que podem acontecer, ou estados ruins em que podemos entrar. Além de Remi, acho que há 4 desenvolvedores no mundo fora da equipe do Flutter que sabem dessas coisas! (exagerando obviamente).

A questão de oferecer suporte a widgets sem estado é boa. Por um lado, eu entendo, os StatefulWidgets são estranhamente prolixos. Por outro lado, aqui estamos realmente falando sobre verbosidade pura. Não há bugs que podem acontecer por ter que definir 2 classes, não há como você bagunçar, o compilador não permite, nunca há nada de interessante que eu queira fazer no StatelessWidget. Portanto, não estou convencido de que este seja um problema importante ... CERTAMENTE seria bom ter, mas são os últimos 5% ao mesmo tempo, não é algo para ficar preso.

Por outro lado ... aquela sintaxe de remi com suporte a palavras-chave é absolutamente linda e insanamente flexível / poderosa. E se ele oferece suporte a StatelessWidget gratuitamente, isso é apenas extra 🔥

O suporte a StatelessWidget é um grande negócio da IMO. Opcional, mas ainda assim muito legal.

Embora eu concorde que isso não é crítico, as pessoas já estão lutando para usar funções em vez de StatelessWidget.
Exigir que as pessoas usem um StatefulWidget para usar Builders (já que a maioria dos Builders provavelmente teria um equivalente de propriedade) só aprofundaria o conflito.

Não apenas isso, mas em um mundo onde podemos criar funções de ordem superior no dart (https://github.com/dart-lang/language/issues/418), poderíamos nos livrar das classes por completo:

<strong i="9">@StatelessWidget</strong>
Widget Example(BuildContext context, {Key key, String param}) {
  final value = keyword StreamBuilder();

  return Text('$value');
}

então usado como:

Widget build(context) {
  // BuildContext and Key are automatically injected
  return Example(param: 'hello');
}

Isso é algo que é suportado por Functional_widget - que é um gerador de código onde você escreve uma função e ele gera uma classe para você - que também suporta HookWidget .

A diferença é que ter suporte para funções de ordem superior no Dart eliminaria a necessidade de geração de código para suportar tal sintaxe.

Estou adivinhando o que @Hixie quis dizer com mais confiável, é que não sofre com a ordem das operações / problema condicional que os ganchos têm, já que isso é muito "não confiável" de um ponto de vista arquitetônico (embora eu perceba que é uma regra fácil de aprender e não violar uma vez aprendido).

Mas nem a sua proposta com a palavra-chave. Acho que o caso de nova palavra-chave é bastante forte:

  • Mais flexível e composível do que enxertar no Estado
  • Sintaxe ainda mais sucinta
  • Funciona sem estado, o que é uma opção muito boa de se ter

O que eu não gosto nisso é que estamos nos preocupando com o custo de definir propriedades em algum objeto simples várias vezes / compilar, mas defendendo uma solução que basicamente criará um milhão de níveis de contexto e um monte de custos de layout. Estou entendendo mal?

A outra desvantagem é essa ideia de magia. Mas se você vai fazer algo mágico, acho que uma nova palavra-chave é uma forma eficaz de fazer isso, pois torna mais fácil destacar e chamar a atenção da comunidade e explicar o que é e como funciona. Basicamente, seria tudo o que alguém falaria no próximo ano no Flutter e tenho certeza de que veríamos uma explosão de plug-ins legais surgindo a partir dele.

Estou adivinhando o que @Hixie quis dizer com mais confiável, é que não sofre com a ordem das operações / problema condicional que os ganchos têm, já que isso é muito "não confiável" de um ponto de vista arquitetônico (embora eu perceba que é uma regra fácil de aprender e não violar uma vez aprendido).

Mas os ganchos também não sofrem com esse problema, pois são estaticamente analisáveis ​​e, portanto, podemos ter um erro de compilação quando são mal utilizados.

Isso não é problema

Da mesma forma, se os erros personalizados forem proibidos, como mencionei anteriormente, Property sofre exatamente do mesmo problema.
Não podemos escrever razoavelmente:

Property property;

<strong i="12">@override</strong>
void initProperties() {
  if (condition) {
    property = init(property, MyProperty());
  }
}

já que mudar condition de verdadeiro para falso não eliminará a propriedade.

Também não podemos chamá-lo em um loop. Realmente não faz sentido, já que é uma tarefa única. Qual é o caso de uso de executar a propriedade em um loop?

E o fato de podermos ler propriedades em qualquer ordem parece perigoso
Por exemplo, podemos escrever:

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

Este é um exemplo tão estranho. Tem certeza de que o AnimatedContainer ainda não pode fazer isso?

Claro. O exemplo aqui é utilizar 3 animações em algum widget para fazer "X". OX é intencionalmente simplificado no exemplo para destacar a quantidade de clichê.

Não se concentre em como os estou usando. Em um exemplo real, o "núcleo" do widget teria cem linhas ou algo assim, as propriedades animadas não seriam tão simples e teríamos vários manipuladores e outras funções definidas. Suponha que eu esteja fazendo algo que não é manipulado por um dos widgets implícitos (não é difícil, pois, com exceção do AnimatedContainer, eles têm um único propósito).

O ponto é que, ao construir algo assim, os construtores não funcionam muito bem, pois eles colocam você em um buraco de legibilidade (e gravabilidade) para começar, como tal, eles são adequados para casos de uso simples, eles não "compõem" Nós vamos. Componha sendo a composição de 2 ou mais coisas.

Não se concentre em como os estou usando. Em um exemplo real, ...

... e de volta à estaca zero. Por que você não traz um exemplo real ?

Você precisa de um exemplo real de como usar animações complexas?
https://github.com/gskinnerTeam/flutter_vignettes

Mostrar alguma animação complexa arbitrária não faria nada além de ofuscar o exemplo. É suficiente dizer que há muitos casos de uso para o uso de vários animadores (ou qualquer outro objeto com estado que você possa imaginar) dentro de algum widget

Claro. O exemplo aqui é utilizar 3 animações em algum widget para fazer "X". OX é intencionalmente simplificado no exemplo para destacar a quantidade de clichê.

o "núcleo" do widget teria cem linhas ou algo assim

Em outro post você postou um exemplo com clichê que obscurece o "core", mas agora você nos diz que o core seria centenas de linhas? Então, na realidade, o clichê seria minúsculo em comparação com o núcleo? Você não pode; t ter as duas coisas.
Você está constantemente mudando seus argumentos.

Não se concentre em como os estou usando. Em um exemplo real, ...

... e de volta à estaca zero. Por que você não traz um exemplo real ?

Provavelmente porque leva muito tempo para criar um exemplo real quando apenas brincando com várias ideias. A intenção é que o leitor imagine como ele poderia ser usado em uma situação real, sem mencionar que existem maneiras de contornar isso. Claro que se pode usar um contêiner animado, mas e se não pudessem? E se fosse muito complexo para fazer apenas com um contêiner animado.

Agora, que os escritores não usam exemplos realmente reais, que podem ser considerados bons ou ruins, não tenho opinião sobre isso, estou apenas comentando sobre a tendência neste tópico de trazer melhorias para problemas que não resolver totalmente o problema em questão. Esta parece ser uma grande fonte de confusão entre os proponentes de ganchos e oponentes, já que cada um parece estar falando além do outro até certo ponto, então eu apoio a proposta de Hixie de criar alguns aplicativos reais de forma que um oponente não possa dizer que um exemplo "real" foi não mostrado e um proponente não pode dizer que se deva apenas imaginar um cenário do mundo real.

Acho que disse que seria bobagem ter uma classe com 100 linhas, onde metade é clichê. Que é exatamente o que estou descrevendo aqui. O núcleo, por maior que seja, não deve ser ofuscado por um monte de ruído, o que certamente acontece ao usar vários construtores.

E o motivo é a capacidade de varredura, legibilidade e manutenção em uma grande base de código. Não é a escrita das linhas, embora escrever em construtores seja uma perda de produtividade devido à tendência de entrar no inferno das chaves.

Em outro post você postou um exemplo com clichê que obscurece o "core", mas agora você nos diz que o core seria centenas de linhas? Então, na realidade, o clichê seria minúsculo em comparação com o núcleo? Você não pode; t ter as duas coisas.
Você está constantemente mudando seus argumentos.

Novamente, esse problema não é sobre clichê, mas capacidade de leitura e reutilização.
Não importa se temos 100 linhas.
O que importa é o quão legível / sustentável / reutilizável essas linhas são.

Mesmo se o argumento fosse sobre clichês, por que eu, o usuário, deveria tolerar tal clichê em qualquer caso, dada uma forma suficientemente equivalente de expressar a mesma coisa? Programar envolve criar abstrações e automatizar o trabalho, não vejo motivo para refazer a mesma coisa indefinidamente em várias classes e arquivos.

Você precisa de um exemplo real de como usar animações complexas?
https://github.com/gskinnerTeam/flutter_vignettes

Certamente, você não pode esperar que eu investigue todo o seu projeto. Qual arquivo exatamente devo olhar?

Mostrar alguma animação complexa arbitrária não faria nada além de ofuscar o exemplo.

Exatamente o oposto. Mostrando alguma animação complexa arbitrária que não pode ser resolvido por qualquer solução existente seria o exemplo, e é isso que Hixie continua pedindo, eu acredito.

Apenas escaneie os gifs e comece a imaginar como você pode construir algumas dessas coisas. Esse repo é, na verdade, 17 aplicativos independentes. Você também não pode esperar que eu escreva alguma animação arbitrária apenas para provar que podem existir animações complexas. Eu os construo há 20 anos, começando no Flash, cada um diferente do anterior. E não é específico para animações de qualquer maneira, eles são apenas a API mais simples e familiar para ilustrar um ponto maior.

Como você sabe, quando você usa um animador, há cerca de 6 coisas que você precisa fazer todas as vezes, mas também precisa de ganchos de ciclo de vida ?? Ok, agora estenda isso para QUALQUER COISA que tenha 6 passos que você tem que fazer toda vez ... E você precisa usar em 2 lugares. Ou você precisa usar 3 deles de uma vez. É tão óbvio que é um problema aparente, não sei o que mais posso acrescentar para explicá-lo.

Programar envolve criar abstrações e automatizar o trabalho, não vejo motivo para refazer a mesma coisa indefinidamente em várias classes e arquivos.

__Tudo__? Portanto, o desempenho e a capacidade de manutenção não são relevantes?

Chega um ponto em que "automatizar" um trabalho de parto e "fazer" um trabalho de parto é a mesma coisa.

Pessoal, está tudo bem se você não tem tempo ou inclinação para criar exemplos reais, mas por favor, se você não estiver interessado em criar exemplos para explicar o problema, você também não deve esperar que as pessoas se sintam compelidas a resolver o problema ( o que é muito mais trabalhoso do que criar exemplos para mostrar o problema). Ninguém aqui é obrigado a fazer nada por ninguém, é um projeto de código aberto onde todos tentamos ajudar uns aos outros.

@TimWhiting , você se importaria de colocar um arquivo de licença em seu https://github.com/TimWhiting/local_widget_state_approaches repo? Algumas pessoas não podem contribuir sem uma licença aplicável (BSD, MIT ou similar, idealmente).

por que deveria eu, o usuário, tolerar tal clichê em qualquer caso, dada uma forma suficientemente equivalente de expressar a mesma coisa?

Sim, a manutenção e o desempenho são importantes, é claro. Quero dizer, quando há uma solução equivalente, devemos escolher aquela que tem menos clichê, é mais fácil de ler, é mais reutilizável e assim por diante. Isso não quer dizer que os ganchos sejam a resposta, já que não avaliei seu desempenho, por exemplo, mas, pela minha experiência, eles são mais fáceis de manter. Ainda não tenho certeza sobre seu argumento de como isso afeta seu trabalho se uma construção semelhante a um gancho for colocada no núcleo do Flutter.

Apenas escaneie os gifs e comece a imaginar como você pode construir algumas dessas coisas.

Eu examinei os gifs. Eu não usaria widgets de construtor.
Muitas das animações são tão complexas que, se eu soubesse que você as implementou usando os construtores de nível superior, provavelmente não usaria seu pacote.

De qualquer forma, essa discussão parece estar saindo do controle com mais divergências pessoais. Devemos nos concentrar na tarefa principal em mãos. Não tenho certeza de como os proponentes do gancho podem mostrar exemplos menores se os oponentes, como eu disse antes, encontrarem melhorias que não resolvam verdadeiramente o problema apresentado. Acho que devemos contribuir com o repositório do @TimWhiting por enquanto.

Mostrar alguma animação complexa arbitrária que não pode ser resolvida por nenhuma solução existente seria o exemplo, e é isso que Hixie continua perguntando, eu acredito.

Mostrar exemplos de algo que não é possível hoje está fora do escopo desta edição.
Essa questão é sobre como melhorar a sintaxe do que já é viável, não desbloquear algumas coisas que não são possíveis hoje.

Qualquer solicitação de fornecimento de algo que não é possível hoje está fora do assunto.

@TimWhiting , você se importaria de colocar um arquivo de licença em seu https://github.com/TimWhiting/local_widget_state_approaches repo? Algumas pessoas não podem contribuir sem uma licença aplicável (BSD, MIT ou similar, idealmente).

Feito. Desculpe, não tive muito tempo para trabalhar nos exemplos, mas provavelmente irei falar disso ainda esta semana.

Mostrar alguma animação complexa arbitrária que não pode ser resolvida por nenhuma solução existente seria o exemplo, e é isso que Hixie continua perguntando, eu acredito.

Mostrar exemplos de algo que não é possível hoje está fora do escopo desta edição.
Essa questão é sobre como melhorar a sintaxe do que já é viável, não desbloquear algumas coisas que não são possíveis hoje.

Qualquer solicitação de fornecimento de algo que não é possível hoje está fora do assunto.

Deixe-me reformular o que eu disse.

Mostrar alguns casos de uso complexos e arbitrários difíceis de escrever e que podem ser melhorados significativamente sem afetar o desempenho seria o exemplo, e é isso que Hixie continua perguntando, eu acredito.

Eu entendo a pressão para menos clichê, mais capacidade de reutilização, mais mágica. Eu gosto de ter que escrever menos código também, e a linguagem / framework fazendo mais trabalho é bem apetitoso.
Até agora, nenhum dos exemplos / combinações de solução apresentados aqui melhoraria drasticamente o código. Isto é, se nos importamos mais do que apenas quantas linhas de códigos temos que escrever.

Pessoal, está tudo bem se você não tem tempo ou inclinação para criar exemplos reais, mas por favor, se você não estiver interessado em criar exemplos para explicar o problema, você também não deve esperar que as pessoas se sintam compelidas a resolver o problema ( o que é muito mais trabalhoso do que criar exemplos para mostrar o problema). Ninguém aqui é obrigado a fazer nada por ninguém, é um projeto de código aberto onde todos tentamos ajudar uns aos outros.

@TimWhiting , você se importaria de colocar um arquivo de licença em seu https://github.com/TimWhiting/local_widget_state_approaches repo? Algumas pessoas não podem contribuir sem uma licença aplicável (BSD, MIT ou similar, idealmente).

Passei cerca de 6 horas criando esses vários exemplos e trechos de código. Mas realmente não vejo sentido em fornecer exemplos concretos de animações complexas apenas para provar que elas podem existir.

A solicitação é basicamente transformar isso em algo que não pode ser tratado pelo AnimatedContainer:

Container(margin: EdgeInsets.symmetric(vertical: value2 * 20, horizontal: value3 * 30), color: Colors.red.withOpacity(value1));

Isso é tão trivial a ponto de ser quase intencionalmente obtuso para o assunto. É tão difícil imaginar que posso ter alguns botões pulsando, algumas partículas se movendo, talvez alguns campos de texto que desaparecem durante o dimensionamento ou algumas cartas que se viram? Talvez eu esteja fazendo uma barra de som com 15 barras independentes, talvez eu esteja deslizando um menu, mas também preciso da capacidade de deslizar itens individuais de volta para fora. E assim por diante e por diante. E isso é apenas para animações. Ele se aplica a qualquer caso de uso oneroso no contexto de um widget.

Acho que forneci excelentes exemplos cananônicos do problema com ambos os construtores e a reutilização de estado simples:
https://github.com/flutter/flutter/issues/51752#issuecomment -671566814
https://github.com/flutter/flutter/issues/51752#issuecomment -671489384

Você simplesmente tem que imaginar muitas dessas instâncias (escolha seu veneno), espalhadas por todo um projeto de mais de 1000 arquivos de classe, e você terá a imagem perfeita dos problemas de legibilidade e manutenção que estamos tentando evitar.

As imagens de exemplo que @esDotDev forneceu, mostrando como o aninhamento torna o código mais difícil de ler, não seriam suficientes para você, @Rudiksz ? O que está faltando com eles? Suponho que não haja métricas de desempenho lá, mas @rrousselGit certifique-se de que eles não têm menos desempenho do que os construtores.

@esDotDev Acho que a questão é ter um exemplo canônico singular a partir do qual todas as soluções de gerenciamento de ciclo de vida podem ser comparadas (não apenas ganchos, mas também outros no futuro). É o mesmo princípio que TodoMVC, você não necessariamente apontaria para várias outras implementações no React, Vue, Svelte, etc, mostrando a diferença entre eles, você gostaria que todos implementassem o mesmo aplicativo, _então_ você pode comparar.

Isso faz sentido para mim, mas não entendo por que precisa ser maior do que uma única página.

Gerenciar várias animações é o exemplo perfeito de algo que é comum, requer um monte de clichês, é sujeito a erros e não tem uma boa solução no momento. Se isso não estiver mostrando o ponto, se as pessoas vão dizer que nem mesmo entendem como as animações podem ser complexas, então, claramente, qualquer caso de uso será derrubado pelo contexto do caso de uso, e não pela questão arquitetônica que nós está tentando ilustrar.

Claro, o repositório de @TimWhiting não tem um aplicativo completo, ele tem páginas singulares como exemplos, como você disse, se você pudesse fazer um exemplo canônico de animação para aquele repositório a partir do qual outros pudessem implementar sua solução, isso funcionaria.

Eu também não acho que precisamos de um aplicativo enorme ou algo assim, mas deve haver complexidade suficiente semelhante à de TodoMVC. Basicamente, precisa ser o suficiente para que seus oponentes não consigam dizer "bem, eu poderia fazer isso melhor desta ou daquela maneira".

@Hixie A solicitação de aplicativos reais para comparar abordagens é falha.

Existem duas falhas:

  • Ainda não concordamos com o problema, como você mesmo disse não o entende
  • Não podemos implementar exemplos em condições reais de produção, pois teremos peças em falta.

Por exemplo, não podemos escrever um aplicativo usando:

final snapshot = keyword StreamBuilder();

como isso não é implementado.

Também não podemos julgar o desempenho, pois estamos comparando um POC com o código de produção.

Não podemos avaliar se algo como "ganchos não podem ser chamados condicionalmente" também está sujeito a erros, já que não há integração do compilador para apontar erros quando há um uso indevido.

Julgando o desempenho dos designs, avaliando a usabilidade da API, implementando coisas antes de termos implementações ... tudo isso faz parte do design da API. Bem-vindo ao meu trabalho. :-) (Trivia Flutter: você sabia que as primeiras milhares de linhas de RenderObject e RenderBox et al foram implementadas antes de criarmos o dart: ui?)

Isso não muda o fato de que você está solicitando o impossível.

Algumas das propostas feitas aqui fazem parte da linguagem ou do analisador. É impossível para a comunidade implementar isso.

Não tenho tanta certeza, outras estruturas e linguagens fazem design de API o tempo todo, não acho que seja muito diferente aqui, ou que o Flutter tem algumas diferenças ou dificuldades esmagadoras para design de API do que outras linguagens. Eles fazem isso sem ter suporte de compilador ou analisador, são apenas provas de conceito.

Eu reuni um exemplo de um cenário de animação 'complexo' que faz bom uso de 3 animações e bastante carregado com clichês e cruft.

É importante observar que eu poderia ter feito qualquer animação que precise de um forte snap de volta à posição inicial (eliminando todos os widgets implícitos), ou rotação no eixo z, ou escala em um único eixo, ou qualquer outro caso de uso não coberto pelos IWs. Eu me preocupei que isso não fosse levado a sério (embora meus designers me entreguem essas coisas o dia todo), então eu construí algo mais 'mundo real'.

Então aqui está um andaime simples, com 3 painéis que abrem e fecham. Ele usa 3 animadores com estados discretos. Neste caso, eu realmente não preciso do controle total do AnimatorController, TweenAnimationBuilder faria bem, mas o aninhamento resultante em minha árvore seria muito indesejável. Não consigo aninhar os TABs na árvore, pois os painéis dependem dos valores uns dos outros. AnimatedContainer não é uma opção aqui, já que cada painel precisa deslizar para fora da tela, eles não "comprimem".
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));

Portanto, das 100 linhas ou mais naquele corpo, cerca de 40% ou mais é puro clichê. 15 linhas em particular, onde algo está faltando ou digitado incorretamente, pode causar bugs difíceis de localizar.

Se usarmos algo como StatefulProperty, isso reduziria o clichê para 15% ou mais (economiza cerca de 25 linhas). De maneira crítica, isso resolveria completamente o problema de bugs sorrateiros e lógica de negócios duplicada, mas ainda é um pouco prolixo, especialmente porque requer StatefulWidget, que é um acerto de 10 linhas logo de cara.

Se usarmos algo como 'palavra-chave', reduzimos as linhas padronizadas para essencialmente 0%. Todo o foco da aula pode ser na lógica de negócios (única) e nos elementos da árvore visual. Tornamos o uso de StatefulWIdgets muito mais raro em geral, a grande maioria das visualizações se torna 10 ou 20% menos prolixo e mais focado.

Além disso, é importante notar que o cenário do Painel acima é do mundo real e, no mundo real, obviamente, essa abordagem não é muito boa, então não a usamos e você não a veria na base de código. Nem usaríamos construtores aninhados, porque eles parecem nojentos, então você também não verá isso.

Construímos um widget SlidingPanel dedicado, que pega uma propriedade IsOpen e se abre e fecha. Geralmente, essa é a solução em cada um desses casos de uso em que você precisa de algum comportamento específico, move a lógica stateful para algum widget ultrespecífico e o usa. Você basicamente escreve seu próprio ImplicitlyAnimatedWidget.

Isso geralmente funciona bem, mas ainda é tempo e esforço, e literalmente o ÚNICO motivo de existir é porque usar animações é muito difícil (que existe porque reutilizar componentes com estado em geral é muito difícil). No Unity ou AIR, por exemplo, eu não criaria uma classe dedicada simplesmente para mover um painel em um único eixo, seria apenas uma única linha de código para abrir ou fechar, não haveria nada para um widget dedicado fazer. No Flutter, temos que criar um widget dedicado, porque é literalmente a única maneira razoável de encapsular a inicialização e desmontagem do AnimatorController (a menos que queiramos aninhar, aninhar, aninhar com TAB)

Meu ponto principal aqui é que esse tipo de coisa é por que exemplos do mundo real são tão difíceis de encontrar. Como desenvolvedores, não podemos permitir que essas coisas existam em nossas bases de código, então trabalhamos em torno delas com soluções alternativas menos do que ideais, mas eficazes. Então, quando você olha para a base de código, você apenas vê essas soluções alternativas em vigor e tudo parece bem, mas pode não ser o que a equipe queria fazer, pode ter demorado 20% mais para chegar lá, pode ter sido um total dor para depurar, nada disso é evidente olhando para o código.

Para completar, aqui está o mesmo caso de uso, feito com construtores. A contagem de linhas é fortemente reduzida, não há chance de bugs, não há necessidade de aprender o conceito estrangeiro RE TickerProviderMixin ... mas esse aninhamento é triste e a forma como as várias variáveis ​​são espalhadas pela árvore (valores finais dinâmicos, valor1, valor2 etc. ) torna a lógica de negócios muito mais difícil de ler do que o necessário.

`` `dardo
classe _SlidingPanelViewState estende State{
bool isLeftMenuOpen = true;
bool isRightMenuOpen = true;
bool isBtmMenuOpen = true;

@sobrepor
Construção de widget (contexto BuildContext) {
retornar TweenAnimationBuilder(
tween: Tween (início: 0, fim: isLeftMenuOpen? 1: 0),
duração: widget.slideDuration,
construtor: (_, leftAnimValue, __) {
retornar TweenAnimationBuilder(
tween: Tween (início: 0, fim: isRightMenuOpen? 1: 0),
duração: widget.slideDuration,
construtor: (_, rightAnimValue, __) {
retornar TweenAnimationBuilder(
tween: Tween (início: 0, fim: isBtmMenuOpen? 1: 0),
duração: widget.slideDuration,
construtor: (_, btmAnimValue, __) {
leftPanelSize duplo = 320;
double leftPanelPos = -leftPanelSize * (1 - leftAnimValue);
double rightPanelSize = 230;
double rightPanelPos = -rightPanelSize * (1 - rightAnimValue);
double bottomPanelSize = 80;
double bottomPanelPos = -bottomPanelSize * (1 - btmAnimValue);
return Stack (
crianças: [
// Bg
Recipiente (cor: Colors.white),
// Área de conteúdo principal
Posicionado(
topo: 0,
esquerda: leftPanelPos + leftPanelSize,
inferior: bottomPanelPos + bottomPanelSize,
right: rightPanelPos + rightPanelSize,
filho: ChannelInfoView (),
),
// Painel esquerdo
Posicionado(
topo: 0,
esquerda: leftPanelPos,
inferior: bottomPanelPos + bottomPanelSize,
largura: leftPanelSize,
filho: ChannelMenu (),
),
// Painel inferior
Posicionado(
esquerda: 0,
direito: 0,
inferior: bottomPanelPos,
height: bottomPanelSize,
filho: NotificationsBar (),
),
// Painel direito
Posicionado(
topo: 0,
right: rightPanelPos,
inferior: bottomPanelPos + bottomPanelSize,
largura: rightPanelSize,
filho: SettingsMenu (),
),
// Botões
Linha(
crianças: [
Button ("left", () => setState (() => isLeftMenuOpen =! IsLeftMenuOpen)),
Button ("btm", () => setState (() => isBtmMenuOpen =! IsBtmMenuOpen)),
Button ("right", () => setState (() => isRightMenuOpen =! IsRightMenuOpen)),
],
)
],
);
},
);
},
);
},
);
}
}

Este último é interessante ... Eu originalmente iria sugerir que os construtores deveriam estar em torno dos widgets posicionados ao invés da pilha (e eu ainda sugeriria isso para os painéis esquerdo e direito), mas então percebi que o inferior afeta todos os três, e percebi que o construtor apenas dando a você um argumento child na verdade não é suficiente, porque você realmente deseja manter ambos Container e Row constantes em constrói. Suponho que você pode simplesmente criá-los acima do primeiro construtor.

Meu ponto principal aqui é que esse tipo de coisa é por que exemplos do mundo real são tão difíceis de encontrar. Como desenvolvedores, não podemos permitir que essas coisas existam em nossas bases de código, então trabalhamos em torno delas com soluções alternativas menos do que ideais, mas eficazes. Então, quando você olha para a base de código, você apenas vê essas soluções alternativas em vigor e tudo parece bem, mas pode não ser o que a equipe queria fazer, pode ter demorado 20% mais para chegar lá, pode ter sido um total dor para depurar, nada disso é evidente olhando para o código.

Para que fique registado, isto não é um acidente. Isso é muito por design. Ter esses widgets como seus próprios melhora o desempenho. Queríamos muito que fosse assim que as pessoas usassem o Flutter. Isso é o que eu estava me referindo acima quando mencionei "uma grande parte da filosofia de design do Flutter é orientar as pessoas para a escolha certa".

Eu originalmente iria sugerir que os construtores deveriam estar em torno dos widgets posicionados em vez da pilha (e ainda sugeriria isso para os painéis esquerdo e direito)

Na verdade, omiti por uma questão de sucinto, mas normalmente gostaria de ter um contêiner de conteúdo aqui, e ele usaria os tamanhos de todos os 3 menus para definir sua própria posição. Significa que todos os 3 precisam estar no topo da árvore, e se houver reconstrução, eu preciso reconstruir a visão inteira, sem contornar isso. Este é basicamente o seu arcabouço clássico no estilo desktop.

É claro que poderíamos começar a destruir a árvore, sempre uma opção, usamos muito para widgets maiores, mas nunca vi isso melhorar a grokability à primeira vista, há uma etapa cognitiva extra que o leitor precisa fazer. apontar. No segundo em que você quebra a árvore, de repente estou seguindo uma trilha de migalhas de pão de atribuições variáveis ​​para descobrir o que está sendo passado aqui e como tudo se encaixa fica obstruído. Apresentar a árvore como uma árvore digestível é sempre mais fácil de raciocinar com base na minha experiência.

Provavelmente um bom caso de uso para um RenderObject dedicado.

Ou 3 AnimatorObjects facilmente gerenciados que podem se conectar ao ciclo de vida do widget: D

Ter esses widgets como seus próprios melhora o desempenho.

Já que estamos sendo concretos aqui: Neste caso por ser um andaime, cada subvisualização já é seu próprio widget, esse cara é responsável apenas por preparar e traduzir as crianças. O filho seria BottomMenu (), ChannelMenu (), SettingsView (), MainContent () etc.

Neste caso, estamos envolvendo um monte de widgets independentes, com outra camada de widgets independentes, simplesmente para gerenciar o clichê ao movê-los. Não acredito que seja uma vitória por desempenho? Neste caso, estamos sendo empurrados para o que a estrutura _pensa_ que queremos fazer, e não o que realmente queremos fazer, que é escrever uma visão igualmente performante de uma forma mais sucinta e coerente.

[Editar] Vou atualizar os exemplos para adicionar este contexto

O motivo pelo qual sugiro um RenderObject dedicado é que ele tornaria o layout melhor. Eu concordo que o aspecto da animação estaria em um widget stateful, ele apenas passaria as três duplas 0..1 (valor1, valor2, valor3) para o objeto de renderização em vez de ter a matemática da pilha. Mas isso é principalmente um espetáculo secundário para esta discussão; no ponto em que você está fazendo isso, você ainda deseja fazer a simplificação oferecida por Hooks ou algo semelhante naquele widget com estado.

Em uma nota mais relevante, tive uma chance ao criar uma demonstração para o projeto de @TimWhiting : https://github.com/TimWhiting/local_widget_state_approaches/pull/1
Estou curioso para saber como seria uma versão do Hooks. Não tenho certeza se posso ver uma boa maneira de torná-lo mais simples, especialmente mantendo as características de desempenho (ou melhorando-as; há um comentário lá mostrando um local onde está abaixo do ideal).

(Também estou muito curioso para saber se esse é o tipo de coisa em que, se encontrássemos uma maneira de simplificá-lo, terminaríamos aqui ou se estivessem faltando coisas críticas que precisaríamos resolver antes de terminarmos.)

O material de restauração é crítico para este exemplo? Estou tendo dificuldade em acompanhar por causa disso, e não sei realmente o que o RestorationMixin faz. Suponho que ... vai demorar um pouco para entender isso para mim. Tenho certeza que Remi vai lançar a versão de ganchos em 4 segundos :)

O uso da API de restauração usando HookWidget vez de StatefulHookWidget não é suportado no momento.

Idealmente, devemos ser capazes de mudar

final value = useState(42);

em:

final value = useRestorableInt(42);

Mas é preciso pensar um pouco, já que a API de restauração atual não foi realmente projetada com ganchos em mente.

Como uma observação lateral, os ganchos React vêm com um recurso "chave", geralmente usado assim:

int userId;

Future<User> user = useMemo(() => fetchUser(id), [id]);

onde este código significa "armazenar em cache o resultado do retorno de chamada e reavaliar o retorno de chamada sempre que algo dentro da matriz mudar"

O Flutter_hooks fez uma reimplementação 1to1 disso (já que é apenas uma porta), mas não acho que seja isso que gostaríamos de fazer para um código otimizado do Flutter.

Provavelmente desejaríamos:

int userId;

Future<User> user = useMemo1(id, (id) => fetchUser(id));

que faria a mesma coisa, mas removeria a pressão da memória evitando uma alocação de lista e usando um corte de função

Não é crítico neste estágio, mas vale a pena mencionar se estamos planejando usar flutter_hooks para exemplos.

@Hixie ,

Foi um exemplo interessante, parabéns por ter pensado nisso!
É um bom exemplo de que, por padrão, a implementação de "ativo" e "duração" está em todo lugar (e dependem uma da outra).
Até o ponto em que existem inúmeras chamadas "if (active)" / "controller.repeat"

Já com ganchos, toda a lógica é tratada declarativamente e concentrada em um único lugar, sem duplicata.

O exemplo também mostra como os ganchos podem ser usados ​​para armazenar objetos em cache facilmente - o que corrigiu o problema de reconstrução de ExpensiveWidget com muita frequência.
Obtemos os benefícios dos construtores const, mas ele funciona com parâmetros dinâmicos.

Também obtemos uma recarga a quente melhor. Podemos alterar a duração do Timer.periodic para a cor de fundo e ver imediatamente as alterações em vigor.

@rrousselGit você tem um link? Não vi nada de novo no repositório do @TimWhiting .

O uso da API de restauração usando HookWidget vez de StatefulHookWidget não é suportado no momento.

Seja qual for a solução que encontrarmos, precisamos ter certeza de que ela não precisa saber sobre até o último mixin. Se alguém quiser usar nossa solução em combinação com algum outro pacote que introduz um mixin como TickerProviderStateMixin ou RestorationMixin, deve ser capaz de fazê-lo.

https://github.com/TimWhiting/local_widget_state_approaches/pull/3

Concordo, mas não estou preocupado com isso. useAnimationController não exige que os usuários se preocupem com SingleTickerProvider, por exemplo.

AutomaritKeepAlive poderia se beneficiar do mesmo tratamento.
Uma das coisas que eu estava pensando é ter um gancho "useKeepAlive (bool)"

Isso evita o mixin e o "super.build (contexto)" (o último sendo bastante confuso)

Outro ponto interessante são as mudanças necessárias durante a refatoração.

Por exemplo, podemos comparar a diferença entre as mudanças necessárias para implementar TickerMode para a abordagem bruta vs ganchos:

Algumas outras coisas estão misturadas no diff, mas podemos ver que:

  • StatefulWidget necessário para mover a lógica para um ciclo de vida completamente diferente
  • As mudanças de ganchos são puramente aditivas. As linhas existentes não foram editadas / movidas.

Imo, isso é muito importante e uma vitória fundamental deste estilo de objetos de estado independentes. Ter tudo baseado em contextos aninhados em uma árvore é fundamentalmente mais difícil e complicado de refatorar e alterar, o que durante o curso de um projeto tem um efeito invisível, mas definitivo na qualidade final da base de código.

Concordou!
Isso também torna as revisões de código muito mais fáceis de ler:

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

Não está claro na segunda diferença que Container está inalterado e que apenas Text mudou

@rrousselGit Você acha que faz sentido tentar e fazer uma versão que represente como ela ficaria com o suporte a palavras-chave? É substancialmente semelhante a ganchos com apenas uma palavra-chave 'usar' dedicada ou se torna ainda mais fácil de seguir? É difícil comparar a abordagem do gancho em um pé de igualdade, porque ele tem tantos conceitos estranhos como useEffect, useMemo etc, que eu acho que o fazem parecer mais mágico do que é?

Outra coisa a se notar é que tudo isso realmente funcionaria, se você tivesse vários widgets, todos precisariam compartilhar essa lógica de 'gradação de cores', mas usassem a cor resultante de maneiras totalmente diferentes. Com a abordagem do estilo ganchos, simplesmente agrupamos qualquer lógica que faça sentido para reutilizar e simplesmente a usamos. Não há recantos arquitetônicos a que somos forçados, é verdadeiramente agnóstico e flexível.

Na abordagem com estado, somos forçados a

  • copie e cole a lógica (muito insustentável)
  • usando um construtor (não super legível, especialmente ao usar aninhamento)
  • mixagem (não compõe bem, muito fácil para diferentes mixins entrarem em conflito em seu estado compartilhado)

A principal coisa que penso é que neste último, você imediatamente tem um problema de arquitetura, onde devo colocar isso em minha árvore, como posso encapsular isso da melhor maneira, deve ser um construtor ou talvez um widget personalizado? Com o primeiro, a única decisão é em qual arquivo salvar esse pedaço de lógica reutilizada, não há nenhum impacto em sua árvore. Isso é muito bom arquitetonicamente quando você deseja usar alguns desses encapsulamentos lógicos juntos, movê-los para cima e para baixo em sua hierarquia ou mover para um widget irmão, etc.

Não sou, de forma alguma, um desenvolvedor especialista. Mas esse novo estilo de reutilizar a lógica e escrevê-la em um só lugar é realmente útil.

Eu não uso o Vue.js há muito tempo, mas eles até têm sua própria API (inspirada nos Hooks) para sua próxima versão, pode valer a pena dar uma olhada.

E CMIIW, acho que as regras de ganchos de reação (não use condicional), não se aplicam com a API de composição. Portanto, você não precisa usar o linter para fazer cumprir as regras.

Mais uma vez, a seção de motivação reforça fortemente o OP aqui:

MOTIVAÇÃO
O código de componentes complexos se torna mais difícil de raciocinar à medida que os recursos aumentam com o tempo. Isso acontece especialmente quando os desenvolvedores estão lendo código que não escreveram eles próprios.
[Havia] Falta de um mecanismo limpo e gratuito para extrair e reutilizar a lógica entre vários componentes.

As palavras-chave são "limpo" e "gratuito". Os mixins são gratuitos, mas não são limpos. Os construtores são limpos, mas não são gratuitos no sentido de legibilidade, nem no sentido da arquitetura do widget, pois são mais difíceis de mover pela árvore e raciocinar em termos de hierarquia.

Eu também acho que isso é importante notar na discussão sobre legibilidade: "_acontece particularmente quando os desenvolvedores estão lendo código que não escreveram_". É claro que _seu_ construtor aninhado pode ser fácil de ler, você sabe o que está lá e pode ignorá-lo confortavelmente, está lendo o código de outra pessoa, como você faz em qualquer projeto maior, ou seu próprio código de semanas / meses atrás, quando se torna bastante irritante / difícil de analisar e refatorar essas coisas.

Algumas outras seções especialmente relevantes.

Por que simplesmente ter componentes não é suficiente:

Criar ... componentes nos permite extrair partes repetíveis da interface, juntamente com sua funcionalidade, em partes reutilizáveis ​​de código. Isso por si só pode levar nosso aplicativo muito longe em termos de capacidade de manutenção e flexibilidade. No entanto, nossa experiência coletiva provou que isso por si só pode não ser suficiente, especialmente quando seu aplicativo está ficando muito grande - pense em várias centenas de componentes. Ao lidar com aplicativos tão grandes, compartilhar e reutilizar o código torna-se especialmente importante.

Sobre por que reduzir a fragmentação lógica e encapsular as coisas com mais força é uma vitória:

a fragmentação é o que torna difícil entender e manter um componente complexo. A separação de opções obscurece as preocupações lógicas subjacentes. Além disso, ao trabalhar em uma única preocupação lógica, temos que "pular" constantemente os blocos de opções para o código relevante. Seria muito melhor se pudéssemos colocar o código relacionado à mesma preocupação lógica.

Estou curioso para saber se há outras propostas de exemplos canônicos que estamos tentando melhorar além da que enviei.
Se for o ponto de partida que as pessoas desejam usar, ótimo. No entanto, eu diria que o maior problema com isso agora é a verbosidade; não há muito código para reutilizar nesse exemplo. Portanto, não está claro para mim se é uma boa representação do problema conforme descrito no OP.

Estive pensando um pouco mais sobre como expressar as características que eu pessoalmente procuraria em uma solução, e isso me fez perceber um dos grandes problemas que vejo com a atual proposta do Hooks, que é uma razão pela qual eu não deseja mesclá-lo no framework Flutter: Locality and Encapsulation, ou melhor, a falta deles. O design dos Ganchos usa o estado global (por exemplo, o estático para rastrear qual widget está sendo construído atualmente). IMHO esta é uma característica de design que devemos evitar. Em geral, tentamos tornar as APIs autocontidas, então se você chamar uma função com um parâmetro, você deve ter certeza de que ela não será capaz de fazer nada com valores fora desse parâmetro (é por isso que passamos o BuildContext , em vez de ter o equivalente a useContext ). Não estou dizendo que essa é uma característica que todos necessariamente desejam; e é claro que as pessoas podem usar Ganchos se isso não for um problema para elas. Só que é algo que eu gostaria de evitar fazer mais no Flutter. Cada vez que tivemos um estado global (por exemplo, nas ligações), acabamos nos arrependendo.

Os ganchos provavelmente poderiam ser métodos no contexto (acho que estavam na versão anterior), mas, honestamente, não vejo muito valor em mesclá-los como estão atualmente. O Merge precisaria de algumas aventuras para separar o pacote, como aumento de desempenho ou ferramentas de depuração específicas do gancho. Caso contrário, eles apenas aumentariam a confusão, por exemplo, você teria 3 maneiras oficiais de ouvir um listenable: AnimatedBuilder, StatefulWidget & useListenable.

Então, para mim, o caminho a percorrer é melhorar a geração de código - propus algumas mudanças: https://github.com/flutter/flutter/issues/63323

Se essas sugestões fossem realmente implementadas, as pessoas que desejassem soluções mágicas do tipo SwiftUI em seus aplicativos poderiam apenas fazer um pacote e não incomodar ninguém.

Discutir a validade dos ganchos está meio fora do assunto neste estágio, já que ainda não concordamos sobre o problema, tanto quanto eu sei.

Conforme declarado algumas vezes nesta edição, existem muitas outras soluções, várias das quais são recursos de linguagem.
Os ganchos são meramente uma porta de uma solução existente de outra tecnologia que é relativamente barata de implementar em sua forma mais básica.

Esse recurso pode seguir um caminho completamente diferente dos ganchos, como o que o SwiftUI ou o Jetpack Compose fazem; a proposta "nomeada mixin", ou o açúcar da sintaxe para a proposta Builders.

Insisto no fato de que esse problema em seu cerne está pedindo uma simplificação de padrões como o StreamBuilder:

  • StreamBuilder tem uma legibilidade / gravabilidade ruim devido ao seu aninhamento
  • Mixins e funções não são uma alternativa possível ao StreamBuilder
  • Copiar e colar a implementação de StreamBuilder em todos os StatefulWidgets não é razoável

Todos os comentários até agora mencionaram alternativas de StreamBuilder, tanto para diferentes comportamentos (criar um objeto descartável, fazer solicitações HTTP, ...) ou propor diferentes sintaxes.

Não tenho certeza do que mais há a dizer, então não vejo como podemos progredir mais.
O que é isso que você não entende / discorda nesta declaração @Hixie?

@rrousselGit Você poderia criar um aplicativo de demonstração que mostre isso? Tentei criar um aplicativo de demonstração que mostrasse o que entendi ser o problema, mas aparentemente não acertei. (Não tenho certeza de qual é a diferença entre "padrões como StreamBuilder" e o que fiz no aplicativo de demonstração.)

  • StreamBuilder tem uma legibilidade / gravabilidade ruim devido ao seu aninhamento

Você já disse que a verbosidade não é o problema. Aninhar é apenas outro aspecto de ser prolixo. Se o aninhamento for realmente um problema, devemos ir atrás de Padding, Expanded, Flexible, Center, SizedBox e todos os outros widgets que adicionam aninhamento sem motivo real. Mas o aninhamento pode ser facilmente resolvido dividindo widgets monolíticos.

Copiar e colar a implementação de StreamBuilder em todos os StatefulWidgets não é razoável

Você quer dizer copiar e colar as linhas de código que criam e descartam os fluxos que os StatefulWidgets precisam criar e descartar? Sim, é absolutamente razoável.

Se você tem dez ou Deus proíbe centenas de _diferentes_ StatefulWidgets personalizados que precisam criar / descartar seus próprios fluxos - "usar" na terminologia dos ganchos -, você terá problemas maiores com que se preocupar do que a reutilização ou aninhamento "lógico". sobre por que meu aplicativo tem que criar tantos streams diferentes em primeiro lugar.

Para ser justo, acho que é normal alguém pensar que um determinado padrão não é razoável em seu aplicativo. (Isso não significa necessariamente que o framework deva suportar isso nativamente, mas seria bom pelo menos permitir que um pacote resolvesse isso.) Se alguém não quiser usar stream.listen ou StreamBuilder(stream) , é isso mesmo, e talvez como resultado possamos encontrar um padrão que seja melhor para todos.

Para ser justo, acho que é normal alguém pensar que um determinado padrão não é razoável em seu aplicativo.

Estou 100% na mesma página que você.
Claro, as pessoas podem fazer o que quiserem em seus aplicativos. O que estou tentando entender é que todos os problemas e dificuldades descritos neste tópico são resultado de maus hábitos de programação e, na verdade, têm muito pouco a ver com Dart ou Flutter. É tipo, apenas minha opinião, mas eu diria que se alguém está escrevendo um aplicativo que cria dezenas de streams em todo o lugar, talvez deva revisar o design do aplicativo antes de pedir que a estrutura seja "aprimorada".

Por exemplo, a implementação de gancho que fez parte do repositório de exemplo.

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

Tenho um mau pressentimento sobre isso, então verifiquei alguns dos detalhes internos e adicionei algumas impressões de depuração para verificar o que está acontecendo.
Você pode ver na saída abaixo que o gancho Ouvível verifica se ele foi atualizado a cada tique da animação. Por exemplo, o widget que "usa" esse recurso de escuta foi atualizado? A duração foi alterada? A instância foi substituída?
O gancho memorizado, nem sei o que há com isso. Provavelmente, é feito para armazenar em cache um objeto, mas em cada construção o widget verifica se o objeto mudou. O que? Porque? Claro, porque ele é usado dentro de um widget stateful e algum outro widget na árvore pode mudar o valor, então precisamos pesquisar as mudanças. Este é o comportamento de pesquisa literal que é exatamente o oposto da programação "reativa".

O que é pior, os ganchos "novo" e "antigo" têm o mesmo tipo de instância, ambos têm os mesmos valores e, ainda assim, a função itera sobre os valores para verificar se eles mudaram. Em _tiquetaque de cada animação única_.

Esta é a saída que obtenho, ad infinitum.

/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

Todo esse trabalho é feito em cada tique da animação. Se eu adicionar outro gancho como "cor final = useAnimation(animationColor); ", para animar a cor também, agora o widget verifica _duas_ vezes se ele foi atualizado.

Estou sentado aqui assistindo um texto animado para frente e para trás sem nenhuma alteração no estado do aplicativo ou em qualquer um dos widgets ou na árvore de widgets, e ainda os ganchos estão constantemente verificando se a árvore / widgets foram atualizados. Fazer com que todos os widgets que "usam" esses ganchos específicos façam esse comportamento de pesquisa é ruim.

Lidar com a inicialização / atualização / lógica de descarte de objetos de estado dentro do método de construção é apenas um projeto ruim. Nenhuma quantidade de melhorias obtidas na capacidade de reutilização, hotreload ou carga cognitiva justifica o impacto no desempenho.
Novamente, na minha opinião. Sendo os ganchos um pacote, qualquer pessoa pode usá-los se achar que os ganhos justificam o overhead.

Além disso, não acho que qualquer quantidade de recursos de linguagem, mágica do compilador ou abstração possam evitar tais verificações desnecessárias, se começarmos a tentar abstrair tudo dentro do processo de construção. Portanto, ficamos com alternativas como estender o StatefulWidget. Algo que já pode ser feito e foi dispensado inúmeras vezes.

@Hixie, você não respondeu à pergunta. O que é que você não entende / concorda na lista de marcadores listada acima?

Não posso dar um exemplo sem saber o que você quer que eu demonstre.

@Rudiksz Com certeza, qualquer solução que consideraríamos realmente precisaria ter um perfil e uma avaliação comparativa para ter certeza de que não está piorando as coisas. Parte da maneira como construí o aplicativo de demonstração que enviei para @TimWhiting visa cobrir exatamente os tipos de padrões que podem ser fáceis de bagunçar. (E, conforme observado anteriormente, isso deixa espaço para melhorias, consulte o TODO no código.)

@rrousselGit Eu realmente não queria me

  • Primeiro, eu evitaria usar Streams em geral. ValueListenable é IMHO um padrão muito melhor para mais ou menos os mesmos casos de uso.
  • Não acho que o StreamBuilder seja particularmente difícil de ler ou escrever; mas, como @satvikpendem comentou anteriormente , minha história é com árvores profundamente aninhadas em HTML, e tenho olhado para árvores Flutter por 4 anos (eu disse antes que a competência central do Flutter é como caminhar com eficiência em árvores gigantes), então provavelmente tenho uma tolerância maior do que a maioria das pessoas, e minha opinião aqui não é realmente relevante.
  • Quanto a se mixins e funções podem ser uma alternativa possível para StreamBuilder, acho que Hooks demonstra muito bem que você pode definitivamente usar funções para ouvir streams, e mixins podem claramente fazer qualquer coisa que as classes podem fazer aqui, então não vejo por que eles fariam também não é uma solução.
  • Finalmente, com relação às implementações de copiar e colar, isso é uma questão subjetiva. Eu pessoalmente não copio e colo a lógica em initState / didUpdateWidget / dispose / build, eu escrevo de novo a cada vez e parece normal. Quando fica "fora de controle", eu fatoro em um widget como o StreamBuilder. Então, novamente, minha opinião provavelmente não é relevante aqui.

Como regra geral, se eu tenho o problema que você está vendo ou não, não é relevante. Sua experiência é válida independentemente da minha opinião. Fico feliz em trabalhar para encontrar soluções para os problemas que você enfrenta, mesmo que não sinta esses problemas. O único efeito de eu mesmo não ter experimentado o problema é que é mais difícil para mim entender bem o problema, como vimos nesta discussão.

O que eu gostaria que você demonstrasse é o padrão de codificação que você considera inaceitável, em uma demonstração que é significativa o suficiente para que, quando alguém cria uma solução, você não a descartaria dizendo que pode ficar difícil de usar quando em uma situação diferente (ou seja, inclua todas as situações relevantes, por exemplo, certifique-se de incluir as partes de "atualização" ou quaisquer outras partes que você acha que são importantes para lidar), ou dizendo que a solução funciona em um caso, mas não no caso geral (por exemplo, para tomar seu feedback anterior, certificando-se de que os parâmetros estão vindo de vários lugares e atualizando em várias situações, de modo que seja óbvio que uma solução como a propriedade acima não levaria em consideração o código comum de outra forma).

A questão é que você está pedindo exemplos para algo que está no domínio do "óbvio" para mim.

Não me importo de dar alguns exemplos, mas não tenho ideia do que você está esperando, pois não entendo o que você não entende.

Já disse tudo o que tinha a dizer.
A única coisa que posso fazer sem entender o que você não entende é me repetir.

Posso fazer alguns dos trechos aqui rodarem, mas isso é equivalente a me repetir.
Se o snippet não fosse útil, não vejo por que ser capaz de executá-lo mudaria alguma coisa.

Quanto a se mixins e funções podem ser uma alternativa possível para StreamBuilder, acho que Hooks demonstra muito bem que você pode definitivamente usar funções para ouvir streams, e mixins podem claramente fazer qualquer coisa que as classes podem fazer aqui, então não vejo por que eles fariam também não é uma solução.

Ganchos não devem ser considerados funções.
Eles são uma nova construção de linguagem semelhante a Iterable / Stream

As funções não podem fazer o que os ganchos fazem - não têm um estado ou a capacidade de fazer com que os widgets sejam reconstruídos.

O problema com os mixins é demonstrado no OP. TL; DR: Conflito de nomes em variáveis ​​e é impossível reutilizar o mesmo mixin várias vezes.

@rrousselGit Bem, como você não se importa em dar alguns exemplos e como os exemplos que estou pedindo são óbvios, vamos começar com alguns desses exemplos óbvios e iterar a partir daí.

Eu não disse que os exemplos são óbvios, mas o problema é.
O que eu quis dizer é que não posso criar novos exemplos. Tudo o que tenho a dizer já está neste tópico:

Não consigo pensar em nada a acrescentar a esses exemplos.

Mas FWIW estou trabalhando em um aplicativo meteorológico de código aberto usando Riverpod. Vou vinculá-lo aqui quando estiver pronto.


Fiz uma enquete no Twitter com algumas perguntas sobre Construtores relacionadas aos problemas discutidos aqui:

https://twitter.com/remi_rousselet/status/1295453683640078336

A votação ainda está pendente, mas aqui estão os números atuais:

Screenshot 2020-08-18 at 07 01 44

O fato de que 86% de 200 pessoas desejam uma maneira de escrever Construtores que não envolvam aninhamento fala por si.

Para ser claro, nunca sugeri que não devêssemos abordar esse problema. Se eu achasse que não deveríamos resolver isso, o problema estaria encerrado.

Acho que vou tentar fazer um exemplo que use os snippets aos quais você vinculou.

Posso ajudá-lo a fazer exemplos com base nos snippets vinculados, mas preciso saber por que esses snippets não eram bons o suficiente
Caso contrário, a única coisa que posso fazer é compilar esses snippets, mas duvido que seja o que você deseja.

Por exemplo, aqui está uma essência sobre os diversos ValueListenableBuilder + TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

Por exemplo, aqui está uma essência sobre os diversos ValueListenableBuilder + TweenAnimationBuilder https://gist.github.com/rrousselGit/a48f541ffaaafe257994c6f98992fa73

FWIW, este exemplo particular pode ser implementado mais facilmente no mobx.
Na verdade, é mais curto do que a implementação de ganchos.

Os observáveis ​​do Mobx são ValueNotifiers em esteróides e seu widget Observer é a evolução do ValueListenableBuilder do Flutter - ele pode ouvir mais de um ValueNotifier.
Ser um substituto imediato para a combinação ValueNotifier / ValueListenableBuilder significa que você ainda escreve código Flutter idiomático, o que na verdade é um fator importante.

Como ele ainda usa o construtor Tween integrado do Flutter, não há necessidade de aprender / implementar novos widgets / ganchos (em outras palavras, não precisa de novos recursos) e não tem nenhum dos acertos de desempenho dos ganchos.

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 sendo tão simples quanto ...

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

Aqui está outra implementação que nem precisa de construtores de animação. O método de construção do widget é tão puro quanto pode ser, quase como um arquivo html semântico ... como um modelo.

https://gist.github.com/Rudiksz/cede1a5fe88e992b158ee3bf15858bd9

@Rudiksz O comportamento do campo "total" é quebrado em seu snippet. Não corresponde ao exemplo, onde os dois contadores podem ser animados juntos, mas terminam a animação em momentos diferentes e com curvas diferentes.
Da mesma forma, não tenho certeza do que este exemplo adiciona à variante ValueListenableBuilder .

Quanto à sua última essência, TickerProvider está quebrado, pois não suporta TickerMode - nem os ouvintes são removidos ou controladores descartados.

E Mobx provavelmente está fora do assunto. Não estamos discutindo como implementar o estado ambiente / ValueListenable vs Stores vs Streams, mas sim como lidar com o estado local / construtores aninhados - que Mobx não resolve de forma alguma

––––

Além disso, tenha em mente que, no exemplo dos ganchos, useAnimatedInt pode / deve ser extraído para um pacote e que não há duplicação da duração / curva entre a animação de texto individual e o total.

Quanto ao desempenho, com Ganchos estamos reconstruindo apenas um único Elemento, enquanto com Construtores com estamos reconstruindo 2-4 Construtores.
Portanto, os Hooks podem muito bem ser mais rápidos.

O comportamento do campo "total" é quebrado em seu snippet. Não corresponde ao exemplo, onde os dois contadores podem ser animados juntos, mas terminam a animação em momentos diferentes e com curvas diferentes.

Você claramente nem tentou executar o exemplo. Ele se comporta exatamente como seu código.

Quanto à sua última essência, TickerProvider está quebrado, pois não oferece suporte a TickerMode.

Eu não sei o que você quer dizer com isso. Refatorei _seu_ exemplo, que não usa TickerMode. Você novamente está mudando os requisitos.

Quanto ao desempenho, com Ganchos estamos reconstruindo apenas um único Elemento, enquanto com Construtores com estamos reconstruindo 2-4 Construtores. Portanto, os Hooks podem muito bem ser mais rápidos.

Não apenas não. Seus widgets de gancho pesquisam constantemente por mudanças em cada construção. Construtores baseados em listas de valores são "reativos".

Da mesma forma, não tenho certeza do que este exemplo adiciona à variante ValueListenableBuilder .

E Mobx provavelmente está fora do assunto. Não estamos discutindo como implementar o estado ambiente / ValueListenable vs Stores vs Streams, mas sim como lidar com o estado local / construtores aninhados - que Mobx não resolve de forma alguma

Você deve estar brincando. Peguei seu exemplo e "lidei" com ValueListenableBuilders * aninhados
Mas esta frase aqui descreve toda a sua atitude em relação a esta discussão. Se não forem ganchos, é "fora do assunto", mas você diz que não se importa se são ganchos que serão usados ​​como solução.
Me dá um tempo.

Você claramente nem tentou executar o exemplo. Ele se comporta exatamente como seu código.

Não é verdade. O primeiro contador é animado em 5 segundos e o segundo em 2 segundos - e ambos usam uma curva diferente também.

Com os dois trechos que forneci, você poderia incrementar os dois contadores ao mesmo tempo e, durante cada quadro da animação, o "total" estaria correto. Mesmo quando o segundo contador para de animar enquanto o primeiro contador ainda está animando

Por outro lado, sua implementação não considera este caso, porque fundiu os 2 TweenAnimationBuilders em um.
Para consertar, teríamos que escrever:

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

Os dois TweenAnimationBuilders são necessários para respeitar o fato de que ambos os contadores podem ser animados individualmente. E os dois Observer são necessários porque os primeiros Observer não podem observar counters.secondCounter


Não apenas não. Seus widgets de gancho pesquisam constantemente por mudanças em cada construção. Construtores baseados em listas de valores são "reativos".

Você está ignorando o que Element faz, que por acaso é a mesma coisa que os ganchos: comparando runtimeType e keys, e decidindo se deve criar um novo Elemento ou atualizar o existente

Peguei seu exemplo e "lidei" com ValueListenableBuilders * aninhados e construtores de interpolação

Supondo que o problema com a contagem total seja corrigido, qual aninhamento foi removido?

Observer(
  builder: (context) => TweenAnimationBuilder<int>(
    duration: const Duration(seconds: 5),
    curve: Curves.easeOut,
    tween: IntTween(end: counters.firstCounter),
    builder: (context, value, child) {
      return Text('$value');
    },
  ),
),

não é diferente de:

ValueListenableBuilder<int>(
  valueListenable: firstCounter,
  builder: (context, value, child) => TweenAnimationBuilder<int>(
    duration: const Duration(seconds: 5),
    curve: Curves.easeOut,
    tween: IntTween(end: value),
    builder: (context, value, child) {
      return Text('$value');
    },
  ),
),

em termos de aninhamento.

Se você está se referindo à sua essência, como mencionei antes, essa abordagem quebra TickerProvider / TickerMode . O vsync precisa ser obtido usando SingleTickerProviderClientStateMixin ou não suporta a lógica de muting, o que pode causar problemas de desempenho.
Expliquei isso em um artigo meu: https://dash-overflow.net/articles/why_vsync/

E com essa abordagem, temos que reimplementar a lógica Tween em cada local que originalmente desejaria um TweenAnimationBuilder. Isso leva a uma duplicação significativa, especialmente considerando que a lógica não é tão trivial

Com os dois trechos que forneci, você poderia incrementar os dois contadores ao mesmo tempo e, durante cada quadro da animação, o "total" estaria correto. Mesmo quando o segundo contador para de animar enquanto o primeiro contador ainda está animando

Por outro lado, sua implementação não considera este caso, porque fundiu os 2 TweenAnimationBuilders em um.

Sim, essa é uma troca que eu estava disposto a fazer. Eu poderia facilmente imaginar um caso em que a animação seria apenas um feedback visual para mudanças acontecendo e a precisão não é importante. Tudo depende dos requisitos.

Suspeitei que você faria objeções, portanto, a segunda versão que resolve esse problema exato, enquanto torna o código ainda mais limpo.

Se você está se referindo à sua essência, como mencionei antes, essa abordagem quebra TickerProvider / TickerMode. O vsync precisa ser obtido usando SingleTickerProviderClientStateMixin ou não oferece suporte à lógica de muting, o que pode causar problemas de desempenho.

Então você cria o tickerprovider no widget e o passa para os contadores. Eu também não descartei os controladores de animação. Esses detalhes são tão triviais de implementar que não achei que eles fossem acrescentar nada ao exemplo. Mas aqui estamos criticando-os.

Implementei a classe Counter () para fazer o que seu exemplo faz e nada mais.

E com essa abordagem, temos que reimplementar a lógica Tween em cada local que originalmente desejaria um TweenAnimationBuilder. Isso leva a uma duplicação significativa, especialmente considerando que a lógica não é tão trivial

O que? porque? Por favor, explique, por que não pude criar mais de uma instância da classe Counter e usá-los em vários widgets?

@Rudiksz Não tenho certeza se suas soluções estão realmente resolvendo os problemas apresentados. Você diz

Implementei a classe Counter () para fazer o que seu exemplo faz e nada mais.

e ainda

Sim, essa é uma troca que eu estava disposto a fazer. Eu poderia facilmente imaginar um caso em que a animação seria apenas um feedback visual para mudanças acontecendo e a precisão não é importante. Tudo depende dos requisitos.

Então você cria o tickerprovider no widget e o passa para os contadores. Eu também não descartei os controladores de animação. Esses detalhes são tão triviais de implementar que não achei que eles fossem acrescentar nada ao exemplo. Mas aqui estamos criticando-os.

Você forneceu um código que é apenas ostensivamente equivalente à versão de gancho de @rrousselGit , mas não é realmente equivalente, já que você fazermos o repositório de

Você claramente nem tentou executar o exemplo. Ele se comporta exatamente como seu código.

Não apenas não.

Você deve estar brincando. Peguei seu exemplo e "lidei" com ValueListenableBuilders * aninhados e construtores de interpolação

Por favor, evite acusações como essas, elas só servem para tornar este fio cáustico sem resolver os problemas subjacentes. Você pode apresentar seus pontos de vista sem ser acusatório, depreciativo ou zangado com a outra parte, que é o efeito dos comentários que vejo neste tópico, não vejo tal comportamento de outras pessoas em relação a você. Também não tenho certeza de qual é o efeito de reagir emoji à sua própria postagem.

@rrousselGit

Posso ajudá-lo a fazer exemplos com base nos snippets vinculados, mas preciso saber por que esses snippets não eram bons o suficiente
Caso contrário, a única coisa que posso fazer é compilar esses snippets, mas duvido que seja o que você deseja.

O motivo pelo qual estou pedindo um aplicativo mais elaborado do que apenas um snippet é que, quando postei um exemplo que tratou de um de seus snippets, você disse que não era bom o suficiente porque também não tratava de outro caso que não era não está em seu snippet (por exemplo, não manipulou didUpdateWidget, ou não lidou com dois desses snippets lado a lado, ou outras coisas perfeitamente razoáveis ​​que você gostaria que fossem tratadas). Espero que, com um aplicativo mais elaborado, possamos torná-lo elaborado o suficiente para que, uma vez que tenhamos uma solução, não haja nenhum momento de "pegadinha" em que algum novo problema que precise ser tratado também seja apresentado. Obviamente, ainda é possível que todos percamos algo que precisa ser resolvido, mas a ideia é minimizar a chance.

Com relação às postagens recentes menos que totalmente acolhedoras, por favor, pessoal, vamos nos concentrar apenas na criação de exemplos no repositório do

Você pode enviar sua solução lá se achar que atendeu a todos os requisitos.

Os requisitos foram alterados após o fato. Eu forneci duas soluções. Um que faz um compromisso (muito razoável) e um que implementa o comportamento exato do exemplo fornecido.

Você forneceu um código que é apenas ostensivamente equivalente à versão de gancho de @rrousselGit , mas não é realmente equivalente,

Eu não implementei a "solução de ganchos", implementei o exemplo ValueListenableBuilder, focando especificamente no "problema de aninhamento". Ele não faz todas as coisas que os ganchos fazem, eu simplesmente mostrei como um item na lista com marcadores de queixas pode ser simplificado usando uma solução alternativa.

Se você tem permissão para trazer pacotes externos para a discussão, então eu também.

Reutilização: dê uma olhada no exemplo no repo abaixo
https://github.com/Rudiksz/cbl_example

Observação:

  • é uma demonstração de como você pode encapsular a "lógica" de widgets externos e ter widgets que são enxutos, com aparência quase html
  • não cobre tudo que cobre os ganchos. Essa não é a questão. Achei que estivéssemos discutindo alternativas para a estrutura padrão do Flutter, e não o pacote de ganchos.
  • Não cobre _todos_ os casos de uso que toda equipe de marketing pode apresentar
  • mas, o próprio objeto Counter é bastante flexível, ele pode ser usado autônomo (veja o título AppBar), como parte de um widget complexo que precisa calcular totais de contadores diferentes, ser reativo ou responder às entradas do usuário.
  • Cabe aos widgets consumidores personalizar a quantidade, valor inicial, duração, tipo de animação dos contadores que desejam usar.
  • os detalhes da maneira como as animações são tratadas podem ter desvantagens. Se a equipe do Flutter disser que sim, usar controladores e interpolações de animação fora dos widgets, de alguma forma quebra a estrutura, vou reconsiderar. Definitivamente, há coisas a melhorar. Substituir o tickerprovider personalizado que uso por um criado pelo mixin do widget de consumo é trivial. Eu não fiz isso.
  • Esta é apenas mais uma solução alternativa para o padrão "Builder". Se você disser que precisa ou deseja usar Builders, nada disso se aplica.
  • Ainda assim, o fato é que existem maneiras de simplificar o código, sem recursos extras. Se você não gosta, não compre. Não estou defendendo nada disso para fazer parte da estrutura.

Editar: Este exemplo tem um bug, se você iniciar um "incremento" enquanto a mudança é animada, o contador é zerado e incrementado a partir do valor atual. Não resolvi de propósito, porque não sei os requisitos exatos que você pode ter para esses "contadores". Novamente, é trivial alterar os métodos de incremento / decremento para corrigir esse problema.

Isso é tudo.

@Hixie Devo interpretar seu comentário como uma forma de dizer que meu exemplo (https://github.com/TimWhiting/local_widget_state_approaches/blob/master/lib/hooks/animated_counter.dart) não é bom o suficiente?

Além disso, poderíamos ter uma teleconferência do Zoom / Google?

Também não tenho certeza de qual é o efeito de reagir emoji à sua própria postagem.

Nada. É totalmente irrelevante para nada. Por que você trouxe isso à tona?

@rrousselGit Só você pode saber se é bom o suficiente. Se encontrarmos uma maneira de refatorar esse exemplo para que ele seja limpo e curto e não tenha código duplicado, você ficará satisfeito? Ou há coisas que você acha que devemos oferecer suporte que não são tratadas por aquele exemplo que precisamos tratar para satisfazer esse bug?

Só você pode saber se é bom o suficiente

Eu não posso ser o juiz disso. Para começar, não acredito que possamos capturar o problema usando um conjunto finito de aplicativos.

Não me importo de trabalhar como você quer, mas preciso de orientação, pois não entendo como isso nos fará progredir.

Acho que fornecemos muitos trechos de código que mostram o problema de nossa perspectiva. Eu realmente não acho que ficará mais claro com mais exemplos de código, se os mostrados não estiverem fazendo isso.

Por exemplo, se ver vários construtores aninhados que são horríveis de ler, ou 50 linhas de puro clichê que tem muitas oportunidades para bugs, não demonstrar o problema com força suficiente, não há para onde ir.

É muito estranho oferecer mixins e funções são uma solução aqui, quando todo o ask está encapsulado , é reutilizável. As funções não podem manter o estado. Mixins não são encapsulados. Esta sugestão perde todo o ponto de todos os exemplos e argumentos fornecidos e ainda mostra um profundo mal-entendido da pergunta.

Para mim, acho que batemos 2 pontos na terra, e não acho que possamos discutir seriamente com qualquer um deles.

  1. Construtores aninhados são inerentemente difíceis de ler
  2. Além dos construtores aninhados, _não há maneira_ de encapsular e compartilhar o estado que possui ganchos de ciclo de vida do widget.

Como afirmado muitas vezes, e até mesmo na enquete de Remi, simplesmente coloque: Queremos recursos do construtor, sem o detalhamento e os fechamentos aninhados do construtor. Isso não resume tudo? Estou totalmente confuso por que um outro exemplo de código é necessário para prosseguir aqui.

A pesquisa de Remi nos mostra que cerca de 80% dos desenvolvedores do Flutter preferem algum tipo de habilidade para evitar construtores aninhados em seu código, quando possível. Isso realmente fala por si mesmo. Você não precisa tirar isso de nós neste tópico, quando o sentimento da comunidade é tão claro.

Do meu ponto de vista, os problemas são claros, e eles ficam ainda mais claros quando você olha para estruturas concorrentes que dedicam parágrafos à descrição do raciocínio aqui. Vue, React, Flutter são todos primos, todos são derivados do React e todos enfrentam esse problema com o estado de reutilização que deve ser vinculado ao ciclo de vida do widget. Todos eles descrevem em detalhes por que implementaram algo assim. Está tudo bem aí. É tudo relevante.

@rrousselGit você poderia dar um exemplo de ter muitos ganchos múltiplos? Por exemplo, estou fazendo uma animação com possivelmente dezenas de AnimationControllers. Com o Flutter normal, posso fazer:

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

Mas com ganchos não posso chamar useAnimationController em um loop. Suponho que seja um exemplo trivial, mas não consegui encontrar a solução em lugar nenhum para esse tipo de caso de uso.

@satvikpendem

alguns exemplos de meus aplicativos que estão em produção (alguns dos ganchos, como enviar solicitação com paginação, podem mesclar / refatorar em um único gancho, mas isso é irrelevante aqui):

busca de dados simples com paginação:

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

lógica do formulário (formulário de login com verificação do número de telefone e temporizador de reenvio):

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

para animação, acho que @rrousselGit já forneceu um exemplo suficiente.

Não quero falar sobre como a natureza combinável dos ganchos pode tornar a refatoração acima do código muito mais fácil, reutilizável e mais limpa, mas se você quiser, posso postar versões refatoradas também.

Como afirmado muitas vezes, e até mesmo na enquete de Remi, simplesmente coloque: Queremos recursos do construtor, sem o detalhamento e os fechamentos aninhados do construtor. Isso não resume tudo? Estou totalmente confuso por que um outro exemplo de código é necessário para prosseguir aqui.

Eu literalmente forneci exemplos de como você pode reduzir o detalhamento e evitar o aninhamento de construtores usando um exemplo fornecido por Remi.
Peguei seu código, coloquei em meu aplicativo, executei e reescrevi. O resultado final, no que diz respeito à funcionalidade, foi quase idêntico - tanto quanto pude deduzir apenas executando o código, porque o código não veio com requisitos. Claro que podemos discutir os casos extremos e problemas potenciais, mas em vez disso foi chamado de fora do tópico.

Para casos de uso simples, eu uso Builders, para casos de uso complexos, não uso Builders. O argumento aqui é que, sem usar Builders, não há maneira fácil de escrever código conciso e reutilizável. Implicitamente, isso também significa que Builders são essenciais e a única maneira de desenvolver aplicativos Flutter. Isso é comprovadamente falso.

Acabei de mostrar um código funcional de prova de conceito que o demonstra. Ele não usa Builders ou ganchos e não cobre 100% do "conjunto infinito de problemas" que esse problema específico do github parece querer resolver. Foi chamado de fora do tópico.
Sidenote, também é muito eficiente, mesmo sem qualquer benchmark, acho que até bate os widgets do Builder. Estou feliz em mudar de ideia se for provado que estou errado, e se algum dia eu achar que o Mobx se torna um gargalo de desempenho, deixarei o Mobx e mudarei para os construtores vanilla em um piscar de olhos.

Hixie trabalha para o Google, ele tem que ser paciente e educado com você e não pode reclamar de sua falta de engajamento. O melhor que ele pode fazer é forçar mais exemplos.

Eu não xinguei ninguém, nem fiz ataques pessoais. Eu só reagi aos argumentos aqui apresentados, compartilhei minha opinião
(que eu sei que é impopular e em minoria) e até tentei apresentar contra-exemplos reais com código. Eu poderia fazer mais, estou disposto a discutir onde meus exemplos falham e ver maneiras de melhorá-los, mas sim, ser chamado de fora do tópico é meio desagradável.

Não tenho nada a perder, a não ser talvez ser banido, então não me importo em chamar você para fora.
É evidente que vocês dois estão prontos que os ganchos são a única solução ("porque React faz") para quaisquer problemas que você experimenta e que, a menos que uma alternativa preencha 100% do "conjunto infinito de problemas" que você está imaginando, você nem vai pensar em se envolver.

Isso não é razoável e mostra falta de desejo de realmente se envolver.


Claro, tudo acima "é apenas minha opinião".

Vejo a utilidade dos ganchos nesse exemplo, mas acho que não entendo como funcionaria no meu caso, onde parece que você gostaria de inicializar muitos objetos de uma vez, neste caso AnimationController s, mas na realidade, pode ser qualquer coisa. Como os ganchos lidam com este caso?

Basicamente, existe uma maneira de enganchar

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

Em

var xs = []
for (int i = 0; i < 3; i++)
     xs[i] = useState(i);

Sem violar as regras do gancho? Porque eu listei o equivalente em Flutter normal. Não tenho muita experiência com ganchos no Flutter, então tenha paciência comigo.

Eu quero simplesmente criar uma matriz de objetos de gancho (AnimationControllers, por exemplo) sob demanda com todos os seus initState e dispose já instanciados, só não tenho certeza de como funciona em ganchos.

@satvikpendem pense em ganchos como propriedades em uma classe. você os define em um loop ou os nomeia manualmente um por um?

em seu exemplo definindo assim

var x1 = useState(1);
var x2 = useState(2);
var x3 = useState(3);

é útil para este caso de uso:

var isLoading = useState(1);
var selectedTab = useState(2);
var username = useState(3); // text field

você vê como cada useState está relacionado a uma parte nomeada de sua lógica de estado? (como o useState de isLoading está conectado quando o aplicativo está em estado de carregamento)

em seu segundo snippet, você está chamando useState em um loop. você está pensando em useState como um detentor de valor que não faz parte da sua lógica de estado. esta lista é necessária para mostrar um monte de itens em um ListView ? em caso afirmativo, você deve pensar em cada item da lista como um estado e não individualmente.

final listData = useState([]);

isso é apenas para useState e posso ver alguns casos de uso (que eu acho que são muito raros) para chamar alguns ganchos em uma condição ou em um loop. para esses tipos de ganchos, deve haver outro gancho para lidar com a lista de dados em vez de um. por exemplo:

var single = useTest("data");
var list = useTests(["data1", "data2"]);
// which is equivalent to
var single1 = useTest("data1");
var single2 = useTest("data2");

Entendo, então, com ganchos, parece que precisamos criar um gancho separado para lidar com casos com uma matriz de itens, como vários AnimationControllers.

Isso é o que eu tinha inicialmente e não parece funcionar:

  final animationControllers = useState<List<AnimationController>>([]);

  animationControllers.value = List<AnimationController>.generate(
    50,
    (_) => useAnimationController(),
  );

mas suponho que se eu escrever meu próprio gancho para lidar com vários itens, isso deve funcionar, certo?

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

Isso significa que, se tivermos uma versão singular de um gancho, simplesmente não podemos usá-la para uma versão com vários itens e, em vez disso, teremos que reescrever a lógica? Ou existe uma maneira melhor de fazer isso?

Se alguém também quiser dar um exemplo de não-ganchos que eu gostaria de saber também, estive pensando sobre esta peça do quebra-cabeça da reutilização. Talvez haja uma maneira de encapsular esse comportamento em uma classe, que tem seu próprio campo AnimationController, mas se isso for criado dentro de um loop, o gancho também seria, quebrando as regras. Talvez pudéssemos considerar como o Vue faz isso, o que não é afetado por condicionais e loops para sua implementação de ganchos.

@satvikpendem

Não acho que minha declaração seja válida para AnimationController ou useAnimationController

porque embora você possa ter mais de um AnimationController você não necessariamente os armazena em um array para usá-los no método de classe. por exemplo:

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

(você não cria uma lista e faz referência a eles como animation[0] )

honestamente, em minha experiência em reagir e agitar com ganchos, raramente precisei chamar algum tipo de ganchos em um loop. mesmo assim, a solução era direta e fácil de implementar. agora eu penso sobre isso, definitivamente poderia ser resolvido de uma maneira melhor criando um componente (widget) para cada um deles que é IMO é a solução "mais limpa".

para responder à sua pergunta se existe uma maneira mais fácil de lidar com vários AnimationController , sim, há:

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

  • você também pode usar useState se AnimationController s forem dinâmicos.

(também se sincroniza novamente quando o ticker é alterado)

@rrousselGit você poderia dar um exemplo de ter muitos ganchos múltiplos? Por exemplo, estou fazendo uma animação com possivelmente dezenas de AnimationControllers. Com o Flutter normal, posso fazer:

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

Mas com ganchos não posso chamar useAnimationController em um loop. Suponho que seja um exemplo trivial, mas não consegui encontrar a solução em lugar nenhum para esse tipo de caso de uso.

Os ganchos fazem isso de maneira diferente.

Não criamos mais uma lista de controladores, em vez disso, movemos a lógica do controlador para o item:

Widget build(context) {
  return ListView(
    children: [
      for (var i = 0; i < 50; i++)
        HookBuilder(
          builder: (context) {
            final controller = useAnimationController();
          },
        ),
    ],
  );
}

Ainda criamos nossos 50 controladores de animação, mas eles pertencem a um widget diferente.

Talvez você possa compartilhar um exemplo de por que você precisava disso, e poderíamos tentar converter para ganchos e adicioná-lo ao repositório de Tim?

Hixie trabalha para o Google, ele tem que ser paciente e educado com você e não pode reclamar de sua falta de engajamento. O melhor que ele pode fazer é forçar mais exemplos.

@Hixie , se é assim que você se sente, por favor, diga (aqui ou entre em contato comigo em particular).

Eu literalmente forneci exemplos de como você pode reduzir o detalhamento e evitar o aninhamento de construtores usando um exemplo fornecido por Remi.

Obrigado, mas não está claro para mim como você extrairia um padrão comum desse código ao aplicar essa lógica a diferentes casos de uso.

No OP, mencionei que atualmente temos 3 opções:

  • usar Builders e ter código aninhado
  • não fatorar o código de forma alguma, que não é escalonado para uma lógica de estado mais complexa (eu diria que StreamBuilder e seu AsyncSnapshot é uma lógica de estado complexa).
  • tente fazer alguma arquitetura usando mixins / oop / ..., mas termine com uma solução muito específica para o problema que qualquer caso de uso que seja _tiny_ um pouco diferente exigirá uma reescrita.

Parece-me que você usou a terceira escolha (que está na mesma categoria das iterações anteriores das propostas Property ou addDispose ).

Eu já fiz uma grade de avaliação para julgar o padrão:

Você poderia executar sua variante nisso? Especialmente o segundo comentário sobre a implementação de todos os recursos de StreamBuilder sem duplicar o código se usado várias vezes.

Meu plano neste ponto sobre este bug é:

  1. Pegue os exemplos de https://github.com/flutter/flutter/issues/51752#issuecomment -675285066 e crie um aplicativo usando Flutter puro que mostra esses vários casos de uso juntos.
  2. Tente projetar uma solução que permita a reutilização de código para aqueles exemplos que satisfaça os vários requisitos-chave que foram discutidos aqui e que se encaixe em nossos princípios de design.

Se alguém quiser ajudar em alguma dessas coisas, estou definitivamente feliz em ter ajuda. É improvável que chegue nisso logo porque estou trabalhando primeiro na transição do NNBD.

@rrousselGit Claro, estou fazendo um aplicativo em que muitos widgets podem se mover pela tela (vamos chamá-los de Box es), e eles devem ser capazes de se mover independentemente um do outro (então deve haver pelo menos um AnimationController para cada Box ). Aqui está uma versão que fiz com apenas um AnimationController compartilhado entre os vários Widgets, mas no futuro posso animar cada widget independentemente, por exemplo, para fazer transformações complicadas, como implementar um CupertinoPicker , com seu efeito de roda de rolagem personalizado .

Existem três caixas em uma pilha que se movem para cima e para baixo quando você clica em um FloatingActionButton.

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

void main(List<String> args) => runApp(const App());

class App extends HookWidget {
  const App({Key key});

  static const Duration duration = Duration(milliseconds: 500);
  static const Curve curve = Curves.easeOutBack;

  <strong i="11">@override</strong>
  Widget build(BuildContext context) {
    final AnimationController controller =
        useAnimationController(duration: duration);
    final Animation<double> animation = Tween<double>(
      begin: 0,
      end: 300,
    )
        .chain(
          CurveTween(
            curve: curve,
          ),
        )
        .animate(controller);
    final ValueNotifier<bool> isDown = useState<bool>(false);
    final ValueNotifier<int> numBoxes = useState<int>(3);

    return MaterialApp(
      home: SafeArea(
        child: Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              if (!isDown.value) {
                controller.forward();
                isDown.value = true;
              } else {
                controller.reverse();
                isDown.value = false;
              }
            },
          ),
          body: AnimatedBuilder(
            animation: animation,
            builder: (_, __) => Boxes(
              numBoxes: numBoxes.value,
              animation: animation,
            ),
          ),
        ),
      ),
    );
  }
}

class Boxes extends StatelessWidget {
  Boxes({
    <strong i="12">@required</strong> this.numBoxes,
    <strong i="13">@required</strong> this.animation,
  });

  final int numBoxes;
  final Animation<double> animation;

  <strong i="14">@override</strong>
  Widget build(BuildContext context) {
    return Stack(
      children: List<Widget>.generate(
        numBoxes,
        (int index) => Positioned(
          top: (animation.value) + (index * (100 + 10)),
          left: (MediaQuery.of(context).size.width - 100) / 2,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
        ),
      ),
      // ],
    );
  }
}

Neste caso, cada caixa se move em uníssono, mas pode-se imaginar um cenário mais complexo, como criar uma visualização para uma função de classificação, por exemplo, ou mover elementos em uma lista animada, onde o widget pai conhece os dados sobre onde cada Box deve ser e deve ser capaz de animar cada um como achar melhor.

O problema parece ser que os AnimationControllers e os Box es que os usam para impulsionar seus movimentos não estão na mesma classe, portanto, seria necessário passar pelo AnimationController mantendo uma matriz deles para usar em um Builder, ou cada Box mantém seu próprio AnimationController.

Com ganchos, dado que Box es e o widget pai não estão na mesma classe, como eu faria uma lista de AnimationControllers para o primeiro caso em que cada Box é passado em um AnimationController? Isso parece não ser necessário com base na sua resposta acima com o HookBuilder, mas se eu mover o estado para o widget filho como você disse e escolher fazer com que cada Box tenha seu próprio Controlador de Animação via useAnimationController , Me deparo com outro problema: como exporia o AnimationController criado à classe pai para que ele coordenasse e execute as animações independentes para cada filho?

No Vue, você pode emitir um evento de volta para o pai através do padrão emit , então no Flutter eu preciso de alguma solução de gerenciamento de estado superior como Riverpod ou Rx onde o pai atualiza o estado global e o filho ouve o global Estado? Parece que não deveria, pelo menos para um exemplo simples como este. Obrigado por esclarecer minhas confusões.

@satvikpendem Desculpe, não fui claro. Você poderia mostrar como faria sem ganchos, em vez do problema em que está bloqueando com ganchos?

Eu quero ter uma compreensão clara do que você está tentando fazer, ao invés de onde você está travando

Mas, como um palpite rápido, acho que você está procurando a curva de intervalo e tem um único controlador de animação.

@rrousselGit Claro, aqui está

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

Na verdade, quero vários controladores de animação, um para cada widget, pois eles podem se mover independentemente uns dos outros, com suas próprias durações, curvas, etc. Observe que o código acima parece ter um bug que eu não consegui descobrir, onde deve animar de forma limpa, mas basicamente deve animar 3 caixas para cima e para baixo com um clique de botão. Podemos imaginar um cenário onde ao invés de cada um ter a mesma curva, eu dou a cada um deles uma curva diferente, ou faço 100 caixas, cada uma com uma duração maior ou menor que a anterior, ou faço as pares subirem e os ímpares diminuem, e assim por diante.

Com o Flutter normal, initState e dispose podem ter loops, mas não é o que parece com ganchos, então estou me perguntando como se pode combater isso. Da mesma forma, não quero colocar a classe Box dentro do widget pai, pois não quero encapsular ambos; Devo ser capaz de manter a lógica pai a mesma, mas trocar Box por Box2 por exemplo.

Obrigado!
Eu empurrei seu exemplo para o repositório de

TL; DR, com ganchos (ou construtores), pensamos declarativamente em vez de imperativamente. Então, em vez de ter uma lista de controladores em um widget, direcioná-los imperativamente - o que move o controlador para o item e implementa uma animação implícita.

Obrigado @rrousselGit! Eu estava lutando com esse tipo de implementação por um tempo depois de começar a usar ganchos, mas agora entendo como funciona. Acabei de abrir um PR para uma versão com um alvo diferente para cada controlador de animação, pois isso pode ser mais atraente para entender por que os ganchos são úteis como eu disse acima:

Podemos imaginar um cenário onde ao invés de cada um ter a mesma curva, eu dou a cada um deles uma curva diferente, ou faço 100 caixas, cada uma com uma duração maior ou menor que a anterior, ou faço as pares subirem e os ímpares diminuem, e assim por diante.

Eu estava tentando fazer a versão declarativa, mas suponho que não entendi o método didUpdateWidget/Hook lifecycle, então não sabia como conduzir a animação quando um adereço filho é alterado do pai, mas seu código o esclareceu.

Encontrei um exemplo do mundo real em minha base de código hoje, então achei melhor compartilhá-lo.

Portanto, neste cenário, estou trabalhando com o Firestore e tenho alguns clichês que desejo executar com cada StreamBuilder, então fiz meu próprio construtor personalizado. Eu também preciso trabalhar com um ValueListenableque permite ao usuário reordenar a lista. Por motivos de custo monetário relacionados ao Firestore, isso requer uma implementação muito específica (cada item não pode armazenar seu próprio pedido, em vez disso, a lista deve salvá-lo como um campo de IDs concatenados), isso porque o Firestore cobra por cada gravação, você pode potencialmente economizar muito dinheiro dessa maneira. Acaba lendo algo assim:

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

Parece que seria muito mais fácil raciocinar sobre, se eu pudesse escrever mais como:

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

Isso perde a otimização em relação às reconstruções granulares, mas isso não faria qualquer IRL diferente, uma vez que todos os elementos visuais estão no nó folha mais inferior, todos os wrappers são de estado puro.

Tal como acontece com muitos cenários do mundo real, o conselho de "Apenas não use X" não é realista, já que o Firebase tem apenas um método de conexão que é Streams, e sempre que eu quiser esse comportamento de soquete, não tenho escolha a não ser use um Stream. É a vida.

Isso perde a otimização em relação às reconstruções granulares, mas isso não faria qualquer IRL diferente, uma vez que todos os elementos visuais estão no nó folha mais inferior, todos os wrappers são de estado puro.

Ainda faz diferença. O fato de um nó ser visual ou não não afeta o fato de custar algo para reconstruí-lo.

Provavelmente, eu fatoraria esse exemplo em diferentes widgets (os IDEs têm ferramentas de refatoração de um clique para tornar isso realmente fácil). _buildItemList provavelmente deve ser um widget, assim como a parte com raiz em FamilyStreamBuilder .

Nós realmente não perdemos a reconstrução granular.
Na verdade, os ganchos melhoram esse aspecto, permitindo armazenar facilmente em cache a instância do widget usando useMemoized .

Existem alguns exemplos no repo da Tim que fazem isso.

Provavelmente, eu fatoraria esse exemplo em diferentes widgets (os IDEs têm ferramentas de refatoração de um clique para tornar isso realmente fácil). _buildItemList provavelmente deve ser um widget, assim como a parte com raiz em FamilyStreamBuilder .

O fato é que eu realmente não quero fazer isso, porque não tenho nenhuma preocupação com o desempenho nesta exibição. Portanto, 100% das vezes, vou favorecer a localidade e a coerência do código em vez da microotimização como esta. Essa visualização só é reconstruída quando o usuário inicia a ação (~ média uma vez a cada 10 segundos) ou quando os dados de back-end são alterados e eles estão em uma lista aberta (quase nunca acontece). Também acontece de ser apenas uma visualização simples que é principalmente uma lista, e a lista tem uma tonelada de suas próprias otimizações acontecendo internamente. Eu sei que build () pode tecnicamente disparar a qualquer momento, mas na prática qualquer reconstrução aleatória é muito rara.

imo é significativamente mais fácil trabalhar e depurar essa visualização se toda essa lógica estiver agrupada em um widget, principalmente como um esforço para tornar minha vida mais fácil quando eu voltar a ela no futuro :)

Outra coisa a notar é que o aninhamento basicamente "me forçou a sair" do método de construção, já que não havia como começar a construir minha árvore dentro de 3 fechamentos e 16 espaços no buraco.

E sim, você poderia dizer que faz sentido apenas mover para um widget separado. Mas por que não permanecer no método de construção? Se pudéssemos reduzir o clichê ao que realmente _precisa_ ser, então não há necessidade de ter o trabalho de legibilidade e manutenção de dividir as coisas em 2 arquivos. (Supondo que o desempenho não seja uma preocupação, o que geralmente não é)

Lembre-se que neste cenário eu já criei um widget customizado para lidar com meu Stream Builder. Agora precisaria fazer outro para lidar com a composição desses construtores ?? Parece um pouco exagerado.

porque não tenho nenhuma preocupação com o desempenho nesta visualização

Oh, eu não iria refatorá-lo em widgets para desempenho, os construtores já deveriam cuidar disso. Eu iria refatorá-lo para facilitar a leitura e reutilização. Não estou dizendo que essa é a maneira "certa" de fazer isso, apenas estou dizendo como estruturaria o código. De qualquer forma, isso não é aqui nem ali.

de jeito nenhum eu poderia começar a construir minha árvore dentro de 3 fechamentos e 16 espaços no buraco

Posso ter um monitor mais largo do que você ...: - /

então não há necessidade de ter o incômodo de legibilidade e manutenção de dividir as coisas em 2 arquivos

Eu colocaria os widgets no mesmo arquivo, FWIW.

Enfim, é um bom exemplo, e acredito que você prefira usar um widget com uma sintaxe diferente do que usar mais widgets.

Posso ter um monitor mais largo do que você ...: - /

Eu tenho um ultrawide: D, mas o dartfmt obviamente limita todos nós a 80. Portanto, perder 16 é significativo. O principal problema é que o fim da minha declaração é que },);});},),),); não é realmente divertido quando algo fica bagunçado. Tenho que ser extremamente cuidadoso sempre que edito essa hierarquia, e ajudantes IDE comuns, como swap with parent param de funcionar.

Eu colocaria os widgets no mesmo arquivo, FWIW.

100%, mas ainda acho que pular verticalmente em um único arquivo é mais difícil de manter. Inevitável, claro, mas tentamos reduzir quando possível e "manter as coisas juntas".

Porém, crucialmente, mesmo se eu refatorar a lista principal em seu próprio widget (que eu concordo, é mais legível do que um método de construção aninhado), ainda é muito mais legível sem o aninhamento no widget pai. Posso entrar, entender toda a lógica em uma rápida olhada, ver o widget _MyListView () claramente e pular direto para ele, confiante de que entendo o contexto ao redor. Também posso adicionar / remover dependências adicionais com relativa facilidade, portanto, é muito bem escalável.

dartfmt obviamente limita todos nós a 80

Quer dizer, esse é um dos motivos pelos quais geralmente não uso o dartfmt e, quando o faço, o configuro para 120 ou 180 caracteres ...

Sua experiência aqui é totalmente válida.

Na verdade, eu também, 120 o dia todo :) Mas o pub.dev reduz ativamente os plug-ins que não são formatados em 80, e tenho a impressão de que eu (nós) estamos em minoria quando alteramos esse valor.

Bem, isso é um absurdo, devemos consertar isso.

pub.dev não reduz plug-ins que não respeitem o dartfmt. Mostra apenas um comentário na página de pontuação, mas a pontuação não é afetada
Mas, sem dúvida, há mais problemas com o dartfmt do que apenas o comprimento da linha.

Um comprimento de linha muito grande leva a coisas que são mais legíveis em várias linhas para estar em uma única linha, como:

object
  ..method()
  ..method2();

que pode se tornar:

object..method()..method2();

Estou vendo isso?
image
Pacote em questão: https://pub.dev/packages/sized_context/score

Interessante - definitivamente não era assim antes, já que o provedor não usava o dartfmt por um tempo.
Eu estou corrigido.

Sim, definitivamente é um comportamento novo, quando publiquei originalmente na primavera passada, certifiquei-me de que estava marcando todas as caixas, e dartfmt não era obrigatório.

Depois de todas essas discussões, espero que vejamos o suporte nativo para a solução tipo gancho no flutter. ou useHook ou use Hook ou qualquer coisa que a equipe vibrante possa sentir que sua característica não é como React 😁🤷‍♂️

usamos ganchos de uma forma como final controller = useAnimationController(duration: Duration(milliseconds: 800));
Não é melhor usar o novo recurso do programa Darts _Extension_ copiado de kotlin / swift para essa sintaxe?

algo como: final controller = AnimationController.use(duration: Duration(milliseconds: 800));
com esta abordagem, quando a equipe de flutter / dardo decide adicionar use Hook vez da sintaxe atualmente disponível useHook , acho que Annotation para essa função de extensão a tornou lida para ser usada como
final controller = use AnimationController(duration: Duration(milliseconds: 800));

também é compreensível / significativo ter use palavra-chave usada como const e new :
new Something
const Something
use Something

como um bônus a essa recomendação, acho que finalmente até as funções construtoras / geradoras podem usar / se beneficiar dos Annotation propostos. então o compilador dart com alguma personalização o converte para suportar a palavra-chave use .

Característica tão bonita e específica de vibração / dardo 😉

Estou correto ao presumir que os exemplos em https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful agora representam os problemas que as pessoas desejam resolver?

Não tenho certeza de como todo mundo se sente, mas acho que os problemas estão de alguma forma representados lá (o que significa que não posso ter certeza porque alguém pode apontar algo que não está representado).

Tentei uma solução intermediária nesse repositório. É composto como ganchos, mas não depende da ordem das chamadas de função ou da não permissão de loops, etc. Ele usa StatefulWidgets diretamente. Envolve um mixin, bem como propriedades com estado que são identificadas exclusivamente por chaves. Não estou tentando promover isso como a solução definitiva, mas como um meio-termo entre as duas abordagens.

Eu a chamei de abordagem lifecycleMixin, é muito próxima da abordagem LateProperty que foi discutida aqui, mas a principal diferença é que ela tem mais ciclos de vida implementados e pode ser facilmente redigida. (na parte de ciclos de vida, eu não usei ciclos de vida de widget além de initState e descarte muito, então eu posso ter bagunçado totalmente lá).

Eu gosto dessa abordagem porque:

  1. Tem muito pouca penalidade de tempo de execução.
  2. Não há lógica / funções criando ou gerenciando o estado no caminho de construção (as compilações podem ser puras - apenas o estado de busca).
  3. O gerenciamento do ciclo de vida é mais claro ao otimizar reconstruções por meio de um construtor. (Mas você não sacrifica a capacidade de reutilização e composição de pequenos pedaços de estado).
  4. Como você pode reutilizar a criação de bits de estado, uma biblioteca pode ser feita de bits comuns de estado que devem ser criados e descartados de certas maneiras, para que haja menos clichês em seu próprio código.

Não gosto dessa abordagem (em comparação com ganchos) pelos seguintes motivos:

  1. Não sei se cobre tudo o que os ganchos podem fazer.
  2. Você tem que usar as chaves para identificar exclusivamente as propriedades. (Portanto, ao compor as peças de lógica que constroem algum estado, você deve anexar à chave para identificar exclusivamente cada parte do estado - tornar a chave um parâmetro posicional obrigatório ajuda, mas eu adoraria uma solução de nível de linguagem para acessar um id único para uma variável).
  3. Ele usa extensivamente extensões para criar funções reutilizáveis ​​para criar bits comuns de estado. E as extensões não podem ser importadas automaticamente pelos IDEs.
  4. Você pode se confundir se misturar ciclos de vida de widgets diferentes / acessá-los entre widgets sem gerenciá-los explicitamente de maneira correta.
  5. A sintaxe do construtor é um pouco estranha, de modo que o estado criado está no escopo da função de construção, mas deixando a função de construção pura.
  6. Ainda não implementei todos os exemplos, então pode haver um caso de uso que não posso cobrir.

Contra- exemplo
Exemplo de contadores animados

estrutura
bits comuns de lógica de composição de estado reutilizável

Não tenho certeza de quanto tempo tenho, estudos de pós-graduação estão sempre me mantendo ocupado, mas adoraria algum feedback. @rrousselGit Quão perto isso é dos ganchos, você pode ver alguns buracos óbvios na capacidade de reutilização ou composição?

Não estou tentando promover minha solução, mas sim encorajar uma discussão positiva em um meio-termo. Se chegarmos a um acordo sobre o que está faltando ou o que essa solução nos oferece, acho que estaremos fazendo um bom progresso.

@TimWhiting O principal problema que tenho com essa abordagem é a falta de robustez. Um grande motivador aqui é a necessidade de confiabilidade dos construtores, de forma sucinta. O id mágico e a capacidade de colidir no ciclo de vida, ambos criam novos vetores para a ocorrência de bugs, e eu continuaria a recomendar à minha equipe que usem builders, pois apesar de serem muito desagradáveis ​​de ler, pelo menos sabemos que eles são 100 % livre de bugs.

Em relação aos exemplos, ainda acho que o exemplo perfeito é simplesmente usar um AnimationController, com um valor de duração vinculado ao widget. Mantém tudo simples e familiar. Não há necessidade de ser mais esotérico do que isso, é um pequeno caso de uso perfeito para clichês reutilizáveis, ele precisa de ganchos de ciclo de vida e todas as soluções podem ser facilmente julgadas por sua capacidade de usar várias animações de forma sucinta.

Todo o resto é apenas uma variação desse mesmo caso de uso de 'Stateful Controller'. Quero fazer X em initState e Y em estado dispose e atualizar Z quando minhas dependências mudarem. Não importa o que sejam X, Y e Z.

Eu me pergunto se @rrousselGit poderia fornecer algumas dicas aqui, ou se tem algum dado sobre quais ganchos são mais usados ​​atualmente. Suponho que seja 80% de fluxo e animações, mas seria bom saber o que as pessoas estão usando mais.

Com relação à reconstrução de partes da árvore, os construtores são naturalmente adequados para essa tarefa, devemos apenas deixá-los fazer isso. Um controlador com estado pode ser facilmente conectado a renderizadores sem estado de qualquer maneira, se isso for o que você quiser (olá, todas as classes de transição).

Assim como podemos fazer:

var anim = get AnimationController();
return Column(
  _someExpensiveBuildMethod(),
  FadeTransition(opacity: anim, child: ...)
)

Sempre poderíamos fazer:

var foo = get ComplicatedThingController();
return Column(
  _someExpensiveBuildMethod(),
  ComplicatedThing(controller: foo, child: ...)
)

@esDotDev Concordo, as chaves e a sintaxe do construtor são a principal desvantagem da abordagem do lifecycleMixin. Não sei se você pode contornar isso, exceto usando uma abordagem de estilo de ganchos com suas restrições associadas, ou uma mudança de linguagem em ser capaz de associar declarações de variáveis ​​com bits de estado com ciclos de vida. É por isso que continuarei usando ganchos e deixarei que outros usem widgets com estado, a menos que uma solução melhor apareça. No entanto, acho que é uma alternativa interessante para quem não gosta das restrições dos ganchos, embora venha com restrições próprias.

Estou correto ao presumir que os exemplos em https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful agora representam os problemas que as pessoas desejam resolver?

Sinceramente, não tenho certeza.
Eu diria _sim_. Mas isso realmente depende de como você interpretará esses exemplos.

Neste tópico, temos um histórico de não nos entendermos, então não posso garantir que isso não aconteça novamente.

Em parte, é por isso que não gosto de usar exemplos de código e sugeri extrair um conjunto de regras.
Os exemplos são subjetivos e têm várias soluções, algumas das quais podem não resolver o problema mais amplo.

Eu me pergunto se @rrousselGit poderia fornecer algumas dicas aqui, ou se tem algum dado sobre quais ganchos são mais usados ​​atualmente. Suponho que seja 80% de fluxo e animações, mas seria bom saber o que as pessoas estão usando mais.

Eu acho muito homogêneo.

Embora, se alguma coisa, useStream e animações sejam provavelmente os menos usados:

  • useStream geralmente tem um equivalente melhor dependendo de sua arquitetura. Poderia usar context.watch , useBloc , useProvider , ...
  • poucas pessoas se dão ao trabalho de fazer animações. Essa raramente é a prioridade, e TweenAnimationBuilder outros widgets animados implicitamente cobrem grande parte da necessidade.
    Talvez isso mudasse se eu adicionasse meus useImplicitlyAnimatedInt hooks em flutter_hooks.

@esDotDev Acabou de remover a necessidade de chaves / ids na abordagem do lifecycleMixin. Ainda é um pouco estranho na sintaxe do construtor. Mas possivelmente isso poderia ser ajudado eventualmente também. O único problema que estou encontrando é com o sistema de tipos. Ele tenta lançar coisas de certas maneiras que não estão funcionando. Mas provavelmente só precisa de um elenco cuidadoso ou domínio do sistema de tipos. No que diz respeito à mistura de ciclos de vida, acho que isso poderia ser melhorado lançando algumas exceções razoáveis ​​quando uma parte específica do estado que você tenta acessar não é acessível pelo ciclo de vida do widget. Ou um lint que, dentro de um construtor de ciclo de vida, você só deve acessar o ciclo de vida do construtor.

Obrigado Remi, isso me surpreende, acho que as pessoas usariam oAnimation com muita frequência para direcionar a grande coleção de widgets de Transição no núcleo, mas acho que a maioria das pessoas usa apenas os vários Implícitos, pois são muito agradáveis ​​de ler e não têm aninhamento .

Mesmo assim, apesar do AnimatorController ser muito bem servido com um conjunto de widgets implícitos e explícitos, eu ainda acho que é um ótimo exemplo de uma 'coisa que precisa manter o estado e se vincular aos parâmetros e ciclo de vida do widget'. E serve como um pequeno exemplo perfeito do problema a ser resolvido (o fato é que se resolve totalmente no Flutter c / como uma dúzia de widgets apesar de tudo), que todos podemos discutir e manter o foco na arquitetura e não no conteúdo.

Por exemplo, considere como, se var anim = AnimationController.use(context, duration: widget.duration ?? _duration); fosse um cidadão de primeira classe, virtualmente nenhuma dessas animações implícitas ou explícitas realmente precisaria existir. Ele os torna redundantes, pois todos são criados para gerenciar o problema central: compor facilmente uma coisa com estado (AnimationController) dentro do contexto de um widget. TAB torna-se quase inútil, já que você pode fazer a mesma coisa com AnimatedBuilder + AnimatorController.use() .

Isso realmente ilustra a necessidade do caso de uso geral, se você observar a enorme massa de widgets que surgiram em torno das animações. Precisamente porque é tão complicado / sujeito a bugs reutilizar a lógica de configuração / desmontagem do núcleo, temos mais de 15 widgets, todos lidando com coisas muito específicas, mas a maioria de cada um deles está repetindo o mesmo padrão de animação com apenas um punhado de linhas exclusivas de código em muitos casos.

Isso serve para mostrar que sim, nós também podemos fazer isso para reutilizar nossa própria lógica com estado: fazer um widget para cada permutação de uso. Mas que chatice e dor de cabeça de manutenção! Muito mais agradável ter uma maneira fácil de compor pequenos objetos com estado, com ganchos de ciclo de vida, e se quisermos fazer widgets dedicados para renderização ou um construtor reutilizável, podemos facilmente colocá-los em camadas.

Pelo que vale a pena, eu uso algo como useAnimation pesadamente em meu aplicativo, em vez dos widgets de animação normais. Isso ocorre porque estou usando um SpringAnimation que não é bem compatível com widgets como AnimatedContainer, por exemplo; todos eles assumem uma animação baseada no tempo, com curve e duration vez de animação baseada em simulação, que aceitaria um argumento Simulation .

Fiz uma abstração sobre useAnimation mas com molas, então a chamei de useSpringAnimation . O widget de wrapper com o qual usei esse gancho é semelhante a um AnimatedContainer mas era muito mais fácil de fazer porque eu poderia reutilizar todo o código de animação como você diz @esDotDev , já que grande parte da lógica é a mesma. Eu poderia até fazer minha própria versão de todos os widgets animados usando novamente useSpringAnimation mas não necessariamente para o meu projeto. Isso mostra mais uma vez o poder da reutilização da lógica do ciclo de vida que os ganchos fornecem.

Por exemplo, considere como, se var anim = AnimationController.use (contexto, duração: widget.duration ?? _duration); se fosse um cidadão de primeira classe, virtualmente nenhuma dessas animações implícitas ou explícitas realmente precisava existir. Ele os torna redundantes, pois todos são criados para gerenciar o problema central: compor facilmente uma coisa com estado (AnimationController) dentro do contexto de um widget. TAB torna-se quase inútil, já que você pode fazer a mesma coisa com AnimatedBuilder + AnimatorController.use ().

Lendo meus comentários acima, parece ser exatamente o que fiz com meu gancho de animação de primavera. Encapsulei a lógica e simplesmente usei o AnimatedBuilder. Para torná-los implícitos, de modo que quando eu alterasse o prop como no AnimatedContainer, ele se animasse, eu apenas adicionei o método didUpdateWidget (chamado didUpdateHook em flutter_hooks ) para execute a animação do valor antigo para o novo valor.

Estou correto ao presumir que os exemplos em https://github.com/TimWhiting/local_widget_state_approaches/tree/master/lib/stateful agora representam os problemas que as pessoas desejam resolver?

Sinceramente, não tenho certeza.
Eu diria _sim_. Mas isso realmente depende de como você interpretará esses exemplos.

Neste tópico, temos um histórico de não nos entendermos, então não posso garantir que isso não aconteça novamente.

Em parte, é por isso que não gosto de usar exemplos de código e sugeri extrair um conjunto de regras.
Os exemplos são subjetivos e têm várias soluções, algumas das quais podem não resolver o problema mais amplo.

Eu também diria que devemos incluir todos os exemplos de código que foram discutidos nesta edição, acho que há uma lista acima em algum lugar que @rrousselGit fez. Eu poderia fazer um PR adicionando-os ao repositório local_state, mas eles não são todos exemplos de código completos, então nem todos podem realmente ser compilados e executados. Mas eles mostram os problemas potenciais, pelo menos.

Eu poderia fazer um PR adicionando-os ao repositório local_state

Isso seria muito útil.

Eu gostaria de salientar que este segmento não definiu a reutilização ou como é a reutilização. Acho que devemos ser dolorosamente específicos ao definir isso, para que a conversa não perca o foco.

Apenas mostramos o que _não_ reutilizável no que se refere ao Flutter.

Houve alguns exemplos de uso, e os ganchos fornecem claramente um exemplo total de reutilização de estado de widget. Não tenho certeza de onde vem a confusão, pois parece simples em seu rosto.

A reutilização pode ser simplesmente definida como: _Qualquer coisa que um widget-construtor pode fazer._

O pedido é por algum objeto com estado que pode existir dentro de qualquer widget, que:

  • Encapsula seu próprio estado
  • Pode se configurar / desmontar de acordo com chamadas initState / dispose
  • Pode reagir quando as dependências mudam no widget

E faz isso de uma maneira sucinta, fácil de preparar e sem clichês, como:
AnimationController anim = AnimationController.stateful(duration: widget.duration);
Se isso funcionar em widgets sem estado e com estado. Se ele reconstruir quando widget. Algo muda, se ele pode executar seu próprio init () e dispose (), então você basicamente tem um vencedor e tenho certeza de que todos apreciariam.

A principal coisa com a qual estou lutando é como fazer isso de maneira eficiente. Por exemplo, ValueListenableBuilder aceita um argumento filho que pode ser usado para melhorar o desempenho de forma mensurável. Não vejo uma maneira de fazer isso com a abordagem de propriedade.

Tenho certeza de que isso não é um problema. Faríamos isso da mesma maneira que os widgets XTransition funcionam agora. Se eu tiver algum estado complexo e quiser que ele tenha um filho caro, eu faria apenas um pequeno widget de invólucro para ele. Assim como podemos fazer:
FadeTransition(opacity: anim, child: someChild)

Podemos fazer isso facilmente com qualquer coisa que quisermos renderizada, passando a 'coisa' em um Widget para renderizá-la novamente.
MyThingRenderer(value: thing, child: someChild)

  • Isso não _requer_ aninhamento como o builder faz, mas opcionalmente o suporta (.child pode ser um build fxn)
  • Ele mantém a capacidade de ser usado diretamente sem um widget de embalagem
  • Sempre podemos fazer um construtor e usar essa sintaxe dentro do construtor para mantê-lo mais limpo. Ele também abre a porta para vários tipos de construtores, construídos em torno do mesmo objeto principal, que não envolvem copiar código colado em todo o lugar.

Acordado com @esDotDev. Como mencionei anteriormente, um título alternativo para isso seria "Syntax sugar for Builders".

A principal coisa com a qual estou lutando é como fazer isso de maneira eficiente. Por exemplo, ValueListenableBuilder aceita um argumento filho que pode ser usado para melhorar o desempenho de forma mensurável. Não vejo uma maneira de fazer isso com a abordagem de propriedade.

Tenho certeza de que isso não é um problema. Faríamos isso da mesma maneira que os widgets XTransition funcionam agora. Se eu tiver algum estado complexo e quiser que ele tenha um filho caro, eu faria apenas um pequeno widget de invólucro para ele. Assim como podemos fazer:

Não há necessidade disso.
Um dos benefícios desse recurso é que podemos ter uma lógica de estado que é "armazenar em cache a instância do widget se seus parâmetros não mudarem".

Com ganchos, isso seria useMemo no React:

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

Com este código, myWidget reconstruirá _apenas_ quando value mudar. Mesmo se o widget que chama useMemo reconstruir por outros motivos.

Isso é semelhante a um construtor const para widgets, mas permite parâmetros dinâmicos.

Há um exemplo fazendo isso no repo de Tim.

O pedido é por algum objeto com estado que pode existir dentro de qualquer widget, que:

  • Encapsula seu próprio estado
  • Pode se configurar / desmontar de acordo com chamadas initState / dispose
  • Pode reagir quando as dependências mudam no widget

Acho que é difícil entender por que, por esses parâmetros, StatefulWidget não faz o trabalho melhor do que faz. É por isso que fiz a pergunta sobre o que realmente queremos aqui em uma solução. Como alguém que usa flutter_hooks , acho mais divertido trabalhar com eles do que StatefulWidget , mas isso é apenas para evitar verbosidade - não porque eu pense em termos de ganchos. Na verdade, acho difícil raciocinar sobre atualizações de interface do usuário com ganchos em comparação com Widget s.

  • Pode reagir quando as dependências mudam no widget

Você quer dizer uma dependência que foi criada / adquirida dentro do widget? Ou uma dependência muito abaixo do widget na árvore?

Não estou negando que há um problema que causa verbosidade / confusão no Flutter, apenas hesito em confiar que todos tenham o mesmo modelo mental do que é "reutilizar". Estou muito grato pela explicação; e quando as pessoas têm modelos diferentes, elas criam soluções diferentes.

Porque usar um SW para fazer isso é bom para um caso de uso específico, mas não é bom para abstrair a lógica reutilizável do caso de uso em muitos SWs. Veja a configuração / desmontagem da Animação como exemplo. Este não é um SW em si, é algo que queremos usar neles. Sem o suporte de primeira classe para compartilhar o estado encapsulado, você acaba tendo que fazer um construtor, ou seja, TweenAnimationBuilder, ou fazer uma tonelada de Widgets específicos, ou seja, AnimatedContainer etc. Realmente muito mais elegante se você puder apenas agrupar essa lógica e reutilizá-la da maneira que você quiser dentro de uma árvore.

Em termos de dependência de widget, quero dizer apenas que se widget.foo mudar, a coisa com estado terá a oportunidade de fazer qualquer atualização necessária. No caso de stateful AnimationController, ele verificaria se a duração mudou e, se mudou, atualizaria sua instância interna do AnimatorController. Isso evita que cada implementador da Animação tenha que lidar com a alteração da propriedade.

<insert whatever>
final myWidget = useMemo(() => MyWidget(pameter: value), [value]);

Com este código, myWidget reconstruirá _apenas_ quando value mudar. Mesmo que o widget que chama useMemo reconstruído por outros motivos.

Ah, entendo, Memoized retorna um widget em si, e então você passa [valor] como o gatilho de reconstrução, legal!

A chave sobre AnimatedOpacity não é nem a reconstrução do pai nem do filho. Na verdade, quando você dispara uma animação usando AnimatedOpacity, literalmente nada é reconstruído após o primeiro quadro em que você dispara a animação. Ignoramos totalmente a fase de construção e fazemos tudo no objeto de renderização (e na árvore de renderização, é apenas repintar, não retransmitir e, na verdade, usa uma camada, portanto, até mesmo a pintura é mínima). Isso faz uma diferença significativa no desempenho e no uso da bateria. Qualquer solução que surgirem aqui precisa ser capaz de manter esse tipo de desempenho se quisermos incorporá-la à estrutura central.

Infelizmente, não tive tempo de reunir os exemplos desta edição no repositório estadual local, meu mal. Posso não ser capaz de fazer isso a curto prazo, então se alguém quiser pegar, eu ficaria bem com isso.

Com relação ao desempenho de ter ganchos definidos dentro do método de construção / renderização (que eu acho que alguém mencionou anteriormente nesta edição), eu estava lendo os documentos do React e vi este FAQ, pode ser útil. Basicamente, ele pergunta se os ganchos são lentos devido à criação de funções em cada renderização, e eles dizem não devido a alguns motivos, um dos quais é ser capaz de memorizar funções usando um gancho como useMemo ou useCallback .

https://reactjs.org/docs/hooks-faq.html#are -hooks-slow-because-of-creation-functions-in-render

Basicamente, ele pergunta se os ganchos são lentos devido à criação de funções em cada renderização, e eles dizem não devido a alguns motivos, um dos quais é ser capaz de memorizar funções usando um gancho como useMemo ou useCallback .

A preocupação não é com o custo de criar fechamentos, eles são, de fato, relativamente baratos. É a diferença entre executar qualquer código e não executar nenhum código que é a chave para o desempenho que o Flutter exibe nos casos ideais hoje. Nós gastamos muito esforço criando algoritmos que literalmente evitam a execução de certos caminhos de código (por exemplo, a fase de construção sendo totalmente ignorada para AnimatedOpacity ou a maneira como evitamos percorrer a árvore para realizar atualizações, mas em vez disso, apenas visamos os nós afetados).

Eu concordo. Não sou muito versado em componentes internos de Flutter nem em componentes internos de ganchos, mas você está certo que os ganchos precisarão (se ainda não o fizeram) descobrir quando devem ser executados ou não, e o desempenho não deve regredir.

É a diferença entre executar qualquer código e não executar nenhum código que é a chave para o desempenho que o Flutter exibe nos casos ideais hoje

Como mencionado anteriormente algumas vezes, os ganchos melhoram isso.
O exemplo animado no repositório de Tim é prova disso. A variante de ganchos é reconstruída com menos frequência do que a variante StatefulWidget graças a useMemo

Já que está sendo discutido sobre soluções para esse problema em algum lugar neste tópico, estou rotulando-o como uma proposta também.

Eu realmente gostaria de ver os ganchos incorporados à vibração como foi feito com o react. Eu olho para o estado em vibração da mesma forma que costumava fazer quando eu costumava reagir pela primeira vez. Desde que usei ganchos, eu pessoalmente nunca mais voltaria.

É muito mais legível OMI. Atualmente você tem que declarar duas classes com um widget stateful versus hooks onde você simplesmente coloca usestate.

Também traria alguma familiaridade para flutter que os desenvolvedores geralmente não têm quando olham para o código de flutter. Obviamente, comparar flutter com react é um caminho perigoso a seguir, mas eu realmente acho que minha experiência de desenvolvedor com ganchos é melhor do que minha experiência sem eles.

Não estou odiando o flutter aliás, na verdade é meu framework favorito, mas acho que é uma ótima oportunidade para aumentar a legibilidade e a experiência de desenvolvimento.

Acho que há definitivamente uma oportunidade de melhorar as convenções de nomenclatura e torná-las mais vibrantes.

Coisas como UseMemoized e UseEffect parecem estranhas, e parece que queremos alguma maneira de não ter que executar o código init () no build fxn.

Atualmente a inicialização com ganchos é assim (eu acho?):

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

Eu aprecio a brevidade deste código, mas certamente é muito menos do que o ideal do ponto de vista de legibilidade e "código de autodocumentação". Há muita magia implícita acontecendo aqui. Idealmente, temos algo que é explícito sobre seus ganchos init / dispose e não força a construção quando usado com um widget sem estado.

Coisas como useMemoized e useEffect poderiam ser melhor nomeadas de forma mais explícita hook ComputedValue() e 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);
}

Eu gosto disso, mas não tenho certeza de como me sinto sobre o uso da palavra-chave hook , e não acho que isso resolva a questão dos conceitos estrangeiros. Apresentar novas palavras-chave não parece a melhor abordagem em minha mente, withSideEffect ou withComputedValue ? Não sou um designer de linguagem, então minhas palavras não valem nada.

Eu realmente sinto que a funcionalidade em forma de gancho no flutter será uma grande ajuda para suavizar a curva de aprendizado para os desenvolvedores do React, que é realmente o público-alvo quando as empresas estão tomando a decisão entre o ReactNative e o Flutter.

Ecoando @lemusthelroy , Flutter é de longe meu framework favorito e estou muito animado para ver as direções que ele leva. Mas eu sinto que os conceitos de programação funcional podem ser uma grande ajuda no crescimento do framework em uma direção ainda relativamente inexplorada. Acho que algumas pessoas estão descartando a ideia no intuito de se distanciar de React, o que é lamentável, mas compreensível.

Sim, há dois lados dessa moeda, eu acho. Uma nova palavra-chave é um evento importante, então a propagação do conhecimento seria muito rápida, mas o outro lado é certamente que agora é algo novo para _todo mundo_. Se for possível sem isso também é legal! Só não tenho certeza se é ... pelo menos não tão elegantemente.

Opinião: A inclinação da comunidade para nomear ganchos como a solução de fato para este problema origina-se de um preconceito para funções. As funções são mais simples de compor do que os objetos, especialmente em uma linguagem de tipagem estática. Acho que o modelo mental de Widgets para muitos desenvolvedores é efetivamente apenas o método build .

Acho que se você enquadrar o problema em termos básicos, é mais provável que você projete uma solução que funcione bem no resto da biblioteca.

Quanto à palavra-chave hook em termos de conceitos básicos; pode-se considerá-lo declarando e definindo uma função a partir de algum tipo de modelo (uma macro), e o prefixo hook está, na verdade, apenas alertando que a função embutida tem estado interno (estática de estilo c. )

Eu me pergunto se não existe algum tipo de arte anterior em Swift FunctionBuilders.

Enquanto estamos sonhando, vou esclarecer meu palpite sobre qual seria o código necessário:

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

Onde Hook é um hack de nível de sistema de tipo que ajuda a analisar estaticamente se o gancho resultante foi chamado de acordo com o que os desenvolvedores familiarizados com o gancho conhecem como leis de gancho. Como esse tipo de coisa, o tipo Hook pode ser documentado como algo que se parece muito com uma função, mas tem estado mutável interno estático.

Eu me encolho um pouco enquanto escrevo isso porque é uma raridade do ponto de vista da linguagem. Então, novamente, Dart é a linguagem que nasceu para escrever interfaces de usuário. Se esse tipo de esquisitice deveria existir em algum lugar, talvez este seja o lugar. Só não essa estranheza em particular.

Opinião: A inclinação da comunidade para nomear ganchos como a solução de fato para este problema origina-se de um preconceito para funções. As funções são mais simples de compor do que os objetos, especialmente em uma linguagem de tipagem estática. Acho que o modelo mental de Widgets para muitos desenvolvedores é, efetivamente, apenas o método de construção.

Não tenho certeza do que você quer dizer com isso. A abordagem de gancho que também uso com meu get_it_mixin torna a árvore de widgets mais fácil de ler do que usar um Builder.

Artigo interessante sobre ganchos React

@ nt4f04uNd Todos os seus pontos foram tratados anteriormente, incluindo desempenho, por que ele precisa ser um recurso central, widgets de estilo funcional versus de classe e por que outras coisas além dos ganchos parecem não funcionar. Eu sugiro que você leia toda a conversa para entender os vários pontos.

Eu sugiro que você leia toda a conversa para entender os vários pontos.

É justo dizer, considerando que eles não leram todo o tópico, mas não tenho certeza se isso torna as coisas mais claras ao ler o resto do tópico. Existem pessoas cuja prioridade é manter os Widgets como estão e outro grupo que quer fazer algo totalmente diferente ou tornar os Widgets mais modulares.

Embora isso possa ser verdade, este problema mostra que existem problemas que não podem ser resolvidos com widgets como estão atualmente, então, se quisermos resolver os problemas, não temos escolha a não ser fazer algo novo. Este é o mesmo conceito que ter Future s e posteriormente introduzir a sintaxe async/await , a última torna possíveis coisas que simplesmente não eram possíveis sem uma nova sintaxe.

No entanto, as pessoas estão sugerindo que o tornemos parte da estrutura. O React não pode adicionar uma nova sintaxe ao Javascript porque não é o único framework disponível (bem, pode por meio das transformações Babel), mas o Dart é projetado especificamente para funcionar com Flutter (Dart 2, pelo menos, não a versão original), então temos um muito mais capacidade de fazer ganchos trabalharem em conjunto com a linguagem subjacente. O React, por exemplo, precisa do Babel para JSX, e tem que usar um linter para erros useEffect , embora possamos torná-lo um erro em tempo de compilação. Ter um pacote torna a adoção muito mais difícil, como você pode imaginar a tração que os ganchos React teriam (não) obtido se fosse um pacote de terceiros.

Não haveria nenhum problema se pudesse haver um terceiro tipo de widget, ou seja, HookWidget, além dos widgets Stateless e Stateful atuais. Deixe a comunidade decidir qual usar. Já existe um pacote do Remi, mas tem limitações inevitavelmente. Eu tentei e ele reduziu significativamente o clichê, mas tive que abandoná-lo de forma inescrupulosa devido às limitações. Eu tenho que criar widgets com estado apenas para usar o método init. Pode haver grandes benefícios adicionais se fizer parte da estrutura principal com o suporte de idioma. Além disso, um HookWidget pode permitir que a comunidade crie aplicativos mais otimizados e com melhor desempenho.

Eu tenho que criar widgets com estado apenas para usar o método init.

Na verdade, você não precisa fazer isso, useEffect () é capaz de fazer initCall dentro da construção. Os documentos não fazem nenhum esforço para explicar isso, e basicamente assumem que você é um desenvolvedor do React que já sabe como os ganchos funcionam.

Estava usando assim mas tive alguns outros problemas com as limitações do pacote e não me lembro exatamente quais eram.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

drewwarren picture drewwarren  ·  3Comentários

aegis123 picture aegis123  ·  3Comentários

eseidelGoogle picture eseidelGoogle  ·  3Comentários

zoechi picture zoechi  ·  3Comentários

Hixie picture Hixie  ·  3Comentários