Començarem amb una versió senzilla de l'aplicació que llegirà les dades i les mostrarà a la pantalla. Hi haurà un botó que activarà la lectura de les dades i aquestes es mostraran a la pantalla principal. 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 calendaris. El programa principal tindrà el contingut habitual i cridarà a la pantalla principal.
main.dart
import 'package:calendaris/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ó 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("Tasques per a avui"),),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton(
onPressed: () {
setState(() {
});
},
child: Text("Actualitza"),
),
),
],
),
);
}
}
Quan piquem aquest botó, s'ha de fer una crida web per obtenir les dades. La informació que rebrem inclourà diverses línies (o cap, si no hi ha cites aquell dia) i tres camps de text separats per comes per a cada una.
Tasca 1,17.15,18.15 Tasca 2,19.0,20.0
Crearem una classe, que anomenarem DadesRebudes, que ens permeti empaquetar la informació rebuda i extreure'n els camps que ens facin falta en cada moment. Aquesta classe la crearem al fitxer dades_rebudes.dart; aquest fitxer el crearem a la carpeta data; que crearem dins de la carpeta lib. El constructor de la classe tindrà tres elements, que seran llistes de text, que correspondran als tres camps que es reben per a cada cita; i retornarà un objecte DadesRebudes que contindrà la informació rebuda. Cada llista, doncs, tindrà tants elements com cites hi hagi. La lectura ens dona un únic camp de text que separem pels salts de línia (\n) per obtenir una llista, anomenada fileres, que conté un element per a cada cita. Quan recorrem aquesta llista, separem cada element pel caràcter coma per obtenir, en la variable caselles, els tres camps que conté. Un cop tenim els tres camps, els anem afegint a la llista que els correspon. A l'acabar, retornem les tres llistes. Si la informació no arriba correctament es creen tres llistes buides.
dades_rebudes.dart
class DadesRebudes {
final List<String> nom;
final List<String> inici;
final List<String> fi;
DadesRebudes({
required this.nom,
required this.inici,
required this.fi
});
DadesRebudes dades(String dadesRebut){
List<String> fileres = dadesRebut.split("\n");
List<String> noms = [];
List<String> inicis = [];
List<String> finals = [];
if(fileres.isNotEmpty){
for(int i = 0; i < fileres.length; i++){
List<String> caselles = fileres[i].split(",");
if(caselles.length >= 3){
noms.add(caselles[0]);
inicis.add(caselles[1]);
finals.add(caselles[2]);
}
}
return DadesRebudes(
nom: noms,
inici: inicis,
fi: finals,
);
} else {
return DadesRebudes(
nom: [],
inici: [],
fi: [],
);
}
}
}
Ara ens falta la classe que s'encarrega de fer la crida web i retornar les dades. Aquesta classe l'anomenarem CridaDades i estarà en el fitxer crida_dades.dart, que crearem a la carpeta data. Els codis per accedir als fitxers de Google (de moment el de l'script) els posarem en un fitxer separat perquè quedin més amagats i, al mateix temps, siguin més fàcils de modificar. Per facilitar la comprensió, a continuació hi ha l'arbre de carpetes i fitxers:
lib
main.dart
data
claus_google.dart
crida_dades.dart
dades_rebudes.dart
screens
pant_principal.dart
El fitxer de les claus serà el següent:
claus_google.dart
class ClausGoogle{
static const script
= "^^fycbxqrJpVA-KT1sUd8HIta643R3bH4ixpDahttayGSGjkpHUBjPQ";
}
A la classe CridaDades, crearem el mètode llegirDades. 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 DadesRebudes, el que hem creat en una 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 dadesLlegides. 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, 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
...
Atenció: Cal tenir en compte que l'absència d'aquest permís pot donar problemes a l'executar l'aplicació en el dispositiu, malgrat aquesta funcioni correctament en el simulador. Per assegurar que l'aplicació funcionarà en qualsevol dispositiu Android, convé posar aquesta línia sempre que s'hagn de fer connexions a internet.
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 dadesLlegides, perquè trigarà en arribat; per això afegim l'ordre await, per indicar que ha d'esperar el resultat. De moment, la classe serà:
crida_dades.dart
import 'package:calendaris/data/claus_google.dart'; import 'package:calendaris/data/dades_rebudes.dart'; import 'package:http/http.dart' as http;
class CridaDades{
Future<DadesRebudes> llegirDades() async{
final dadesLlegides = await http.get(Uri.parse(
"https://script.google.com/macros/s/${ClausGoogle.script}/exec"
));
}
}
Evidentment, falten coses; ja que, de moment, el mètode no retorna res.
La variable dadesLlegides 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 CridaDades{
Future<DadesRebudes> llegirDades() async{
final dadesLlegides = await http.get(Uri.parse(
"https://script.google.com/macros/s/${ClausGoogle.script}/exec"
));
if(dadesLlegides.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 DadesRebudes. 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 dadesLlegides contindrà les dades. Aquestes les volem retornar en el format DadesRebudes; per això creem un objecte d'aquest tipus. Atès que en força a inicialitzar-lo, ho fem amb cinc llistes buides. Després cridem al mètode que ens convertirà la informació.
crida_dades.dart
import 'package:calendaris/data/claus_google.dart'; import 'package:calendaris/data/dades_rebudes.dart'; import 'package:http/http.dart' as http;
class CridaDades{
Future<DadesRebudes?> llegirDades() async{
final dadesLlegides = await http.get(Uri.parse(
"https://script.google.com/macros/s/${ClausGoogle.script}/exec"
));
if(dadesLlegides.statusCode == 200){
DadesRebudes dadesRebudes = DadesRebudes(nom: [], inici: [], fi: []);
return dadesRebudes.dades(dadesLlegides.body);
} else {
return null;
}
}
}
Ens falta completar la pantalla principal. El primer que cal fer és crear una variable futur de tipus DadesRebudes? que anomenarem _rebut i que contindrà, quan es completi la transmissió, la informació 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 DadesRebudes 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 CridaDades, que podem anomenar cridaDades. Aquest objecte, una instància de la classe, és necessari per poder fer servir el mètode llegirDades.
...
class _PantPrincipalState extends State<PantPrincipal> {
Future<DadesRebudes?>? _rebut;
CridaDades cridaDades = CridaDades();
@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 llegirDades de cridaDades i es guardarà el resultat a la variable _rebut.
...
child: TextButton(
onPressed: () {
setState(() {
_rebut = cridaDades.llegirDades();
});
},
child: Text("Actualitza"),
),
...
I ara ens cal fer alguna cosa amb el que s'hagi rebut a _rebut; que, recordem, és un futur. Així, _rebut 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(() {
_rebut = cridaDades.llegirDades();
});
},
child: Text("Actualitza"),
),
),
FutureBuilder(future: _rebut, 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.
...
if(snapshot.connectionState == ConnectionState.waiting){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return Text("Error: ${snapshot.error}");
} else if(snapshot.hasData){
return escriuResultats(snapshot.data);
} 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 emprant una funció. En qualsevol altre cas podem mostrar un text alternatiu.
Ens falta definir la funció escriuResultats, que ha de retornar un giny. Primer comprovarem que tenim totes les dades necessàries (cinc llistes). Si és així, creem una llista de ginys que serà el que retornarem; atès que això ja és directament mostrable, ja que els descendents (children) d'una columna no són més que una llista de ginys. Aquestas llista està inicialment buida i l'omplim amb un bucle que recorre el contingut de les llistes de dades. Aquest bucle afegeix un giny de text amb la informació que ens interessa (magnitud, valor i unitats. El valor ens arriba amb un punt per separar els decimals, que fàcilment podem substituir per la coma normativa.
...
Widget escriuResultats(DadesRebudes? dades){
if(dades != null){
if(dades.nom.length >=5){
List<Widget> contingut = [];
for(int i = 0; i < dades.nom.length; i++){
Widget element = Row(
children: [
Text(
"${dades.nom[i]}: ${dades.darrer[i].replaceAll('.', ',')} ${dades.unitats[i]}"
),
],
);
contingut.add(element);
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: contingut,
),
);
}
}
return Text("No hi ha esdeveniments programats");
}
Amb això, ja tindríem la pantalla principal finalitzada i podem considerar acabada aquesta primera versió de l'aplicació.
pant_principal.dart
import 'package:calendaris/data/crida_dades.dart'; import 'package:calendaris/data/dades_rebudes.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<DadesRebudes?>? _rebut;
CridaDades cridaDades = CridaDades();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Tasques per a avui"),),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton(
onPressed: () {
setState(() {
_rebut = cridaDades.llegirDades();
});
},
child: Text("Actualitza"),
),
),
FutureBuilder(future: _rebut, builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return Text("Error: ${snapshot.error}");
} else if(snapshot.hasData){
return escriuResultats(snapshot.data);
} else {
return Text("No s'han trobat resultats");
}
})
],
),
);
}
}
Widget escriuResultats(DadesRebudes? dades){
if(dades != null){
if(dades.nom.isNotEmpty){
List<Widget> contingut = [];
for(int i = 0; i < dades.nom.length; i++){
Widget element = Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(dades.nom[i]),
Text(dades.inici[i]),
Text(dades.fi[i]),
],
),
);
contingut.add(element);
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: contingut,
),
);
}
}
return Text("No hi ha esdeveniments programats");
}

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