En la nostra aplicació ens caldrà entrar dues dades numèriques: el corrent nominal i la caiguda de tensió. Els dos elements tindran el mateix aspecte i només canviaran alguns detalls; per això sembla més raonable crear primer un element genèric que es pugui personalitzar, així després el podrem repetir dos cops (en aquesta aplicació) però també el podríem reutilitzar en altres.
Aquest element el posarem en el fitxer entra_num.dart que crearem a la carpeta components, que ja teníem. Atès que hi haurà paràmetres variables, crearem un giny amb estat que anomenarem EntraNum.
entra_num.dart
import 'package:flutter/material.dart';
class EntraNum extends StatefulWidget {
const EntraNum({super.key});
@override
State<EntraNum> createState() => _EntraNumState();
}
class _EntraNumState extends State<EntraNum> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
Cada un d'aquests elements tindrà un títol, diferent per a cada un. Caldrà, doncs, crear una variable per al títol i fer que actuï com a paràmetre. Aquesta variable l'hem de declarar com a final a l'inici de la classe EntraNum i posar-la com a paràmetre obligatori a la definició de la classe. El this indica que és una variable de la pròpia classe.
class EntraNum extends StatefulWidget {
final String titol;
const EntraNum({super.key, required this.titol});
@override
State<EntraNum> createState() => _EntraNumState();
}
...
Més endavant haurem d'afegir variables; però, de moment, amb aquesta en tenim prou. Ara comencem a definir els elements del giny.
Per començar, el nostre giny tindrà dues línies de text i una filera amb dos botons. El primer text serà el títol i indicarà per a què serveix el giny, quina variable es pot ajustar. El segon text serà el valor de la variable i els botons serviran per decrementar i incrementar aquest valor. Com estaran en tres fileres, cal definir una columna; de moment hi posem el mínim que representi el que hem comentat.
...
class _EntraNumState extends State<EntraNum> {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("títol"),
Text("valor"),
Row()
],
);
}
}
Ara ja podríem cridar al giny EntraNum des de la pantalla principal. Així, a mesura que anem fent proves, podrem anar veient com queda. De moment, en posarem només un i veurem els dos textos amb dificultat; ja que els colors no contrasten bé.
...
class _PantPralState extends State<PantPral> {
@override
Widget build(BuildContext context) {
return Column(
children: [
SelectorTens(),
EntraNum(),
],
);
}
}
Per poder veure millor els dos textos, anem a donar-los estil. En el fitxer estils.dart, afegirem dos estils.
...
static const TextStyle estilTextTitols = TextStyle(
color: ColorsApp.text,
fontSize: 14,
fontWeight: FontWeight.bold,
);
static const TextStyle estilTextValors = TextStyle(
color: ColorsApp.text,
fontSize: 18,
fontWeight: FontWeight.bold,
);
...
I els aplicarem als dos textos. Aprofitem també per posar ja la variable corresponent com a títol.
import 'package:flutter/material.dart'; import 'package:resistled_app/core/estils.dart';
class EntraNum extends StatefulWidget {
final String titol;
const EntraNum({super.key, required this.titol});
@override
State<EntraNum> createState() => _EntraNumState();
}
class _EntraNumState extends State<EntraNum> {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(widget.titol, style: Estils.estilTextTitols),
Text("valor", style: Estils.estilTextValors),
Row()
],
);
}
}
Ara tocaria afegir els dos botons a la part inferior. Cada un d'aquests botons només tindrà un símbol que indicarà si serveix per augmentar o disminuir, per això sembla raonable fer servir un IconButton.
import 'package:flutter/material.dart'; import 'package:resistled_app/core/colors.dart'; import 'package:resistled_app/core/estils.dart';
...
return Column(
children: [
Text(widget.titol, style: Estils.estilTextTitols),
Text("valor", style: Estils.estilTextValors),
Row(
children: [
IconButton(
onPressed: () {}, icon: Icon(Icons.remove), color: ColorsApp.text,
),
IconButton(
onPressed: () {}, icon: Icon(Icons.add), color: ColorsApp.text,
),
],
),
],
);
...
Ara els botons no tenen aspecte de botons. Per arreglar-ho, tancarem cada botó dins d'un contenidor i li donarem una decoració. Per estructurar bé l'aplicació, definirem totes les decoracions en un fitxer separat; així, si en algun moment els volem canviar, només els cal modificar en un únic lloc. Abans de res, dins de la carpeta core, crearem el fitxer decora.dart, on definirem els estils que intervindran en la nostra aplicació.
decora.dart
import 'package:flutter/material.dart'; import 'package:resistled_app/core/colors.dart';
class DecoraApp {
static BoxDecoration decorBotons = BoxDecoration(
color: ColorsApp.primari,
borderRadius: BorderRadius.circular(12),
);
}
A més, aprofitem per afegir una alineació al centre del contingut de la columna. La filera de botons ens quedaria així.
import 'package:flutter/material.dart'; import 'package:resistled_app/core/colors.dart'; import 'package:resistled_app/core/decora.dart'; import 'package:resistled_app/core/estils.dart';
...
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: DecoraApp.decorBotons,
child: IconButton(onPressed: () {}, icon: Icon(Icons.remove), color: ColorsApp.text),
),
Container(
decoration: DecoraApp.decorBotons,
child: IconButton(onPressed: () {}, icon: Icon(Icons.add), color: ColorsApp.text),
),
],
),
...
Observem que els botons ens queden enganxats. Una manera senzilla de separar-los és afegint una caixa entremig, amb una amplada apropiada.
...
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: DecoraApp.decorBotons,
child: IconButton(onPressed: () {}, icon: Icon(Icons.remove), color: ColorsApp.text),
),
SizedBox(width: 15),
Container(
decoration: DecoraApp.decorBotons,
child: IconButton(onPressed: () {}, icon: Icon(Icons.add), color: ColorsApp.text),
),
],
),
...
Ara ja tenim el contingut de l'element, però el voldríem posar dins d'un rectangle arrodonit, amb un fons i uns marges; de manera que tingui un disseny similar al del selector de tensió. Per posar els marges farem servir un farciment i per ajustar la forma exterior i el color afegirem un contenidor.
...
class _EntraNumState extends State<EntraNum> {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: ColorsApp.component,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(widget.titol, style: Estils.estilTextTitols),
Text("valor", style: Estils.estilTextValors),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: DecoraApp.decorBotons,
child: IconButton(onPressed: () {}, icon: Icon(Icons.remove), color: ColorsApp.text),
),
SizedBox(width: 15),
Container(
decoration: DecoraApp.decorBotons,
child: IconButton(onPressed: () {}, icon: Icon(Icons.add), color: ColorsApp.text),
),
],
),
],
),
),
);
}
}
El bloc ja té pràcticament l'aspecte desitjat. Per veure com quedaria posicionat i ajustar els marges, ara seria el moment de posar el segon element a la pantalla principal. Volem que els dos blocs d'ajust estiguin de costat; per tant, caldrà crear una filera.
...
class _PantPralState extends State<PantPral> {
@override
Widget build(BuildContext context) {
return Column(
children: [
SelectorTens(),
Row(
children: [
EntraNum(titol: "Caiguda de tensió"),
EntraNum(titol: "Corrent nominal")
],
),
],
);
}
}
Tenim els elements enganxats a l'esquerra. Caldria centrar-los i posar-hi uns marges, per tal que quedin arrenglerats amb els botons del selector de tensió. Hem de deixar 20 píxels de separació entre els elements, això ho podem fer amb una caixa. Per deixar el matge per cada costat podem emprar un farciment.
...
class _PantPralState extends State<PantPral> {
@override
Widget build(BuildContext context) {
return Column(
children: [
SelectorTens(),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20),
child: Row(
children: [
EntraNum(titol: "Caiguda de tensió"),
SizedBox(width: 20,),
EntraNum(titol: "Corrent nominal")
],
),
),
],
);
}
}
Ens falta ajustar l'amplada dels requadres, ja que ara tenen l'amplada mínima necessària per al contingut i els amrges. En realitat, volem que el conjunt ocupi tota l'amplada; per tant, podem fer servir un bloc d'expandir a cada element.
...
class _PantPralState extends State<PantPral> {
@override
Widget build(BuildContext context) {
return Column(
children: [
SelectorTens(),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20),
child: Row(
children: [
Expanded(child: EntraNum(titol: "Caiguda de tensió")),
SizedBox(width: 20,),
Expanded(child: EntraNum(titol: "Corrent nominal")),
],
),
),
],
);
}
}
Amb això acabaríem el disseny dels blocs d'ajust; però, de moment, no fan res.
Necessitem tenir les dues variables (corrent nominal i caiguda de tensió) que emmagatzemen els valors introduïts per l'usuari. Si les definim en el component genèric no les podrem diferenciar; però, a més, no les tindrem disponibles a l'hora de fer els càlculs. Cal, doncs, definir aquestes variables a la pantalla principal.
...
class _PantPralState extends State<PantPral> {
double caigudaTens = 1.6; // V
double correntNom = 10; // mA
@override
Widget build(BuildContext context) {
...
En l'element genèric, cal afegir com a paràmetres el valor que cal mostrar i les funcions que permeten incrementar i decrementar el valor de la variable (que encara no hem definit).
entra_num.dart
import 'package:flutter/material.dart'; import 'package:resistled_app/core/colors.dart'; import 'package:resistled_app/core/decora.dart'; import 'package:resistled_app/core/estils.dart';
class EntraNum extends StatefulWidget {
final String titol;
final String valor;
final Function() incrementar;
final Function() decrementar;
const EntraNum({
super.key,
required this.titol,
required this.valor,
required this.incrementar,
required this.decrementar,
});
@override
State<EntraNum> createState() => _EntraNumState();
}
class _EntraNumState extends State<EntraNum> {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: ColorsApp.component,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(widget.titol, style: Estils.estilTextTitols),
Text(widget.valor, style: Estils.estilTextValors),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: DecoraApp.decorBotons,
child: IconButton(
onPressed: () {
widget.decrementar();
},
icon: Icon(Icons.remove),
color: ColorsApp.text,
),
),
SizedBox(width: 15),
Container(
decoration: DecoraApp.decorBotons,
child: IconButton(
onPressed: () {
widget.incrementar();
},
icon: Icon(Icons.add),
color: ColorsApp.text,
),
),
],
),
],
),
),
);
}
}
I a la pantalla principal definim les funcions.
...
class _PantPralState extends State<PantPral> {
double caigudaTens = 1.6; // V
double correntNom = 10; // mA
@override
Widget build(BuildContext context) {
return Column(
children: [
SelectorTens(),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20),
child: Row(
children: [
Expanded(child: EntraNum(titol: "Caiguda de tensió",
valor: caigudaTens.toStringAsFixed(1).replaceAll('.', ','),
incrementar: (){setState(() {caigudaTens += 0.1;});},
decrementar: (){setState(() {caigudaTens -= 0.1;});},
)),
SizedBox(width: 20,),
Expanded(child: EntraNum(titol: "Corrent nominal",
valor: correntNom.toStringAsFixed(0),
incrementar: (){setState(() {correntNom++;});},
decrementar: (){setState(() {correntNom--;});},
)),
],
),
),
],
);
}
}
Hem posat com a valors inicials les caràcterístiques d'un led vermell força habitual. Per a la caiguda de tensió hem considerat que els valors són enters i per al corrent nominal que són reals amb un decimal; així és com normalment es solen indicar en els fulls de característiques. Les funcions d'increment i decrement treballen de manera coherent amb aquest criteri. En la definició de l'element genèric s'ha posat que són valors reals per tal que serveixi per als dos casos.
Estaria bé introduir algunes millores. Fixem-nos que ara l'usuari pot decrementar fins a valors negatius o posar valors massa elevats. Anem a modificar el programa per tal que els valors de caiguda de tensió estiguin entre 0,4 i 3,3 V i els valors de corrent entre 1 i 50 mA. Difícilment trobareu led que surtin fora d'aquests rangs; però, si cal, sempre es poden modificar els valors del programa.
La caiguda de tensió es mostrarà amb un decimal i, a més, canviarem el punt decimal per una coma, atès que així és com està normalitzat.
...
class _PantPralState extends State<PantPral> {
double caigudaTens = 1.6; // V
double correntNom = 10; // mA
final double maxCaig = 3.3;
final double minCaig = 0.4;
final double maxCorr = 50.0;
final double minCorr = 1.0;
@override
Widget build(BuildContext context) {
return Column(
children: [
SelectorTens(),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20),
child: Row(
children: [
Expanded(
child: EntraNum(
titol: "Caiguda de tensió",
valor: caigudaTens.toStringAsFixed(1).replaceAll('.', ','),
incrementar: () {
setState(() {
if (caigudaTens < maxCaig) {caigudaTens += 0.1;}
});
},
decrementar: () {
setState(() {
if (caigudaTens > minCaig) {caigudaTens -= 0.1;}
});
},
),
),
SizedBox(width: 20),
Expanded(
child: EntraNum(
titol: "Corrent nominal",
valor: correntNom.toStringAsFixed(0),
incrementar: () {
setState(() {
if (correntNom < maxCorr) {correntNom++;}
});
},
decrementar: () {
setState(() {
if (correntNom > minCorr) {correntNom--;}
});
},
),
),
],
),
),
],
);
}
}
Ja podem donar per acabada aquesta part i podem passar a l'element següent.

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