Gestión del estado en aplicaciones Flutter


Principios generales


Flutter es un marco reactivo, y para un desarrollador especializado en desarrollo nativo, su filosofía puede ser inusual. Por lo tanto, comenzamos con una breve reseña.


La interfaz de usuario en Flutter, como en la mayoría de los marcos modernos, consiste en un árbol de componentes (widgets). Cuando un componente cambia, este y todos sus componentes secundarios se vuelven a representar (con optimizaciones internas, que se describen a continuación). Cuando la pantalla cambia globalmente (por ejemplo, girando la pantalla), se redibuja todo el árbol de widgets.


Este enfoque puede parecer ineficaz, pero de hecho le da al programador control sobre la velocidad del trabajo. Si actualiza la interfaz al más alto nivel sin necesidad, todo funcionará lentamente, pero con el diseño correcto de los widgets, las aplicaciones en Flutter pueden ser muy rápidas.


Flutter tiene dos tipos de widgets: sin estado y con estado. Los primeros (análogos a los componentes puros en React) no tienen estado y están completamente descritos por sus parámetros. Si las condiciones de visualización no cambian (por ejemplo, el tamaño del área en la que se debe mostrar el widget) y sus parámetros, el sistema reutiliza la representación visual del widget creada anteriormente, por lo que el uso de widgets sin estado tiene un buen efecto en el rendimiento. Al mismo tiempo, de todos modos, cada vez que se redibuja el widget, se crea formalmente un nuevo objeto y se inicia el constructor.


Los widgets con estado mantienen cierto estado entre representaciones. Para hacer esto, son descritos por dos clases. La primera de las clases, el widget en sí, describe los objetos que se crean durante cada representación. La segunda clase describe el estado del widget y sus objetos se transfieren a los objetos de widget creados. Los widgets de estado con estado son una fuente importante de rediseño de interfaz. Para hacer esto, debe cambiar sus propiedades dentro de la llamada al método SetState. Por lo tanto, a diferencia de muchos otros marcos, Flutter no tiene un seguimiento de estado implícito; cualquier cambio en las propiedades del widget fuera del método SetState no conduce a volver a dibujar la interfaz.


Ahora, después de describir los conceptos básicos, puede comenzar con una aplicación simple que utiliza widgets sin estado y con estado:


Aplicación base
import 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new MyHomePage(), ), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { Random rand = Random(); @override Widget build(BuildContext context) { return new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }); } } 

Ejemplo completo


Resultado


Si necesitas condiciones más tenaces


Sigamos adelante. El estado con estado de los widgets se mantiene entre las interfaces de redibujado, pero solo mientras el widget sea necesario, es decir Realmente ubicado en la pantalla. Realicemos un experimento simple: coloque nuestra lista en la pestaña:


Aplicación Tab
 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { Random rand = Random(); TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } 

Ejemplo completo


Resultado


Al inicio, puede ver que al cambiar entre pestañas, el estado se elimina (se llama al método dispose ()), cuando se devuelve, se vuelve a crear (el método initState ()). Esto es razonable, ya que almacenar el estado de widgets no visualizables consumirá recursos del sistema. En el caso en que el estado del widget debe sobrevivir a su ocultamiento completo, son posibles varios enfoques:


Primero, puede usar objetos separados (ViewModel) para almacenar el estado. Dart a nivel de lenguaje admite constructores de fábrica que se pueden usar para crear fábricas y singletones que almacenan los datos necesarios.


Me gusta más este enfoque, porque Le permite aislar la lógica empresarial de la interfaz de usuario. Esto es especialmente cierto debido al hecho de que Flutter Release Preview 2 agregó la capacidad de crear interfaces de píxeles perfectos para iOS, pero debe hacerlo, por supuesto, en los widgets correspondientes.


En segundo lugar, es posible utilizar el enfoque de elevación de estado, familiar para los programadores de React, cuando los datos se almacenan en componentes ubicados aguas arriba. Como Flutter vuelve a dibujar la interfaz solo cuando se llama al método setState (), estos datos se pueden cambiar y usar sin renderizar. Este enfoque es algo más complejo y aumenta la conectividad de los widgets en la estructura, pero le permite especificar el nivel de almacenamiento de datos puntual.


Finalmente, hay bibliotecas de almacenamiento de estado como flutter_redux .


Para simplificar, usamos el primer enfoque. Hagamos una clase ListData separada, singleton, que almacena los valores para nuestra lista. Al mostrar, usaremos esta clase.


Aplicación de recuperación de datos con pestañas
 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: ListData().build), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListData { static ListData _instance = ListData._internal(); ListData._internal(); factory ListData() { return _instance; } Random _rand = Random(); Map<int, int> _values = new Map(); Widget build (BuildContext context, int index) { if (!_values.containsKey(index)) { _values[index] = _rand.nextInt(100); } return Text('Random number ${_values[index]}',); } } 

Ejemplo completo


Resultado


Guardar una posición de desplazamiento


Si se desplaza hacia abajo en la lista del ejemplo anterior, luego cambia entre las pestañas, es fácil notar que la posición de desplazamiento no se guarda. Esto es lógico, ya que no está almacenado en nuestra clase ListData, y el propio estado del widget no sobrevive al cambiar entre pestañas. Implementamos el almacenamiento de estado de desplazamiento manualmente, pero por diversión lo agregaremos no a una clase separada y no a ListData, sino a un estado de nivel superior para mostrar cómo trabajar con él.


Preste atención a los widgets ScrollController y NotificationListener (así como al DefaultTabController utilizado anteriormente). El concepto de widgets que no tienen su propia pantalla debería ser familiar para los desarrolladores que trabajan con React / Redux: los componentes del contenedor se usan activamente en este paquete. En Flutter, los widgets que no se muestran se usan comúnmente para agregar funcionalidad a los widgets secundarios. Esto le permite dejar los widgets visuales ligeros y no procesar eventos del sistema donde no son necesarios.


El código se basa en la solución propuesta por Marcin Szałek en Stakoverflow ( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on-a-tabbarview-loses-its-scroll-position ). El plan es el siguiente:


  1. Agregue un ScrollController a la lista para trabajar con la posición de desplazamiento.
  2. Agregue NotificationListener a la lista para pasar el estado de desplazamiento.
  3. Guardamos la posición de desplazamiento en _MyHomePageState (que está un nivel por encima de las pestañas) y la asociamos con el desplazamiento de la lista.

Aplicación con posición de desplazamiento de guardado
 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { double listViewOffset=0.0; TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [new ListTab( getOffsetMethod: () => listViewOffset, setOffsetMethod: (offset) => this.listViewOffset = offset, ), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListTab extends StatefulWidget { ListTab({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key); final GetOffsetMethod getOffsetMethod; final SetOffsetMethod setOffsetMethod; @override _ListTabState createState() => _ListTabState(); } class _ListTabState extends State<ListTab> { ScrollController scrollController; @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); } @override Widget build(BuildContext context) { return NotificationListener( child: new ListView.builder( controller: scrollController, itemBuilder: ListData().build, ), onNotification: (notification) { if (notification is ScrollNotification) { widget.setOffsetMethod(notification.metrics.pixels); } }, ); } } 

Ejemplo completo


Resultado


Experimentando el cierre de la aplicación


Guardar información durante la duración de la aplicación es bueno, pero a menudo desea guardarlo entre sesiones, especialmente teniendo en cuenta el hábito de los sistemas operativos de cerrar aplicaciones en segundo plano cuando no hay suficiente memoria. Las principales opciones para el almacenamiento de datos persistentes en Flutter son:


  1. Las preferencias compartidas ( https://pub.dartlang.org/packages/shared_preferences ) es una envoltura de NSUserDefaults (en iOS) y SharedPreferences (en Android) y le permite almacenar una pequeña cantidad de pares clave-valor. Ideal para almacenar configuraciones.
  2. sqflite ( https://pub.dartlang.org/packages/sqflite ) es un complemento para trabajar con SQLite (con algunas limitaciones). Admite consultas y ayudantes de bajo nivel. Además, por analogía con Room, le permite trabajar con versiones de esquema de base de datos y establecer el código para actualizar el esquema al actualizar la aplicación.
  3. Cloud Firestore ( https://pub.dartlang.org/packages/cloud_firestore ) es parte de la familia oficial de complementos FireBase.

Para demostrarlo, guardaremos el estado de desplazamiento en Preferencias compartidas. Para hacer esto, agregue la restauración de la posición de desplazamiento al inicializar el estado _MyHomePageState y guarde al desplazarse.


Aquí tenemos que detenernos en el modelo asincrónico Flutter / Dart, ya que todos los servicios externos funcionan en llamadas asincrónicas. El principio de funcionamiento de este modelo es similar a node.js: hay un hilo principal de ejecución (hilo), que se ve interrumpido por llamadas asincrónicas. En cada interrupción posterior (y la IU los hace constantemente), se procesan los resultados de las operaciones asincrónicas completadas. Al mismo tiempo, es posible ejecutar cálculos pesados ​​en subprocesos en segundo plano (a través de la función de cálculo).


Por lo tanto, escribir y leer en SharedPreferences se realiza de forma asíncrona (aunque la biblioteca permite la lectura sincrónica de la memoria caché). Para empezar, nos ocuparemos de la lectura. El enfoque estándar para la recuperación de datos asíncronos se ve así: inicie el proceso asíncrono y, una vez completado, ejecute SetState y escriba los valores recibidos. Como resultado, la interfaz de usuario se actualizará utilizando los datos recibidos. Sin embargo, en este caso, no estamos trabajando con datos, sino con la posición de desplazamiento. No necesitamos actualizar la interfaz, solo necesitamos llamar al método jumpTo en el ScrollController. El problema es que el resultado del procesamiento de una solicitud asincrónica puede volver en cualquier momento y no será necesariamente qué y dónde desplazarse. Para garantizar que se realice una operación en una interfaz totalmente inicializada, necesitamos ... todavía desplazarnos dentro de setState.


Obtenemos algo como este código:


Ajuste de estado
  @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); _restoreState().then((double value) => scrollController.jumpTo(value)); } Future<double> _restoreState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getDouble('listViewOffset'); } void setScroll(double value) { setState(() { scrollController.jumpTo(value); }); } 

Con el registro, todo es más interesante. El hecho es que en el proceso de desplazamiento, los eventos que informan sobre esto ocurren constantemente. Iniciar una grabación asincrónica cada vez que se cambia el valor puede causar errores de aplicación. Necesitamos procesar solo el último evento de la cadena. En términos de programación reactiva, esto se llama rebote y lo usaremos. Dart admite las características principales de la programación reactiva a través de flujos de datos, por lo que tendremos que crear un flujo a partir de actualizaciones de posición de desplazamiento y suscribirlo, convirtiéndolo usando Debounce. Para convertir, necesitamos la biblioteca stream_transform . Como enfoque alternativo, puede usar RxDart y trabajar en términos de ReactiveX.


Resulta el siguiente código:


Registro de estado
  StreamSubscription _stream; StreamController<double> _controller = new StreamController<double>.broadcast(); @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); _stream = _controller.stream.transform(debounce(new Duration(milliseconds: 500))).listen(_saveState); } void _saveState(double value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setDouble('listViewOffset', value); } 

Ejemplo completo

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


All Articles