Flutter StatefulWidget. Usos
Revisión del 16:03 25 sep 2021 de Angelfg (discusión | contribuciones) (→Scroll infinito aplicado a ListView)
Scroll infinito aplicado a ListView
- En este punto vamos a aprender como podemos cargar de forma dinámica un número indeterminado de elementos que se van a visualizar en una lista.
- Para ello partimos del ejemplo realizado en la UD3 - Componentes => ListView.separated.
- En dicho ejemplo disponíamos de un StatelessWidget que visualizaba con un ListView.separated, un conjunto de datos en una lista.
- Lo que voy a hacer en este ejemplo es hacer crecer la lista de forma dinámica, añadiendo nuevos elementos a la misma, al llegar al final de los elementos cargados actualmente.
- Para ello voy a hacer alguna modificación al código.
- Convertiré el StatelessWidget en un StatefulWidget, ya que al añadir nuevos elementos a la lista, voy a necesitar redibujar el Widget.
- Creamos una nueva página en la carpeta pages/stateful de nombre listasinfinite_page.dart
- En todas las páginas haremos lo mismo:
- Importamos la librería de 'material' (recordar que existe un snippet que lo hace automático).
- Creamos una clase (ListasInfinitePage) que derive de StatefulWidget. Se creará (si lo hacéis con un Snippet) una clase asociada que deriva de State.
- Modificamos la clase State y en su métod build hacemos que devuelva un Scaffold con una Appbar (hacerlo con un Snippet)
- Por lo tanto partimos del siguiente código (el atributo _datos no tiene ningún dato, ya que los iremos cargando dinámicamente):
import 'package:flutter/material.dart'; class ListasInfinitePage extends StatefulWidget { const ListasInfinitePage({Key? key}) : super(key: key); @override _ListasInfinitePageState createState() => _ListasInfinitePageState(); } class _ListasInfinitePageState extends State<ListasInfinitePage> { final _datos = <String>[]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Ejemplo de listas con ListView.separated'), ), body: _obtenerLista() ); } Widget _obtenerLista(){ return ListView.separated( itemBuilder: (buildContext, index) => _elementoLista(index), separatorBuilder: (buildContext, index) => Divider(thickness: 10,color: Colors.green,), itemCount: _datos.length); } Widget _elementoLista(pos) { var elemLista = _datos.elementAt(pos); return ListTile( title: Text(elemLista), subtitle: Text('Texto aclaratorio de $elemLista'), leading: Icon(Icons.accessibility), trailing: Icon(Icons.more_vert), onTap: () {}, ); } }
- Vamos a conseguir que se carguen 'de internet' de forma simulada datos, y haremos que aparezca un indicador de progreso y que al terminar de cargarse los datos, haga un pequeño scroll hacia arriba para indicar que disponemos de nuevos datos.
- Primero voy a crear un método que permita añadir un número de elementos determinado a la lista. Lógicamente, al añadirse, se tendrá que llamar al método setState para que redibuje el StatefulWidget:
/** * Añade el número de elementos indicados a la lista */ void _addElementosLista(int num_elementos){ int ultimoElemento = _datos.length; for(int cont=ultimoElemento; cont < ultimoElemento+ num_elementos; cont++){ _datos.add('Elem $cont'); } setState(() { }); }
- Al iniciarse el StatefulWidget, voy a sobreescribir el método initState() para que cargue datos la lista.
- Recordar que ya comenté en la sección de Eventos algo acerca de ciclo de vida de los Widget´s
class _ListasInfinitePageState extends State<ListasInfinitePage> { final _datos = <String>[]; final _numElementosListaAdd = 10; @override void initState() { // TODO: implement initState super.initState(); _addElementosLista(_numElementosListaAdd); } .............
- Ahora lo que tengo que hacer es 'controlar' cuando llego al final de la lista, para que en ese caso, cargar otro conjunto de elementos llamando al método.
- Para ello hacemos uso del ScrollController.
- Los pasos son parecidos a los que ya hicimos con con el TextField, empleando el TextEditingController.
- Creamos un objeto de la clase ScrollController.
- Asociamos el controlador a la lista, con la propiedad del constructor controller.
- En el método initState, 'nos subscribimos' a los cambios de scroll que haya en la lista.
- Si lo dejáramos así, al cerrar la pantalla y volver a abrirla (dentro de nuestra aplicación) estaríamos 'suscribiéndonos' continuamente sin haber liberado previamente los recursos que reservamos al suscribirnos. Por lo tanto debemos de 'desuscribirmos' al cerrar la pantalla. Esto lo hacemos en el método dispose().
class _ListasInfinitePageState extends State<ListasInfinitePage> { final _datos = <String>[]; final _numElementosListaAdd = 10; ScrollController _scrollController = ScrollController(); @override void initState() { // TODO: implement initState super.initState(); _addElementosLista(_numElementosListaAdd); _scrollController.addListener(() { print('SCROLL'); }); } ................. Widget _obtenerLista(){ return ListView.separated( controller: _scrollController, itemBuilder: (buildContext, index) => _elementoLista(index), separatorBuilder: (buildContext, index) => Divider(thickness: 10,color: Colors.green,), itemCount: _datos.length); } ................... @override void dispose() { // TODO: implement dispose super.dispose(); _scrollController.dispose(); }
- Ahora podemos acceder a los métodos de la clase ScrollPosition accediendo con scrollController.position.
- En esa clase están definidas muchas propiedades. Una de ellas es pixels que nos devuelve la posición actual en pixeles, y maxScrollExtent que nos devuelve la posición máxima en pixeles. Por tanto, cuando ambos valores sean iguales significa que hemos llegado al final del scroll de la lista y podremos añadir un nuevo grupo de elementos.
Código completo listasinfinite_page.dart:
import 'package:flutter/material.dart'; class ListasInfinitePage extends StatefulWidget { const ListasInfinitePage({Key? key}) : super(key: key); @override _ListasInfinitePageState createState() => _ListasInfinitePageState(); } class _ListasInfinitePageState extends State<ListasInfinitePage> { final _datos = <String>[]; final _numElementosListaAdd = 10; ScrollController _scrollController = ScrollController(); @override void initState() { // TODO: implement initState super.initState(); _addElementosLista(_numElementosListaAdd); _scrollController.addListener(() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { _addElementosLista(_numElementosListaAdd); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Ejemplo de listas con ListView.separated'), ), body: _obtenerLista() ); } Widget _obtenerLista(){ return ListView.separated( controller: _scrollController, itemBuilder: (buildContext, index) => _elementoLista(index), separatorBuilder: (buildContext, index) => Divider(thickness: 10,color: Colors.green,), itemCount: _datos.length); } Widget _elementoLista(pos) { var elemLista = _datos.elementAt(pos); return ListTile( title: Text(elemLista), subtitle: Text('Texto aclaratorio de $elemLista'), leading: Icon(Icons.accessibility), trailing: Icon(Icons.more_vert), onTap: () {}, ); } /** * Añade el número de elementos indicados a la lista */ void _addElementosLista(int num_elementos){ int ultimoElemento = _datos.length; for(int cont=ultimoElemento; cont < ultimoElemento+ num_elementos; cont++){ _datos.add('Elem $cont'); } setState(() { }); } @override void dispose() { // TODO: implement dispose super.dispose(); _scrollController.dispose(); } }
Datos provenientes de Internet
- En el ejemplo anterior, disponemos los datos 'localmente' y podemos añadir otros 10 a la lista de forma inmediata.
- En la realidad, los datos pueden venir de Internet, por lo que puede haber un cierto retraso en disponer de los mismos para poder ser visualizados en la lista.
- Vamos a ver como podemos resolver este problema.
- La idea es usar un Future para simular que va a haber un retardo en obtener los nuevos datos (por ejemplo, de una consulta a un servicio API REST y que nos devuelva los datos en formato de JSON).
- Durante la descarga de los datos, vamos a visualizar un Widget que nos indique que se está procediendo a la descarga. Para ello vamos a usar una variable booleana (_isLoading).
- El código hasta este punto será el siguiente:
Código completo listasinfinite_page.dart:
import 'dart:async'; import 'package:flutter/material.dart'; class ListasInfinitePage extends StatefulWidget { const ListasInfinitePage({Key? key}) : super(key: key); @override _ListasInfinitePageState createState() => _ListasInfinitePageState(); } class _ListasInfinitePageState extends State<ListasInfinitePage> { final _datos = <String>[]; final _numElementosListaAdd = 10; bool _isLoading = false; // Usado para saber cuando estamos descargando datos de internet ScrollController _scrollController = ScrollController(); @override void initState() { // TODO: implement initState super.initState(); _addElementosLista(_numElementosListaAdd); _scrollController.addListener(() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { //_addElementosLista(_numElementosListaAdd); _cargarElementosInternet(); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Ejemplo de listas con ListView.separated'), ), body: _obtenerLista() ); } Widget _obtenerLista(){ return ListView.separated( controller: _scrollController, itemBuilder: (buildContext, index) => _elementoLista(index), separatorBuilder: (buildContext, index) => Divider(thickness: 10,color: Colors.green,), itemCount: _datos.length); } Widget _elementoLista(pos) { var elemLista = _datos.elementAt(pos); return ListTile( title: Text(elemLista), subtitle: Text('Texto aclaratorio de $elemLista'), leading: Icon(Icons.accessibility), trailing: Icon(Icons.more_vert), onTap: () {}, ); } /** * Añade el número de elementos indicados a la lista */ void _addElementosLista(int num_elementos){ int ultimoElemento = _datos.length; for(int cont=ultimoElemento; cont < ultimoElemento+ num_elementos; cont++){ _datos.add('Elem $cont'); } setState(() {}); } /** * Añade datos a la lista de datos pero de forma asíncrona */ Future _cargarElementosInternet() { setState(() { // Indicamos que estamos cargando para que se dibuje el Widget de carga _isLoading = true; }); return Future.delayed(Duration(seconds: 3), vienenDatosInternet); // Aquí simulamos que tardamos un tiempo en cargar los datos } void vienenDatosInternet(){ _isLoading = false; _addElementosLista(_numElementosListaAdd); } @override void dispose() { // TODO: implement dispose super.dispose(); _scrollController.dispose(); } }
- Podéis probarlo y comprobar como al llegar abajo de todo, al cabo de 3 segundos, la lista se recarga con otros 10 elementos.
- Ahora vamos a hacer que mientras descarga los datos, aparezca un Widget que indique esa situación.
- Para ello vamos a hacer uso de:
- Widget Stack que permite poner un Widget encima de otro visualmente. Lo que vamos a hacers poner encima de la pantalla el Widget que visualiza un CircularProgressIndicator.
- Widget CircularProgressIndicator: Hace aparecer un círculo con animación que indica que se está realizando un proceso.
.................. @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Ejemplo de listas con ListView.separated'), ), body: Stack(children: [ _obtenerLista(), _visualizarWidgetDescarga(), ]) ); } Widget _visualizarWidgetDescarga(){ if (_isLoading){ return Center( child: Column( mainAxisAlignment: MainAxisAlignment.end, children:[ CircularProgressIndicator(color: Colors.red,), SizedBox(height: 10,) ] ), ); } else{ // Si no se está cargando debemos devolver un Widget, por lo que devuelvo un contendor vacío return Container(); } } ....................
- Ahora como paso final, vamos a hacer que cuando termine de cargar, hago un pequeño scroll para arriba y así el usuario se de cuenta de que ya dispone de nuevos datos...
- Para ello debemos hacer uso del ScrollController visto anteriormente, y hacer una pequeña animación, en la que vamos a indicar a qué posición debe moverse (lo sabemos por _scrollController.position.pixel / _scrollController.offset => posición actual a la que tenemos que sumar el desplazamiento que queramos que tenga)
- Haré uso de la propiedad animateTo del ScrollController.
void vienenDatosInternet(){ _isLoading = false; _addElementosLista(_numElementosListaAdd); // Ya tenemos los datos, movemos el scroll _scrollController.animateTo( _scrollController.offset+100, duration: Duration(seconds: 1), curve: Curves.decelerate ); }
Código completo listasinfinite_page.dart:
import 'dart:async'; import 'package:flutter/material.dart'; class ListasInfinitePage extends StatefulWidget { const ListasInfinitePage({Key? key}) : super(key: key); @override _ListasInfinitePageState createState() => _ListasInfinitePageState(); } class _ListasInfinitePageState extends State<ListasInfinitePage> { final _datos = <String>[]; final _numElementosListaAdd = 10; bool _isLoading = false; // Usado para saber cuando estamos descargando datos de internet ScrollController _scrollController = ScrollController(); @override void initState() { // TODO: implement initState super.initState(); _addElementosLista(_numElementosListaAdd); _scrollController.addListener(() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { //_addElementosLista(_numElementosListaAdd); _cargarElementosInternet(); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Ejemplo de listas con ListView.separated'), ), body: Stack(children: [ _obtenerLista(), _visualizarWidgetDescarga(), ]) ); } Widget _visualizarWidgetDescarga(){ if (_isLoading){ return Center( child: Column( mainAxisAlignment: MainAxisAlignment.end, children:[ CircularProgressIndicator(color: Colors.red,), SizedBox(height: 10,) ] ), ); } else{ // Si no se está cargando debemos devolver un Widget, por lo que devuelvo un contendor vacío return Container(); } } Widget _obtenerLista(){ return ListView.separated( controller: _scrollController, itemBuilder: (buildContext, index) => _elementoLista(index), separatorBuilder: (buildContext, index) => Divider(thickness: 10,color: Colors.green,), itemCount: _datos.length); } Widget _elementoLista(pos) { var elemLista = _datos.elementAt(pos); return ListTile( title: Text(elemLista), subtitle: Text('Texto aclaratorio de $elemLista'), leading: Icon(Icons.accessibility), trailing: Icon(Icons.more_vert), onTap: () {}, ); } /** * Añade el número de elementos indicados a la lista */ void _addElementosLista(int num_elementos){ int ultimoElemento = _datos.length; for(int cont=ultimoElemento; cont < ultimoElemento+ num_elementos; cont++){ _datos.add('Elem $cont'); } setState(() {}); } /** * Añade datos a la lista de datos pero de forma asíncrona */ Future _cargarElementosInternet() { setState(() { // Indicamos que estamos cargando para que se dibuje el Widget de carga _isLoading = true; }); return Future.delayed(Duration(seconds: 2), vienenDatosInternet); // Aquí simulamos que tardamos un tiempo en cargar los datos } void vienenDatosInternet(){ _isLoading = false; _addElementosLista(_numElementosListaAdd); // Ya tenemos los datos, movemos el scroll _scrollController.animateTo( _scrollController.offset+100, duration: Duration(seconds: 1), curve: Curves.decelerate ); } @override void dispose() { // TODO: implement dispose super.dispose(); _scrollController.dispose(); } }