Aquesta primera versió és molt senzilla i només llegirem el camp "descrip". La idea és començar per un camp únic per no complicar el programa i donar el pes a entendre els passos que seguim per obtenir les dades del servei web.
En aquest exemple ja no explicarem tots els passos que ja s'han comentat en exemples previs, per donar èmfasi en les particularitats d'aquesta aplicació. Tampoc ens preocuparem gaire per l'aspecte de l'aplicació.
La nostra aplicació està en la carpeta dones_destacades. El programa principal tindrà el contingut habitual i cridarà a la pantalla principal.
main.dart
import 'package:dones_destacades/screens/pant_principal.dart'; import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: PantPrincipal()
);
}
}
La pantalla principal estarà en el fitxer pant_principal.dart, dins de la carpeta screens; que haurem creat a la carpeta lib. Inicialment, la pantalla principal tindrà un botó, que serà el que picarem per obtenir les dades. Aquest botó té sentit perquè més endavant podrem escollir diversos tipus de dades a obtenir. El botó l'hem posat dins d'un farciment i aquest dins d'una columna, atès que tindrà altres coses a sota.
pant_principal.dart
import 'package:flutter/material.dart';
class PantPrincipal extends StatefulWidget {
const PantPrincipal({super.key});
@override
State<PantPrincipal> createState() => _PantPrincipalState();
}
class _PantPrincipalState extends State<PantPrincipal> {
@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(() {
});
},
child: Text("Dones catalanes"),
),
),
],
),
);
}
}
Quan piquem aquest botó, s'ha de fer una crida web per obtenir les dades. La informació que rebrem inclourà diversos camps. De moment només ens interessa un d'ells però més endavant en voldrem altres. Crearem una classe, que anomenarem DadesRebudesDones, que ens permeti empaquetar la informació rebuda i extreure'n els camps que ens facin falta. Aquesta classe la crearem al fitxer dades_rebudes_dones.dart; aquest fitxer el crearem a la carpeta data; que crearem dins de la carpeta lib. De moment, el constructor de la classe tindrà un element de text, que anomenem descrip, i retornarà un objecte DadesRebudesDones que contindrà el camp "descrip" del camp "info" de la informació rebuda. La funció fromJson rep, com a paràmetre, un diccionari (map) que és una parella de clau (sempre text) i valor (de tipus dynamic). En aquest cas, s'empra factory, un tipus especial de constructor que és útil en determinats casos. L'estructura i el funcionament d'aquesta classe no són fàcils d'explicar; el que importa és que l'estructura d'aquest tipus de classe és sempre similar, com veurem a mesura que anem avançant en aquest exemple.
dades_rebudes_dones.dart
class DadesRebudesDones {
final String descrip;
DadesRebudesDones({required this.descrip});
factory DadesRebudesDones.fromJson(Map<String, dynamic> jsonRebut){
return DadesRebudesDones(descrip: jsonRebut["info"]["descrip"]);
}
}
Ara ens falta la classe que s'encarrega de fer la crida web i retornar les dades. Aquesta classe l'anomenarem CridaLlistaDones i estarà en el fitxer crida_llista_dones.dart, que crearem a la carpeta data. Per facilitar la comprensió, a continuació hi ha l'arbre de carpetes i fitxers:
lib
main.dart
data
crida_llista_dones.dart
dades_rebudes_dones.dart
screens
pant_principal.dart
A la classe CridaLlistaDones, crearem el mètode demanarLlistaDones. Des que es faci la petició de llegir les dades fins que aquestes arribin pot passar un temps; o, fins i tot, poden no arribar mai. Per tant, aquest mètode no el podrem definir com els que hem fet servir fins ara. Per un costat, el resultat del mètode serà un futur (Future), un tipus especial d'estructura de dades que s'omple de manera diferida. Per un altre costat, s'ha declarat com a asíncron (async) per tal que la resta de l'aplicació pugui continuar funcionant mentre s'espera l'arribada de les dades. Aquest mètode retornarà un objecte de tipus DadesRebudesDones, el que hem creat a la classe anterior.
Ara és quan anem a fer la crida web; amb la funció http.get. El resultat de la lectura el guardarem a la variable llistaDonesRebuda. Per tal que la funció http.get estigui disponible, hem de fer dues coses. Per un costat, cal anar al fitxer pubspec.yaml per dir-li que la volem fer servir. 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 http ens ajuda a autocompletar, el normal és que ja ens doni la darrera versió. En el cas de la imatge, la darrera era la 1.5.0.

Un cop afegit, cal picar al botó per baixar el paquet (
).
Per un altre costat, cal importar el paquet http.dart; ja que no ho fa de manera automàtica.
Si es preveu que l'aplicació es pugui instal·lar en dispositius que tinguin versions antiques d'Android (anteriors a la 10), cal donar permís d'accés a internet. Això ho farem anar al fitxer AndroidManifest.xml que es troba a la carpeta main de la carpeta src de la carpeta app de la carpeta android i afegir la línia del permís després de la de manifest i abans de la d'application:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
...
Obtindrem les dades del servidor amb la comanda http.get, a la qual se li ha de passar l'adreça en format URI. Per convertir l'adreça de l'enllaç a aquest format, fem servir la funció Uri.parse. El resultat no es pot assignar directament a la variable llistaDonesRebuda, perquè trigarà en arribat; per això afegim l'ordre await, per indicar que ha d'esperar el resultat. De moment, la classe serà:
crida_llista_dones.dart
import 'dart:convert'; import 'package:dones_destacades/data/dades_rebudes_dones.dart'; import 'package:http/http.dart' as http; // No el troba automàticament, s'ha d'entrar a mà
class CridaLlistaDones{
Future<DadesRebudesDones> demanarLlistaDones() async{
final llistaDonesRebuda = await
http.get(Uri.parse("https://recursos.citcea.upc.edu/dones/service.php?tipus=cat"));
}
}
Evidentment, falten coses; ja que, de moment, el mètode no retorna res.
La variable llistaDonesRebuda no és directament la informació, sinó que conté tot un informe de com ha anat la comunicació. Un dels elements és statusCode que tindrà un codi numèric normalitzat que indica el resultat de les transmissions web. En el nostre cas, només ens interessa el cas que el codi sigui 200 (transmissió correcta); ja que en els altres casos no tenim dades.
El nostre programa, doncs, tindrà una sentència if, que només tractarà les dades si la transmissió ha estat correcta. Si no ha estat correcta, tenim diverses opcions. Una d'elles seria crear una excepció:
class CridaLlistaDones{
Future<DadesRebudesDones> demanarLlistaDones() async{
final llistaDonesRebuda = await
http.get(Uri.parse("https://recursos.citcea.upc.edu/dones/service.php?tipus=cat"));
if(llistaDonesRebuda.statusCode == 200){
...
} else {
throw Exception("Alguna cosa ha fallat");
}
}
}
Però aquesta opció ens farà que l'aplicació es tanqui o es pengi si la transmissió no ha estat correcta. Per tant, potser és millor que es retorni alguna cosa; per exemple un element nul. Si posem això, però, ens sortirà un error; ja que estarem retornant alguna cosa que no és del tipus dadesRebudesDones. Caldrà, doncs, posar un interrogant a aquest tipus, per indicar que també pot retornar un nul.
Si la comunicació ha estat correcta, l'element body de la variable llistaDonesRebuda contindrà les dades. Aquestes, que estan en JSON, s'hauran de descodificar i agafar l'element que ens interessa (tal com hem definit a la classe DadesRebudesDones; això serà el que retornarem.
crida_llista_dones.dart
import 'dart:convert'; import 'package:dones_destacades/data/dades_rebudes_dones.dart'; import 'package:http/http.dart' as http; // No el troba automàticament, s'ha d'entrar a mà
class CridaLlistaDones{
Future<DadesRebudesDones?> demanarLlistaDones() async{
final llistaDonesRebuda = await
http.get(Uri.parse("https://recursos.citcea.upc.edu/dones/service.php?tipus=cat"));
if(llistaDonesRebuda.statusCode == 200){
var jsonDecodificat = jsonDecode(llistaDonesRebuda.body);
DadesRebudesDones dadesRebudesDones = DadesRebudesDones.fromJson(jsonDecodificat);
return dadesRebudesDones;
} else {
return null;
}
}
}
Ens falta completar la pantalla principal. El primer que cal fer és crear una variable futur de tipus dadesRebudesDones? que anomenarem _descrip i que contindrà, quan es completi la transmissió, el valor del camp que ens interessa. El símbol _ davant indica que aquesta és una variable que queda restringida a l'àmbit d'aquesta classe. En aquest cas hi ha dos interrogants, un indica que DadesRebudesDones pot tenir un valor nul i l'altre que el futur pot acabar sent nul, no existir. També hem de crear un objecte de tipus CridaLlistaDones, que podem anomenar cridaLlistaDones. Aquest objecte, una instància de la classe, és necessari per poder fer servir el mètode demanarLlistaDones.
...
class _PantPrincipalState extends State<PantPrincipal> {
Future<DadesRebudesDones?>? _descrip;
CridaLlistaDones cridaLlistaDones = CridaLlistaDones();
@override
Widget build(BuildContext context) {
return Scaffold(
...
Ens falta, també, definir l'acció del botó. Quan es premi el botó, es cridarà al mètode demanarLlistaDones de cridaLlistaDones i es guardarà el resultat a la variable _descrip.
...
child: TextButton(
onPressed: () {
setState(() {
_descrip = cridaLlistaDones.demanarLlistaDones();
});
},
child: Text("Dones catalanes"),
),
...
I ara ens cal fer alguna cosa amb el que s'hagi rebut a _descrip; que, recordem, és un futur. Així, _descrip no conté només les dades (si han arribat) sinó que també conté informació sobre l'estat de la comunicació. Flutter té un giny especial per tractar aquest tipus de situacions, el constructor de futurs. Aquest rep la variable que hem de tractar i un constructor (builder) que té una instantània (snapshot) de l'estat del futur.
...
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton(
onPressed: () {
setState(() {
_descrip = cridaLlistaDones.demanarLlistaDones();
});
},
child: Text("Dones catalanes"),
),
),
FutureBuilder(future: _descrip, builder: (context, snapshot){
})
],
),
...
Aquest FutureBuilder ha de retornar alguna cosa (un giny) que es pugui mostrar dins de la columna. La instantània ens proporciona una sèrie de paràmetres i, fent servir condicionals if, podem fer que es mostri una cosa diferent segons l'estat.
...
FutureBuilder(future: _descrip, builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return Text("Error: ${snapshot.error}");
} else if(snapshot.hasData){
return Text("${snapshot.data?.descrip}");
} else {
return Text("No s'han trobat resultats");
}
})
...
Si les dades encara estan a l'espera (waiting), podem fer que ens mostri un cercle giratori que indica que s'està carregant. Si hi ha un error, mostrarem un text que ens indiqui quin ha estat. Si s'ha rebut la informació, la mostrarem; en aquest cas en un altre text. En qualsevol altre cas podem mostrar un text alternatiu.
Amb això, ja tindríem la pantalla principal finalitzada i podem considerar acabada aquesta primera versió de l'aplicació.
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';
class PantPrincipal extends StatefulWidget {
const PantPrincipal({super.key});
@override
State<PantPrincipal> createState() => _PantPrincipalState();
}
class _PantPrincipalState extends State<PantPrincipal> {
Future<DadesRebudesDones?>? _descrip;
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(() {
_descrip = cridaLlistaDones.demanarLlistaDones();
});
},
child: Text("Dones catalanes"),
),
),
FutureBuilder(future: _descrip, builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return Text("Error: ${snapshot.error}");
} else if(snapshot.hasData){
return Text("${snapshot.data?.descrip}");
} else {
return Text("No s'han trobat resultats");
}
})
],
),
);
}
}

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