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 StatelessWidget
hay "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 InheritedWidget
cada 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 InheritedWidget
para 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 ScopedModel
en la raíz de nuestra aplicación:ScopedModel<MyModel>(
model: MyModel(),
child: MyApp(...)
)
Ahora cualquier widget descendente podrá acceder MyModel
usando 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';
},
);
}
}
ScopedModel
ganó 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 Model
y 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 StatelessWidget
y 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 myBloc
en el ejemplo anterior? ¿Cómo llamamos dispose()
para deshacernos de él? Las transmisiones requieren uso StreamController
, lo cual debería ser closed
tan 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 State
en StatefulWidget
tiene 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
Provider
Es 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 Provider
será el paquete preferido para administrar el estado. Por supuesto, también se permiten otros paquetes, pero si tiene alguna duda, Google recomienda visitarlo Provider
.Provider
construido "con widgets, para widgets".Provider
le 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 Provider
ayuda 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, Provider
es 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,Provider
viene 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 Provider
donde se necesita:import 'package:provider/provider.dart';
Proveedor baseCree la base Provide
r 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 builder
crea 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 MyWidget
obtiene una instancia MyModel
utilizando el widget Consumer . Este widget nos proporciona builder
nuestro 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 Consumer
es ú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 MyWidget
que 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 Provider
lo tanto, no podrá "ver" que hemos actualizado correctamente la propiedad foo
y ordenado que el widget se MyWidget
actualice en respuesta.ChangeNotifierProviderPero hay esperanza! Puede hacer que nuestra clase MyModel
implemente 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 foo
con getter
y 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 OtherWidget
actualizaciones actualicen la propiedad foo
en la instancia MyModel
, se MyWidget
actualizará automáticamente para reflejar este cambio. ¿Guay, verdad?Por cierto. Probablemente haya notado un controlador de botones OtherWidget
con 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 OtherWidget
tan pronto como cambie el modelo MyModel
. Quizás no necesitamos esto. Al final, OtherWidget
simplemente 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 Provider
así como así.StreamProviderA primera vista, no está claro por qué es necesario StreamProvider
. Al final, puede usar lo habitual StreamBuilder
si necesita consumir una transmisión en Flutter. Por ejemplo, aquí escuchamos la transmisión onAuthStateChanged
proporcionada 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 StreamProvider
de 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.FutureProviderSimilar al ejemplo anterior, FutureProvider
es una alternativa al estándar FutureBuilder
cuando 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 Consumer
que en el ejemplo StreamProvider
anterior.ValueListenableProviderValueListenable 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, ValueNotifier
utiliza 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 Consumer
anidada :ValueListenableProvider
counter
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 counter
desde 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 counter
desde 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++;
},
);
}
}
MultiProviderSi 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(...)
)
)
)
MultiProvider
Le 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(...),
)
ProxyProviderProxyProvider
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) {
},
);
Conclusión
Cuando se usa, InheritedWidget
Provider
le 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 Provider
ha convertido en un paquete que vale la pena probar sin demora.