Conceptos básicos de Dart Streams

Esta es la segunda parte de mi serie sobre Flutter Architecture:



Las transmisiones son el componente principal de RxVMS , su comprensión es absolutamente necesaria para trabajar con esta biblioteca, por lo que nos detendremos en ellas con más detalle en esta publicación.


Resultó que incluir Rx en esta publicación lo haría demasiado largo, así que lo dividí en dos partes.


Déjalo fluir


Leí muchos comentarios que dicen que las transmisiones, y especialmente Rx, son demasiado complicadas de entender y, como resultado, de usar.


Me gustaría que supieras que no me considero un gurú de Rx. Aprovechar todo su poder no es fácil, y admito que sigo estudiando. Pero permítame corregir un error desde el principio: no tiene que ser un asistente de Rx para comenzar a obtener muchos beneficios al usar hilos y esta tecnología . Haré todo lo posible para explicarle los flujos de la manera más accesible.


¿Qué son los hilos?


En mi opinión, la mejor analogía con los hilos es la cinta transportadora. Puede poner algo en un extremo y este "algo" se transferirá automáticamente al otro. A diferencia de la tubería física, los hilos manipulan objetos de datos, transfiriéndolos automáticamente desde el principio, pero ¿dónde? Como en la tubería real, si no hay nada para capturar los datos en el otro extremo, simplemente "caerán" y desaparecerán (esto, por supuesto, no es del todo cierto para Dart Streams, pero es mejor manejar las transmisiones como si ese fuera el caso) .



Para evitar la pérdida de datos, puede establecer una "trampa" en la salida de la transmisión. De esta manera, puede capturar datos y realizar las manipulaciones necesarias cuando los objetos de datos lleguen al final de la secuencia.



Recuerda:


  • Si la trampa no está configurada, los datos simplemente desaparecerán para siempre y no habrá forma de recuperarla (de nuevo, no exactamente con Dart Streams, pero es mejor fingir que sí)
  • Después de enviar datos a la secuencia, no es necesario pausar el programa y esperar hasta que llegue al final, todo esto sucede en segundo plano.
  • La trampa puede recibir datos en cualquier momento, no es necesario inmediatamente después del envío (pero no se preocupe, las transmisiones son realmente muy rápidas). Imagine que no sabe qué tan rápido o cuánto se mueve la cinta transportadora. Esto significa que colocar algo en la corriente está completamente separado de la reacción al elemento en el otro extremo. Su trampa funcionará y atrapará el artículo cuando llegue allí. (Algunos de ustedes ya se darán cuenta de que esto encaja bien con la forma reactiva en que Flutter actualiza sus widgets)
  • Puede establecer una trampa mucho antes de que comience el trabajo y aparezca el primer elemento
  • El flujo funciona según el principio FIFO. Los datos siempre vienen en el orden en que se colocan en la secuencia.

¿Qué es el Rx?


Rx, abreviatura de Extensiones reactivas, son corrientes de esteroides. Este es un concepto muy similar a Streams, que fue inventado para el marco .Net por el equipo de Microsoft. Dado que .Net ya tenía el tipo Stream, que se usa para E / S de archivo, llamaron a los flujos Rx Observables y crearon muchas funciones para manipular los datos que pasaban por ellos. Dart tiene Streams integrado en su especificación de idioma que ya ofrece la mayor parte de esta funcionalidad, pero no todas. Por eso se desarrolló el paquete RxDart; Se basa en Dart Streams, pero extiende su funcionalidad. Cubriré Rx y RxDart en la próxima parte de esta serie.


Algunos términos


Dart Streams y Rx usan alguna terminología que puede parecer aterradora, así que aquí está la traducción. Primero viene el término Dart, luego Rx.


  • Stream / Observable . Esta es la "tubería" descrita anteriormente. Stream se puede convertir en Observable y donde sea que se espere Stream, puede asignar un Observable. Así que no se confunda si mezclo estos términos en el proceso de explicar
  • escuchar / suscribirse - establece la trampa del oyente
  • StreamController / Asunto . El lado "izquierdo" de la cinta transportadora donde coloca los datos en la secuencia. Difieren ligeramente en sus propiedades y características, pero tienen el mismo propósito.
  • Emitir un artículo / datos . El momento en que los datos aparecen a la salida de la "tubería"

Creación de transmisiones


Si tiene la intención de continuar estudiando el tema, clone este proyecto con ejemplos. Usaré el sistema de prueba Dart / Flutter.


Para crear una secuencia, crea un StreamController


var controller = new StreamController<String>(); controller.add("Item1"); //      

El tipo de plantilla (en este caso, la cadena) que se pasa al crear StreamController determina el tipo de objetos que podemos enviar a la secuencia. ¡Puede ser CUALQUIER tipo! Puede crear un StreamController<List<MyObject>>() si lo desea y la secuencia transferirá toda la hoja en lugar de un solo objeto.


Ajuste de trampa


Si ejecutó la prueba especificada, entonces no pudo ver nada, porque nada captó nuestra línea en la salida de la transmisión. Ahora pon la trampa:


 var controller = new StreamController<String>(); controller.stream.listen((item) => print(item)); //  controller.add("Item1"); controller.add("Item2"); controller.add("Item3"); 

Ahora la trampa se establece utilizando el método .listen() . El registro se parece a controller.stream.listen , pero si lo desplaza hacia atrás, como una especie de álbum de los años 60, aparecerá el verdadero significado de lo que está escrito: "escuche la transmisión de este controlador"


.listen() pasar una determinada función al método .listen() para manipular de alguna manera los datos entrantes. La función debe aceptar un parámetro del tipo especificado al crear StreamController, en este caso, String.


Si ejecuta el código anterior, verá


 Item1 Item2 Item3 

En mi opinión, el mayor problema para los recién llegados a Streams es que puedes determinar la reacción para el elemento emitido mucho antes de que el primer elemento se ponga en la secuencia, lo que provoca una llamada a esta reacción.


Fin de escuchar


El código anterior perdió la parte pequeña pero importante. listen() devuelve una StreamSubscription , un objeto de suscripción de transmisión. Una llamada a su método .cancel() finaliza la suscripción, liberando recursos y evitando que se llame a su función de escucha después de que sea innecesaria.


 var controller = new StreamController<String>(); StreamSubscription subscription = controller.stream.listen((item) => print(item)); // This is the Trap controller.add("Item1"); controller.add("Item2"); controller.add("Item3"); //    ,        //  ,     Stream   await Future.delayed(Duration(milliseconds: 500)); subscription.cancel; 

Detalles del oyente


La función para listen() puede ser una función lambda o simple.


 void myPrint(String message) { print(message); } StreamSubscription subscription = controller.stream.listen((item) => print(item)); //  - StreamSubscription subscription2 = controller.stream.listen(myPrint); //    StreamSubscription subscription3 = controller.stream.listen((item) { print(item); print(item.toUpperCase); }); // - 

Nota importante: la mayoría de las transmisiones de Dart solo permiten una suscripción única, es decir, no se pueden volver a suscribir después de que se complete la suscripción; esto generará una excepción. Esta es su diferencia con otras implementaciones de Rx.


La firma completa de listen() ve así:


  /* excerpt from the API doc * The [onError] callback must be of type `void onError(error)` or * `void onError(error, StackTrace stackTrace)`. If [onError] accepts * two arguments it is called with the error object and the stack trace * (which could be `null` if the stream itself received an error without * stack trace). * Otherwise it is called with just the error object. * If [onError] is omitted, any errors on the stream are considered unhandled, * and will be passed to the current [Zone]'s error handler. * By default unhandled async errors are treated * as if they were uncaught top-level errors. * * If this stream closes and sends a done event, the [onDone] handler is * called. If [onDone] is `null`, nothing happens. * * If [cancelOnError] is true, the subscription is automatically canceled * when the first error event is delivered. The default is `false`. */ StreamSubscription<T> listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError}); 

Esto significa que puede hacer más que simplemente pasar un controlador por los datos enviados. También puede tener un controlador para errores y otro para cerrar la transmisión en el lado del controlador ( onDone ). Las excepciones que se onError() desde el Stream onError() si lo proporciona, de lo contrario, simplemente se tragan y nunca sabrá que algo salió mal.


Ejemplo de hilo de aleteo


Para facilitar la comprensión de los siguientes capítulos, hice una rama de repositorio separada.
Por favor clonala


Como primer ejemplo, tomé la conocida aplicación de contador que obtienes al crear un nuevo proyecto de Flutter y la reorganicé un poco. Agregué una clase de modelo para mantener el estado de la aplicación, que es básicamente un valor de contador:


 class Model { int _counter = 0; StreamController _streamController = new StreamController<int>(); Stream<int> get counterUpdates => _streamController.stream; void incrementCounter() { _counter++; _streamController.add(_counter); } } 

aquí puede ver una plantilla muy típica: en lugar de publicar todo el StreamController, simplemente publicamos su propiedad Stream.


Para que el modelo esté disponible para la interfaz de usuario, lo convertí en un campo estático en el objeto Aplicación porque no quería ingresar a InheritedWidget o ServiceLocator. Por un simple ejemplo, esto se saldrá con la suya, ¡pero no lo haría en esta aplicación!


Añadir a main.dart :


 class _MyHomePageState extends State<MyHomePage> { int _counter = 0; StreamSubscription streamSubscription; @override void initState() { streamSubscription = MyApp.model.counterUpdates.listen((newVal) => setState(() { _counter = newVal; })); super.initState(); } //   State   ,   , //       @override void dispose() { streamSubscription?.cancel(); super.dispose(); } 

initState() buen lugar para configurar al oyente, y como buenos ciudadanos de Darts, siempre lanzamos la suscripción en dispose() , ¿verdad?


En el árbol de widgets, solo necesitamos adaptar el controlador onPressed del botón FAB (botón con una acción flotante).


 floatingActionButton: new FloatingActionButton( onPressed: MyApp.model.incrementCounter, tooltip: 'Increment', child: new Icon(Icons.add), ), 

De esta manera, creamos una separación limpia entre Ver y Modelo usando Stream.


Aplicar StreamBuilder


Fuente


En lugar de usar initState() y setState() para nuestras necesidades, Flutter viene con un conveniente widget de StreamBuilder . Como habrás adivinado, se necesita una función Stream y un método de construcción que se llama cada vez que Stream devuelve un nuevo valor. Y ahora no necesitamos inicialización explícita y lanzamiento:


 body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Text( 'You have pushed the button this many times:', ), StreamBuilder<int>( initialData: 0, stream: MyApp.model.counterUpdates, builder: (context, snappShot) { String valueAsString = 'NoData'; if (snappShot != null && snappShot.hasData) { valueAsString = snappShot.data.toString(); } return Text( valueAsString, style: Theme.of(context).textTheme.display1, ); }), ], ), ), 

Ya casi hemos terminado, lo prometo. Aquí hay tres cosas que debes saber:


  • La gran ventaja de usar StreamBuilder sobre la primera solución es que llamar a setState() en listen() siempre reorganiza toda la página, mientras que StreamBuilder solo llamará a su builder
  • La variable snapShot contiene los datos más recientes recibidos de Stream. Siempre verifique que contenga datos válidos antes de usarlo.
  • Basado en los principios de inicialización durante, StreamBuilder no puede obtener un valor durante el primer fotograma. Para evitar esto, pasamos el valor de initialData , que se usa para el primer ensamblaje, es decir, para el primer fotograma de la pantalla. Si no pasamos initialData , se initialData nuestro constructor por primera vez con datos no válidos. Una alternativa al uso de initialData es devolver un widget de marcador de posición si snapShot no snapShot válido, que se muestra hasta que obtengamos datos válidos, por ejemplo:


     // ,           StreamBuilder<int>( stream: MyApp.model.databaseUpdates, builder: (context, snappShot) { if (snappShot != null && snappShot.hasData) { return Text( snappShot.data.toString(), style: Theme.of(context).textTheme.display1, ); } //      ,   Spinner return CircularProgressIndicator (); }) 


En la próxima publicación veremos cómo convertir datos en nuestras transmisiones y hacerlo sobre la marcha. Muchas gracias a Scott Stoll por leer las pruebas y comentarios importantes.



Source: https://habr.com/ru/post/450950/


All Articles