Todo el mundo ha oído que las aplicaciones interactivas se pueden descomponer en tres partes: modelo, vista y controlador.
Cualquiera que haya dado a Flutter una prueba de manejo estará familiarizado con su soporte de estilo de reacción para construir vistas / controladores con widgets y devoluciones de llamada. No todo el mundo parece tener tan claro cómo Flutter apoya la parte de «modelo» de MVC.
Un widget depende del modelo cuando su método de construcción recupera valores del modelo o cuando sus devoluciones de llamada modifican los valores del modelo. Si un widget depende del modelo, debe reconstruirse si el modelo cambia. Exactamente cómo se consigue que eso suceda es el tema de este artículo.
Este artículo revisa cómo las clases StatefulWidget
y InheritedWidget
de Flutter se pueden usar para vincular los elementos visuales de una aplicación a su modelo.
Concluye con una clase ModelBinding
pequeña pero con suerte útil que debería ser fácil de colocar en una aplicación. Si está ansioso por comenzar, puede descargue la clase ModelBinding
junto con una pequeña demostración ahora.
Índice
Descargos de responsabilidad, expectativas de los lectores
El enfoque para crear aplicaciones MVC que se describe en este artículo no es la única forma de hacerlo. Hay muchas formas de vincular Flutter a modelos y si está planeando algo grande, es posible que desee examinar sus opciones, algunas de las cuales he enumerado al final de este artículo.
Por otro lado, incluso si no decide adoptar la pequeña clase ModelBinding presentada en la sección final, puede aprender algo nuevo sobre Flutter leyendo este artículo.
Este no es un artículo introductorio de Flutter, espero que los lectores ya tengan algo de experiencia con la API de Flutter. Por ejemplo, se asume que:
- Se siente cómodo escribiendo clases en Dart y comprende la sobrecarga de operator == y hashcode, así como los métodos genéricos.
- Estás familiarizado con las clases básicas de widgets de Flutter y sabes cómo escribir nuevas.
El modelo de la aplicación
Por el bien de los ejemplos que siguen, necesitamos un modelo de aplicación de muestra. Por el bien de nuestro corto período de atención, he mantenido este modelo lo más simple posible. Aquí solo hay un campo de valor y el clase real incluye operador ==
y anulaciones de hashCode
.
class Model { const Model({ this.value = 0 }); final int value; // .. operator ==, hashCode }
Con suerte, es obvio cómo se pueden extender los valores que representa el modelo.
Este es un modelo inmutable, por lo que cambiarlo significa reemplazarlo. Los enfoques MVC que se muestran a continuación se pueden usar con un modelo mutable, solo que es un poco más complicado de esa manera.
Enlace al modelo con StatefulWidget
Este es el enfoque más simple para integrar un modelo, perfecto para una aplicación por la tarde.
Un StatefulWidget
está asociado con un objeto de estado persistente. El método de construcción del objeto State
crea el subárbol contribuido por el widget, al igual que lo hace el método de construcción del StatelessWidget
.
El método setState
del objeto State
hace que el widget se vuelva a construir, la próxima vez que haya transcurrido el intervalo de velocidad de fotogramas de la pantalla.
Si el objeto de estado del widget con estado es el propietario del modelo, su método de construcción configura los widgets que crea usando valores del modelo y llama a setState cuando cambia el modelo.
La definición del widget con estado ViewController
del ejemplo requiere un poco de repetición:
class ViewController extiende StatefulWidget { _ViewControllerState createState () => _ViewControllerState (); } class _ViewControllerState extiende State < ViewController > { // aquí va el estado persistente del widget @anular Widget acumulación ( BuildContext contexto) { // devuelve un widget basado en el estado persistente } }
En este caso, el estado persistente del widget es una instancia de modelo. El método de construcción crea un botón que muestra el valor actual del modelo y reemplaza el modelo cuando se presiona.
El método updateModel
restablece el modelo actual y llama a setState
para desencadenar una reconstrucción. En otras palabras, presionar el botón actualiza el modelo y luego reconstruye el widget ViewController
.
Modelo currentModel = Model (); void updateModel ( modelo newModel) { if (newModel ! = currentModel) { setState (() { currentModel = newmodel; }); } } @anular Widget acumulación ( BuildContext contexto) { return RaisedButton ( onPressed : () { updateModel ( Model (valor : currentModel.value + 1 )); }, child : Text ( 'Hola mundo $ { currentModel.value }' ), ); }
El enfoque descrito anteriormente es un muñeco de paja. No es ideal para una aplicación a escala relativamente grande:
- Cuando el modelo cambia, ViewController y todo el árbol de widgets que crea se reconstruyen. No solo los widgets que dependen del modelo, todos ellos.
- Si un widget secundario necesita buscar el valor del modelo, entonces el modelo se le debe pasar con un parámetro de constructor o mediante un cierre, como en el ejemplo trivial anterior.
- Si un widget descendiente necesita buscar el valor del modelo, entonces el modelo se le debe pasar a través de una cadena de parámetros de constructor.
- Si un widget secundario desea modificar el valor del modelo, debe pasar una devolución de llamada proporcionada por ViewController. En el ejemplo, ese sería el parámetro onPressed de RaisedButton.
Con suerte, está claro que StatefulWidgets
, aunque es adecuado para crear widgets con algún estado persistente interno, no es ideal para compartir un modelo en una aplicación compleja. Y, como ingenieros de software, lo complejo es nuestro sustento.
La clase InheritedWidget
tiene algunas propiedades especiales que la hacen muy adecuada para compartir un modelo con un árbol de widgets:
- Dado un
BuildContext
, encontrar el antepasadoInheritedWidget
más cercano de un tipo en particular es barato, solo una búsqueda de tabla. -
InheritedWidgets
realiza un seguimiento de sus dependientes, es decir, losBuildContexts
desde los que se accedió alInheritedWidget
. Cuando se reconstruye unInheritedWidget
, todos sus dependientes también se reconstruyen.
Probablemente ya se ha encontrado con el widget heredado del tema. El método estático Theme.of
(context) devuelve ThemeData
del tema y registra el contexto como dependiente del tema.
Si el tema se reconstruye con un valor ThemeData
nuevo y diferente, todos los widgets que se refieren a él con Theme.of ()
se reconstruyen automáticamente.
Es bastante fácil usar una subclase personalizada de InheritedWidget
de la misma manera, como host para el modelo de la aplicación. Esta subclase de InheritedWidget
se llama ModelBinding
porque conecta los widgets de la aplicación al modelo.
class ModelBinding extends InheritedWidget { ModelBinding({ Key key, this.model = const Model(), Widget child, }) : assert(model != null), super(key: key, child: child); final Model model; @override bool updateShouldNotify(ModelBinding oldWidget) => model != oldWidget.model; }
Se llama al método updateShouldNotify
cuando se reconstruye ModelBinding
. Si devuelve verdadero, entonces se reconstruyen todos los widgets dependientes.
El método BuildContext hereitFromWidgetOfExactType ()
se usa para buscar un widget heredado. Debido a que es un poco feo y por otras razones que se harán evidentes más adelante, generalmente está envuelto en un método estático.
Agregar el método de búsqueda a la clase Model hace posible recuperar el modelo de cualquier descendiente de ModelBinding
con: Model.of
(contexto):
Ahora ViewController
puede ser sin estado y su referencia al modelo se convierte en:
// Model.of(context) works from any ModelBinding descendant Text('Hello World ${Model.of(context).value}')
Cualquier descendiente de ModelBinding
puede hacer lo mismo, no es necesario pasar el modelo hacia abajo. Si el modelo cambia, los widgets dependientes como ViewController
se reconstruyen automáticamente.
El ModelBinding
en sí debe ser construido por un widget con estado. Para cambiar el modelo, ese widget con estado debe reconstruirse llamando a su método setState
. Ahora el widget de la aplicación de nivel superior se vuelve con estado y una devolución de llamada que actualiza el modelo debe pasarse al ViewController
.
class App extends StatefulWidget { @override _AppState createState() => _AppState(); } class _AppState extends State<App> { Model currentModel = Model(); void updateModel(Model newModel) { if (newModel != currentModel) { setState(() { currentModel = newModel; }); } } @override Widget build(BuildContext context) { return MaterialApp( home: ModelBinding( model: currentModel, child: Scaffold( body: Center( child: ViewController(updateModel: updateModel), ), ), ), ); } }
En este caso, Model es solo un valor inmutable simple, por lo que reemplazarlo es tan simple como asignar un nuevo currentModel
.
Reemplazarlo podría ser más complicado, por ejemplo, si el modelo contenía objetos que requirieran administración de por vida, reemplazar el modelo podría requerir desechar partes del modelo anterior.
Limitaciones de la versión 0 de InheritedWidget
Por el lado positivo, la versión InheritedWidget
de nuestro enlace de modelo facilita que los widgets se refieran al modelo y automatiza la reconstrucción de esos widgets «dependientes» cuando el modelo cambia.
En el lado negativo, todavía es necesario conectar una devolución de llamada updateModel
a través del árbol de widgets, a los widgets que necesitan cambiar el modelo.
Enlace al modelo con InheritedWidget
, versión 1
Esta versión del enfoque InheritedWidget
para vincular widgets a un modelo simplifica las actualizaciones del modelo. Ahora, cualquier descendiente de ModelBinding
puede obtener el modelo y actualizarlo, y no es necesario pasar una devolución de llamada específica del modelo.
Entonces, como antes, cualquier descendiente de ModelBinding
puede obtener un valor de modelo con Model.of
(contexto) y, al hacerlo, convertirse en un modelo dependiente automáticamente reconstruido. Ahora cualquier descendiente de ModelBinding
puede actualizar el modelo con Model.update
(context, newModel)
. Lo cual es bueno.
Model.update ()
, es necesario introducir un widget con estado adicional. Esto se complica un poco, así que si usas sombrero, agárratelo.ModelBinding
es ahora un widget con estado que rastrea el valor actual del modelo. ModelBinding
construye un hijo privado _ModelBindingScope InheritedWidget
que tiene una referencia al estado un _ModelBindingState
; esencialmente a su padre de widget con estado.
Para cambiar el valor del modelo actual de ModelBinding
, reconstruye ModelBinding
con setState ()
, que a su vez reconstruye el widget _ModelBindingScope
heredado. Los métodos de modelo estático para obtener el modelo o actualizarlo, busque _ModelBindingScope
:
static Model of(BuildContext context) { _ModelBindingScope scope = context.inheritFromWidgetOfExactType(_ModelBindingScope); return scope.modelBindingState.currentModel; } static void update(BuildContext context, Model newModel) { _ModelBindingScope scope = context.inheritFromWidgetOfExactType(_ModelBindingScope); scope.modelBindingState.updateModel(newModel); }
Cualquier descendiente de ModelBinding
ahora puede actualizar o cambiar el modelo usando estos métodos. El botón del ejemplo hace ambas cosas:
RaisedButton( onPressed: () { Model model = Model.of(context); Model.update(context, Model(value: model.value + 1)); }, child: Text('Hello World ${Model.of(context).value}'), );
Limitaciones de la versión 1 de InheritedWidget
Esta versión es generalmente útil y pesa menos de 100 líneas de código. Se puede utilizar tal cual o como punto de partida para un sistema de enlace de modelos más complejo.
Existe una limitación que realmente debe eliminarse: el tipo de datos Model tiene una conexión privada a la implementación ModelBinding
. Las aplicaciones deberían poder definir sus tipos de modelo de la forma que deseen, no deberían necesitar ampliar la clase de modelo definida aquí. Para abordar eso, agregaremos un parámetro de tipo.
Enlazar al modelo con InheritedWidget
, Finale
Esta versión tiene en cuenta el tipo de modelo. Ahora, el tipo de modelo de la aplicación es el parámetro de tipo de ModelBinding
, y ModelBinding
proporciona las funciones estáticas genéricas de (contexto) y actualizar (contexto).
La implementación es un poco más complicada (nuevamente) para el parámetro de tipo, por lo que se dejó para el final. En el lado positivo, la implementación es todavía muy pequeña, alrededor de 64 líneas de código, y el uso de la clase de widget ModelBinding
y sus dos métodos estáticos debería ser muy sencillo.
La aplicación crea un ModelBinding
de tipo MyModel
como este:
ModelBinding<MyModel>( initialState: MyModel(), child: child, )
El modelo de la aplicación, MyModel
, se recupera y actualiza con versiones genéricas de y métodos de actualización como este:
RaisedButton( onPressed: () { MyModel model = ModelBinding.of<MyModel>(context); ModelBinding.update<MyModel>(context, MyModel(value: model.value + 1)); }, child: Text('Hello World ${ModelBinding.of<MyModel>(context).value}'), )
El resultado es que ModelBinding
et al. puede incluirse de forma segura en una pequeña biblioteca, en lugar de conectarse a la aplicación. Solo es necesario exportar la clase ModelBinding
.
Resumen
Este artículo tenía la intención de explicar los conceptos básicos de las clases StatefulWidget
y InheritedWidget
de Flutter y mostrar cómo se podría usar este último para vincular el modelo de una aplicación a sus widgets.
Hay mucho más para compartir sobre ambas clases y sobre clases relacionadas como InheritedModel
y InheritedNotifier
. Todo eso tendrá que esperar.
ModelBinding
es una clase simple basada en InheritedWidget
que se puede usar para vincular un tipo de modelo arbitrario de tipo T
a los widgets de una aplicación. Úselo por descargando el ejemplo y colocando su página de código en su aplicación. Y si no te funciona: cámbialo.
Ver el ejemplo completo de ModelBinding.
Esta es solo una muestra rápida de artículos recientes sobre este popular tema. El proyecto Flutter mantiene una lista más completa en flutter.io Página de administración de estado.
- Modelo con alcance, El paquete ScopedModel
ScopedModel tiene un modelo y los dependientes se envuelven con un widget ScopedModelDescendant que se reconstruye cuando cambia el modelo. La reconstrucción se maneja mediante una devolución de llamada con un parámetro de modelo.
Espero que te haya sido de utilidad. Gracias por leer este post.
Añadir comentario