Flutter: ¿Cómo podemos listar elementos?

Listar información en una aplicación es muy común en casi todos los desarrollos. Flutter proporciona ListView para esta tarea, un widget que es muy fácil de utilizar. En este artículo además de ver las tres variantes en que podemos usar este widget, personalizaremos y reutilizaremos widgets (esto de manera muy básica).

Vamos a seguir trabajando con los datos que cargamos en el artículo anterior (si no lo has visto, puedes verlo aquí). Iniciaremos como siempre con el archivo starter de muestro proyecto:

Archivo main.dart:

import 'package:listview/ui/pages/home.dart';
import 'package:listview/ui/pages/list1.dart';
import 'package:listview/ui/pages/list2.dart';
import 'package:listview/ui/pages/list3.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Listas de objetos',
      theme: ThemeData(
        primarySwatch: Colors.purple,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Home(),
      routes: {
        '/lista1': (context) => Lista1Page(),
        '/lista2': (context) => Lista2Page(),
        '/lista3': (context) => Lista3Page()
      },
    );
  }
}

Como pueden ver estamos trabajando con rutas nombradas para llevarle a las tres páginas que nos mostrará las tres variantes de utilizar ListView. Este este caso el archivo starter inicia el widget Home().

Archivo home.dart

import 'package:flutter/material.dart';
import 'package:listview/ui/widgets/custom_buttom.dart';
class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            CustomButtom('ListView.separated(...)', '/lista1'),
            CustomButtom('ListView.builder(...)', '/lista2'),
            CustomButtom('ListView(...)', '/lista3')
          ],
        ),
      ),
    );
  }
}

El widget Home() tiene la responsabilidad de mostrar en la página tres botones, donde cada uno debe enrutar para cada variante de utilizar ListView. Como pueden apreciar se está utilizando un widget personalizado, ya que, en el botón a dibujar solo cambiar el título del mismo y la ruta. Veamos que tiene el widget personalizado CustomButtom:

import 'package:flutter/material.dart';
class CustomButtom extends StatelessWidget {
  final String title ;
  final String route ;
  CustomButtom(this.title, this.route);
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      color: Colors.red,
      elevation: 5,
      child: Text(title, style: TextStyle(color: Colors.white)),
      onPressed: () {
        Navigator.pushNamed(context, route);
      }
    );
  }
}

Simplemente nada, solo estamos tratando de reutilizar y escribir menos código, aunque sabemos que no lo escribimos; solo copiamos y pegamos 🙂

Visto esto, veamos las tres variantes de usar ListView

Archivo list1.dart

import 'package:flutter/material.dart';
import 'package:listview/models/recarga.dart';
import 'package:listview/models/data.dart';
import 'package:listview/ui/widgets/recarga_list_tile.dart';
class Lista1Page extends StatefulWidget {
  @override
  State createState() => new _RecargaPage();
}
class _RecargaPage extends State<Lista1Page> {
  List<Recarga> _recargas = [] ;
  @override
  void initState() {
    _recargas = RECARGAS.map((recarga) => Recarga.fromMap(recarga)).toList() ;
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: new AppBar(
            title: Text("Recargas")
        ),
        body: Center(child:
        Column(children: <Widget>[
            Expanded(
            child:
            (_recargas.length == 0)
                ?
            CircularProgressIndicator()
                :
            ListView.separated(
                itemCount: _recargas.length,
                padding: const EdgeInsets.all(4.0),
                separatorBuilder: (context, index) => Divider(
                  color: Colors.black54,
                ),
                itemBuilder: (context, index) {
                  return RecargaListTile(_recargas[index]) ;
                }
            )
            )
        ],)
    ));
  }
}

Aquí mostramos parte de lo que vimos en el artículo anterior al cargar los datos. Solo resaltar la variante de ListView en las líneas 36-45, donde estamos utilizando ListView.separated(…) que nos permite mostrar un widget por cada uno de los elementos de la lista separados por un separador (que es otro widget y además es obligatorio su uso) que en este caso es el widget Divider(…).

Archivo list2.dart

import 'package:flutter/material.dart';
import 'package:listview/models/recarga.dart';
import 'package:listview/models/data.dart';
import 'package:listview/ui/widgets/recarga_list_tile.dart';
class Lista2Page extends StatefulWidget {
  @override
  State createState() => new _RecargaPage();
}
class _RecargaPage extends State<Lista2Page> {
  List<Recarga> _recargas = [] ;
  @override
  void initState() {
    _recargas = RECARGAS.map((recarga) => Recarga.fromMap(recarga)).toList() ;
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: new AppBar(
            title: Text("Recargas")
        ),
        body: Center(child:
        Column(children: <Widget>[
            Expanded(
            child:
            (_recargas.length == 0)
                ?
            CircularProgressIndicator()
                :
            ListView.builder(
                itemCount: _recargas.length,
                itemBuilder: (context, index) {
                  return RecargaListTile(_recargas[index]) ;
                }
            )
            )
        ],)
    ));
  }
}

En este caso estamos usando la variante ListView.builder(…) que nos permite mostrar solamente un widget por cada elemento de la lista. En esta variante y la anterior podemos hacer uso del índice del elemento en la lista (index).

Archivo list3.dart:

import 'package:flutter/material.dart';
import 'package:listview/models/recarga.dart';
import 'package:listview/models/data.dart';
import 'package:listview/ui/widgets/recarga_list_tile.dart';
class Lista3Page extends StatefulWidget {
  @override
  State createState() => new _RecargaPage();
}
class _RecargaPage extends State<Lista3Page> {
  List<Recarga> _recargas = [] ;
  @override
  void initState() {
    _recargas = RECARGAS.map((recarga) => Recarga.fromMap(recarga)).toList() ;
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: new AppBar(
            title: Text("Recargas")
        ),
        body: Center(child:
        Column(children: <Widget>[
          Expanded(
              child:
              (_recargas.length == 0)
                  ?
              CircularProgressIndicator()
                  :
              ListView(
                  children: _recargas.map((offer) => RecargaListTile(offer)).toList()
              )
          )
        ],)
        ));
  }
}

En este último ejemplo estamos adicionando la lista de widgets en la propiedad children de ListView. Como pueden ver en los tres casos estamos utilizando un widget personalizado RecargaListTile que nos muestra la información de la recarga en un ListTile.

Archivo recarga_list_tile.dart

import 'package:flutter/material.dart';
import 'package:listview/models/recarga.dart';
class RecargaListTile extends StatelessWidget {
  final Recarga r ;
  RecargaListTile(this.r);
  @override
  Widget build(BuildContext context) {
    return ListTile(
        key: UniqueKey(),
        enabled: true,
        leading: Icon(r.enviada ? Icons.send : Icons.info_outline, size: 40, color: (r.enviada ? Colors.green : Colors.blue)),
        title: Text('${r.telefono} (\$${r.monto.toString()})',  style: TextStyle(fontSize:16, color: Colors.black)),
        subtitle: Text(r.nombre, style: TextStyle(color: Colors.black54)),
        trailing: Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Icon(r.cobrada ? Icons.money_off : Icons.attach_money, color: r.cobrada ? Colors.green : Colors.red)
            ]
        )
    );
  }
}

Hasta aquí las variantes de ListView ejemplificadas, pueden ver todo el código en este enlace github