ListView en Flutter

Este es el tercer tutorial de las serie Fundamentos de Flutter donde aprendemos a utilizar el ListView en diferentes entornos, con datos por defecto, con datos desde una lista estática y obteniendo los datos desde un archivo externo.

Índice

  1. Qué es un ListView
  2. Llenando un ListView desde una lista
  3. Llenando un ListView desde un Json
  4. De String a Icon
  5. Tu turno
  6. El código

1. Qué es un ListView

Un ListView es un widget de Flutter que nos permite visualizar en forma de lista un conjunto de widgets dentro de él. Existen dos tipos de ListViews el primero es el listview.builder el cual es el indicado para renderizar muchos elementos en la pantalla ya que se crean conforme se va haciendo scroll en la pantalla. El segundo es el listview normal que renderiza todos los elementos desde el inicio, por lo que si la cantidad de elementos en su interior es demasiado grande este demorará en cargar. En este tutorial aprenderemos a crear un listview normal.

Un ListView solo funciona dentro de un Scaffold. Así que podemos colocar el ListView dentro del body de un Scaffold. Sin embargo, para evitar colocar demasiado código dentro del Scaffold y sea ilegible usamos una función que nos devuelva un ListView y solo referenciamos a esa función dentro del body. A continuación veamos el creación de un ListView.

ListView _milista() {
  return ListView(
    children: <Widget>[
      // ListTile es un widget que funciona como un bloque del ListView
      ListTile(
        // Tiene muchas propiedades que nos ayudan a ordenar la información.
        // Titulo
        title: Text('Primero'),
        // Subtitulo
        subtitle: Text('Este es el primer Tile'),
        // Icono inicial del bloque
        leading: Icon(Icons.add),
        // Funcion que se ejecuta al hacer click en él
        onTap: () {},
      ),
      ListTile(
        title: Text('Primero'),
        onTap: () {},
      )
    ],
  );
}

Una propiedad también de importante es el pading y este nos permite colocar un padding a nuestro ListView. Sin embargo, el tipo de dato que acepta esta propiedad es algo especial. Esta propiedad acepta un EdgeInsetsGeometry que a su vez es una clase padre de EdgeInsets. Así que podemos usar este último en la propiedad. Además podemos agregar el pading a todos lo lados como en el siguiente primer ejemplo o de un tamaño definido para la horizontal y vertical, como en el segundo ejemplo.

// Pading a todos los lados
padding: EdgeInsets.all(10),
// Pading exacto a los lados verticales y horizontales
padding: EdgeInsets.symmetric(horizontal: 23.0, vertical: 10.0),

Esa es la forma más fácil de crear un listView con elementos estáticos, pero no siempre se tiene elementos estáticos así que a continuación crearemos un listView a partir de una lista o arreglo.

2. Llenando un ListView desde una lista

En el código anterior vimos que el children de la ListView acepta una lista de widgets <Widget>[]. Aprovecharemos eso y crearemos un método que nos devuelva una lista de Widgets que serán llenados recorriendo mediante un for nuestra lista de string previamente creada.

class HomePage extends StatelessWidget {
  final opciones = ['Primero', 'Segundo', 'Tercedddro'];
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hola')),
      body: ListView(
        children: crearItems(),
      ),
    );
  }

  List<Widget> crearItems() {
    final lista = List<Widget>();
    for (String opt in opciones) {
      final tempWidget = ListTile(title: Text(opt));
      lista.add(tempWidget);
      // Divider es un widget que crea una division entre cada elemento de la lista.
      // Agregamos un Divider debajo de cada elemento de la lista
      lista.add(Divider());
    }
    return lista;
  }
}

Acabamos de crear una lista a partir de una Lista. Ahora usando tu conocimientos del anterior tutorial ya puedes crear un botón que agregue elementos a la lista de tal forma que la lista aumente automáticamente. No te olvides que para esto la clase debe ser del tipo StatefulWidget.

Puedes notas que para crear un objeto se puede proceder de distintas maneras, donde el new es opcional.
final List<Widget> lista = [];
List<Widget> lista = [];
List<Widget> lista = new List<Widget>();
List<Widget> lista = List<Widget>();
var lista = new List<Widget>();
var lista = List<Widget>();
final lista = new List<Widget>();
final lista = List<Widget>();

Otra forma para agregar varios elementos a la lista sin repetir código es usando el método de cascada de la siguiente forma

lista..add(tempWidget)..add(Divider());
// Es igual a usar
lista.add(tempWidget);
lista.add(Divider());

Otro método aún más elegante de llenar una lista es usando el método forEach como en el siguiente ejemplo.

List<Widget> _crearItems() {
  final lista = new List<Widget>();
  opciones.forEach((element) {
    final tempWidget = ListTile(
      title: Text(element),
    );
    lista.add(tempWidget);
  });
  return lista;
}

Otra forma aún más simplificada para llenar un ListView es usando el método map. Este método devuelve un Iterable con los elementos que retornamos en la función flecha. Por ese motivo debemos usar el método toList() para convertirlo en una lista de Widgets.

List<Widget> _crearItems(){
  return opciones.map((item) =>
    ListTile(title: Text(item))
  ).toList();
}

3. Llenando un ListView desde un Json

Para usar un archivo externo lo primero que tenemos que hacer es declararlo dentro del archivo pubspec.yaml. Dentro encontraremos la sección assets comentada. Solo debemos quitar el símbolo # de la parte delantera y dejarlo de la siguiente forma.

assets:
  - data/menu.json

En nuestro caso trabajaremos con un archivo menu.json que colocaremos dentro de la carpeta data y contiene el siguiente código.

{
  "menu": [
    {
      "icono": "add_alert",
      "texto": "Primero :D"
    },
    {
      "icono": "accessibility",
      "texto": "Segundo :3"
    },
    {
      "icono": "folder_open",
      "texto": "Tercero :p"
    }
  ]
}

Lo que haremos es llenar un ListView a partir de ese archivo .json. Para esto crearemos un archivo llamado lib/src/providers/menu_provider.dart donde colocaremos la clase que nos permitirá obtener una lista a partir del archivo .json.

Para esto usaremos el método rootBundle del paquete flutter/services.dart. Usando su método loadString() obtendremos el contenido de un archivo externo en este caso nuestro archivo json. Sin embargo, este método devuelve un Future<String> y recordemos que un Future es como una promesa, algo que esperamos recibir pero aún no tenemos así que es adecuado para usar el async-await para recibirlo.

import 'dart:convert';
import 'package:flutter/services.dart';

class MenuProvider {

  // Creamos la lista que recibiá el objeto del json
  // como es un objeto usmos dynamic
  List<dynamic> opciones = [];

  // Creamos una función que devuelve un Future
  // El Futuro es una promesa por lo que podemos usar async-await
  Future<List<dynamic>> cargarData() async {

    // Obtenemos el contenido del archivo usando loadString
    final respuesta = await rootBundle.loadString('data/menu.json');

    // Usamos json.decode para convertir el contenido a un map
    Map<String, dynamic> dataMap = json.decode(respuesta);

    // Usamos el key 'menu' del map para optener una lista de objetos
    opciones = dataMap['menu'];

    // Retornamos la lista
    return opciones;
  }
}

// creamos el objeto, el cual lo obtendremos desde otro archivo
final menuProvider = new MenuProvider();

Existe un Widget que se construye a si mismo a partir de la ultima instantánea (snapshot) que viene desde un Future. Nos referimos a un FutureBuilder. Básicamente este widget construye otro widget a partir de un Future. Así que utilizaremos esto para crear nuestro ListView.

Widget listView() {
  return FutureBuilder(
    // A esta propiedad le damos el future
    future: menuProvider.cargarDatos(),
    // A esta propiedad le damos los datos iniciales mientras carga el Future
    initialData: [],
    // Esta propiedad lo explicamos debajo
    builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
      return ListView(
        children: listaItems(snapshot.data),
      );
    });
}

La propiedad builder contiene una función que devuelve el Widget que se creará a partir del Future proporcionado a la propiedad future. Esta función tiene dos parámetros, el primero es el context que contiene información sobre nuestra aplicación en general y el segundo es elsnapshot, que es la última instantánea de los datos que trajimos mediante el Future.

Así que usaremos esta instantánea para retornar un ListView cuya propiedad children será creada mediante una función que tenga como parámetro el snapshot.data, que es el objeto con los datos obtenidos del Future a partir del menu.json. Entonces pasemos ahora a crear la lista de Widgets que necesita el ListView.

List<Widget> listaItems(List<dynamic> data) {

  // Creamos la lista de Widgets que necesitará el children del ListView
  final List<Widget> opciones = [];

  // Usamos forEach para llenar los elementos de la lista
  data.forEach((element) {
    final widgetTemp = ListTile(
      title: Text(element['texto']),
    );

    opciones.add(widgetTemp);
  });

  // Retornamos la lista de Widgets
  return opciones;
}

Listo!! como ves es una especie de cadena que se forma con las diferentes funciones. Comienza con la creación del ListView que lo hace mediante un FutureBuilder que necesita del Future con los datos del menu.json para retornar mediante la su propiedad builder un ListView que se crea a partir del Future. Entonces creamos otro método que nos devuelva la lista de widgets para nuestro ListView.

De seguro te diste cuenta que dentro del archivo json tenemos los nombres de los iconos. Flutter no tiene una forma nativa de obtener un icono a partir de un texto. Asi que explicaremos un método que podemos usar para convertir de texto a tipo Icon.

4. De String a Icon

Para esto crearemos una carpeta utils dentro del cual crearemos un archivo llamado string_to_icon.dart donde primero crearemos un map de los iconos que utilizaremos.

Un map no es más que una lista donde cada elemento tiene un key y un value que pueden ser de cualquier tipo. De tal manera que podamos encontrar el value a partir del key.
// IconData es clase padre de Icons
final icons = <String, IconData>{
  'add_alert'     : Icons.add_alert,
  'accessibility' : Icons.accessibility,
  'folder_open'   : Icons.folder_open
};

Ahora crearemos una función que reciba como parámetro un string y devuelva un icono leyendo el map que creamos anteriormente. Así que solo servirá para los iconos mapeados en nuestro map.

Icon getIcon(String nombreIcono){
  return Icon(icons[nombreIcono]);
}

5. Tu turno

Usando estos conceptos que acabamos de aprender te animo a realizar el siguiente proyecto. Una aplicación en Flutter donde muestres un ListView con los datos e icono obtenidos a partir de un archivo json, como se muestra a continuación.

Cuando quieras puedes ver el código del proyecto terminado. Pero ándale inténtalo tu mismo!!

6. El código

El código de este proyecto está en el siguiente repositorio de GitHub: github.com/andygeek/list_app_flutter