Aplicacions amb Flutter, Dart i Flame

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

Cerquem més informació de la persona triada

Aquest web ens ofereix la possibilitat de rebre força més dades per a una persona concreta, només cal saber el seu identificador (Id). Per exemple, per a l'Àngels Ferrer (Id = 1826) podem rebre aquesta informació:

{
  "info": {
    "tipus": "Dona concreta",
    "descrip": "Dades de la dona demanada a la peticio",
    "creat": "20250911",
    "autoria": {
      "autor": "Oriol Boix",
      "contacte": "oriol.boix@upc.edu"
    }
  },
  "dades": [
    {
      "Id": "1826",
      "Ambit": "PS",
      "Nom": "Àngels Ferrer Sensat",
      "Data neix": "19040518",
      "Data mort": "19921130",
      "Nom casada": "",
      "Altre nom": "",
      "Professio": "Científica catalana",
      "Fet destacat 1": "Va ser innovadora en metodologies d'ensenyament",
      "Fet destacat 2": "Va defensar l'ensenyament experimental de les ciències naturals",
      "Fet destacat 3": "",
      "Foto": "d1826.jpg",
      "Font de la foto": "https://giliet.wordpress.com/2015/12/02/la-senyora-angeleta/"
    }
  ]
}

En la pantalla on es mostra el detall, doncs, podem mostrar tota aquesta informació (inclosa la fotografia). Això implicarà que caldrà fer una nova crida al web, un cop conegut l'identificador.

Podem començar per crear la classe DadesRebudesDetall, per empaquetar totes les dades rebudes. Aquest cop només ens interessa retornar informació de l'apartat dades, el qual conté una llista d'un sol element; per tant ja no cal gestionar la llista, només hem de mirar el contingut del primer element.

dades_rebudes_detall.dart
class DadesRebudesDetall {
  final String id;
  final String ambit;
  final String nom;
  final String dataNeix;
  final String dataMort;
  final String casada;
  final String alies;
  final String professio;
  final String destacat1;
  final String destacat2;
  final String destacat3;
  final String foto;
  final String font;

  DadesRebudesDetall({
    required this.id,
    required this.ambit,
    required this.nom,
    required this.dataNeix,
    required this.dataMort,
    required this.casada,
    required this.alies,
    required this.professio,
    required this.destacat1,
    required this.destacat2,
    required this.destacat3,
    required this.foto,
    required this.font,
  });

  factory DadesRebudesDetall.fromJson(Map<String, dynamic> jsonRebut){
    var dadesJsonRebut = jsonRebut["dades"] as List;

    return DadesRebudesDetall(
      id: dadesJsonRebut[0]["Id"],
      ambit: dadesJsonRebut[0]["Ambit"],
      nom: dadesJsonRebut[0]["Nom"],
      dataNeix: dadesJsonRebut[0]["Data neix"],
      dataMort: dadesJsonRebut[0]["Data mort"],
      casada: dadesJsonRebut[0]["Nom casada"],
      alies: dadesJsonRebut[0]["Altre nom"],
      professio: dadesJsonRebut[0]["Professio"],
      destacat1: dadesJsonRebut[0]["Fet destacat 1"],
      destacat2: dadesJsonRebut[0]["Fet destacat 2"],
      destacat3: dadesJsonRebut[0]["Fet destacat 3"],
      foto: dadesJsonRebut[0]["Foto"],
      font: dadesJsonRebut[0]["Font de la foto"],
    );
  }
}

Ara anem a preparar la crida al web. En aquest cas serà similar a les anteriors però caldrà afegir-hi un paràmetre.

crida_detall.dart
import 'dart:convert';
import 'package:dones_destacades/data/dades_rebudes_detall.dart';
import 'package:http/http.dart' as http;  // No el troba automàticament, s'ha d'entrar a mà
class CridaDetall{
  Future<DadesRebudesDetall?> demanarDetall(String ident) async{
    final detallRebut = await 
      http.get(Uri.parse("https://recursos.citcea.upc.edu/dones/service.php?tipus=concreta&id=$ident"));
    if(detallRebut.statusCode == 200){
      var jsonDecodificat = jsonDecode(detallRebut.body);
      DadesRebudesDetall dadesRebudesDetall = DadesRebudesDetall.fromJson(jsonDecodificat);
      return dadesRebudesDetall;
    } else {
      return null;
    }
  }
}

A la pantalla de detall haurem de carregar les dades de la persona triada a la pantalla principal, això voldrà dir que la crida al canvi de pantalla haurà d'incloure un paràmetre, l'identificador corresponent. Però, en canvi, ja no caldrà passar tota la informació de la persona; per això ara el paràmetre persona contindrà només l'identificador.

pant_principal.dart
import 'package:dones_destacades/data/crida_llista_ambits.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:dones_destacades/screens/pant_detalls.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(){
    return _PantPrincipalState();
  }
}

class _PantPrincipalState extends State<PantPrincipal> {
  Map<String, String> _dadesAmbits = {};
  Future<DadesRebudesDones?>? _dadesDones;
  CridaLlistaDones cridaLlistaDones = CridaLlistaDones();
  CridaLlistaAmbits cridaLlistaAmbits = CridaLlistaAmbits();
  bool _carregat = false;

  @override
  void initState() {
    super.initState();
    _carregarDades();
  }

  Future<void> _carregarDades() async {
    final dades = await cridaLlistaAmbits.demanarLlistaAmbits();
    setState(() {
      _dadesAmbits = dades;
      _carregat = true;
    });
  }


  @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;
        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();
    String ambitAct = "";
    if(_carregat){
      ambitAct = _dadesAmbits[donaDeLaLlista.ambit].toString();
    }
    return Padding(
      padding: const EdgeInsets.all(5.0),
      child: GestureDetector(
        onTap: () => Navigator.push(context, MaterialPageRoute(
            builder: (context) => PantDetalls(
              persona: donaDeLaLlista.id,
              dadesAmbits: _dadesAmbits,
              carregat: _carregat),
          ),
        ),
        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)),
            if(_carregat && ambitAct.isNotEmpty) 
              Text("Àmbit: ${unescape.convert(ambitAct)}", style: TextStyle(fontSize: 13)),
          ]
        )
      )
    );
  }
}

Ara ens falta la pantalla de detall. Aquí caldrà fer la crida per llegir les dades, de la mateixa manera com ho vam fer a la pantalla principal. La part de mostrar dades s'amplia per donar cabuda a les noves dades. Comencem per mostrar la imatge i les dades que són senzilles de mostrar. Per a les que poden no tenir informació, creem una variable que ens permet donar-los un contingut alternatiu (una cadena buida).

Pot ser que hi hagi fotografia de la persona o que no, cal comprovar-ho. A més, atès que voldrem centrar la foto i posar-li marges, l'haurem de posar en un farciment. Per simplificar-ho, posem aquesta part dins d'una funció (posaFoto).

pant_detalls.dart
import 'package:dones_destacades/data/crida_detall.dart';
import 'package:dones_destacades/data/dades_rebudes_detall.dart';
import 'package:flutter/material.dart';
import 'package:html_unescape/html_unescape.dart';
class PantDetalls extends StatefulWidget {
  final String persona;
  final Map<String, String> dadesAmbits;
  final bool carregat;

  const PantDetalls({super.key, required this.persona, required this.dadesAmbits, required this.carregat});

  @override
  State<PantDetalls> createState() => _PantDetallsState();
}

class _PantDetallsState extends State<PantDetalls> {
  DadesRebudesDetall? _dadesDetall;
  CridaDetall cridaDetall = CridaDetall();
  bool _rebut = false;

  @override
  void initState() {
    super.initState();
    _carregarDadesDona();
  }

  Future<void> _carregarDadesDona() async {
    final dades = await cridaDetall.demanarDetall(widget.persona);
    setState(() {
      _dadesDetall = dades;
      _rebut = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    var unescape = HtmlUnescape();
    String ambitAct = "";
    if(_rebut && widget.carregat){
      if(_dadesDetall != null){
        ambitAct = widget.dadesAmbits[_dadesDetall?.ambit].toString();
      }
    }
    String casada = _dadesDetall?.casada ?? "";
    String alies = _dadesDetall?.alies ?? "";
    String destacat2 = _dadesDetall?.destacat2 ?? "";
    String destacat3 = _dadesDetall?.destacat3 ?? "";
    String imatge = _dadesDetall?.foto ?? "";
    String font = _dadesDetall?.font ?? "";
    return Scaffold(
      appBar: AppBar(
        title: Text(unescape.convert(_dadesDetall?.nom ?? "No trobat")),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if(imatge.isNotEmpty) posaFoto(imatge),
          Text(
            unescape.convert(_dadesDetall?.nom ?? ""),
            style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)
          ),
          if(casada.isNotEmpty) Text(
            "Nom de casada: ${unescape.convert(casada)}",
            style: TextStyle(fontSize: 13)
          ),
          if(alies.isNotEmpty) Text(
            "Àlies: ${unescape.convert(alies)}",
            style: TextStyle(fontSize: 13)
          ),
          SizedBox(height: 5),
          Text(unescape.convert(
            _dadesDetall?.professio ?? ""),
            style: TextStyle(fontSize: 14)
          ),
          if(widget.carregat && ambitAct.isNotEmpty) Text(
            "Àmbit: ${unescape.convert(ambitAct)}",
            style: TextStyle(fontSize: 13)
          ),
          Text(unescape.convert(
            _dadesDetall?.destacat1 ?? ""),
            style: TextStyle(fontSize: 13)
          ),
          if(destacat2.isNotEmpty) Text(
            unescape.convert(destacat2),
            style: TextStyle(fontSize: 13)
          ),
          if(destacat3.isNotEmpty) Text(
            unescape.convert(destacat3),
            style: TextStyle(fontSize: 13)
          ),
          SizedBox(height: 25),
          if(font.isNotEmpty) Text(
            "Font de la imatge: ${unescape.convert(font)}",
            style: TextStyle(fontSize: 10)
          ),
        ],
      ),
    );
  }

  Padding posaFoto(String imatge){
    return Padding(
      padding: const EdgeInsets.all(20.0),
      child: Container(
        alignment: Alignment(0, -1),
        child: Image.network("https://recursos.citcea.upc.edu/dones/fotos/$imatge"),
      ),
    );
  } 
}

Ens falta mostrar les dates de naixement i, si escau, defunció. Aquesta part, atès el format en el que es faciliten les dates, és més complexa; per això anirem pas a pas.

El primer que farem serà crear una funció que mostri les dues dates entre parèntesi i separades per un guió mitjà (–).

  Text posaDates(String? dn, String? dm){
    String dNeix = dn ?? "";
    String dMort = dm ?? "";
    return Text("($dNeix – $dMort)");
  }

Aquesta funció té les dues dates com a paràmetres i la cridarem en el moment de posar la resta de dades de la persona:

      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if(imatge.isNotEmpty) posaFoto(imatge),
          Text(
            unescape.convert(_dadesDetall?.nom ?? ""),
            style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)
          ),
          posaDates(_dadesDetall?.dataNeix, _dadesDetall?.dataMort),
          if(casada.isNotEmpty) Text(
            "Nom de casada: ${unescape.convert(casada)}",
            style: TextStyle(fontSize: 13)
          ),
          if(alies.isNotEmpty) Text(
            "Àlies: ${unescape.convert(alies)}",
            style: TextStyle(fontSize: 13)
          ),
          SizedBox(height: 5),
          Text(unescape.convert(
            _dadesDetall?.professio ?? ""),
            style: TextStyle(fontSize: 14)
          ),
          if(widget.carregat && ambitAct.isNotEmpty) Text(
            "Àmbit: ${unescape.convert(ambitAct)}",
            style: TextStyle(fontSize: 13)
          ),
          Text(unescape.convert(
            _dadesDetall?.destacat1 ?? ""),
            style: TextStyle(fontSize: 13)
            ),
          if(destacat2.isNotEmpty) Text(unescape.convert(destacat2), style: TextStyle(fontSize: 13)),
          if(destacat3.isNotEmpty) Text(unescape.convert(destacat3), style: TextStyle(fontSize: 13)),
          SizedBox(height: 25),
          if(font.isNotEmpty) Text(
            "Font de la imatge: ${unescape.convert(font)}",
            style: TextStyle(fontSize: 10)
          ),
        ],
      ),

El resultat no és gaire útil, atès que obtindrem coses com:

(19040518 – 19921130)

Ara anem a modificar aquesta funció per fer que les dates siguin més entenedores.

Segons la pàgina on es descriu el format de les dades, una data completa es representa amb vuit digits; els quatre primers són l'any, els següents el més i els darrers el dia. Per exemple 20210619 correspon al 19 de juny de 2021. Si no se sap el dia o el mes, el seu valor és zero. Per exemple 20210600 indica juny de 2021 i 20210000 representa l'any 2021. Si només se'n sap el segle, el dia i el mes estan a zero i les dues darreres xifres de l'any es substitueixen per asteriscs; per exemple 20**0000 correspon al segle XXI. Si la data és anterior a la nostra era (aC) s'indica afegint una a al final, per exemple 03**0000a fa referència al segle IV aC.

La funció arreglaData s'encarrega de gestionar això i, a més, convertir la data numèrica en textual. Hi ha unes variables que guarden els segles, els mesos i les preposicions que corresponen a cada mes. Abans de res, es separen els dígits per obtenir l'any (amb dues i quatre xifres), el segle (en realitat, meny u), el mes i el dia. Amb això, i partint dels criteris esmentats, es composa la data en format text; tenint en compte que les llistes de mesos i preposicions van ve 0 a 11 i, en canvi, els mesos van d'1 a 12.

  String arreglaData(String dat){
    var unescape = HtmlUnescape();
    List<String> segles = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", 
      "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX", 
      "XXI", "XXII", "XXIII", "XXIV", "XXV", "XXVI", "XXVII", "XXVIII"];
    List<String> mesos = ["gener", "febrer", "març", "abril", "maig", "juny", 
		  "juliol", "agost", "setembre", "octubre", "novembre", "desembre"];
    List prep = ["de ", "de ", "de ", "d'", "de ", "de ", 
		  "de ", "d'", "de ", "d'", "de ", "de "];
    String resp = "";
    if(dat.isNotEmpty){
      String any = dat.substring(2, 4);  // Any, dues xifres
      String sS = dat.substring(0, 2);  // Segle menys u
		  String aA = dat.substring(0, 4);  // Any, quatre xifres
		  String mM = dat.substring(4, 6);  // Mes
		  String dD = dat.substring(6, 8);  // Dia
      if(any == "**"){
        resp = "s. ${segles[int.parse(sS)]}";
      } else {
        if((dD == "00") && (mM == "00")){
          resp = aA;
        } else {
          if(dD == "00"){
            resp = "${mesos[int.parse(mM) - 1]} de ${int.parse(aA)}";
          } else {
            if(dD.substring(0, 1) == "0"){
              dD = dD.substring(1);  // Elimina 0 esquerra
            }
            resp = "$dD ${prep[int.parse(mM) - 1]}${mesos[int.parse(mM) - 1]} de $aA";
          }
        }
      }
      if(dat.length > 8){
        if(dat.substring(8) == "a"){
          resp = "$resp aC";
        }
      }
    }
    return unescape.convert(resp);
  }

Ara modificarem la funció posaDates perquè posi les dates formatades.

  Text posaDates(String? dn, String? dm){
    String dNeix = arreglaData(dn ?? "");
    String dMort = arreglaData(dm ?? "");
    return Text("($dNeix – $dMort)");
  }

Tal com ho tenim ara, obtindríem resultats com aquests:

(12 de març de 1947 – 19 de setembre de 2013)
(16 de setembre de 1933 – )
(s. IV aC – s. IV aC)

En aquest darrer cas, només sabem que la persona va viure al s. IV aC; per això no té massa sentit posar dues dates. Podem modificar la funció per tal que si les dates de naixement i mort coincideixen (fet que només pot succeir si són segles) només se'n posi una:

(s. IV aC)

La versió final de la panttalla de detalls quedarà així:

pant_detalls.dart
import 'package:dones_destacades/data/crida_detall.dart';
import 'package:dones_destacades/data/dades_rebudes_detall.dart';
import 'package:flutter/material.dart';
import 'package:html_unescape/html_unescape.dart';
class PantDetalls extends StatefulWidget {
  final String persona;
  final Map<String, String> dadesAmbits;
  final bool carregat;

  const PantDetalls({super.key, required this.persona, required this.dadesAmbits, required this.carregat});

  @override
  State<PantDetalls> createState() => _PantDetallsState();
}

class _PantDetallsState extends State<PantDetalls> {
  DadesRebudesDetall? _dadesDetall;
  CridaDetall cridaDetall = CridaDetall();
  bool _rebut = false;

  @override
  void initState() {
    super.initState();
    _carregarDadesDona();
  }

  Future<void> _carregarDadesDona() async {
    final dades = await cridaDetall.demanarDetall(widget.persona);
    setState(() {
      _dadesDetall = dades;
      _rebut = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    var unescape = HtmlUnescape();
    String ambitAct = "";
    if(_rebut && widget.carregat){
      if(_dadesDetall != null){
        ambitAct = widget.dadesAmbits[_dadesDetall?.ambit].toString();
      }
    }
    String casada = _dadesDetall?.casada ?? "";
    String alies = _dadesDetall?.alies ?? "";
    String destacat2 = _dadesDetall?.destacat2 ?? "";
    String destacat3 = _dadesDetall?.destacat3 ?? "";
    String imatge = _dadesDetall?.foto ?? "";
    String font = _dadesDetall?.font ?? "";
    return Scaffold(
      appBar: AppBar(
        title: Text(unescape.convert(_dadesDetall?.nom ?? "No trobat")),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if(imatge.isNotEmpty) posaFoto(imatge),
          Text(unescape.convert(
            _dadesDetall?.nom ?? ""),
            style: TextStyle(fontSize: 15,
            fontWeight: FontWeight.bold)
          ),
          posaDates(_dadesDetall?.dataNeix, _dadesDetall?.dataMort),
          if(casada.isNotEmpty) Text(
            "Nom de casada: ${unescape.convert(casada)}",
            style: TextStyle(fontSize: 13)
          ),
          if(alies.isNotEmpty) Text(
            "Àlies: ${unescape.convert(alies)}",
            style: TextStyle(fontSize: 13)
          ),
          SizedBox(height: 5),
          Text(unescape.convert(_dadesDetall?.professio ?? ""), style: TextStyle(fontSize: 14)),
          if(widget.carregat && ambitAct.isNotEmpty) Text(
            "Àmbit: ${unescape.convert(ambitAct)}",
            style: TextStyle(fontSize: 13)
          ),
          Text(unescape.convert(_dadesDetall?.destacat1 ?? ""), style: TextStyle(fontSize: 13)),
          if(destacat2.isNotEmpty) Text(unescape.convert(destacat2), style: TextStyle(fontSize: 13)),
          if(destacat3.isNotEmpty) Text(unescape.convert(destacat3), style: TextStyle(fontSize: 13)),
          SizedBox(height: 25),
          if(font.isNotEmpty) Text(
            "Font de la imatge: ${unescape.convert(font)}",
            style: TextStyle(fontSize: 10)
          ),
        ],
      ),
    );
  }

  Text posaDates(String? dn, String? dm){
    String dNeix = arreglaData(dn ?? "");
    String dMort = arreglaData(dm ?? "");
    if(dn == dm){
      return Text("($dNeix)");
    } else {
      return Text("($dNeix – $dMort)");
    }
  }

  String arreglaData(String dat){
    var unescape = HtmlUnescape();
    List<String> segles = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", 
      "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX", 
      "XXI", "XXII", "XXIII", "XXIV", "XXV", "XXVI", "XXVII", "XXVIII"];
    List<String> mesos = ["gener", "febrer", "març", "abril", "maig", "juny", 
		  "juliol", "agost", "setembre", "octubre", "novembre", "desembre"];
    List prep = ["de ", "de ", "de ", "d'", "de ", "de ", 
		  "de ", "d'", "de ", "d'", "de ", "de "];
    String resp = "";
    if(dat.isNotEmpty){
      String any = dat.substring(2, 4);  // Any, dues xifres
      String sS = dat.substring(0, 2);  // Segle menys u
		  String aA = dat.substring(0, 4);  // Any, quatre xifres
		  String mM = dat.substring(4, 6);  // Mes
		  String dD = dat.substring(6, 8);  // Dia
      if(any == "**"){
        resp = "s. ${segles[int.parse(sS)]}";
      } else {
        if((dD == "00") && (mM == "00")){
          resp = aA;
        } else {
          if(dD == "00"){
            resp = "${mesos[int.parse(mM) - 1]} de ${int.parse(aA)}";
          } else {
            if(dD.substring(0, 1) == "0"){
              dD = dD.substring(1);  // Elimina 0 esquerra
            }
            resp = "$dD ${prep[int.parse(mM) - 1]}${mesos[int.parse(mM) - 1]} de $aA";
          }
        }
      }
      if(dat.length > 8){
        if(dat.substring(8) == "a"){
          resp = "$resp aC";
        }
      }
    }
    return unescape.convert(resp);
  }

  Padding posaFoto(String imatge){
    return Padding(
      padding: const EdgeInsets.all(20.0),
      child: Container(
        alignment: Alignment(0, -1),
        child: Image.network("https://recursos.citcea.upc.edu/dones/fotos/$imatge"),
      ),
    );
  } 
}

 

 

 

 

 

 

 

 

 

 

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