Diferencia entre revisiones de «Flutter StatefulWidget. Usos»

De MediaWiki
Ir a la navegación Ir a la búsqueda
Línea 1084: Línea 1084:
 
     );
 
     );
  
/*    return Consumer<ProviderPrueba>(    // OTRA FORMA DE HACERLO. Con Consumer indicamos los widgets que tienen que redibujarse si se produce un notifyListeners()
 
      builder: (context,provider,child) =>
 
          ListView(
 
              children: provider.datos.map((e) => ListTile(
 
                      title: Text('Dato:$e'),
 
                      onTap: () { provider.remove(e); },
 
                    )
 
              ).toList()
 
        )
 
    );
 
*/
 
 
   }
 
   }
 
}
 
}

Revisión del 19:37 8 nov 2021

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.


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)


Flutter dart scrollinifinito 1.JPG


  • 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: () {},
    );
  }


}



  • 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

    _scrollController.dispose();
    super.dispose();
  }


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

    _scrollController.dispose();
    super.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).


  • 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.
Flutter dart scrollinifinito 2.JPG



El código hasta este punto es 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

    _scrollController.dispose();
    super.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

    _scrollController.dispose();
    super.dispose();
  }

}



Pull

  • Lo que voy a hacer ahora es que si hacemos el scroll en sentido contrario (desplanzado el dedo de arriba-abajo) aparezca un indicador de progreso y los datos se inicialicen o cargar nuevos datos pero borrando todos los anteriores, para que la lista no crezca de forma indefinida.
Este es un ejemplo de uso, pero existen otros, como cuando buscamos correo en la aplicación del teléfono y al hacer esta operación (pull) aparecen los correos nuevos en la parte superior, desplanzando los antiguos hacia abajo.


  • Para ello sólo tengo que 'envolver' un Widget que tenga un scroll vertical con un Widget RefreshIndicator.
El widget RefreshIndicator tiene entre sus propiedades, la propiedad onRefresh la cual espera recibir un Future (que no devuelve nada). Cuando el Future acaba, el indicador de progreso que aparece en la parte superior desaparece.
Fijarse que esto podríamos aplicarlo para el ejemplo anterior, pero entonces los datos no se cargarían cuando llegamos al final de la lista, sólo al hacer un scroll en sentido contrario...
Flutter dart scrollinifinito 3.JPG


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 RefreshIndicator(
            onRefresh: _cargarElementosInternet2,
            child: ListView.separated(
                      controller: _scrollController,
                      itemBuilder: (buildContext, index) => _elementoLista(index),
                      separatorBuilder: (buildContext, index) => Divider(thickness: 10,color: Colors.green,),
                      itemCount: _datos.length),
          );
  }

  Future _cargarElementosInternet2(){
    setState(() {
      _datos.clear();   // Borramos todo
    });
    
    return Future.delayed(Duration(seconds: 2), vienenDatosInternet); // Aquí simulamos que tardamos un tiempo en cargar los datos
  }

  /**
   * Devuelve el Widget que va a visualizar la lista en la posición indicada
   */
  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
    if (_datos.length > _numElementosListaAdd){
      _scrollController.animateTo(
          _scrollController.offset+100, duration: Duration(seconds: 1), curve: Curves.decelerate
      );

    }
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _scrollController.dispose();
  }

}




Visualizar mensajes

  • Más información en:


  • Cuando queremos informar al usuario de algo podemos hacer uso de los SnackBars, que son mensajes emergentes que aparecen en la parte inferior y que al cabo de unos segundos desaparecen.




Llamando a setState desde un StatelessWidget

  • Cuando estamos en una pantalla, puede suceder que alguno de los widget´s sean Stateless y otros Stateful.
En el caso de que queremos 'actualizar' el Stateful deberíamos poder llamar al método setState de dicho Widget.
Para ello debemos pasar al constructor del StatelessWidget una referencia a la función setState.
Veamos un ejemplo.


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 (LlamadaSetstateStatelessPage) que derive de StatefulWidget.
  • Modificamos su método build de la clase State y hacemos que devuelva un Scaffold con una Appbar (hacerlo con un Snippet)


Flutter dart llamada setState 1.JPG


  • Archivo: llamada_setstate_stateless_page.dart:
En este ejemplo, el Widget principal es un StatefulWidget.
Al llamar al método setState va a 'redibujar' todo el árbol de Widget, incluído los StatelessWidget. Se podría modificar el ejemplo para que sólo redibujara algún StatefulWidget concreto.
import 'package:flutter/material.dart';

class LlamadaSetstateStatelessPage extends StatefulWidget {
  const LlamadaSetstateStatelessPage({Key? key}) : super(key: key);

  @override
  _LlamadaSetstateStatelessPageState createState() => _LlamadaSetstateStatelessPageState();
}

class _LlamadaSetstateStatelessPageState extends State<LlamadaSetstateStatelessPage> {
  int cont = 0;

  _actualizarEstado(){
    setState(() {
      cont++;

    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de llamada a setState desde un StalessWidget'),
        ),
        body: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _BotonStateLess(actualizarTexto: _actualizarEstado,),
            Text('Has pulsado $cont veces el botón')
          ],
        )
    );
  }
}

class _BotonStateLess extends StatelessWidget {
  final void Function() actualizarTexto;

  const _BotonStateLess({Key? key, required this.actualizarTexto}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(right: 10),
      width: 100,
      height: 50,
      child: TextButton(
          onPressed: () => actualizarTexto(),
          child: Text('PULSAME'),
        style: TextButton.styleFrom(
          backgroundColor: Colors.yellow
        ),
      ),
    );
  }
}




Paso de datos entre Widgets

  • En este punto vou a tratar como enviar datos desde un Widget a otro.
Debemos de tener claro que una aplicación Flutter, cada pantalla está conformada por un árbol de Widget (un Widget dentro de otro) y estos Widget no tienen por qué estar definidos en la misma clase, pudiendo estar en ficheros diferentes los cuales serán impartados cuando se necesiten.
Por otro lado, una aplicación está compuesta por varias pantallas y puede ser necesario enviar información de una pantalla a otra.



Paso de datos entre Widgets dentre de la misma pantalla

  • En este punto podemos encontrarnos con dos escenarios.


Paso de datos entre Widgets a una distancia de un nivel de profundidad

  • Con esto quiero decir que un Widget quiere pasar datos a otro que está 'dentro' de él, pero sólo a un nivel.
En este caso, la forma más fácil de enviar datos es empleando el constructor del Widget al que queremos pasar los datos.


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 (PasoDatosDirectoPage) que derive de StatelessWidget.
  • Modificamos su método build y hacemos que devuelva un Scaffold con una Appbar (hacerlo con un Snippet)
Nota: Lo que voy a explicar se aplica también a un StatefullWidget.


Flutter dart paso datos 1.JPG


  • Archivo: paso_datos_directo_page.dart:
import 'package:flutter/material.dart';

class PasoDatosDirectoPage extends StatelessWidget {
  final datos = ['Elemento 1','Elemento 2', 'Elemento 3',
                 'Elemento 4','Elemento 5', 'Elemento 6', ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de paso de datos a 1 nivel'),
        ),
        body: ListView(
            children: datos.map((e) => _ElementoLista(dato: e)).toList()
        )
    );
  }
}


/**
 * Widget que podría estar definido en un archivo separado
 * Espera recibir como dato una cadena
 */
class _ElementoLista extends StatelessWidget {
  final String dato;
  const _ElementoLista({Key? key, required this.dato}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(dato),
    );
  }
}



Paso de datos entre Widgets a una distancia de varios niveles de profundidad

  • En este caso, la solución anterior deja de ser viable ya que complicaríamos el código pasando datos de un Widget a otro.
Para evitarlo, se suele hacer uso de los InheritedWidget que son Widget que sólo guardan información que va a ser compartida por varios Widget en el árbol de Widget´s de la pantalla.
  • También se puede hacer uso del Widget ChangeNotifier. Este widget permite que otros widget´s del árbol 'escuchen' los cambios que se produzcan en los datos guardados, y que se reconstruyan (se redibujan) cuando esto suceda. Es necesario emplear un provider, el ChangeNotifierProvider el cual va a suministrar el ChangeNotifier creado previamente a los widgets de nuestra aplicación.
  • Por lo que estuve leyendo, la segunda forma es más sencilla de emplear.
Queda escrita la explicación de lo que comprendí hasta ahora del InheritedWidget, pero nos centraremos en la segunda forma.



Empleando ChangeNotifier-ChangeNotifierProvider
  • El empleo de providers nos facilita mucho la vida para:
  • Acceder a información desde diferentes widgets al provider.
  • Que dichos Widgets (tanto Stateless como Stateful) se puedan actualizar cuando los datos del provider cambien, de forma automática.


  • El procedimiento es muy sencillo.
Dentro de dicha clase declaramos las variables que van a conformar nuestra 'fuente de datos'. Fijaros que podemos emplear funciones Future, que vayan a buscar información a internet, por ejemplo...
Declaramos los métodos que van a modificar los datos guardados.
En todos los métodos donde se modifiquen los datos y queramos 'notificar' a los Widgets que hacen uso de ellos (están escuchando) de un cambio, llamamos al método notifyListeners.
  • Desde cualquier Widget podremos acceder a los datos/métodos de la clase anterior de la forma: ProviderPrueba provider = Provider.of<ProviderPrueba>(context,listen: true); siendo ProviderPrueba el nombre que le hemos dado a la clase anterior. El atributo del constructor listen indica si el Widget tiene que redibujarse si se produce algún cambio en los datos. Siempre que podamos, mejor tenerlo a false.
Ahora, a través del objeto 'provider' podremos acceder a los datos y métodos.
Como habéis observado, necesitamos acceder al context para poder hacerlo, por lo que normalmente la referencia se coje desde el método build del Widget...


Creamos una nueva página en la carpeta pages/stateful de nombre probando_provider.dart (crear los directorios dentro de lib si no están creados previamente).
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 (ProbandoProvider) que derive de StatelessWidget. Haremos que devuelva un Scaffold con una Appbar (hacerlo con un Snippet)


  • Veamos un ejemplo, en el que tenemos dos listas que cargan los datos de un List y una etiqueta que muestra cuantos datos tiene la lista.
Las listas son StatefulWidget y la etiqueta es un Stateless.

[Imagen:Flutter_dart_provider_1_stateful.JPG | 300px | center]]


Clase: ProviderPrueba

Clase que representa el provider y donde definomos los datos y métodos que van a modificar dichos datos.
Al hacer uso de la clase ChangeNotifier podemos llamar al método notifyListeners().
import 'package:flutter/cupertino.dart';

class ProviderPrueba with ChangeNotifier{

  List<int>_datos = <int>[5,6,7,8];

  List<int>get datos{
    return _datos;
  }

  void add(int valor){
    _datos.add(valor);

    notifyListeners();
  }

  void remove(int valor){
    _datos.remove(valor);

    notifyListeners();
  }


}


Clase: ProbandoProvider

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:holamundo/providers/prueba1/provider_prueba.dart';
import 'package:provider/provider.dart';

class ProbandoProvider extends StatelessWidget {
   ProbandoProvider({Key? key}) : super(key: key);


  @override
  Widget build(BuildContext context) {
    return  ChangeNotifierProvider(
        create: (BuildContext context)  => ProviderPrueba(),
    child: Scaffold(
        appBar: AppBar(
          title: Text('Provider'),
        ),
        body: Column(
          children: [
            Expanded(child: _CreandoLista()),
            Expanded(child: _CrearTexto(),),
            Expanded(child: _CreandoLista()),

          ],
        )
    )
    );
  }

}

class _CrearTexto extends StatelessWidget {
  const _CrearTexto({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    ProviderPrueba provider = Provider.of<ProviderPrueba>(context,listen: false); // No se actualiza si no es true
    print('Se dibuja');
    final aleat = Random();

    return GestureDetector(
      onTap: () { provider.add(aleat.nextInt(100)); },
      child: Ink(
          child: Text('Número elementos:${provider.datos.length}')
      ),
    );

  }
}


class _CreandoLista extends StatefulWidget {

  const _CreandoLista({Key? key}) : super(key: key);

  @override
  _CreandoListaState createState() => _CreandoListaState();
}

class _CreandoListaState extends State<_CreandoLista> {

  @override
  Widget build(BuildContext context) {
    ProviderPrueba provider = Provider.of<ProviderPrueba>(context,listen: true);  // Con true, indicamos que si se produce un notifyListeners() debe ser redibujado
    return ListView(
          children: provider.datos.map((e) => ListTile(
          title: Text('Dato:$e'),
          onTap: () { provider.remove(e); },
        )
      ).toList()
    );

  }
}



  • Nota: Tenemos otra forma de implentar el provider en los Widget que van a hacer uso de ellos, y es envolver el Widget con un Consumer Widget el cual dispone del método builder, que recibe entre sus parámtros la referencia al provider.
En el ejemplo anterior, el código de las listas quedaría así:
class _CreandoListaState extends State<_CreandoLista> {

  @override
  Widget build(BuildContext context) {

    return Consumer<ProviderPrueba>(
      builder: (_, provider, __) =>
          ListView(
              children: provider.datos.map((e) => ListTile(
                      title: Text('Dato:$e'),
                      onTap: () { provider.remove(e); },
                    )
              ).toList()
        )
    );
  }
}



Empleando InheritedWidget
Nota: A día de hoy no encuentro información suficiente para explicar como se puede hacer para que no sea necesario redibujar todo el árbol de Widget si se produce un cambio en los datos guardados en el InheritedWidget.
Un ejemplo de explicación es el que podéis encontrar en el siguiente artículo.
Se supone que dicho Widget es 'inmutable' pero se puede guardar variables cuyos valores pueden ser modificados. En varios artículos indica que es necesario 'envolver' dicho Widget en un StatefulWidget.
Debido a que trabajamos a un 'nivel' más bajo, se recomienda hacer uso de providers como está explicado en la sección anterior.
Por lo tanto, me voy a centrar en cómo puede ser usado para guardar información que sea accedida desde cualquier Widget del árbol de Widget.


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 (PasoDatosNivelesPage) que derive de StatelessWidget.
  • Modificamos su método build y hacemos que devuelva un Scaffold con una Appbar (hacerlo con un Snippet)
Nota: Lo que voy a explicar se aplica también a un StatefullWidget.


Flutter dart paso datos 2.JPG
Imagen obtenida de https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html


  • Lo primero que debemos hacer es crear un Widget que derive de InheritedWidget.
Al hacerlo, será necesario sobreescribir un método y crear un constructor y un método llamado of.
  • Sobreescribir el método updateShouldNotify: Dicho método devuelve un booleano indicando si los Widgets que son hijos deben ser notificados de algún cambio en los datos.
Se debe cambiar la clase que está después del modificador covariant por la clase que estamos creando.
En el interior de la función se suele hacer uso del objeto oldWidget para comparar los valores guardados en InheritedWidget antes de que se redibujen los Widget, por si tienen que redibujarse o no en función de si han cambiado los datos.
  @override
  bool updateShouldNotify(covariant ProviderDataInherited oldWidget) {  // Se debe poner el nombre de la clase que estamos definiendo ProviderDataInherited  
    // TODO: implement updateShouldNotify
    return oldWidget.contBoton1!=contBoton1 || oldWidget.contBoton2!=contBoton2;
  }
  • Crear un constructor al que le vamos a pasar un Widget que representa el Widget 'hijo' de este InheritedWidget. Recordar que este Widget tiene que ser 'padre' de todos los Widget que van a hacer uso de los datos guardados en el mismo. Este constructor llamará al constructor padre (super) enviando este mismo widget.
Por otro lado, dentro de esta clase guardaremos los datos que queremos compartir entre los diferentes widget´s.
  ProviderDataInherited({required child}) : super(child: child);


  • Crear un método static que devuelva un objeto de la clase InheritedWidget que estamos definiendo.
  static ProviderDataInherited of(BuildContext context){
    return context.dependOnInheritedWidgetOfExactType<ProviderDataInherited>()!;
  }
Habría que controlar que pasa cuando dependOnInheritedWidgetOfExactType devuelva nulo (esto significa que no encuentra un Widget en el árbol de Widget que sea de la clase InheritedWidget). En este caso, hago uso del operador !.



  • Clase ProviderDataInherited:
En nuestro ejemplo, voy a guardar la información relativa a cuantas veces se pulsaron dos botones.
import 'package:flutter/material.dart';

class ProviderDataInherited extends InheritedWidget{

  int contBoton1 = 0;
  int contBoton2 = 0;

  ProviderDataInherited({required child}) : super(child: child);



  @override
  bool updateShouldNotify(covariant ProviderDataInherited oldWidget) {
    // TODO: implement updateShouldNotify
    return oldWidget.contBoton1!=contBoton1 || oldWidget.contBoton2!=contBoton2;
  }

  static ProviderDataInherited of(BuildContext context){
    return context.dependOnInheritedWidgetOfExactType<ProviderDataInherited>()!;
  }

}



  • Ahora debemos de crear nuestro StalessWidget o StatefulWidget y hacer que el padre sea nuestra clase ProviderDataInherited, pasando como child, el Widget del que van a 'colgar' todos nuestros widget´s.
Podríamos enviar unos datos iniciales al constructor del ProviderDataInherited si quisiéramos.
En la solución está todo dentro del mismo archivo físico pero podrían estar en archivos diferentes.
Al ser padre de todos los Widget, en cualquier momento y desde cualquier Widget podemos acceder a los datos guarados de la forma: ProviderDataInherited.of(context).
Flutter dart paso datos 3.JPG


  • Clase: PasoDatosNivelesPage:
Como comenté anteriormente, no está claro como hacer que todos aquellos widget´s que hagan uso de los datos, se actualicen si se produce algún cambio en los mismos.
En este ejemplo, el grupo de botón-textos conforman un StatefulWidget (son dos) y cuando se actualiza uno de ellos, no se actualiza el otro.
import 'package:flutter/material.dart';


class ProviderDataInherited extends InheritedWidget{

  int contBoton1 = 0;
  int contBoton2 = 0;

  ProviderDataInherited({required child}) : super(child: child);



  @override
  bool updateShouldNotify(covariant ProviderDataInherited oldWidget) {
    // TODO: implement updateShouldNotify
    return true;
  }

  static ProviderDataInherited of(BuildContext context){
    return context.dependOnInheritedWidgetOfExactType<ProviderDataInherited>()!;
  }

}

class PasoDatosNivelesPage extends StatelessWidget {
  const PasoDatosNivelesPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ProviderDataInherited(
        child: Scaffold(
            appBar: AppBar(
              title: Text('Ejemplo de uso de datos empleando un InheritedWidget'),
            ),
            body: Row(
              children: [
                Expanded(child: _Elemento1()),
                Expanded(child: _Elemento2()),
              ],
            )
        )
    );
  }
}

class _Elemento1 extends StatefulWidget {
  const _Elemento1({Key? key}) : super(key: key);

  @override
  _Elemento1State createState() => _Elemento1State();
}

class _Elemento1State extends State<_Elemento1> {

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(' de veces pulsado botón1: ${ProviderDataInherited.of(context).contBoton1}'),
        Text(' de veces pulsado botón2: ${ProviderDataInherited.of(context).contBoton2}'),
        TextButton(onPressed: () {
                        setState(() => ProviderDataInherited.of(context).contBoton1++);
                      },
            child: Text('Botón-1'),
            style: TextButton.styleFrom(backgroundColor: Colors.indigo) ,
        )
      ]

    );
  }
}

class _Elemento2 extends StatefulWidget {
  const _Elemento2({Key? key}) : super(key: key);

  @override
  _Elemento2State createState() => _Elemento2State();
}

class _Elemento2State extends State<_Elemento2> {

  @override
  Widget build(BuildContext context) {
    return Column(
        children: [
          TextButton(onPressed: () {
                          setState(() => ProviderDataInherited.of(context).contBoton2++);
                        },
              child: Text('Botón-2'),
              style: TextButton.styleFrom(backgroundColor: Colors.yellow),
          ),
          Text(' de veces pulsado botón2: ${ProviderDataInherited.of(context).contBoton2}'),
          Text(' de veces pulsado botón1: ${ProviderDataInherited.of(context).contBoton1}'),

        ]

    );
  }
}



Paso de datos entre páginas mediante navegación




Enlace a la página principal de la UD4

Enlace a la página principal del curso




-- Ángel D. Fernández González -- (2021).