En el que portem fet fins ara, quan consultem les dades ens apareix el camp Ambit, que fins ara hem ignorat, que conté un codi de dues lletres que identifica l'àmbit de la ciència o la tecnologia en el qual va destacar més aquella persona. Per exemple:
{
"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"
},
Aquest codi correspona amb una llista de codis que podem obtenir al mateix web. El format d'aquesta llista, codificada en JSON, és el següent:
{
"info": {
"tipus": "Camps",
"descrip": "Camps en els que s'han classificat les dones",
"creat": "20250907",
"autoria": {
"autor": "Oriol Boix",
"contacte": "oriol.boix@upc.edu"
}
},
"dades": [
{
"Codi": "PA",
"Nom": "Antropologia, paleontologia i arqueologia"
},
{
"Codi": "AT",
"Nom": "Arquitectura, urbanisme, paisatgisme i edificació"
},
...
Ara consultarem aquest codi a la llista i afegirem l'àmbit en els resultats. Hem de tenir en compte, però, dues coses. Per un costat, aquesta llista de codis no té interès per a l'usuari de l'aplicació; per això no té sentit mostrar-la amb un constructor de futurs, com hem fet amb les dades anteriors. Per un altre costat, aquesta llista de codis és breu, força estable i sempre la mateixa per al que tenim fet fins ara i el que farem més endavant. Per tant, el més raonable és llegir la llista en el moment de carregar l'aplicació i guardar-la en un diccionari; així podrem consultar cada codi en el moment que ens convingui, sense haver-ho de tornar a consultar a internet.
En llegir la llista de dones, hem creat unes classes que ens han permès empaquetar la informació de manera convenient. Ara, però, no ens cal empaquetar-la; atès que només volem agafar les dades que ens interessen (la llista de codis i els àmbits que els corresponen) i guardar-la en un diccionari. Per tant, farem que la mateixa funció que llegeix les dades sigui la que les processi i retorni el diccionari desitjat.
Veiem primer el que hi ha en aquesta funció i després ho comentem.
crida_llista_ambits.dart
import 'dart:convert'; import 'package:http/http.dart' as http; // No el troba automàticament, s'ha d'entrar a mà
class CridaLlistaAmbits{
Future<Map<String, String>> demanarLlistaAmbits() async{
final llistaAmbitsRebuda = await
http.get(Uri.parse("https://recursos.citcea.upc.edu/dones/service.php?tipus=llista"));
if(llistaAmbitsRebuda.statusCode == 200){
final jsonDecodificat = jsonDecode(llistaAmbitsRebuda.body);
if(jsonDecodificat.containsKey("dades") && jsonDecodificat["dades"] is List){
final List<dynamic> dadesLlista = jsonDecodificat['dades'];
final Map<String, String> dadesProcessades = {};
for (final element in dadesLlista) {
if (element is Map<String, String> && element.containsKey('Codi') && element.containsKey('Nom')) {
dadesProcessades[element['Codi']] = element['Nom'];
}
}
return dadesProcessades;
}
}
return {};
}
}
La funció, com hem dit, retorna un diccionari; en aquest cas, tant la clau com el valor són camps de text. Un cop rebuda la resposta, decodifiquem el JSON, comprovem que hi hagi el camp dades (que és on hi ha la informació que ens interessa) i que aquest camp conté una llista; així sabem que hi ha la informació correcta. El següent pas és guardar en una llista el contingut del camp dades, per poder-ho tractar. Creem un diccionari on guardar la informació i fem un bucle que recorri la llista. Cada element de la llista ha de tenir estructura de diccionari amb dues claus (Codi i Nom), ho comprovem. Si tot és correcte, creem una nova entrada al diccionari final on la clau és el contingut de Codi i el valor és el contingut de Nom. A l'acabar de recórrer el bucle, retornem el diccionari que hem creat i que ja conté la informació desitjada. La funció té diverses condicions if i, per tant, és possible que, si alguna cosa no ha anat bé, no es generi el diccionari; per això, al final hi ha el retorn d'un diccionari buit.
A la pantalla principal, ens interessa que la funció demanarLlistaAmbits es cridi en el moment de carregar la pantalla; per això ho farem en el mètode initState. La lectura de dades des d'internet és asíncrona i, per tant, no es pot posar directament dins del mètode initState; ho hem de fer de manera indirecta. Creem també dues variables globals, una (_dadesAmbits) guardarà el diccionari i l'altra (_carregat) indicarà si el diccionari està ja disponible.
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() => _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()
],
),
);
}
...
En el mètode initState demanem que es faci el que hi ha al mètode original i després cridem la funció _carregarDades, que és una funció asíncrona de tipus futur. Aquesta funció és la que crida a la que fa la lectura de dades. En aquesta funció hi hem posat un setState que detectarà la finalització de l'acció demanada i, quan així sigui, guardarà el diccionari al seu lloc i indicarà que la càrrega ha finalitzat.
Ja hem vist com es carreguen les dades, ara anem a veure com les farem servir. A la pantalla principal, la funció mostraElement escrivia dues línies de text (nom i professió) per a cada persona; ara n'hi afegirem una tercera amb l'àmbit.
...
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),
),
),
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)),
]
)
)
);
}
}
A l'inici, es comprova que el diccionari s'ha carregat i, si és així, es consulta el codi corresponent i es guarda a la variable ambitAct. A la columna, s'ha afegit un nou camp de text que només ho mostra si el contingut té sentit.
També ens pot interessar mostrar l'àmbit a la pantalla de detalls. Atès que el diccionari es carrega a la pantalla principal, l'haurem de passar a aquesta pantalla; juntament amb la variable que controla si s'ha carregat. Podríem passar només l'àmbit corresponent, atès que ja l'hem generat a la pantalla principal; però hem optat per aquesta versió més general perquè existeix la possibilitat que decidíssim no fer servir l'àmbit a la pantalla principal; llavors podríem tenir el diccionari però no el nom de l'àmbit concret.
pant_detalls.dart
import 'package:dones_destacades/data/dades_dona_llista.dart'; import 'package:flutter/material.dart'; import 'package:html_unescape/html_unescape.dart';
class PantDetalls extends StatefulWidget {
final DadesDonaLlista 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> {
@override
Widget build(BuildContext context) {
var unescape = HtmlUnescape();
String ambitAct = "";
if(widget.carregat){
ambitAct = widget.dadesAmbits[widget.persona.ambit].toString();
}
return Scaffold(
appBar: AppBar(
title: Text(unescape.convert(widget.persona.nom)),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(unescape.convert(widget.persona.nom),
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
if(widget.persona.casada.isNotEmpty)
Text("Nom de casada: ${unescape.convert(widget.persona.casada)}", style: TextStyle(fontSize: 13)),
if(widget.persona.alies.isNotEmpty)
Text("Àlies: ${unescape.convert(widget.persona.alies)}", style: TextStyle(fontSize: 13)),
SizedBox(height: 5),
Text(unescape.convert(widget.persona.professio), style: TextStyle(fontSize: 14)),
if(widget.carregat && ambitAct.isNotEmpty)
Text("Àmbit: ${unescape.convert(ambitAct)}", style: TextStyle(fontSize: 13)),
Text(unescape.convert(widget.persona.destacat), style: TextStyle(fontSize: 13)),
],
),
);
}
}
A la pantalla principal caldrà afegir els dos paràmetres en el lloc on es carrega la segona pantalla.
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() => _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,
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)),
]
)
)
);
}
}
Probablement no és raonable mostra l'àmbit en les dues pantalles, potser seria més raonable decidir-se per una i mostrar-lo només un cop.

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