Ahora que conoce la programación de IU declarativa y la diferencia entre el estado efímero y el estado de la aplicación , está listo para aprender a administrar fácilmente el estado de la aplicación.
Usaremos el paquete
provider
. Si eres nuevo en Flutter y no tienes una razón convincente para elegir un enfoque diferente (Redux, Rx, hooks, etc.), este es probablemente el mejor enfoque para comenzar. El paquete provider
es fácil de aprender y no requiere mucho código. También opera con conceptos que son aplicables en todos los demás enfoques.
Sin embargo, si ya tiene mucha experiencia en la gestión del estado de otros marcos reactivos, puede buscar otros paquetes y tutoriales que se enumeran en la página de opciones .
Ejemplo
Como ejemplo, considere la siguiente aplicación simple.
La aplicación tiene dos pantallas separadas: catálogo y carrito de compras (representadas por widgets
MyCatalog
y MyCart
respectivamente). En este caso, esta es una aplicación de compras, pero puede imaginar la misma estructura en una aplicación de red social simple (reemplace el catálogo con "muro" y el carrito con "favoritos").
La pantalla del catálogo incluye una barra de aplicaciones personalizable (
MyAppBar
) y una vista de desplazamiento de varios elementos de la lista ( MyListItems
).
Aquí está la aplicación en forma de árbol de widgets:
Entonces, tenemos al menos 5 subclases
Widget
. Muchos de ellos necesitan acceso para declarar que no son propietarios. Por ejemplo, cadaMyListItem
debería poder agregarse al carrito. También es posible que deban comprobar si el artículo que se muestra actualmente está en el carrito.
Esto nos lleva a nuestra primera pregunta: ¿dónde deberíamos poner el estado actual del cubo?
Condición creciente
En Flutter, tiene sentido colocar el estado sobre los widgets que lo usan.
¿Para qué? En marcos declarativos como Flutter, si desea cambiar la interfaz de usuario, debe reconstruirla. No puedes simplemente ir y escribir
MyCart.updateWith(somethingNew)
. En otras palabras, es difícil forzar el cambio del widget desde el exterior llamando a un método. E incluso si pudiera hacer que funcione, estaría luchando contra el marco en lugar de dejar que lo ayude.
// :
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
Incluso si logra que el código anterior funcione, debe lidiar
MyCart
con lo siguiente en el widget :
// :
Widget build(BuildContext context) {
return SomeWidget(
// .
);
}
void updateWith(Item item) {
// - UI.
}
Deberá tener en cuenta el estado actual de la interfaz de usuario y aplicarle los nuevos datos. Será difícil evitar errores aquí.
En Flutter, creas un nuevo widget cada vez que cambia su contenido. En lugar de
MyCart.updateWith(somethingNew)
(llamada al método), usa MyCart(contents)
(constructor). Dado que solo puede crear nuevos widgets en los métodos de compilación de sus padres, si desea cambiarlo contents
, debe estar en el padre MyCart
o en una versión superior.
//
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}
Ahora
MyCart
solo tiene una ruta de ejecución de código para crear cualquier versión de la interfaz de usuario.
//
Widget build(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
return SomeWidget(
// , .
// ···
);
}
En nuestro ejemplo,
contents
debería estar en formato MyApp
. Cada vez que cambia, reconstruye MyCart en la parte superior (más sobre eso más adelante). De esta MyCart
manera, no tiene que preocuparse por el ciclo de vida, solo declara qué mostrar para cualquier contenido dado. Cuando cambie, el widget antiguo MyCart
desaparecerá y será reemplazado por completo por el nuevo.
Esto es lo que queremos decir cuando decimos que los widgets son inmutables. No cambian, son reemplazados.
Ahora que sabemos dónde colocar el estado del depósito, veamos cómo acceder a él.
Acceso estatal
Cuando un usuario hace clic en uno de los artículos del catálogo, se agrega al carrito. Pero dado que el carrito se acabó
MyListItem
, ¿cómo hacemos esto?
Una opción simple es proporcionar una devolución de llamada que
MyListItem
se pueda invocar al hacer clic. Las funciones de Dart son objetos de primera clase, por lo que puede pasarlas de la forma que desee. Entonces, internamente, MyCatalog
puede definir lo siguiente:
@override
Widget build(BuildContext context) {
return SomeWidget(
// , .
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
print('user tapped on $item');
}
Esto funciona bien, pero para el estado de la aplicación que necesita cambiar desde muchos lugares diferentes, tendrá que pasar muchas devoluciones de llamada, lo que se vuelve aburrido con bastante rapidez.
Afortunadamente, Flutter tiene mecanismos que permiten que los widgets proporcionen datos y servicios a sus descendientes (en otras palabras, no solo a sus descendientes, sino a los widgets posteriores). Como era de esperar de un trémolo, donde todo es un Widget , estos mecanismos son simplemente tipos especiales de widgets:
InheritedWidget
, InheritedNotifier
, InheritedModel
y otros. No los describiremos aquí porque están un poco fuera de línea con lo que estamos tratando de hacer.
En su lugar, usaremos un paquete que funciona con widgets de bajo nivel pero que es fácil de usar. Se llama
provider
.
Con,
provider
no necesita preocuparse por las devoluciones de llamada o InheritedWidgets
. Pero necesitas entender 3 conceptos:
- ChangeNotifier
- ChangeNotifierProvider
- Consumidor
ChangeNotifier
ChangeNotifier
Es una clase simple incluida en el SDK de Flutter que proporciona notificaciones de cambio de estado a sus oyentes. En otras palabras, si algo lo es ChangeNotifier
, puede suscribirse a sus cambios. (Esta es una forma de observable - para aquellos que no están familiarizados con el término.)
ChangeNotifier
En provider
es una forma de encapsular el estado de la aplicación. Para aplicaciones muy simples, puede arreglárselas con uno ChangeNotifier
. En los más complejos, tendrás varios modelos y por tanto varios ChangeNotifiers
. (No es necesario usar ChangeNotifier
con en absoluto provider
, pero es fácil trabajar con esta clase).
En nuestra aplicación de compras de muestra, queremos administrar el estado del carrito en
ChangeNotifier
. Creamos una nueva clase que la amplía, por ejemplo:
class CartModel extends ChangeNotifier {
/// .
final List<Item> _items = [];
/// .
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// ( , 42 ).
int get totalPrice => _items.length * 42;
/// [item] . [removeAll] - .
void add(Item item) {
_items.add(item);
// , , .
notifyListeners();
}
/// .
void removeAll() {
_items.clear();
// , , .
notifyListeners();
}
}
El único fragmento de código específico
ChangeNotifier
es la llamada notifyListeners()
. Llame a este método cada vez que el modelo cambie de tal manera que pueda reflejarse en la interfaz de usuario de su aplicación. Todo lo demás CartModel
es el modelo en sí y su lógica empresarial.
ChangeNotifier
es parte flutter:foundation
y no depende de ninguna clase de nivel superior en Flutter. Es fácil de probar (ni siquiera necesita usar la prueba de widgets para eso). Por ejemplo, aquí hay una prueba unitaria simple CartModel
:
test('adding item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
});
cart.add(Item('Dash'));
});
ChangeNotifierProvider
ChangeNotifierProvider
Es un widget que proporciona una instancia a ChangeNotifier
sus hijos. Viene en un paquete provider
.
Ya sabemos dónde colocarlo
ChangeNotifierProvider
: encima de los widgets que necesitan acceder a él. En caso de CartModel
que implique algo arriba MyCart
y MyCatalog
.
No desea publicar
ChangeNotifierProvider
más de lo necesario (porque no desea contaminar el alcance). Pero en nuestro caso, el único widget que se acabó MyCart
y MyCatalog
- eso MyApp
.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: MyApp(),
),
);
}
Tenga en cuenta que estamos definiendo un constructor que crea una nueva instancia lo
CartModel.
ChangeNotifierProvider
suficientemente inteligente como para no reconstruir a CartModel
menos que sea absolutamente necesario. También llama automáticamente a dispose () en CartModel cuando la instancia ya no es necesaria.
Si desea proporcionar más de una clase, puede usar
MultiProvider
:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: MyApp(),
),
);
}
Consumidor
Ahora que se
CartModel
proporciona a los widgets en nuestra aplicación a través de la declaración ChangeNotifierProvider
en la parte superior, podemos comenzar a usarlo.
Esto se hace a través de un widget
Consumer
.
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text("Total price: ${cart.totalPrice}");
},
);
Tenemos que especificar el tipo de modelo al que queremos acceder. En este caso, lo necesitamos
CartModel
, así que escribimos Consumer<
CartModel>
. Si no especifica generic ( <
CartModel>
), el paquete provider
no podrá ayudarlo. provider
se basa en el tipo y sin el tipo no entenderá lo que desea.
El único argumento requerido para el widget
Consumer
es builder
. Builder es una función que se llama al cambiar ChangeNotifier
. (En otras palabras, cuando llama notifyListeners()
a su modelo, se llaman a todos los métodos de construcción de todos los widgets relevantes Consumer
). Se llama al
constructor con tres argumentos. La primera es
context
, que también obtienes en cada método de construcción.
El segundo argumento de la función del constructor es una instancia
ChangeNotifier
... Esto es lo que pedimos desde el principio. Puede utilizar los datos del modelo para determinar cómo debe verse la interfaz de usuario en un punto determinado.
El tercer argumento es
child
que es necesario para la optimización. Si tiene un subárbol grande de widgets debajo del suyo Consumer
que no cambia cuando cambia el modelo, puede construirlo una vez y obtenerlo a través del constructor.
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// SomeExhibitedWidget, .
child,
Text("Total price: ${cart.totalPrice}"),
],
),
// .
child: SomeExpensiveWidget(),
);
Es mejor colocar sus widgets de consumidor lo más profundo posible en el árbol. No desea reconstruir grandes partes de la interfaz de usuario solo porque algunos detalles han cambiado en alguna parte.
//
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);
En lugar de esto:
//
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
Provider.of
A veces, realmente no necesita los datos del modelo para cambiar la interfaz de usuario, pero aún necesita acceder a él. Por ejemplo, un botón
ClearCart
permite al usuario eliminar todo del carrito. No es necesario mostrar el contenido del carrito, simplemente llame al clear()
.
Podríamos usarlo
Consumer<
CartModel>
para eso, pero sería un desperdicio. Le pedimos al marco que reconstruya el widget, que no necesita ser reconstruido.
Para este caso de uso, podemos usar
Provider.of
con el parámetro listen
establecido en false
.
Provider.of<CartModel>(context, listen: false).removeAll();
El uso de la línea anterior en el método de construcción no reconstruirá este widget cuando se llame
notifyListeners
.
Poniendolo todo junto
Puede consultar el ejemplo que se analiza en este artículo. Si necesita algo un poco más simple, vea cómo se ve una aplicación de contador simple creada con el proveedor .
Cuando estés listo para jugar
provider
contigo mismo, recuerda agregar su dependencia a la tuya primero pubspec.yaml
.
name: my_name
description: Blah blah blah.
# ...
dependencies:
flutter:
sdk: flutter
provider: ^3.0.0
dev_dependencies:
# ...
Ahora puedes
'package:provider/provider.dart'
; y empezar a construir ...