Flutter StatefulWidget. Usos

De MediaWiki
Ir a la navegación Ir a la búsqueda

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.
  • Ya vimos anteriormente en esta Wiki algún ejemplo de paso de datos entre Widgets y alguno de ellos será repetido en esta sección.



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.



Providers: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.


  • Paso 2) Ahora debemos 'envolver' el Widget que va a englobar a todos los demás Widgets que van a hacer uso del provider, con un ChangeNotifierProvider. De esta forma, añadiremos al Context una referencia al provider creando anteriormente y todos los Widget que 'cuelguen' del mismo van a poder tener una referencia al provider a través de la clase Context.


  • Paso 3 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.
Flutter dart provider 1 stateful.JPG


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()
        )
    );
  }
}




  • Cuando queramos hacer uso de múltiples provider, podemos lograrlo definiendo en el Widget que instancia el método runApp, un MultipleProvider de la forma:
void main() => runApp(AppState());

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

  @override
  Widget build(BuildContext context) {

    return MultiProvider(
      providers: [
          ChangeNotifierProvider(create: ( _ ) => ProviderPrueba(),lazy: false,)
      ],
      child: MyApp(),
    );
  }
}


class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {

    return MaterialApp(
          ..............

    );
  }
}
  • NOTA: En el código que crea un ChangeNotifierProvider, el parámetro lazy tiene como valor por defecto true.
Dicho parámetro es empleado cuando creamos un constructor en el Provider con algún código y queremos que se llame de forma inmediata cuando se ejecuta la línea que crear el Provider. En ese caso, debemos enviar el valor false.
Si no lo enviamos o enviamos el valor true, a dicho constructor no se llamará hasta que algún Widget haga uso del provider. Lógicamente sólo se llamará una vez.




Entendiendo las diferentes formas de emplear el ChangeNotifier
  • En los ejemplos anteriores vimos que podemos emplear el ChangeNotifier de 3 formas diferentes:
  • ProviderPrueba provider = Provider.of<ProviderPrueba>(context,listen: true); // Con true, indicamos que si se produce un notifyListeners() debe ser redibujado
  • ProviderPrueba provider = Provider.of<ProviderPrueba>(context,listen: false); // Con false, indicamos que si se produce un notifyListeners() no se redibujará
  • Consumer<ProviderPrueba>(builder: (_, provider, __) { return Widget que puede hacer uso de los datos y métodos del provider}); // El widget se va a redibujar cuando se llama a notifyListener() del provider


  • La pregunta es ¿ cuando usar una u otra forma ?
Lo que tenemos que tener claro es que el 'efecto' que provaca el notifyListener() sobre los widget es 'redibujarlos' y por tanto llamar u>sólamente al método build(), tanto de un StatelessWidget como de un StatefulWidget.
La diferencia es que si lo hacemos sobre un StatelessWidget, al no guardar el estado, se va a redibujar sin cambios.
Si estamos trabajando con un StatefulWidget, el estado se mantiene, como ya vimos en un punto anterior de esta Wiki.


Una de las reglas que debemos aplicar es que debemos intentar que se redibujen el menor posible número de Widgets...
Al estar todos los Widgets en un árbol de Widgets, podríamos hacer StatefulWidget al padre de todos y cualquier cambio que tuviéramos, mandar redibujar con setState a todo el árbol, pero eso no es eficiente...


  • Por lo tanto podemos aplicar las siguientes reglas, partiendo que disponemos de datos que están definidos en un provider, con ChangeNotifier y queremos usarlos o llamar a métodos definidos dentro del provider:



=1ª REGLA=
  • Si estamos dentro de Widget (Stateless o Stateful) y un Widget interno quiere acceder al provider con la necesidad de redibujarse por producirse cambios en los datos desde otros widgets, debemos de emplear el Consumer.
Un ejemplo muy claro de este uso lo vemos en un carrito de la compra, en el que en la página principal, en la AppBar disponemos de texto que nos indica cuantos productos hay en el carrito. Dicho número (widget) no está definido en un Widget separado (en una clase separada de la principal).


  • Veamos un ejemplo:
Nota: Al copìar el código tendréis que modificar los import´s para adaptarlos a las localizaciones de vuestros archivos dart.


Clase: ProductosProvider

Clase que representa el provider y donde definomos los datos y métodos para añadir productos a un carrito de la compra.
Definimos por tanto dos listas, una para los productos y otra para el carrito.
Nota: En un caso real, podríamos definir dos provider diferentes, uno para los productos y otro para guardar los datos del carrito.
import 'package:flutter/material.dart';

class ProductosProvider with ChangeNotifier{

  List<String> _productos = ['Elemento1', 'Elemento2','Elemento3', 'Elemento4','Elemento5', 'Elemento6',
      'Elemento7', 'Elemento8','Elemento9', 'Elemento10','Elemento11', 'Elemento12',];
  List<String> _carrito = [];

  List<String> get productos => _productos;
  List<String> get carrito => _carrito;

  void addProductoCarrito(String elem){
    _carrito.add(elem);
    notifyListeners();
  }

  void removeProductoCarrito(String elem){
    _carrito.remove(elem);
    notifyListeners();
  }
}


  • Clase ListaProductos
Es un StatelessWidget que el número de productos en el carrito en la AppBar.
import 'package:flutter/material.dart';
// import 'package:holamundo/providers/carrito_compra/mostrar_carrito.dart';
import 'package:holamundo/providers/carrito_compra/productos_provider.dart';

import 'package:provider/provider.dart';

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

  @override
  Widget build(BuildContext context) {
    print('DIBUJA SCAFFOLD');
    return  Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.indigo,
            actions: [
              Stack(
                children: [
                  IconButton(
                      onPressed: () {}, icon: Icon(Icons.shopping_cart_rounded)
                  ),
                  Positioned(
                    top: 10,
                    right: 3,
                    child: Consumer<ProductosProvider>(builder: (BuildContext context,  provider, Widget? child) {
                      return Text('${provider.carrito.length}');
                    },

                    ),
                  )
                ],
              )
            ],
            title: Text('Lista de productos'),
          ),
          body: Container()
    );
  }
}


  • Fijarse que está puesto un PRINT en la línea 12 para que comprobar cuantas veces se redibuja el Widget.
Este es un claro candidato a Consumer, ya que tenemos un Widget 'integrado' dentro de la página (no está definido de forma separada, en una clase diferente).



=2ª REGLA=
  • Si podemos o tenemos definido un Widget de forma separada, podremos emplear dentro del mismo, las tres opciones posibles de uso de provider.
Si el acceso al provider lo necesitamos a nivel global (es decir, que en el método build es donde queremos definir el acceso al provider para que los widget dentro del árbol puedan acceder al mismo), emplearemos la forma Provider.of(....)
Ahora la decisión de poner listen a true/false va a depender de si necetimos que el Widget se redibuje si se producen cambios en los datos...
Recordar que el notifyListener() hará que se vuelva a ejecutar el método build
  • Siguiendo con nuestro ejemplo, vamos a suponer que tenemos un Widget definido de forma separada en una nueva clase y que dicho Widget muestra el contenido de los productos del carrito.
Además, a cada elemento del carrito le vamos a:
  • Asociar un IconButton para que se pueda agregar/eliminar de la lista de productos y por tanto añadir/quitar al carrito
Al pulsar sobre el icono que añade el elemento al carrito, se va a cambiar por otro icono para quitarlo del carrito.
Flutter dart provider 1.JPG


  • En este punto debemos de preguntarnos sobre el árbol de Widget que vamos a usar...
  • La lista de productos ¿ es Stateless o Stateful ?
  • Cada elemento que conforma la lista ¿ lo creo en un Widget separado (definiendo una clase Widget) o creo una función que devuelva un Widget dentro del Widget de la lista ?


  • La lista entera va a hacer uso los datos del provider. Al estar definida en una clase separada, ya no necesito envolverla dentro de un Consumer.
Por lo tanto podría hacer uso de la forma Provider.of(....)
Ahora debo determinar si con listen true/false.
Al añadir un elemento al carrito, dicho elemento no 'desaparece' de la lista de productos, visualmente no se quita (cambia el icono) por lo tanto no debería ser necesario poner true como valor al parámetro listen y tampoco hay nada a nivel visual que me obligue a guardar el estado de la lista, por lo tanto es un StatelessWidget.
Nota: En este caso, los datos los obtenemos de un provider. Si estuviera definida en el Widget la lista de productos y esta sufruera modificaciones, tendríamos que definirla como StatefulWidget.


  • Al leer el enunciado, vemos que cada elemento de la lista 'cambia' en base a si está en el carrito o no, modificando el icono que visualiza el IconButton.
Por lo tanto, cada elemento de la lista debería ser un StatefulWidget.
Ahora debemos decidir si quieremos definirlo localmente de la forma:
Widget _elementoLista() => ListTile(......)
O si queremos definirlo de forma separada, en una nueva clase.
En este caso:
  • => Si lo intento definir en forma de función que devuelva un Widget:
  • => Tendría que definir la lista entera como StatefulWidget ya que no puede definir en forma de función que el ListTile sea un StatefulWidget.
  • => O tendría que emplear dentro de la función el acceso al provider con listen:true, para que cuando presione el icono, el provider haga el notifyListener() y haga que se redibuje. El problema es que no sólo redibuja el elemento de la lista, ya que va a redibujar la lista entera.


  • => Si lo defino en una clase separada, puedo definirlo como Staleless o Stateful. Ya vimos que cambia el estado al pulsar sobre el IconButton, por lo tanto debería ser un StatefulWidget.
Como este Widget que representa cada elemento de la lista, no se va a reutilizar en otros Widget, podría definirlo como una clase privada dentro del archivo d.dart donde se encuentra definida la lista de productos.


  • Por lo tanto, en base a lo anterior:
  • Defino la lista como un StatelessWidget con acceso al provider listen:false
  • Defino cada elemeto de la lista como un StatefulWidget en una clase privada y separada dentro del mismo archivo dart donde se encuentre la lista.
  • Ahora bien, al definirlo en una clase separada, dentro del StatefulWidget voy necesitar acceder al provider, para llamar al método que agrega o quita productos al provider.
Por tanto dentro del StatefulWidget que representa cada producto de la lista, accedo con Provider.of(....) con listen:false


  • Notas
  • Al hacer cada elemento de la lista un StatefulWidget, cuando mande redibujar, sólo se redibujará ese elemento de la lista.
  • Si la lista cambiara (por ejemplo, si al añadir un producto a la lista, este se quitara de la lista de productos) tendríamos que usar listen:true en la lista cuando accedemos al provider.
  • Si la lista cambiara de estado por alguna razón (imagina que la lista está dentro de un Container y al pulsar sobre el Container quieres hacer una animación sobre la lista) necesitaríamos que la lista fuera StatefulWidget


  • Siguiendo con nuestro ejemplo:

Clase ListaProductos

Es un StatelessWidget
Dentro del archivo .dart, está definido de forma privada cada elemento de la lista en la clase: _ElementoLista
class ListaProductos extends StatelessWidget {
  const ListaProductos({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('DIBUJA LISTA');
    ProductosProvider provider = Provider.of<ProductosProvider>(context,listen: false);
    return ListView.builder(

              itemCount: provider.productos.length,
              itemBuilder: (_, int pos){
                return _ElementoLista(pos: pos,);
              },
          );
        }
  }
  

class _ElementoLista extends StatefulWidget {
  final int pos;
  const _ElementoLista({Key? key, required this.pos}) : super(key: key);

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

class __ElementoListaState extends State<_ElementoLista> {
  @override
  Widget build(BuildContext context) {
    print('DIBUJA ELEMENTO:${widget.pos}');

    ProductosProvider provider = Provider.of<ProductosProvider>(context, listen:false);
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('${provider.productos[widget.pos]}'),
        IconButton(
          onPressed: (){
            setState(() {
              provider.carrito.contains(provider.productos[widget.pos]) ? provider.removeProductoCarrito(provider.productos[widget.pos]) :
              provider.addProductoCarrito(provider.productos[widget.pos]);

            });
          },
          icon: provider.carrito.contains(provider.productos[widget.pos]) ?  Icon(Icons.remove) : Icon(Icons.add),
          color: provider.carrito.contains(provider.productos[widget.pos]) ? Colors.grey : Colors.green,
        ),


      ],
    );
  }
}


Fijarse que está puesto un print('DIBUJA ELEMENTO:${widget.pos}'); en cada elemento de la lista podéis comprobar como al cambiar el estado del elemento (setState) sólo se redibuja dicho elemento y no la lista entera.


  • Clase ListaProductos
Modificamos la página en la que aparece el carrito para que aparezcan la lista de productos.
class ListaProductosPage extends StatelessWidget {
  const ListaProductosPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('DIBUJA SCAFFOLD');
    return  Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.indigo,
            actions: [
              Stack(
                children: [
                  IconButton(
                      onPressed: () {
                      }, icon: Icon(Icons.shopping_cart_rounded)
                  ),
                  Positioned(
                    top: 10,
                    right: 3,
                    child: Consumer<ProductosProvider>(builder: (BuildContext context,  provider, Widget? child) {
                      return Text('${provider.carrito.length}');
                    },

                    ),
                  )
                ],
              )
            ],
            title: Text('Lista de productos'),
          ),
          body: ListaProductos()
    );
  }
}



  • Podríamos seguir con el ejemplo y crear una página en la que se mostrarían los productos del carrito y que pudiéramos eliminar los productos del mismo y navegar a la misma desde el IconButton que muestra el carrito (la navegación se explica en una sección siguiente)
Esa página:
  • ¿ Es Stateless o Stateful ?
  • ¿ Necesito emplear Consumer ?
  • ¿ Uso Provider.of(...) con listen true o false ?
  • Cada producto que se muestra
  • ¿ Es Stateless o Stateful ?
  • ¿ Lo declaro dentro del Widget que conforma la lista o de forma separada ?
  • ¿ En otro archivo dart o dentro del mismo donde está la lista ?
  • ¿ Necesita acceder al provider ? ¿ De qué forma ?




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).