Flutter: Como usar corretamente um widget herdado?

103

Qual é a maneira correta de usar um InheritedWidget? Até agora eu entendi que isso dá a você a chance de propagar dados na árvore de widgets. Em extremo, se você colocar é como RootWidget, ele estará acessível de todos os Widgets na árvore em todas as rotas, o que é bom porque de alguma forma eu tenho que tornar meu ViewModel / Model acessível para meus Widgets sem ter que recorrer a globais ou Singletons.

MAS InheritedWidget é imutável, então como posso atualizá-lo? E mais importante, como meus Stateful Widgets são acionados para reconstruir suas subárvores?

Infelizmente, a documentação aqui não é muito clara e após discussão com muitos ninguém parece realmente saber qual a maneira correta de usá-la.

Eu adicionei uma citação de Brian Egan:

Sim, vejo isso como uma forma de propagar dados na árvore. O que acho confuso nos documentos da API:

"Widgets herdados, quando referenciados dessa forma, farão com que o consumidor reconstrua quando o próprio widget herdado mudar de estado."

Quando li isso pela primeira vez, pensei:

Eu poderia colocar alguns dados no InheritedWidget e transformá-lo mais tarde. Quando essa mutação acontecer, ele irá reconstruir todos os Widgets que fazem referência ao meu InheritedWidget. O que eu encontrei:

Para alterar o estado de um InheritedWidget, você precisa envolvê-lo em um StatefulWidget. Então, você realmente altera o estado do StatefulWidget e passa esses dados para o InheritedWidget, que passa os dados para todos os seus filhos. No entanto, nesse caso, parece reconstruir a árvore inteira sob o StatefulWidget, não apenas os Widgets que fazem referência ao InheritedWidget. Isso é correto? Ou ele de alguma forma saberá como ignorar os Widgets que fazem referência ao InheritedWidget se updateShouldNotify retornar falso?

Thomas
fonte

Respostas:

108

O problema vem da sua cotação, que está incorreta.

Como você disse, InheritedWidgets são, como outros widgets, imutáveis. Portanto, eles não são atualizados . Eles são criados de novo.

A questão é: InheritedWidget é apenas um widget simples que não faz nada além de armazenar dados . Não tem nenhuma lógica de atualização ou seja o que for. Mas, como qualquer outro widget, está associado a um Element. E adivinha? Essa coisa é mutável e a vibração irá reutilizá-la sempre que possível!

A citação corrigida seria:

InheritedWidget, quando referenciado dessa forma, fará com que o consumidor reconstrua quando InheritedWidget associado a um InheritedElement mudar.

Há uma ótima conversa sobre como widgets / elementos / renderbox são conectados juntos . Mas, em resumo, eles são assim (à esquerda está seu widget típico, no meio estão 'elementos' e à direita estão 'caixas de renderização'):

insira a descrição da imagem aqui

A questão é: quando você instancia um novo widget; flutter irá compará-lo ao antigo. Reutilize seu "Elemento", que aponta para um RenderBox. E transformar as propriedades RenderBox.


Ok, mas como isso responde à minha pergunta?

Ao instanciar um InheritedWidget e, em seguida, chamar context.inheritedWidgetOfExactType(ou MyClass.ofque é basicamente o mesmo); o que está implícito é que ele ouvirá o Elementassociado ao seu InheritedWidget. E sempre que Elementreceber um novo widget, ele forçará a atualização de todos os widgets que chamaram o método anterior.

Em suma, quando você substitui um existente InheritedWidgetpor um novo; vibração verá que mudou. E notificará os widgets vinculados sobre uma modificação potencial.

Se você entendeu tudo, já deve ter adivinhado a solução:

Envolva seu InheritedWidgetinterior de uma forma StatefulWidgetque criará um novo InheritedWidgetsempre que algo mudou!

O resultado final no código real seria:

class MyInherited extends StatefulWidget {
  static MyInheritedData of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(MyInheritedData) as MyInheritedData;

  const MyInherited({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  _MyInheritedState createState() => _MyInheritedState();
}

class _MyInheritedState extends State<MyInherited> {
  String myField;

  void onMyFieldChange(String newValue) {
    setState(() {
      myField = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MyInheritedData(
      myField: myField,
      onMyFieldChange: onMyFieldChange,
      child: widget.child,
    );
  }
}

class MyInheritedData extends InheritedWidget {
  final String myField;
  final ValueChanged<String> onMyFieldChange;

  MyInheritedData({
    Key key,
    this.myField,
    this.onMyFieldChange,
    Widget child,
  }) : super(key: key, child: child);

  static MyInheritedData of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
  }

  @override
  bool updateShouldNotify(MyInheritedData oldWidget) {
    return oldWidget.myField != myField ||
        oldWidget.onMyFieldChange != onMyFieldChange;
  }
}

Mas criar um novo InheritedWidget não reconstruiria a árvore inteira?

Não, não necessariamente. Como seu novo InheritedWidget pode potencialmente ter o mesmo filho de antes. E por exato, quero dizer a mesma instância. Os widgets que possuem a mesma instância de antes não são reconstruídos.

E na maioria das situações (tendo um inheritedWidget na raiz do seu aplicativo), o widget herdado é constante . Portanto, nenhuma reconstrução desnecessária.

Rémi Rousselet
fonte
1
Mas criar um novo InheritedWidget não reconstruiria a árvore inteira? Por que então a necessidade de Ouvintes?
Thomas
1
Para seu primeiro comentário, acrescentei uma terceira parte à minha resposta. Quanto a ser entediante: discordo. Um trecho de código pode gerar isso facilmente. E acessar os dados é tão simples quanto ligar MyInherited.of(context).
Rémi Rousselet
3
Não tenho certeza se você está interessado, mas atualizei a amostra com esta técnica: github.com/brianegan/flutter_architecture_samples/tree/master/… Um pouco menos duplicação agora, com certeza! Se você tiver alguma outra sugestão para essa implementação, adoraria uma revisão de código se você tiver alguns momentos de sobra :) Ainda estou tentando descobrir a melhor maneira de compartilhar esta plataforma cruzada lógica (Flutter e Web) e garantir que seja testável ( especialmente o material assíncrono).
brianegan
4
Como o updateShouldNotifyteste sempre se refere à mesma MyInheritedStateinstância, ele não retornará sempre false? Certamente o buildmétodo de MyInheritedStatecriar novas _MyInheritedinstâncias, mas o datacampo sempre faz referência a thisnão? Estou tendo problemas ... Funciona se eu apenas codificar true.
cdock
2
@cdock Sim, meu mal. Não me lembro porque fiz isso, pois obviamente não vai funcionar. Corrigido pela edição para verdadeiro, obrigado.
Rémi Rousselet
20

TL; DR

Não use computação pesada dentro do método updateShouldNotify e use const em vez de new ao criar um widget


Em primeiro lugar, devemos entender o que são objetos Widget, Elemento e Render.

  1. Objetos de renderização são o que realmente é renderizado na tela. Eles são mutáveis , contêm a lógica de pintura e layout. A árvore de renderização é muito semelhante ao Document Object Model (DOM) na web e você pode ver um objeto de renderização como um nó DOM nesta árvore
  2. Widget - é uma descrição do que deve ser renderizado. Eles são imutáveis e baratos. Portanto, se um widget responde à pergunta "O quê?" (Abordagem declarativa), então um objeto Render responde à pergunta "Como?" (Abordagem imperativa). Uma analogia com a web é um "DOM Virtual".
  3. Element / BuildContext - é um proxy entre objetos Widget e Render . Ele contém informações sobre a posição de um widget na árvore * e como atualizar o objeto Render quando um widget correspondente é alterado.

Agora estamos prontos para mergulhar no método inheritFromWidgetOfExactType de InheritedWidget e BuildContext .

Como exemplo, recomendo que consideremos este exemplo da documentação do Flutter sobre InheritedWidget:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  })  : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FrogColor);
  }

  @override
  bool updateShouldNotify(FrogColor old) {
    return color != old.color;
  }
}

InheritedWidget - apenas um widget que implementa em nosso caso um método importante - updateShouldNotify . updateShouldNotify - uma função que aceita um parâmetro oldWidget e retorna um valor booleano: verdadeiro ou falso.

Como qualquer widget, InheritedWidget tem um objeto Element correspondente. É InheritedElement . InheritedElement chama updateShouldNotify no widget toda vez que construímos um novo widget (chame setState em um ancestral). Quando updateShouldNotify retorna true, InheritedElement itera por meio de dependências (?) E chama o método didChangeDependencies nele.

Onde InheritedElement obtém dependências ? Aqui devemos olhar para o método inheritFromWidgetOfExactType .

inheritFromWidgetOfExactType - Este método definido em BuildContext e cada Elemento implementa a interface BuildContext (Element == BuildContext). Portanto, cada elemento tem esse método.

Vamos dar uma olhada no código de inheritFromWidgetOfExactType:

final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
  assert(ancestor is InheritedElement);
  return inheritFromElement(ancestor, aspect: aspect);
}

Aqui, tentamos encontrar um ancestral em _inheritedWidgets mapeado por tipo. Se o ancestral for encontrado, então chamamos de inheritFromElement .

O código para inheritFromElement :

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Adicionamos ancestral como uma dependência do elemento atual (_dependencies.add (ancestor))
  2. Adicionamos o elemento atual às dependências do ancestral (ancestor.updateDependencies (this, aspect))
  3. Retornamos o widget do ancestral como resultado de inheritFromWidgetOfExactType (return ancestor.widget)

Portanto, agora sabemos de onde InheritedElement obtém suas dependências.

Agora vamos examinar o método didChangeDependencies . Cada elemento tem este método:

  void didChangeDependencies() {
    assert(_active); // otherwise markNeedsBuild is a no-op
    assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
    markNeedsBuild();
  }

Como podemos ver este método apenas marca um elemento como sujo e este elemento deve ser reconstruído no próximo quadro. Reconstruir significa construir o método de chamada no elemento de widget correspondente.

Mas e quanto a "Reconstruções de subárvore inteira quando eu reconstruo InheritedWidget?". Aqui devemos lembrar que os Widgets são imutáveis ​​e se você criar um novo widget, o Flutter reconstruirá a subárvore. Como podemos arranjá-lo?

  1. Widgets de cache manualmente (manualmente)
  2. Use const porque const cria a única instância de valor / classe
maksimr
fonte
1
ótima explicação maksimr. A coisa que mais me confunde é que, se toda a subárvore reconstruída de qualquer maneira quando o inheritedWidget foi substituído, qual é o objetivo de updateShouldNotify ()?
Panda World
3

Dos documentos :

[BuildContext.inheritFromWidgetOfExactType] obtém o widget mais próximo do tipo fornecido, que deve ser o tipo de uma subclasse concreta de InheritedWidget, e registra esse contexto de construção com esse widget de forma que quando esse widget muda (ou um novo widget desse tipo é introduzido, ou o widget vai embora), este contexto de construção é reconstruído para que possa obter novos valores desse widget.

Isso normalmente é chamado implicitamente a partir de métodos estáticos of (), por exemplo, Theme.of.

Como o OP observou, uma InheritedWidgetinstância não muda ... mas pode ser substituída por uma nova instância no mesmo local na árvore de widgets. Quando isso acontecer, é possível que os widgets registrados precisem ser reconstruídos. O InheritedWidget.updateShouldNotifymétodo faz essa determinação. (Veja: docs )

Então, como uma instância pode ser substituída? Uma InheritedWidgetinstância pode ser contida por um StatefulWidget, que pode substituir uma instância antiga por uma nova instância.

kkurian
fonte
-1

InheritedWidget gerencia dados centralizados do aplicativo e os passa para o filho, como podemos armazenar aqui a contagem do carrinho conforme explicado aqui :

Sunil
fonte