Aplicacions amb Flutter, Dart i Flame

Tutorial Flutter Flame Projectes   Recursos CITCEA
Exemples Dart Dades pràctiques     Inici

Obtenim la llista de noms

Tornem-nos a mirar el que ens dona el contingut JSON que rebem:

{
  "info": {
    "tipus": "Dones catalanes",
    "descrip": "Llista de les dones catalanes",
    "creat": "20250824",
    "autoria": {
      "autor": "Oriol Boix",
      "contacte": "oriol.boix@upc.edu"
    }
  },
  "dades": [
    {
      "Id": "1826",
      "Ambit": "PS",
      "Nom": "Àngels Ferrer Sensat",
      "Nom casada": "",
      "Altre nom": "",
      "Professio": "Científica catalana",
      "Fet destacat 1": "Va ser innovadora en metodologies d'ensenyament"
    },
    {
      "Id": "850",
      "Ambit": "MD",
      "Nom": "Àngels García Cazorla",
      "Nom casada": "",
      "Altre nom": "",
      "Professio": "Neuropediatra catalana",
      "Fet destacat 1": "Especialista en malalties neurometabòliques"
    },
...

Veiem que la llista de dones es troba dins de l'etiqueta dades. Abans hem agafat el contingut que teníem a l'etiqueta descrip, que està en un objecte dins de l'etiqueta info. De moment, de cada dona agafarem dues informacions: el nom (Nom) i la professió (Professio). De la mateixa manera que abans, crearem una classe, que anomenarem DadesDonaLlista, que ens permeti empaquetar la informació rebuda i extreure'n els camps que ens facin falta. Aquesta classe la crearem al fitxer dades_dona_llista.dart; aquest fitxer el crearem a la carpeta data, com abans.

dades_dona_llista.dart
class DadesDonaLlista {
  final String nom;
  final String professio;

  DadesDonaLlista({
    required this.nom,
    required this.professio,
  });
  
  factory DadesDonaLlista.fromJson(Map dadesJson) {
    return DadesDonaLlista(
      nom: dadesJson["Nom"],
      professio: dadesJson["Professio"],
    );
  }
}

Abans hem fet servir la classe DadesRebudesDones per agafar la descripció, podem fer servir la mateixa classe per retornar la llista. Per un costat, caldrà un paràmetre (llistaDadesDonaLlista) que serà el que retornarem. Per un altre costat, caldrà extreure la informació en format de llista (variable dadesJsonRebut) i convertir-la en una llista del tipus DadesDonaLlista (la classe que acabem de crear), que anomenem llistaDadesDona.

dades_rebudes_dones.dart
import 'package:dones_destacades/data/dades_dona_llista.dart';
class DadesRebudesDones {
  final String descrip;
  final List<DadesDonaLlista> llistaDadesDonaLlista;

  DadesRebudesDones({required this.descrip, required this.llistaDadesDonaLlista});

  factory DadesRebudesDones.fromJson(Map<String, dynamic> jsonRebut){
    var dadesJsonRebut = jsonRebut["dades"] as List;
    List<DadesDonaLlista> llistaDadesDona = 
      dadesJsonRebut.map((dona) => DadesDonaLlista.fromJson(dona)).toList();

    return DadesRebudesDones(
      descrip: jsonRebut["info"]["descrip"],
      llistaDadesDonaLlista: llistaDadesDona
    );
  }
}

El fitxer crida_llista_dones.dart no necessita cap modificació.

Ara ens falta adaptar la pantalla principal. Començarem per mostrar només el nom d'una dona, per anar adaptant el programa poc a poc. Com ara rebrem més informació, canviarem el nom de la variable _descrip per _dadesDones. Si la instantània té dades, intentarem obtenir-ne la llista. Pot passar que la llista no hi sigui (que hi hagi un nul) i, per tant, cal comprovar-ho; si hi és, agafarem el primer element de la llista i n'escriurem el nom; en cas contrari posarem un missatge d'error.

...
class _PantPrincipalState extends State<PantPrincipal> {
  Future<DadesRebudesDones?>? _dadesDones;
  CridaLlistaDones cridaLlistaDones = CridaLlistaDones();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Dones destacades"),),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextButton(
              onPressed: () {
                setState(() {
                  _dadesDones = cridaLlistaDones.demanarLlistaDones();
                });
              },
              child: Text("Dones catalanes"),
            ),
          ),
          FutureBuilder(future: _dadesDones, builder: (context, snapshot){
            if(snapshot.connectionState == ConnectionState.waiting){
              return CircularProgressIndicator();
            } else if(snapshot.hasError){
              return Text("Error: ${snapshot.error}");
            } else if(snapshot.hasData){
              var llistaDones = snapshot.data?.llistaDadesDonaLlista;
              if(llistaDones != null){
                return Text(llistaDones[0].nom);
              } else {
                return Text("Error a la llista");
              }
            } else {
              return Text("No s'han trobat resultats");
            }
          })
        ],
      ),
    );
  }
}

Quan provem l'aplicació i piquem al botó, al camp de text ens apareix un nom.

&Agrave;ngels Ferrer Sensat

Nota: Pot ser que surti un nom diferent, atès que la base de dades es va ampliant al llarg del temps.

Aquest caràcter &Agrave; correspon a À i està en la forma com es codifiquen els caràcters especials en HTML. Caldrà fer servir una funció per descodificar-los, de manera que haurem d'instal·lar la biblioteca corresponent i importar-la. Per un costat, cal anar al fitxer pubspec.yaml per dir-li que volem fer servir html_unescape. Cal posar-ho a dependencies, indicant quina és la versió que volem. És recomanable indicar la darrera versió; atès que quan comencem a escriure html_u ens ajuda a autocompletar, el normal és que ja ens doni la darrera versió. En el moment d'escriure això, la darrera era la 2.0.0. Un cop afegit, cal picar al botó per baixar el paquet. Per un altre costat, cal importar el paquet html_unescape.dart; ja que no ho fa de manera automàtica.

dependencies:
  flutter:
    sdk: flutter
  http: ^1.5.0
  html_unescape: ^2.0.0

En el programa, crearem un objecte del tipus HtmlUnescape i ja podrem fer servir la funció de conversió.

pant_principal.dart
import 'package:dones_destacades/data/crida_llista_dones.dart';
import 'package:dones_destacades/data/dades_rebudes_dones.dart';
import 'package:flutter/material.dart';
import 'package:html_unescape/html_unescape.dart';  // No ho posa automàticament
class PantPrincipal extends StatefulWidget {
  const PantPrincipal({super.key});

  @override
  State<PantPrincipal> createState() => _PantPrincipalState();
}

class _PantPrincipalState extends State<PantPrincipal> {
  Future<DadesRebudesDones?>? _dadesDones;
  CridaLlistaDones cridaLlistaDones = CridaLlistaDones();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Dones destacades"),),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextButton(
              onPressed: () {
                setState(() {
                  _dadesDones = cridaLlistaDones.demanarLlistaDones();
                });
              },
              child: Text("Dones catalanes"),
            ),
          ),
          FutureBuilder(future: _dadesDones, builder: (context, snapshot){
            if(snapshot.connectionState == ConnectionState.waiting){
              return CircularProgressIndicator();
            } else if(snapshot.hasError){
              return Text("Error: ${snapshot.error}");
            } else if(snapshot.hasData){
              var llistaDones = snapshot.data?.llistaDadesDonaLlista;
              var unescape = HtmlUnescape();
              if(llistaDones != null){
                return Text(unescape.convert(llistaDones[0].nom));
              } else {
                return Text("Error a la llista");
              }
            } else {
              return Text("No s'han trobat resultats");
            }
          })
        ],
      ),
    );
  }
}

Si ara ho tornem a provar, obtindrem:

Àngels Ferrer Sensat

que és el que volíem. Posant qualsevol número vàlid en el claudàtor obtindrem el nom de la dona corresponent.

Però nosaltres volíem mostrar una llista i, de moment, només mostrem una de les dones. Podríem intentar fer un bucle, però no és l'opció recomanable per retornar ginys.

Podem mirar de retornar una llista de blocs de text posada dins d'un visor de llista.

              List<Text> txtList = [Text("")];
              if(llistaDones != null){
                for(int i = 0; i < llistaDones.length; i++){
                  txtList.add(Text(unescape.convert(llistaDones[i].nom)));
                }
                return ListView(children: txtList);  // Això fallarà!

Però això fallarà i farà que l'aplicació es pengi o es tanqui. El motiu és que no té manera de calcular prèviament l'espai que ha de reservar per al visor de llista. Una opció és tancar aquest visor dins d'una caixa que tingui una alçada especificada, però probablement és millor fer servir un element d'expandir.

...
              List<Text> txtList = [Text("")];
              if(llistaDones != null){
                for(int i = 0; i < llistaDones.length; i++){
                  txtList.add(Text(unescape.convert(llistaDones[i].nom)));
                }
                return Expanded(
                  child: ListView(children: txtList)
                );
...

Aquest mètode també és poc professional. El que es recomana en aquests casos és emprar un constructor de visor de llista.

...
            } else if(snapshot.hasData){
              var llistaDones = snapshot.data?.llistaDadesDonaLlista;
              var unescape = HtmlUnescape();
              return Expanded(
               child: ListView.builder(
                  itemBuilder: (context, index) {
                    if(llistaDones != null){
                      return Text(unescape.convert(llistaDones[index].nom));
                    } else {
                      return Text("Error a la llista");
                    }
                  },
                ),
              );
...

A més, li podem passar la llargada de la llista per tal que tingui un funcionament més òptim. En aquest cas, la llista pot ser un nul; per això li hem posat un interrogant i oferim una alternativa (llargada zero) per a aquest cas.

...
            } else if(snapshot.hasData){
              var llistaDones = snapshot.data?.llistaDadesDonaLlista;
              var unescape = HtmlUnescape();
              return Expanded(
                child: ListView.builder(
                  itemCount: llistaDones?.length ?? 0,
                  itemBuilder: (context, index) {
                    if(llistaDones != null){
                      return Text(unescape.convert(llistaDones[index].nom));
                    } else {
                      return Text("Error a la llista");
                    }
                  },
                ),
              );
            } else {
...

Ara la pantalla principal és així:

pant_principal.dart
import 'package:dones_destacades/data/crida_llista_dones.dart';
import 'package:dones_destacades/data/dades_rebudes_dones.dart';
import 'package:flutter/material.dart';
import 'package:html_unescape/html_unescape.dart';  // No ho posa automàticament
class PantPrincipal extends StatefulWidget {
  const PantPrincipal({super.key});

  @override
  State<PantPrincipal> createState() => _PantPrincipalState();
}

class _PantPrincipalState extends State<PantPrincipal> {
  Future<DadesRebudesDones?>? _dadesDones;
  CridaLlistaDones cridaLlistaDones = CridaLlistaDones();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Dones destacades"),),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextButton(
              onPressed: () {
                setState(() {
                  _dadesDones = cridaLlistaDones.demanarLlistaDones();
                });
              },
              child: Text("Dones catalanes"),
            ),
          ),
          FutureBuilder(future: _dadesDones, builder: (context, snapshot){
            if(snapshot.connectionState == ConnectionState.waiting){
              return CircularProgressIndicator();
            } else if(snapshot.hasError){
              return Text("Error: ${snapshot.error}");
            } else if(snapshot.hasData){
              var llistaDones = snapshot.data?.llistaDadesDonaLlista;
              var unescape = HtmlUnescape();
              return Expanded(
                child: ListView.builder(
                  itemCount: llistaDones?.length ?? 0,
                  itemBuilder: (context, index) {
                    if(llistaDones != null){
                      return Text(unescape.convert(llistaDones[index].nom));
                    } else {
                      return Text("Error a la llista");
                    }
                  },
                ),
              );
            } else {
              return Text("No s'han trobat resultats");
            }
          })
        ],
      ),
    );
  }
}

Tota la part de mostrar resultats es va fent gran, per deixar el programa de la pantalla principal a una mida raonable, seria una bona idea extreure tota aquesta part (el constructor de futur) en un mètode separat. Per fer-ho, seleccionem el FutureBuilder, piquem amb el botó dret, piquem l'opció Refactor... i piquem l'opció Extract Method; ens demana que li donem un nom, que pot ser mostraLlista. Llavors, quedarà així:

pant_principal.dart
import 'package:dones_destacades/data/crida_llista_dones.dart';
import 'package:dones_destacades/data/dades_rebudes_dones.dart';
import 'package:flutter/material.dart';
import 'package:html_unescape/html_unescape.dart';  // No ho posa automàticament
class PantPrincipal extends StatefulWidget {
  const PantPrincipal({super.key});

  @override
  State<PantPrincipal> createState() => _PantPrincipalState();
}

class _PantPrincipalState extends State<PantPrincipal> {
  Future<DadesRebudesDones?>? _dadesDones;
  CridaLlistaDones cridaLlistaDones = CridaLlistaDones();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Dones destacades"),),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextButton(
              onPressed: () {
                setState(() {
                  _dadesDones = cridaLlistaDones.demanarLlistaDones();
                });
              },
              child: Text("Dones catalanes"),
            ),
          ),
          mostraLlista()
        ],
      ),
    );
  }

  FutureBuilder<DadesRebudesDones?> mostraLlista() {
    return FutureBuilder(
      future: _dadesDones,
      builder: (context, snapshot){
        if(snapshot.connectionState == ConnectionState.waiting){
          return CircularProgressIndicator();
        } else if(snapshot.hasError){
          return Text("Error: ${snapshot.error}");
        } else if(snapshot.hasData){
          var llistaDones = snapshot.data?.llistaDadesDonaLlista;
          var unescape = HtmlUnescape();
          return Expanded(
            child: ListView.builder(
              itemCount: llistaDones?.length ?? 0,
              itemBuilder: (context, index) {
                if(llistaDones != null){
                  return Text(unescape.convert(llistaDones[index].nom));
                } else {
                  return Text("Error a la llista");
                }
              },
            ),
          );
        } else {
          return Text("No s'han trobat resultats");
        }
      }
    );
  }
}

La llargada del programa és similar (de fet, és més gran); però queda més ordenat, amb blocs més compactes.

A més del nom, havíem agafat la professió; i, de moment, no l'estem mostrant. Anem a ampliar el programa per a que la mostri. Per a cada element de la llista, doncs, posarem una columna. Com el mètode també anirà creixent, separarem també aquesta part en un altre mètode:

...
  FutureBuilder<DadesRebudesDones?> mostraLlista() {
    return FutureBuilder(future: _dadesDones, builder: (context, snapshot){
          if(snapshot.connectionState == ConnectionState.waiting){
            return CircularProgressIndicator();
          } else if(snapshot.hasError){
            return Text("Error: ${snapshot.error}");
          } else if(snapshot.hasData){
            var llistaDones = snapshot.data?.llistaDadesDonaLlista;
            //var unescape = HtmlUnescape();
            return Expanded(
              child: ListView.builder(
                itemCount: llistaDones?.length ?? 0,
                itemBuilder: (context, index) {
                  if(llistaDones != null){
                    return mostraElement(llistaDones[index]);
                  } else {
                    return Text("Error a la llista");
                  }
                },
              ),
            );
          } else {
            return Text("No s'han trobat resultats");
          }
        });
  }

  Column mostraElement(DadesDonaLlista donaDeLaLlista) {
    var unescape = HtmlUnescape();
    return Column(
      children: [
        Text(unescape.convert(donaDeLaLlista.nom)),
        Text(unescape.convert(donaDeLaLlista.professio))
      ]
    );
  }
}

Fixem-nos que ara la variable unescape ja no és necessària al mètode mostraLlista i la posem al mètode mostraElement, on sí la fem servir. En aquest moment el programa funciona exactament igual, l'única diferència és que cada text està posat en una columna, que ens permetrà afegir-hi la professió.

Ara ja ens mostra el nom i la professió, però d'una manera poc elegant. D'entrada, voldríem alinear el text a l'esquerra; la columna ens permet fer-ho, definint un crossAxisAlignment. També caldria separar cada element de la llista del següent; això ho podem fer posant la columna dins d'un farciment. Cal tenir en compte que ara el mètode tornarà un farciment, no una columna. A més, podem donar format al text, posant el nom en lletra més gran i negreta. El mètode ens quedarà així:

...
  Padding mostraElement(DadesDonaLlista donaDeLaLlista) {
    var unescape = HtmlUnescape();
    return Padding(
      padding: const EdgeInsets.all(5.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(unescape.convert(donaDeLaLlista.nom), 
            style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
          Text(unescape.convert(donaDeLaLlista.professio), 
            style: TextStyle(fontSize: 13)),
        ]
      )
    );
  }
}

El programa complet per a la pantalla és el següent:

pant_principal.dart
import 'package:dones_destacades/data/crida_llista_dones.dart';
import 'package:dones_destacades/data/dades_dona_llista.dart';
import 'package:dones_destacades/data/dades_rebudes_dones.dart';
import 'package:flutter/material.dart';
import 'package:html_unescape/html_unescape.dart';  // No ho posa automàticament
class PantPrincipal extends StatefulWidget {
  const PantPrincipal({super.key});

  @override
  State<PantPrincipal> createState() => _PantPrincipalState();
}

class _PantPrincipalState extends State<PantPrincipal> {
  Future<DadesRebudesDones?>? _dadesDones;
  CridaLlistaDones cridaLlistaDones = CridaLlistaDones();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Dones destacades"),),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextButton(
              onPressed: () {
                setState(() {
                  _dadesDones = cridaLlistaDones.demanarLlistaDones();
                });
              },
              child: Text("Dones catalanes"),
            ),
          ),
          mostraLlista()
        ],
      ),
    );
  }

  FutureBuilder<DadesRebudesDones?> mostraLlista() {
    return FutureBuilder(future: _dadesDones, builder: (context, snapshot){
      if(snapshot.connectionState == ConnectionState.waiting){
        return CircularProgressIndicator();
      } else if(snapshot.hasError){
        return Text("Error: ${snapshot.error}");
      } else if(snapshot.hasData){
        var llistaDones = snapshot.data?.llistaDadesDonaLlista;
        //var unescape = HtmlUnescape();
        return Expanded(
          child: ListView.builder(
            itemCount: llistaDones?.length ?? 0,
            itemBuilder: (context, index) {
              if(llistaDones != null){
                return mostraElement(llistaDones[index]);
              } else {
                return Text("Error a la llista");
              }
            },
          ),
        );
      } else {
        return Text("No s'han trobat resultats");
      }
    });
  }

  Padding mostraElement(DadesDonaLlista donaDeLaLlista) {
    var unescape = HtmlUnescape();
    return Padding(
      padding: const EdgeInsets.all(5.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(unescape.convert(donaDeLaLlista.nom), 
            style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
          Text(unescape.convert(donaDeLaLlista.professio), 
            style: TextStyle(fontSize: 13)),
        ]
      )
    );
  }
}

Amb això ja hauríem aconseguit l'objectiu proposat; però ara ens podem posar reptes més ambiciosos.

 

 

 

 

 

 

 

 

 

 

Llicència de Creative Commons
Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.