Aplicacions amb Flutter, Dart i Flame

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

Creem un botó genèric

En exemples anteriors, si es feien servir elements iguals, en creàvem un de genèric (en un fitxer separat) i després el personalitzàvem. En aquest cas hem esperat al final per fer-ho, ja que els botons no són tots similars. De fet, dos dels botons obren pàgines separades i dos modifiquen la pàgina actual. D'altra banda, dos d'ells tenen una icona, un té una imatge i un té un text. El disseny del botó genèric, doncs, té una certa complexitat.

En aquest exemple, a més, no hem dedicat gaire temps al format, però hi ha algunes coses que caldria arreglar abans de donar-lo per acabat; especialment perquè molts elements queden enganxats a la vora de la pantalla i convindria deixar un marge.

Comencem per definir el botó genèric. Creem la carpeta components, on posarem el fitxer boto_generic.dart en el qual tindrem la classe BotoGeneric amb tres paràmetres; el primer serà la funció que s'ha d'executar en prémer el botó, el segon el que veurà l'usuari damunt del botó (text, imatge o icona) i el tercer una variable booleana que indicarà si es tracta d'un botó de text o no.

Aprofitarem per donar una mica de format al botó, perquè realment no es veien gens clars els seus límits i el que fèiem era picar la icona. Posarem un color de fons al botó, fent servir un contenidor i farem que la icona o el text siguin negres. També li farem les vores arrodonides i hi posarem un farciment per donar-li separació.

boto_generic.dart
import 'package:flutter/material.dart';
class BotoGeneric extends StatefulWidget {
  final Function() accio;
  final Widget llegenda;
  final bool esText;
  const BotoGeneric({super.key, required this.accio, required this.llegenda, required this.esText});

  @override
  State<BotoGeneric> createState() => _BotoGenericState();
}

class _BotoGenericState extends State<BotoGeneric> {
  @override
  Widget build(BuildContext context) {
    if(widget.esText){
      return Padding(
        padding: const EdgeInsets.all(8),
        child: Container(
          decoration: BoxDecoration(
            color: Color(0xFFD0D0D0),
            borderRadius: BorderRadius.circular(8),
          ),
          child: TextButton(
            style: TextButton.styleFrom(foregroundColor: Colors.black),
            onPressed: () {
              widget.accio();
            },
            child: widget.llegenda
          ),
        )
      );
    } else {
      return Padding(
        padding: const EdgeInsets.all(8),
        child: Container(
          decoration: BoxDecoration(
            color: Color(0xFFD0D0D0),
            borderRadius: BorderRadius.circular(8),
          ),
          child: IconButton(
            color: Colors.black,
            onPressed: () {
              widget.accio();
            },
            icon: widget.llegenda
          ),
        ),
      );
    }
  }
}

A la pantalla principal caldrà substituir els botons actuals per les crides personalitzades al botó genèric. També posarem al desplegable un marc similar al que hem posat als botons i hi posarem uns farciments per donar-li separació.

Aprofitarem per fer una altra millora. En els lllistats llargs és habitual marcar els elements de manera que es vegi el pas de l'un al següent, una forma molt habitual és alternar entre dos colors de fons. Així, crearem una variable per controlar-ho, que anirà alternant el seu valor, i posarem un o altre color de fons segons el valor.

pant_principal.dart
import 'package:dones_destacades/components/boto_generic.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;
  String titol = "Dones destacades";
  String? _opcioSeleccionada;
  bool fosc = true;

  @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) {
    var unescape = HtmlUnescape();
    fosc = true;
    return Scaffold(
      appBar: AppBar(title: Text("Dones destacades"),),
      body: Column(
        children: [
          Row(
            children: [
              BotoGeneric(
                accio: () {
                  setState(() {
                    _dadesDones = cridaLlistaDones.demanarLlistaDones("cat");
                    titol = "Dones catalanes";
                  });
                }, 
                llegenda: Image.asset("assets/images/senyera.gif", height: 20, width: 20, fit: BoxFit.fill,),
                esText: false
              ),
              BotoGeneric(
                accio: () {
                  setState(() {
                    _dadesDones = cridaLlistaDones.demanarLlistaDones("joves");
                      titol = "Dones joves";
                  });
                }, 
                llegenda: Text("s. XXI"),
                esText: true
              ),
              BotoGeneric(
                accio: () {
                  Navigator.push(context, MaterialPageRoute(
                    builder: (context) => PantDetalls(
                      persona: "dia", 
                      dadesAmbits: _dadesAmbits, 
                      carregat: _carregat
                    )
                  ));
                }, 
                llegenda: Icon(Icons.calendar_month),
                esText: false
              ),
              BotoGeneric(
                accio: () {
                  Navigator.push(context, MaterialPageRoute(
                    builder: (context) => PantDetalls(
                      persona: "aleat", 
                      dadesAmbits: _dadesAmbits, 
                      carregat: _carregat
                    )
                  ));
                }, 
                llegenda: Icon(Icons.casino_outlined),
                esText: false
              ),
            ],
          ),
          if(_carregat && _dadesAmbits.isNotEmpty) Padding(
            padding: const EdgeInsets.all(8),
            child: Container(
              decoration: BoxDecoration(
                color: Color(0xFFD0D0D0),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Padding(
                padding: const EdgeInsets.only(left: 4, right: 4),
                child: DropdownButton<String>(
                  value: _opcioSeleccionada,
                  hint: const Text('Escull un àmbit'),
                  isExpanded: true,
                  items: _dadesAmbits.keys.map((String clau) {
                    return DropdownMenuItem<String>(
                      value: clau,
                      child: Text(unescape.convert(_dadesAmbits[clau]!)),
                    );
                  }).toList(),
                  onChanged: (String? novaClauSeleccionada) {
                    setState(() {
                      // Si, pel motiu que sigui, la clau és un nul, retorna les catalanes
                      _dadesDones = cridaLlistaDones.demanarLlistaDones(novaClauSeleccionada ?? 'cat');
                      titol = unescape.convert(_dadesAmbits[novaClauSeleccionada]!);
                    });
                  },
                ),
              ),
            ),
          ),
          Container(
            alignment: Alignment.topLeft,
            padding: const EdgeInsets.all(8),
            child: Text(titol, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold))
          ),
          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("Pica un botó o selecciona al desplegable");
      }
    });
  }

  Padding mostraElement(DadesDonaLlista donaDeLaLlista) {
    fosc = !fosc;
    var unescape = HtmlUnescape();
    String ambitAct = "";
    if(_carregat){
      ambitAct = _dadesAmbits[donaDeLaLlista.ambit].toString();
    }
    return Padding(
      padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
      child: GestureDetector(
        onTap: () => Navigator.push(context, MaterialPageRoute(
            builder: (context) => PantDetalls(
              persona: donaDeLaLlista.id, 
              dadesAmbits: _dadesAmbits, 
              carregat: _carregat
            ),
          ),
        ),
        child: Container(
          color: fosc == true ? Color(0xFFD0D0D0) : Color(0xFFF0F0F0),
          child: Padding(
            padding: const EdgeInsets.only(left: 8, right: 8, top: 3, bottom: 3),
            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)),
              ]
            ),
          ),
        )
      )
    );
  }
}

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: Padding(
        padding: const EdgeInsets.all(12.0),
        child: 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.5)
            ),
          ],
        ),
      ),
    );
  }

  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<String> 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"),
      ),
    );
  } 
}

Atès que ja donem l'exemple per acabat, a continuació hi ha el contingut de la resta de fitxers.

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()
    );
  }
}

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, dynamic> && element.containsKey('Codi') && element.containsKey('Nom')) {
            dadesProcessades[element['Codi']] = element['Nom'];
          }
        }
        return dadesProcessades;
      }
    }
    return {};
  }
}

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(String tip) async{
    String amb = "";
    if(tip.isEmpty){
      // Si, pel motiu que sigui, la clau és buida, retorna les catalanes
      tip = "cat";
    } else {
      if((tip != "cat") && (tip != "joves")){
        amb = "ambit&ambit=";
      }
    }
    final llistaDonesRebuda = await 
      http.get(Uri.parse("https://recursos.citcea.upc.edu/dones/service.php?tipus=$amb$tip"));
    if(llistaDonesRebuda.statusCode == 200){
      var jsonDecodificat = jsonDecode(llistaDonesRebuda.body);
      DadesRebudesDones dadesRebudesDones = DadesRebudesDones.fromJson(jsonDecodificat);
      return dadesRebudesDones;
    } else {
      return null;
    }
  }
}

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{
    String params = "tipus=concreta&id=$ident";
    if(ident == "dia"){
      params = "tipus=dia";
    }
    if(ident == "aleat"){
      params = "tipus=aleat";
    }
    final detallRebut = await 
      http.get(Uri.parse("https://recursos.citcea.upc.edu/dones/service.php?$params"));
    if(detallRebut.statusCode == 200){
      var jsonDecodificat = jsonDecode(detallRebut.body);
      DadesRebudesDetall dadesRebudesDetall = DadesRebudesDetall.fromJson(jsonDecodificat);
      return dadesRebudesDetall;
    } else {
      return null;
    }
  }
}

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

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

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

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

dades_dona_llista.dart
class DadesDonaLlista {
  final String id;
  final String ambit;
  final String nom;
  final String casada;
  final String alies;
  final String professio;
  final String destacat;


  DadesDonaLlista({
    required this.id,
    required this.ambit,
    required this.nom,
    required this.casada,
    required this.alies,
    required this.professio,
    required this.destacat,
  });
  
  factory DadesDonaLlista.fromJson(Map<String, dynamic> dadesJson) {
    return DadesDonaLlista(
      id: dadesJson["Id"] ?? "",
      ambit: dadesJson["Ambit"] ?? "",
      nom: dadesJson["Nom"] ?? "",
      casada: dadesJson["Nom casada"] ?? "",
      alies: dadesJson["Altre nom"] ?? "",
      professio: dadesJson["Professio"] ?? "",
      destacat: dadesJson["Fet destacat 1"] ?? "",
    );
  }
}

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"],
    );
  }
}

 

 

 

 

 

 

 

 

 

 

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