Detalles sobre el paquete de proveedor para Flutter

Hola Habr!

Nuestros planes a mediano plazo incluyen el lanzamiento del libro Flutter. Con respecto al lenguaje de Dart como tema, aún tomamos una posición más cautelosa, por lo que intentaremos evaluar su relevancia de acuerdo con los resultados de este artículo. Se centrará en el paquete del proveedor y, por lo tanto, en la gestión del estado en Flutter.

El proveedor es un paquete de gestión estatal escrito por Remy Rusle y adoptado por Google y la comunidad Flutter. Pero, ¿qué es la gestión estatal? Para empezar, ¿qué es una condición? Permítame recordarle que el estado son solo datos para representar la IU en su aplicación. La administración del estado es un enfoque para crear estos datos, acceder, manejarlos y eliminarlos. Para comprender mejor el paquete del proveedor, resumimos brevemente la historia de la administración del estado en Flutter.

1. StatefulWidget


StatelessWidget es un componente de interfaz de usuario simple que se muestra solo cuando tiene datos. No StatelessWidgethay "memoria"; se crea y destruye según sea necesario. Flutter también tiene un StatefulWidget , en el que hay una memoria, gracias a él un satélite de larga duración: el objeto State . Esta clase tiene un método setState(), cuando se llama, se inicia un widget que reconstruye el estado y lo muestra en una nueva forma. Esta es la forma más simple de administración de estado de Flutter que se proporciona de inmediato. Aquí hay un ejemplo con un botón que siempre muestra la última vez que se presionó:

class _MyWidgetState extends State<MyWidget> {
  DateTime _time = DateTime.now();  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text(_time.toString()),
      onPressed: () {
        setState(() => _time = DateTime.now());
      },
    );
  }
}

Entonces, ¿cuál es el problema con este enfoque? Suponga que su aplicación tiene un estado global almacenado en la raíz StatefulWidget. Contiene datos destinados a su uso en varias partes de la interfaz de usuario. Estos datos se comparten y se pasan a cada widget secundario en forma de parámetros. Cualquier evento durante el cual se planee cambiar estos datos aparecerá en forma de devoluciones de llamada. Por lo tanto, a través de todos los widgets intermedios, se transfieren muchos parámetros y devoluciones de llamada, lo que pronto puede generar confusión. Peor aún, cualquier actualización de la raíz mencionada anteriormente conducirá a una reconstrucción del árbol de widgets completo, lo cual es ineficiente.

2. InheritedWidget


InheritedWidget es un widget especial cuyos descendientes pueden acceder a él sin un enlace directo. Con solo pasar a InheritedWidget, un widget consumidor puede registrarse para una reconstrucción automática, lo que ocurrirá al reconstruir un widget ancestro. Esta técnica le permite organizar de manera más eficiente la actualización de la interfaz de usuario. En lugar de reconstruir grandes partes de la aplicación en respuesta a un pequeño cambio de estado, puede seleccionar selectivamente solo aquellos widgets específicos que necesitan ser reconstruidos. Ya has trabajado con InheritedWidgetcada vez que usaste MediaQuery.of(context)o Theme.of(context). Es cierto que es menos probable que haya implementado su propio InheritedWidget con preservación del estado. El hecho es que implementarlos correctamente no es fácil.

3. ScopedModel


ScopedModel es un paquete creado en 2017 por Brian Egan, que facilita el uso InheritedWidgetpara almacenar el estado de la aplicación. Primero debe crear un objeto de estado que herede del Modelo y luego llamarlo notifyListeners()cuando cambien sus propiedades. La situación recuerda a la implementación de la interfaz PropertyChangeListener en Java.

class MyModel extends Model {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

Para proporcionar nuestro objeto de estado, envolvemos este objeto en un widget ScopedModelen la raíz de nuestra aplicación:

ScopedModel<MyModel>(
  model: MyModel(),
  child: MyApp(...)
)

Ahora cualquier widget descendente podrá acceder MyModelusando el widget ScopedModelDescendant . La instancia del modelo se pasa al parámetro builder:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<MyModel>(
      builder: (context, child, model) => Text(model.foo),
    );
  }
}

Cualquier widget descendiente también podrá actualizar el modelo, lo que provocará automáticamente una reconstrucción de cualquiera ScopedModelDescendants(siempre que nuestro modelo llame correctamente notifyListeners()):

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = ScopedModel.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

ScopedModelganó popularidad en Flutter como una herramienta para la gestión del estado, pero su uso se limita a la provisión de objetos que heredan la clase Modely usan este patrón de notificación de cambios.

4. BLoC


En la conferencia Google I / O '18 , se introdujo el patrón del Componente lógico de negocios (BLoC), que sirve como otra herramienta más para extraer el estado de los widgets. Las clases BLoC son componentes no UI de larga duración que conservan el estado y lo exponen como flujos y receptores. Llevando el estado y la lógica empresarial más allá de la interfaz de usuario, puede implementar el widget de manera simple StatelessWidgety usar StreamBuilder para la reconstrucción automática. Como resultado, el widget "se vuelve tonto" y se vuelve más fácil de probar.

Ejemplo de clase BLoC:

class MyBloc {
  final _controller = StreamController<MyType>();  Stream<MyType> get stream => _controller.stream;
  StreamSink<MyType> get sink => _controller.sink;
  
  myMethod() {
    //  
    sink.add(foo);
  }  dispose() {
    _controller.close();
  }
}
 ,   BLoC:
@override
Widget build(BuildContext context) {
 return StreamBuilder<MyType>(
  stream: myBloc.stream,
  builder: (context, asyncSnapshot) {
    //  
 });
}

El problema con el patrón BLoC es que no es obvio cómo crear y destruir objetos BLoC. ¿Cómo se creó la instancia myBlocen el ejemplo anterior? ¿Cómo llamamos dispose()para deshacernos de él? Las transmisiones requieren uso StreamController, lo cual debería ser closedtan pronto como sea innecesario; esto se hace para evitar pérdidas de memoria. (No hay tal cosa como un destructor de clase en Dart, sólo una clase Stateen StatefulWidgettiene un método dispose()). Además, no está claro cómo compartir este BLoC entre múltiples widgets. A menudo es difícil para los desarrolladores dominar BLoC. Hay varios paquetes que intentan simplificar esto.

5. Proveedor


ProviderEs un paquete escrito en 2018 por Remy Rusle, similar a ScopedModel, pero cuyas funciones no se limitan a, proporcionar una subclase de Modelo. Esto también es un contenedor que concluye InheritedWidget, pero el proveedor puede proporcionar cualquier objeto de estado, incluidos BLoC, flujos, futuros y otros. Dado que el proveedor es tan simple y flexible, Google anunció en la conferencia Google I / O '19 que en el futuro Providerserá el paquete preferido para administrar el estado. Por supuesto, también se permiten otros paquetes, pero si tiene alguna duda, Google recomienda visitarlo Provider.

Providerconstruido "con widgets, para widgets".Providerle permite colocar cualquier objeto con un estado en el árbol de widgets y abrir el acceso a él para cualquier otro widget (hijo). También Providerayuda a administrar la vida útil de los objetos de estado al inicializarlos con datos y realizar una limpieza después de que se eliminan del árbol de widgets. ¡Por lo tanto, Provideres adecuado incluso para implementar componentes BLoC o puede servir como base para otras soluciones de gestión estatal! O simplemente se usa para implementar dependencias , un término elegante que significa transferir datos a widgets de una manera que le permite aflojar la conexión y mejorar la capacidad de prueba del código. Finalmente,Providerviene con un conjunto de clases especializadas, gracias a las cuales es aún más conveniente de usar. A continuación, veremos más de cerca cada una de estas clases.

  • Proveedor Básico
  • ChangeNotifierProvider
  • StreamProvider
  • Proveedor futuro
  • ValueListenableProvider
  • MultiProvider
  • Proxyprovider

Instalación


Para usarlo Provider, primero agregue una dependencia a nuestro archivo pubspec.yaml:

provider: ^3.0.0

Luego importamos el paquete Providerdonde se necesita:

import 'package:provider/provider.dart';

Proveedor base

Cree la base Provider en la raíz de nuestra aplicación; Esto contendrá una instancia de nuestro modelo:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

El parámetro buildercrea una instancia MyModel. Si desea pasarle una instancia existente, use el constructor aquí Provider.value.

Luego puede consumir esta instancia del modelo en cualquier lugar MyApp, utilizando el widget Consumer:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyModel>(
      builder: (context, value, child) => Text(value.foo),
    );
  }
}

En el ejemplo anterior, la clase MyWidgetobtiene una instancia MyModelutilizando el widget Consumer . Este widget nos proporciona buildernuestro objeto en el parámetro value.

Ahora, ¿qué debemos hacer si queremos actualizar los datos en nuestro modelo? Digamos que tenemos otro widget donde, cuando se hace clic en un botón, la propiedad debe actualizarse foo:

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

Tenga en cuenta la sintaxis específica utilizada para acceder a la instancia MyModel. Funcionalmente, esto es equivalente a acceder al widget Consumer. El widget Consumeres útil en los casos en que el código no puede obtener fácilmente el enlace BuildContext.

¿Qué crees que pasará con el widget original MyWidgetque creamos anteriormente? ¿Se mostrará un nuevo significado en él bar? Lamentablemente no . No es posible escuchar los cambios en los antiguos objetos Dart tradicionales (al menos sin reflexión, que no se proporciona en Flutter). Por Providerlo tanto, no podrá "ver" que hemos actualizado correctamente la propiedad fooy ordenado que el widget se MyWidgetactualice en respuesta.

ChangeNotifierProvider

Pero hay esperanza! Puede hacer que nuestra clase MyModelimplemente una impureza ChangeNotifier. Cambiará un poco la implementación de nuestro modelo y llamaremos a un método especial notifyListeners()cada vez que cambie una de nuestras propiedades. Funciona aproximadamente de la misma manera ScopedModel, pero en este caso es bueno que no necesite heredar de una clase particular del modelo. Es suficiente para darse cuenta de la mezcla ChangeNotifier. Así es como se ve:

class MyModel with ChangeNotifier {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

Como puede ver, reemplazamos nuestra propiedad foocon gettery setter, respaldada por la variable privada _foo. De esta forma, podemos "interceptar" cualquier cambio realizado en la propiedad foo y dejar que nuestros oyentes sepan que nuestro objeto ha cambiado.

Ahora, desde afuera Provider, podemos cambiar nuestra implementación para que use una clase diferente llamada ChangeNotifierProvider:

ChangeNotifierProvider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

¡Me gusta esto! Ahora, cuando nuestras OtherWidgetactualizaciones actualicen la propiedad fooen la instancia MyModel, se MyWidgetactualizará automáticamente para reflejar este cambio. ¿Guay, verdad?

Por cierto. Probablemente haya notado un controlador de botones OtherWidgetcon el que usamos la siguiente sintaxis:

final model = Provider.of<MyModel>(context);

De manera predeterminada, esta sintaxis provocará automáticamente la reconstrucción de la instancia OtherWidgettan pronto como cambie el modelo MyModel. Quizás no necesitamos esto. Al final, OtherWidgetsimplemente contiene un botón que no cambia en absoluto cuando cambia el valor MyModel. Para evitar la reconstrucción, puede usar la siguiente sintaxis para acceder a nuestro modelo sin registrarse para la reconstrucción:

final model = Provider.of<MyModel>(context, listen: false);

Este es otro encanto proporcionado en el paquete Providerasí como así.

StreamProvider

A primera vista, no está claro por qué es necesario StreamProvider. Al final, puede usar lo habitual StreamBuildersi necesita consumir una transmisión en Flutter. Por ejemplo, aquí escuchamos la transmisión onAuthStateChangedproporcionada por FirebaseAuth:

@override
Widget build(BuildContext context {
  return StreamBuilder(
   stream: FirebaseAuth.instance.onAuthStateChanged, 
   builder: (BuildContext context, AsyncSnapshot snapshot){ 
     ...
   });
}

Para hacer lo mismo con ayuda Provider, podríamos proporcionar nuestra transmisión a través StreamProviderde la raíz de nuestra aplicación:

StreamProvider<FirebaseUser>.value(
  stream: FirebaseAuth.instance.onAuthStateChanged,
  child: MyApp(...),
}

Luego consuma el widget hijo, como generalmente se hace con Provider:

@override
Widget build(BuildContext context) {
  return Consumer<FirebaseUser>(
    builder: (context, value, child) => Text(value.displayName),
  );
}

Nuestro código de widget no solo se ha vuelto mucho más limpio, sino que también resume el hecho de que los datos provienen de la transmisión. Si alguna vez decidimos cambiar la implementación base, por ejemplo, a FutureProvider, entonces no se requerirán cambios en el código del widget. Como verá, esto se aplica a todos los demás proveedores que se muestran a continuación.

FutureProvider

Similar al ejemplo anterior, FutureProvideres una alternativa al estándar FutureBuildercuando se trabaja con widgets. Aquí hay un ejemplo:

FutureProvider<FirebaseUser>.value(
  value: FirebaseAuth.instance.currentUser(),
  child: MyApp(...),
);

Para consumir este valor en el widget secundario, utilizamos la misma implementación Consumerque en el ejemplo StreamProvideranterior.

ValueListenableProvider

ValueListenable es una interfaz Dart implementada por la clase ValueNotifier que toma un valor y notifica a los oyentes cuando cambia a otro valor. Es posible, por ejemplo, envolver un contador entero en una clase de modelo simple:

class MyModel {
  final ValueNotifier<int> counter = ValueNotifier(0);  
}

Cuando se trabaja con tipos complejos, ValueNotifierutiliza el operador del ==objeto almacenado en él para determinar si el valor ha cambiado.
Creemos el más simple Provider, que contendrá nuestro modelo principal, y será seguido por una propiedad de escucha Consumeranidada :ValueListenableProvidercounter

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: Consumer<MyModel>(builder: (context, value, child) {
    return ValueListenableProvider<int>.value(
      value: value.counter,
      child: MyApp(...)
    }
  }
}

Tenga en cuenta que este proveedor anidado es de tipo int. Puede haber otros. Si tiene varios proveedores del mismo tipo registrados, el Proveedor devolverá el "más cercano" (antepasado más cercano).

Aquí se explica cómo escuchar una propiedad counterdesde cualquier widget secundario:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<int>(
      builder: (context, value, child) {
        return Text(value.toString());
      },
    );
  }
}

Pero aquí se explica cómo actualizar una propiedad counterdesde otro widget. Tenga en cuenta: necesitamos acceso a la copia original MyModel.

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.counter.value++;
      },
    );
  }
}

MultiProvider

Si usa muchos widgetsProvider, en la raíz de la aplicación obtendrá una estructura fea de muchos archivos adjuntos:

Provider<Foo>.value( 
  value: foo, 
  child: Provider<Bar>.value( 
    value: bar, 
    child: Provider<Baz>.value( 
      value: baz , 
      child: MyApp(...)
    ) 
  ) 
)

MultiProviderLe permite declararlos a todos en el mismo nivel. Es solo azúcar sintáctico: a nivel intra-sistema, todos permanecen anidados de todos modos.

MultiProvider( 
  providers: [ 
    Provider<Foo>.value(value: foo), 
    Provider<Bar>.value(value: bar), 
    Provider<Baz>.value(value: baz), 
  ], 
  child: MyApp(...), 
)

ProxyProvider

ProxyProvider es una clase interesante agregada en el tercer lanzamiento del paqueteProvider. Le permite declarar proveedores que pueden depender de otros proveedores, hasta seis en uno. En este ejemplo, la clase Bar es específica de la instanciaFoo. Esto es útil al compilar un conjunto raíz de servicios que dependen entre sí.

MultiProvider ( 
  providers: [ 
    Provider<Foo> ( 
      builder: (context) => Foo(),
    ), 
    ProxyProvider<Foo, Bar>(
      builder: (context, value, previous) => Bar(value),
    ), 
  ], 
  child: MyApp(...),
)

El primer argumento de tipo genérico es el tipo del que depende el suyo ProxyProvider, y el segundo es el tipo que devuelve.

Cómo escuchar a muchos proveedores al mismo tiempo


¿Qué sucede si queremos que un solo widget escuche a muchos proveedores y se reconstruya cuando alguno de ellos cambie? Puede escuchar hasta 6 proveedores al mismo tiempo utilizando las opciones de widgets Consumer. Recibiremos instancias como parámetros de métodos adicionales builder.

Consumer2<MyModel, int>(
  builder: (context, value, value2, child) {
    //value  MyModel
    //value2  int
  },
);

Conclusión


Cuando se usa, InheritedWidget Providerle permite administrar el estado como es habitual en Flutter. Permite que los widgets accedan a objetos de estado y los escuchen de tal manera que se abstraiga el mecanismo de notificación subyacente. Es más fácil administrar la vida útil de los objetos de estado creando puntos de anclaje para crear estos objetos según sea necesario y deshacerse de ellos cuando sea necesario. Este mecanismo se puede utilizar para implementar fácilmente dependencias e incluso como base para opciones de administración de estado más avanzadas. Con la bendición de Google y el creciente apoyo de la comunidad de Flutter, se Providerha convertido en un paquete que vale la pena probar sin demora.

All Articles