Flutter.dev: Gestión simple del estado de la aplicación

Hola. En septiembre, OTUS lanzará un nuevo curso , Flutter Mobile Developer . La víspera del inicio del curso, tradicionalmente hemos preparado una traducción útil para usted.








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 MyCatalogy MyCartrespectivamente). 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, cadaMyListItemdeberí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 MyCartcon 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 MyCarto en una versión superior.



// 
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}




Ahora MyCartsolo 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, contentsdebería estar en formato MyApp. Cada vez que cambia, reconstruye MyCart en la parte superior (más sobre eso más adelante). De esta MyCartmanera, no tiene que preocuparse por el ciclo de vida, solo declara qué mostrar para cualquier contenido dado. Cuando cambie, el widget antiguo MyCartdesaparecerá 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 MyListItemse 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, MyCatalogpuede 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, InheritedModely 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, providerno necesita preocuparse por las devoluciones de llamada o InheritedWidgets. Pero necesitas entender 3 conceptos:



  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumidor




ChangeNotifier



ChangeNotifierEs 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.)



ChangeNotifierEn provideres 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 ChangeNotifiercon 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 ChangeNotifieres 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 CartModeles el modelo en sí y su lógica empresarial.



ChangeNotifieres parte flutter:foundationy 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



ChangeNotifierProviderEs un widget que proporciona una instancia a ChangeNotifiersus hijos. Viene en un paquete provider.



Ya sabemos dónde colocarlo ChangeNotifierProvider: encima de los widgets que necesitan acceder a él. En caso de CartModelque implique algo arriba MyCarty MyCatalog.



No desea publicar ChangeNotifierProvidermás de lo necesario (porque no desea contaminar el alcance). Pero en nuestro caso, el único widget que se acabó MyCarty 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. ChangeNotifierProvidersuficientemente inteligente como para no reconstruir a CartModelmenos 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 CartModelproporciona a los widgets en nuestra aplicación a través de la declaración ChangeNotifierProvideren 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 providerno podrá ayudarlo. providerse basa en el tipo y sin el tipo no entenderá lo que desea.



El único argumento requerido para el widget Consumeres 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 instanciaChangeNotifier... 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 childque es necesario para la optimización. Si tiene un subárbol grande de widgets debajo del suyo Consumerque 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 ClearCartpermite 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.ofcon el parámetro listenestablecido 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 providercontigo 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 ...






All Articles