Flutter: ¿Cómo usar correctamente un widget heredado?

103

¿Cuál es la forma correcta de utilizar un InheritedWidget? Hasta ahora entendí que te da la oportunidad de propagar datos por el árbol de widgets. En extremo, si pones como RootWidget, será accesible desde todos los Widgets en el árbol en todas las Rutas, lo cual está bien porque de alguna manera tengo que hacer que mi ViewModel / Model sea accesible para mis Widgets sin tener que recurrir a globales o Singletons.

PERO InheritedWidget es inmutable, entonces, ¿cómo puedo actualizarlo? Y lo que es más importante, ¿cómo se activan mis widgets con estado para reconstruir sus subárboles?

Desafortunadamente, la documentación aquí es muy poco clara y después de una discusión con muchos, nadie parece saber realmente cuál es la forma correcta de usarla.

Añado una cita de Brian Egan:

Sí, lo veo como una forma de propagar datos por el árbol. Lo que encuentro confuso, de los documentos de la API:

"Los widgets heredados, cuando se hace referencia a ellos de esta manera, harán que el consumidor reconstruya cuando el widget heredado cambie de estado".

Cuando leí esto por primera vez, pensé:

Podría meter algunos datos en el InheritedWidget y mutarlos más tarde. Cuando ocurra esa mutación, reconstruirá todos los Widgets que hacen referencia a mi InheritedWidget Lo que encontré:

Para mutar el estado de un InheritedWidget, debes envolverlo en un StatefulWidget. Luego, mutas el estado del StatefulWidget y pasas estos datos al InheritedWidget, que entrega los datos a todos sus hijos. Sin embargo, en ese caso, parece reconstruir todo el árbol debajo del StatefulWidget, no solo los Widgets que hacen referencia al InheritedWidget. ¿Es eso correcto? ¿O de alguna manera sabrá cómo omitir los widgets que hacen referencia al InheritedWidget si updateShouldNotify devuelve falso?

Thomas
fuente

Respuestas:

108

El problema proviene de su cotización, que es incorrecta.

Como dijiste, InheritedWidgets son, como otros widgets, inmutables. Por lo tanto, no se actualizan . Se crean de nuevo.

La cosa es: InheritedWidget es solo un widget simple que no hace nada más que almacenar datos . No tiene ninguna lógica de actualización ni nada. Pero, como cualquier otro widget, está asociado con un Element. ¿Y adivina qué? ¡Esta cosa es mutable y Flutter la reutilizará siempre que sea posible!

La cotización corregida sería:

InheritedWidget, cuando se hace referencia a él de esta manera, hará que el consumidor reconstruya cuando InheritedWidget asociado a un InheritedElement cambie.

Hay una gran charla sobre cómo se conectan los widgets / elementos / renderbox . Pero en resumen, son así (la izquierda es su widget típico, el medio es 'elementos' y la derecha son 'cuadros de representación'):

ingrese la descripción de la imagen aquí

La cuestión es: cuando crea una instancia de un nuevo widget; Flutter lo comparará con el anterior. Reutilice su "Elemento", que apunta a un RenderBox. Y mute las propiedades de RenderBox.


Okey, pero ¿cómo responde esto a mi pregunta?

Al crear una instancia de un InheritedWidget y luego llamar context.inheritedWidgetOfExactType(o MyClass.ofque es básicamente lo mismo); lo que está implícito es que escuchará el Elementasociado con su InheritedWidget. Y siempre que Elementobtenga un nuevo widget, forzará la actualización de cualquier widget que haya llamado al método anterior.

En resumen, cuando reemplaza uno existente InheritedWidgetpor uno nuevo; Flutter verá que cambió. Y notificará a los widgets enlazados de una posible modificación.

Si entendiste todo, ya deberías haber adivinado la solución:

¡Envuelva su InheritedWidgetinterior de una manera StatefulWidgetque creará una nueva InheritedWidgetcada vez que algo cambie!

El resultado final en el código real sería:

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

¿Pero crear un nuevo InheritedWidget no reconstruiría todo el árbol?

No, no necesariamente. Como su nuevo InheritedWidget puede tener el mismo hijo que antes. Y por exacto, me refiero a la misma instancia. Los widgets que tienen la misma instancia que tenían antes no se reconstruyen.

Y en la mayoría de las situaciones (tener un widget heredado en la raíz de su aplicación), el widget heredado es constante . Así que no hay reconstrucción innecesaria.

Rémi Rousselet
fuente
1
¿Pero crear un nuevo InheritedWidget no reconstruiría todo el árbol? ¿Por qué entonces la necesidad de oyentes?
Thomas
1
Para su primer comentario, agregué una tercera parte a mi respuesta. En cuanto a ser tedioso: no estoy de acuerdo. Un fragmento de código puede generar esto con bastante facilidad. Y acceder a los datos es tan simple como llamar MyInherited.of(context).
Rémi Rousselet
3
No estoy seguro de si estás interesado, pero actualicé la muestra con esta técnica: github.com/brianegan/flutter_architecture_samples/tree/master/… ¡ Un poco menos de duplicación ahora seguro! Si tiene alguna otra sugerencia para esa implementación, me encantaría una revisión del código si alguna vez tiene unos momentos de sobra :) Aún tratando de descubrir la mejor manera de compartir esta lógica multiplataforma (Flutter y Web) y asegurarse de que sea comprobable ( especialmente las cosas asincrónicas).
brianegan
4
Dado que la updateShouldNotifyprueba siempre se refiere a la misma MyInheritedStateinstancia, ¿no devolverá siempre false? Ciertamente, el buildmétodo MyInheritedStatees crear nuevas _MyInheritedinstancias, pero el datacampo siempre hace referencia a this¿no? Tengo problemas ... Funciona si solo codifico true.
cdock
2
@cdock Sí, mi culpa. No recuerdo por qué hice eso, ya que obviamente no funcionará. Se corrigió editando a verdadero, gracias.
Rémi Rousselet
20

TL; DR

No use mucho cálculo dentro del método updateShouldNotify y use const en lugar de new al crear un widget


En primer lugar, debemos entender qué son los objetos Widget, Element y Render.

  1. Los objetos renderizados son lo que realmente se renderiza en la pantalla. Son mutables , contienen la lógica de la pintura y el diseño. El árbol de renderizado es muy similar al modelo de objetos de documento (DOM) en la web y puede ver un objeto de renderizado como un nodo DOM en este árbol
  2. Widget : es una descripción de lo que se debe representar. Son inmutables y baratos. Entonces, si un widget responde a la pregunta "¿Qué?" (Enfoque declarativo), entonces un objeto Render responde a la pregunta "¿Cómo?" (Enfoque imperativo). Una analogía de la web es un "DOM virtual".
  3. Element / BuildContext : es un proxy entre los objetos Widget y Render . Contiene información sobre la posición de un widget en el árbol * y cómo actualizar el objeto Render cuando se cambia un widget correspondiente.

Ahora estamos listos para sumergirnos en InheritedWidget y el método de BuildContext hereitFromWidgetOfExactType .

Como ejemplo, recomiendo que consideremos este ejemplo de la documentación de 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: solo un widget que implementa en nuestro caso un método importante: updateShouldNotify . updateShouldNotify : una función que acepta un parámetro oldWidget y devuelve un valor booleano: verdadero o falso.

Como cualquier widget, InheritedWidget tiene un objeto Element correspondiente. Es InheritedElement . InheritedElement llama updateShouldNotify en el widget cada vez que construimos un nuevo widget (llama setState en un ancestro). Cuando updateShouldNotify devuelve true, InheritedElement itera a través de dependencias (?) Y llama al método didChangeDependencies en él.

¿Dónde InheritedElement obtiene las dependencias ? Aquí deberíamos mirar el método heritageFromWidgetOfExactType .

HereitFromWidgetOfExactType : este método definido en BuildContext y cada elemento implementa la interfaz BuildContext (Element == BuildContext). Entonces, cada elemento tiene este método.

Veamos el código de heritageFromWidgetOfExactType:

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

Aquí intentamos encontrar un antepasado en _inheritedWidgets mapeado por tipo. Si se encuentra el antepasado, llamamos hereitFromElement .

El código de hereitFromElement :

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Agregamos ancestor como una dependencia del elemento actual (_dependencies.add (ancestor))
  2. Agregamos el elemento actual a las dependencias del ancestro (ancestor.updateDependencies (this, aspect))
  3. Devolvemos el widget del ancestro como resultado de heritageFromWidgetOfExactType (return ancestor.widget)

Entonces ahora sabemos de dónde obtiene InheritedElement sus dependencias.

Ahora veamos el método didChangeDependencies . Cada elemento tiene este método:

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

Como podemos ver, este método solo marca un elemento como sucio y este elemento debe reconstruirse en el siguiente cuadro. Reconstruir significa que el método de llamada se basa en el elemento del widget correspondiente.

Pero, ¿qué pasa con las "reconstrucciones completas del subárbol cuando reconstruyo InheritedWidget?". Aquí debemos recordar que los widgets son inmutables y si crea un nuevo widget, Flutter reconstruirá el subárbol. ¿Cómo podemos solucionarlo?

  1. Almacenar widgets con las manos (manualmente)
  2. Use const porque const crea la única instancia de value / class
maksimr
fuente
1
gran explicación maksimr. Lo que más me confunde es que si todo el subárbol se reconstruyó de todos modos cuando se reemplazó el widget heredado, ¿cuál es el punto de updateShouldNotify ()?
Panda World
3

De los documentos :

[BuildContext. o el widget desaparece), este contexto de compilación se reconstruye para que pueda obtener nuevos valores de ese widget.

Esto se suele llamar implícitamente a partir de métodos estáticos de (), por ejemplo, Theme.of.

Como señaló el OP, una InheritedWidgetinstancia no cambia ... pero se puede reemplazar con una nueva instancia en la misma ubicación en el árbol de widgets. Cuando eso suceda, es posible que los widgets registrados deban reconstruirse. El InheritedWidget.updateShouldNotifymétodo hace esta determinación. (Ver: documentos )

Entonces, ¿cómo podría reemplazarse una instancia? Una InheritedWidgetinstancia puede estar contenida por a StatefulWidget, que puede reemplazar una instancia antigua por una nueva.

kkurian
fuente
-1

InheritedWidget administra los datos centralizados de la aplicación y se los pasa al niño, como si pudiéramos almacenar aquí el recuento de carritos como se explica aquí :

Sunil
fuente