Flutter: come utilizzare correttamente un widget ereditato?


103

Qual è il modo corretto di utilizzare un InheritedWidget? Finora ho capito che ti dà la possibilità di propagare i dati lungo l'albero dei widget. All'estremo, se metti come RootWidget, sarà accessibile da tutti i widget nell'albero su tutti i percorsi, il che va bene perché in qualche modo devo rendere il mio ViewModel / Model accessibile per i miei Widget senza dover ricorrere a globali o singleton.

MA InheritedWidget è immutabile, quindi come posso aggiornarlo? E ancora più importante come vengono attivati ​​i miei widget con stato per ricostruire i loro sottoalberi?

Sfortunatamente la documentazione qui è molto poco chiara e dopo aver discusso con molto nessuno sembra davvero sapere quale sia il modo corretto di usarla.

Aggiungo una citazione di Brian Egan:

Sì, lo vedo come un modo per propagare i dati lungo l'albero. Quello che trovo confuso, dai documenti API:

"I widget ereditati, se referenziati in questo modo, fanno sì che il consumatore si ricostruisca quando il widget ereditato cambia di stato".

Quando l'ho letto per la prima volta, ho pensato:

Potrei inserire alcuni dati in InheritedWidget e modificarli in seguito. Quando si verifica quella mutazione, ricostruirà tutti i widget che fanno riferimento al mio InheritedWidget Cosa ho trovato:

Per modificare lo stato di un InheritedWidget, è necessario avvolgerlo in uno StatefulWidget. Quindi si modifica lo stato di StatefulWidget e si passano questi dati all'InheritedWidget, che passa i dati a tutti i suoi figli. Tuttavia, in quel caso, sembra ricostruire l'intero albero sotto StatefulWidget, non solo i widget che fanno riferimento a InheritedWidget. È corretto? O saprà in qualche modo come saltare i widget che fanno riferimento a InheritedWidget se updateShouldNotify restituisce false?

Risposte:


108

Il problema deriva dalla tua citazione, che non è corretta.

Come hai detto, gli InheritedWidgets sono, come gli altri widget, immutabili. Pertanto non si aggiornano . Vengono creati di nuovo.

Il punto è: InheritedWidget è solo un semplice widget che non fa altro che contenere dati . Non ha alcuna logica di aggiornamento o altro. Ma, come qualsiasi altro widget, è associato a un file Element. E indovina cosa? Questa cosa è mutevole e Flutter la riutilizzerà ogni volta che sarà possibile!

La citazione corretta sarebbe:

InheritedWidget, se referenziato in questo modo, farà sì che il consumatore si ricompili quando InheritedWidget associato a un InheritedElement cambia.

Si parla molto di come i widget / elementi / renderbox sono collegati insieme . Ma in breve, sono così (a sinistra è il tuo widget tipico, al centro gli "elementi" e a destra sono i "riquadri di rendering"):

inserisci qui la descrizione dell'immagine

Il punto è: quando installi un nuovo widget; flutter lo paragonerà a quello vecchio. Riutilizza il suo "Element", che punta a un RenderBox. E mutare le proprietà Disegnanon.


Okey, ma come risponde alla mia domanda?

Quando si crea un'istanza di un InheritedWidget e quindi si chiama context.inheritedWidgetOfExactType(o MyClass.ofche è fondamentalmente lo stesso); ciò che è implicito è che ascolterà gli Elementassociati al tuo InheritedWidget. E ogni volta che Elementottiene un nuovo widget, forzerà l'aggiornamento di tutti i widget che hanno chiamato il metodo precedente.

In breve, quando si sostituisce uno esistente InheritedWidgetcon uno nuovo di zecca; flutter vedrà che è cambiato. E notificherà ai widget associati una potenziale modifica.

Se hai capito tutto, dovresti aver già intuito la soluzione:

Avvolgi il tuo InheritedWidgetinterno in un StatefulWidgetche creerà un nuovo di zecca InheritedWidgetogni volta che qualcosa cambia!

Il risultato finale nel codice effettivo sarebbe:

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

Ma la creazione di un nuovo InheritedWidget non ricostruirebbe l'intero albero?

No, non sarà necessariamente. Poiché il tuo nuovo InheritedWidget può potenzialmente avere lo stesso identico figlio di prima. E per esatto, intendo lo stesso esempio. I widget che hanno la stessa istanza che avevano prima non vengono ricostruiti.

E nella maggior situazione (Avere un inheritedWidget alla radice della vostra applicazione), il ereditato widget è costante . Quindi nessuna ricostruzione non necessaria.


1
Ma la creazione di un nuovo InheritedWidget non ricostruirebbe l'intero albero? Perché allora la necessità di ascoltatori?
Thomas

1
Per il tuo primo commento, ho aggiunto una terza parte alla mia risposta. Quanto all'essere noioso: non sono d'accordo. Uno snippet di codice può generarlo abbastanza facilmente. E accedere ai dati è semplice come chiamare MyInherited.of(context).
Rémi Rousselet

3
Non sono sicuro se sei interessato, ma ho aggiornato l'esempio con questa tecnica: github.com/brianegan/flutter_architecture_samples/tree/master/… Un po 'meno duplicazioni ora di sicuro! Se hai altri suggerimenti per tale implementazione, mi piacerebbe una revisione del codice se hai qualche minuto da perdere :) Sto ancora cercando di capire il modo migliore per condividere questa logica multipiattaforma (Flutter e Web) e assicurarti che sia testabile ( soprattutto la roba asincrona).
brianegan

4
Poiché il updateShouldNotifytest si riferisce sempre alla stessa MyInheritedStateistanza, non tornerà sempre false? Certamente il buildmetodo di MyInheritedStatecreazione di nuove _MyInheritedistanze, ma il datacampo fa sempre riferimento a thisno? Ho problemi ... Funziona se ho solo hard code true.
cdock

2
@cdock Sì, colpa mia. Non ricordo perché l'ho fatto perché ovviamente non funzionerà. Risolto modificando in true, grazie.
Rémi Rousselet

20

TL; DR

Non utilizzare calcoli pesanti all'interno del metodo updateShouldNotify e utilizzare const invece di new quando si crea un widget


Prima di tutto, dobbiamo capire cosa sono oggetti Widget, Element e Render.

  1. Gli oggetti di rendering sono ciò che viene effettivamente visualizzato sullo schermo. Sono mutevoli , contengono la logica della pittura e del layout. L'albero di rendering è molto simile al DOM (Document Object Model) nel Web e puoi guardare un oggetto di rendering come un nodo DOM in questo albero
  2. Widget : è una descrizione di ciò che dovrebbe essere reso. Sono immutabili ed economici. Quindi, se un widget risponde alla domanda "Cosa?" (Approccio dichiarativo), un oggetto Render risponde alla domanda "Come?" (Approccio imperativo). Un'analogia dal web è un "DOM virtuale".
  3. Element / BuildContext - è un proxy tra oggetti Widget e Render . Contiene informazioni sulla posizione di un widget nell'albero * e su come aggiornare l'oggetto Render quando viene modificato un widget corrispondente.

Ora siamo pronti per immergerci nel metodo InheritedWidget e BuildContext inheritFromWidgetOfExactType .

Come esempio, consiglio di considerare questo esempio dalla documentazione di Flutter su 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 - solo un widget che implementa nel nostro caso un metodo importante - updateShouldNotify . updateShouldNotify - una funzione che accetta un parametro oldWidget e restituisce un valore booleano: true o false.

Come ogni widget, InheritedWidget ha un oggetto Element corrispondente. È InheritedElement . InheritedElement chiama updateShouldNotify sul widget ogni volta che creiamo un nuovo widget (chiama setState su un antenato). Quando updateShouldNotify restituisce true, InheritedElement itera attraverso le dipendenze (?) E chiama il metodo didChangeDependencies su di esso.

Dove InheritedElement ottiene le dipendenze ? Qui dovremmo esaminare il metodo inheritFromWidgetOfExactType .

inheritFromWidgetOfExactType : questo metodo definito in BuildContext e ogni elemento implementa l'interfaccia BuildContext (Element == BuildContext). Quindi ogni elemento ha questo metodo.

Diamo un'occhiata al codice di inheritFromWidgetOfExactType:

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

Qui proviamo a trovare un antenato in _inheritedWidgets mappato per tipo. Se viene trovato l'antenato, chiamiamo inheritFromElement .

Il codice per inheritFromElement :

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Aggiungiamo ancestor come dipendenza dell'elemento corrente (_dependencies.add (ancestor))
  2. Aggiungiamo l'elemento corrente alle dipendenze dell'antenato (ancestor.updateDependencies (questo, aspetto))
  3. Restituiamo il widget dell'antenato come risultato di inheritFromWidgetOfExactType (return ancestor.widget)

Quindi ora sappiamo dove InheritedElement ottiene le sue dipendenze.

Ora diamo un'occhiata al metodo didChangeDependencies . Ogni elemento ha questo metodo:

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

Come possiamo vedere, questo metodo contrassegna semplicemente un elemento come sporco e questo elemento dovrebbe essere ricostruito nel frame successivo. Ricostruire significa chiamare il metodo costruito sull'elemento widget corrispondente.

Ma che dire di "Ricostruzioni dell'intero sottoalbero quando ricostruisco InheritedWidget?". Qui dobbiamo ricordare che i widget sono immutabili e se crei un nuovo widget Flutter ricostruirà il sottoalbero. Come possiamo aggiustarlo?

  1. Cache widget manualmente (manualmente)
  2. Usa const perché const crea l'unica istanza di value / class

1
ottima spiegazione maksimr. La cosa che mi confonde di più è che se l'intero sottoalbero viene ricostruito comunque quando viene sostituito inheritedWidget, qual è lo scopo di updateShouldNotify ()?
Panda World

3

Dai documenti :

[BuildContext.inheritFromWidgetOfExactType] ottiene il widget più vicino del tipo dato, che deve essere il tipo di una sottoclasse InheritedWidget concreta, e registra questo contesto di build con quel widget in modo tale che quando quel widget cambia (o viene introdotto un nuovo widget di quel tipo, o il widget scompare), questo contesto di build viene ricostruito in modo che possa ottenere nuovi valori da quel widget.

Questo è tipicamente chiamato implicitamente dai metodi statici of (), ad esempio Theme.of.

Come ha notato l'OP, InheritedWidgetun'istanza non cambia ... ma può essere sostituita con una nuova istanza nella stessa posizione nell'albero del widget. Quando ciò accade, è possibile che i widget registrati debbano essere ricostruiti. Il InheritedWidget.updateShouldNotifymetodo fa questa determinazione. (Vedere: documenti )

Quindi come potrebbe essere sostituita un'istanza? Un InheritedWidgetesempio può essere contenuto da un StatefulWidget, che può sostituire un vecchio esempio con una nuova istanza.


-1

InheritedWidget gestisce i dati centralizzati dell'app e li passa al bambino, come se potessimo memorizzare qui il conteggio dei carrelli come spiegato qui :

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.