Diferencia entre revisiones de «Flutter Widget Componentes»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 32 ediciones intermedias del mismo usuario)
Línea 212: Línea 212:
 
<br />
 
<br />
  
−
===RichText y Text.rich===
+
===Text.rich===
  
 
* Más información:
 
* Más información:
Línea 223: Línea 223:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
const Text.rich(
+
    const Text.rich(
−
TextSpan(
+
      TextSpan(
−
text: 'Este texto es verde',
+
        text: 'Este texto es verde',
−
style: TextStyle(
+
        style: TextStyle(color: Colors.green, fontSize: 20),
−
color: Colors.green,
+
        children: [
−
fontSize: 20,
+
          TextSpan(
−
),
+
            text: ' y esta parte es marrón',
−
children: [
+
            style: TextStyle(color: Colors.brown),
−
TextSpan(
+
          ),
−
text: ' y esta parte es marrón',
+
          TextSpan(
−
style: TextStyle(
+
            text: ' y esta amarilla',
−
color: Colors.brown,
+
            style: TextStyle(color: Colors.yellow),
−
),
+
          ),
−
),
+
          TextSpan(text: ' y esta parte hereda el estilo del TextSpan padre'),
−
TextSpan(
+
        ],
−
text: ' y esta amarilla',
+
      ),
−
style: TextStyle(
+
    )
−
color: Colors.yellow,
 
−
),
 
−
),
 
−
TextSpan(
 
−
text: ' y esta parte hereda el estilo del TextSpan padre',
 
−
),
 
−
],
 
−
),
 
−
)
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* También podemos utilizar directamente '''RichText''':
 
−
 
−
:<syntaxhighlight lang="dart">
 
−
RichText(
 
−
text: const TextSpan(
 
−
text: 'Texto principal',
 
−
style: TextStyle(
 
−
color: Colors.green,
 
−
fontSize: 20,
 
−
),
 
−
children: [
 
−
TextSpan(
 
−
text: ' con otro estilo',
 
−
style: TextStyle(
 
−
color: Colors.red,
 
−
),
 
−
),
 
−
],
 
−
),
 
−
)
 
−
</syntaxhighlight>
 
  
−
: Para ejemplos sencillos suele resultar más cómodo '''Text.rich'''.
 
  
 
<br />
 
<br />
Línea 286: Línea 255:
  
 
:* https://pub.dev/packages/flutter_html
 
:* https://pub.dev/packages/flutter_html
 +
:: o
 +
:* https://pub.dev/packages/flutter_widget_from_html
 +
  
−
* Actualmente '''flutter_html 3.x''' es una versión estable compatible con Dart 3. Ya '''no es necesario utilizar una versión beta''' como ocurría en versiones antiguas de estos apuntes.
+
* Actualmente '''flutter_html 3.x''' es una versión estable compatible con Dart 3.  
  
−
* Podemos instalar los paquetes necesarios desde el terminal:
+
* Podemos instalar los paquetes necesarios desde el terminal (recordar que tenéis que saber hacerlo manualmente editando el pubspec.yaml y debéis comprobar si la versión es compatible con la versión de Dart instalada):
  
 
:<syntaxhighlight lang="bash">
 
:<syntaxhighlight lang="bash">
−
flutter pub add flutter_html
+
flutter pub add flutter_widget_from_html
 
flutter pub add url_launcher
 
flutter pub add url_launcher
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
: De esta forma Flutter añadirá automáticamente versiones compatibles al apartado '''dependencies''' de '''pubspec.yaml'''.
 
: De esta forma Flutter añadirá automáticamente versiones compatibles al apartado '''dependencies''' de '''pubspec.yaml'''.
 +
: El segundo paquete permite 'lanzar' el navegador web en base a una url dada.
  
 
'''<u>Nota:</u>'''
 
'''<u>Nota:</u>'''
Línea 307: Línea 280:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
−
import 'package/flutter_html.dart';
+
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
−
import 'package/url_launcher.dart';
+
import 'package:url_launcher/url_launcher.dart';
  
 
class TextosPage extends StatelessWidget {
 
class TextosPage extends StatelessWidget {
−
const TextosPage({super.key});
+
  const TextosPage({super.key});
 +
 
 +
  Future<void> _lanzarUrl(String url) async {
 +
    final uri = Uri.parse(url);
  
−
Future<void> _lanzarUrl(String url) async {
+
    final lanzada = await launchUrl(uri);
−
final uri = Uri.parse(url);
 
  
−
```
+
    if (!lanzada) {
−
final lanzada = await launchUrl(uri);
+
      throw Exception('No se pudo abrir $url');
 +
    }
 +
  }
  
−
if (!lanzada) {
+
  @override
−
  throw Exception('No se pudo abrir $url');
+
  Widget build(BuildContext context) {
 +
    return Scaffold(
 +
      appBar: AppBar(title: const Text('HTML')),
 +
      body: HtmlWidget(
 +
        '''
 +
            <b>Ejemplo de etiqueta HTML</b>
 +
            <br>
 +
            <a href="https://flutter.dev">Página de Flutter</a>
 +
            ''',
 +
        onTapUrl: ((url) {
 +
          _lanzarUrl(url);
 +
          return true;
 +
        }),
 +
      ),
 +
    );
 +
  }
 
}
 
}
−
```
 
  
−
}
+
</syntaxhighlight>
−
 
 
−
@override
 
−
Widget build(BuildContext context) {
 
−
return Scaffold(
 
−
appBar: AppBar(
 
−
title: const Text('HTML'),
 
−
),
 
−
body: Html(
 
−
data: '''
 
−
<b>Ejemplo de etiqueta HTML</b>
 
−
<br>
 
−
<a href="https://flutter.dev">Página de Flutter</a>
 
−
''',
 
−
onLinkTap: (url, attributes, element) {
 
−
if (url != null) {
 
−
_lanzarUrl(url);
 
−
}
 
−
},
 
−
),
 
−
);
 
−
}
 
−
}
 
−
</syntaxhighlight>
 
  
 
* '''url_launcher''' trabaja actualmente con objetos '''Uri''' mediante la función '''launchUrl'''.
 
* '''url_launcher''' trabaja actualmente con objetos '''Uri''' mediante la función '''launchUrl'''.
Línea 382: Línea 350:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
  
 
class BotonesPage extends StatelessWidget {
 
class BotonesPage extends StatelessWidget {
−
const BotonesPage({super.key});
+
  const BotonesPage({super.key});
  
−
@override
+
  @override
−
Widget build(BuildContext context) {
+
  Widget build(BuildContext context) {
−
return Scaffold(
+
    return Scaffold(
−
appBar: AppBar(
+
      appBar: AppBar(title: const Text('FloatingActionButton')),
−
title: const Text('FloatingActionButton'),
+
      body: const Center(child: Text('Ejemplo de FAB')),
−
),
+
      floatingActionButton: FloatingActionButton(
−
body: const Center(
+
        onPressed: () {
−
child: Text('Ejemplo de FAB'),
+
          debugPrint('Botón pulsado');
−
),
+
        },
−
floatingActionButton: FloatingActionButton(
+
        tooltip: 'Añadir',
−
onPressed: () {
+
        child: const Icon(Icons.add),
−
debugPrint('Botón pulsado');
+
      ),
−
},
+
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
−
tooltip: 'Añadir',
+
    );
−
child: const Icon(Icons.add),
+
  }
−
),
 
−
floatingActionButtonLocation:
 
−
FloatingActionButtonLocation.endFloat,
 
−
);
 
−
}
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
Línea 426: Línea 389:
 
* Aunque Material Design suele reservar el FAB para una acción principal, técnicamente podemos mostrar varios.
 
* Aunque Material Design suele reservar el FAB para una acción principal, técnicamente podemos mostrar varios.
  
−
* Si existen varios FAB en el mismo subárbol, deben tener un '''heroTag''' diferente:
+
* Si existen varios FAB en el mismo subárbol, deben tener un '''heroTag''' diferente
 +
: Un heroTag permite asociar a cada widget FAB un identificador que usan las animaciones para diferenciar un widget de otro:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
floatingActionButton: Row(
+
      floatingActionButton: Row(
−
mainAxisSize: MainAxisSize.min,
+
        mainAxisSize: MainAxisSize.min,
−
children: [
+
        children: [
−
FloatingActionButton(
+
          FloatingActionButton(
−
heroTag: 'fabAdd',
+
            heroTag: 'fabAdd',
−
onPressed: () {
+
            onPressed: () {
−
debugPrint('Añadir');
+
              debugPrint('Añadir');
−
},
+
            },
−
tooltip: 'Añadir',
+
            tooltip: 'Añadir',
−
child: const Icon(Icons.add),
+
            child: const Icon(Icons.add),
−
),
+
          ),
−
const SizedBox(width: 12),
+
          const SizedBox(width: 12),
−
FloatingActionButton(
+
          FloatingActionButton(
−
heroTag: 'fabDelete',
+
            heroTag: 'fabDelete',
−
onPressed: () {
+
            onPressed: () {
−
debugPrint('Eliminar');
+
              debugPrint('Eliminar');
−
},
+
            },
−
tooltip: 'Eliminar',
+
            tooltip: 'Eliminar',
−
child: const Icon(Icons.delete),
+
            child: const Icon(Icons.delete),
−
),
+
          ),
−
],
+
 
−
)
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 462: Línea 425:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
FloatingActionButton.extended(
+
          FloatingActionButton.extended(
−
onPressed: () {
+
            onPressed: () {
−
debugPrint('Añadir');
+
              debugPrint('Añadir');
−
},
+
            },
−
icon: const Icon(Icons.add),
+
            icon: const Icon(Icons.add),
−
label: const Text('Añadir'),
+
            label: const Text('Añadir'),
−
tooltip: 'Añadir elemento',
+
            tooltip: 'Añadir elemento',
−
)
+
          ),
 +
 
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
 +
<br />
 +
[[Imagen:Flutter_dart_widget_17.JPG|300px|center]]
  
 
<br />
 
<br />
Línea 488: Línea 456:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
Row(
+
      body: Row(
−
children: [
+
        children: [
−
TextButton(
+
          TextButton(onPressed: () {}, child: const Text('Aceptar')),
−
onPressed: () {},
+
          TextButton(onPressed: null, child: const Text('Deshabilitado')),
−
child: const Text('Aceptar'),
+
        ],
−
),
+
      ),
−
TextButton(
 
−
onPressed: null,
 
−
child: const Text('Deshabilitado'),
 
−
),
 
−
],
 
−
)
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 505: Línea 467:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
TextButton(
+
          TextButton(
−
onPressed: () {},
+
            onPressed: () {},
−
style: TextButton.styleFrom(
+
            style: TextButton.styleFrom(
−
foregroundColor: Colors.white,
+
              foregroundColor: Colors.white,
−
backgroundColor: Colors.blue,
+
              backgroundColor: Colors.blue,
−
),
+
            ),
−
child: const Text('Púlsame'),
+
            child: const Text('Púlsame'),
−
)
+
          ),
 +
 
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
 +
  
 
<br />
 
<br />
Línea 526: Línea 492:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
TextButton(
+
          TextButton(
−
onPressed: () {},
+
            onPressed: () {},
−
style: ButtonStyle(
+
            style: ButtonStyle(
−
backgroundColor:
+
              backgroundColor: WidgetStateProperty.all(Colors.blue),
−
WidgetStateProperty.all(Colors.blue),
+
              foregroundColor: WidgetStateProperty.all(Colors.white),
−
foregroundColor:
+
            ),
−
WidgetStateProperty.all(Colors.white),
+
            child: const Text('PÚLSAME'),
−
),
+
          ),
−
child: const Text('PÚLSAME'),
 
−
)
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 558: Línea 522:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
AppBar(
+
      appBar: AppBar(
−
title: const Text('Ejemplo'),
+
        title: const Text('Ejemplo'),
−
actions: [
+
        actions: [
−
IconButton(
+
          IconButton(
−
onPressed: () {
+
            onPressed: () {
−
debugPrint('Buscar');
+
              debugPrint('Buscar');
−
},
+
            },
−
tooltip: 'Buscar',
+
            tooltip: 'Buscar',
−
icon: const Icon(Icons.search),
+
            icon: const Icon(Icons.search),
−
),
+
          ),
−
],
+
        ],
−
)
+
      ),
 +
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 575: Línea 540:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
IconButton(
+
          IconButton(
−
onPressed: () {},
+
            onPressed: () {},
−
iconSize: 40,
+
            iconSize: 40,
−
color: Colors.blue,
+
            color: Colors.blue,
−
tooltip: 'Árbol de elementos',
+
            tooltip: 'Árbol de elementos',
−
icon: const Icon(Icons.account_tree),
+
            icon: const Icon(Icons.account_tree),
−
)
+
          ),
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 682: Línea 647:
  
 
* Es habitual crear un directorio:
 
* Es habitual crear un directorio:
−
 
 
:<syntaxhighlight lang="text">
 
:<syntaxhighlight lang="text">
 
assets/
 
assets/
Línea 715: Línea 679:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* Para mostrar una imagen:
+
 
 +
* Normalmente se recomienda crear un directorio con el nombre de '''assets''' en el directorio raíz del proyecto Flutter para guardar los recursos locales.
 +
: Dentro de dicho directorio podremos crear las carpetas y añadir los recursos de cualquier tipo (audio, imágenes, vídeos, iconos,...) que queramos usar y dichos recursos estarán disponibles para cualquiera de las plataformas sobre las que generemos la aplicación (IOS, Android, PC, Web...)
 +
 
 +
 
 +
<gallery caption="Creando directorio de recursos" widths="600" heights="300px" perrow="2">
 +
Image:Flutter_dart_widget_21.JPG | Pulsamos el botón derecho del ratón sobre el directorio raíz del proyecto flutter y creamos el directorio assets.
 +
Image:Flutter_dart_widget_22.JPG | Dentro del directorio assets podemos crear las carpetas que queramos y copiar ahí nuestro recursos.
 +
</gallery>
 +
 
 +
 
 +
* Vamos a descargar una imagen cualquiera de Internet y vamos a guardarla en un directorio /assets/imagenes/ del proyecto Flutter.
 +
: En el ejemplo, yo he descargado esta imagen: https://commons.wikimedia.org/wiki/File:El_sol.jpg (Jazalex, CC BY-SA 3.0 <https://creativecommons.org/licenses/by-sa/3.0>, via Wikimedia Commons)
 +
 
 +
 
 +
: '''IMPORTANTE:''' Aunque no es obligatorio, por motivos de compatibilidad con las diferentes plataformas, <u>los nombres de los recursos guardados en assets deberán estar con minúsculas y cada palabra del nombre unida con un guión bajo.</u>
 +
 
 +
 
 +
* Una vez descargada es necesario 'registrarla' en el proyecto.
 +
: Para ello es necesario editar el archivo '''pubspec.yaml''' y buscar la entrada que pone '''assets'''.
 +
: Dicha entrada estará comentada. Será necesario descomentarla y debemos de dejarla tabulada en la posición que queda después de descomentar la línea (a dos espacios en blanco del lado izquierdo).
 +
 
 +
<br /><br />
 +
 
 +
<center>
 +
{{#ev:youtube|7qiC_TITGe0}}
 +
</center>
 +
 
 +
* Para cargar la imagen localmente podemos hacer uso del [https://api.flutter.dev/flutter/widgets/Image/Image.asset.html constructor Image.asset] el cual lleva como parámetro la ruta al recurso asset que queremos cargar.
 +
 
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
Línea 726: Línea 719:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
  
 
class ImagenesPage extends StatelessWidget {
 
class ImagenesPage extends StatelessWidget {
−
const ImagenesPage({super.key});
+
  const ImagenesPage({super.key});
 +
 
 +
  @override
 +
  Widget build(BuildContext context) {
 +
    return Scaffold(
 +
      appBar: AppBar(title: const Text('Ejemplos de imágenes')),
 +
      body: Column(
 +
        children: [
 +
          Image.network(
 +
            'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
 +
            height: 150,
 +
          ),
 +
          Image.asset('assets/images/sol.jpg'),
 +
        ],
 +
      ),
 +
    );
 +
  }
 +
}
  
−
@override
 
−
Widget build(BuildContext context) {
 
−
return Scaffold(
 
−
appBar: AppBar(
 
−
title: const Text('Ejemplos de imágenes'),
 
−
),
 
−
body: Column(
 
−
children: [
 
−
Image.network(
 
−
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
 
−
height: 150,
 
−
),
 
−
Image.asset(
 
−
'assets/images/sol.jpg',
 
−
),
 
−
],
 
−
),
 
−
);
 
−
}
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 770: Línea 760:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
Image.network(
+
import 'package:flutter/material.dart';
−
'https://picsum.photos/600/400',
+
 
−
width: 200,
+
class ImagenesPage extends StatelessWidget {
−
height: 100,
+
  const ImagenesPage({super.key});
−
fit: BoxFit.cover,
+
 
−
)
+
  @override
 +
  Widget build(BuildContext context) {
 +
    return Scaffold(
 +
      appBar: AppBar(title: const Text('Ejemplos de imágenes')),
 +
      body: Column(
 +
        children: [
 +
          Container(
 +
            color: Colors.amber,
 +
            width: 300,
 +
            height: 400,
 +
            child: Image.network(
 +
            'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
 +
            fit: BoxFit.fill,
 +
            ),
 +
          )
 +
        ],
 +
      ),
 +
    );
 +
  }
 +
}
 +
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* También podemos utilizar '''FittedBox''' para ajustar cualquier widget a las restricciones de su contenedor.
+
 
  
 
<br />
 
<br />
 +
* También podemos utilizar '''FittedBox''' para ajustar cualquier widget a las restricciones de su contenedor.
 +
: Por ejemplo, imaginar que tenemos un texto que crece en tamaño hasta desbordar el tamaño de su contenedor.
 +
: Si lo rodeamos de un FittedBox podemos indicarle que se adapte al contenedor y disminuya su tamaño...
  
−
===Resolución en imágenes===
+
:<syntaxhighlight lang="dart">
 +
Container(
 +
  width: 150,
 +
  height: 80,
 +
  color: Colors.blueGrey,
 +
  child: FittedBox(
 +
    fit: BoxFit.contain, // Ajusta el contenido manteniendo la relación de aspecto
 +
    child: Text(
 +
      '999,999,999', // Texto enorme que no cabría en 150px de ancho
 +
      style: TextStyle(
 +
        fontSize: 100.0, // Tamaño base muy grande
 +
        fontWeight: FontWeight.bold,
 +
        color: Colors.white,
 +
      ),
 +
    ),
 +
  ),
 +
)
 +
</syntaxhighlight>
 +
 
 +
 
 +
<br />
 +
 
 +
===Resolución en imágenes===
  
 
* Más información:
 
* Más información:
Línea 837: Línea 872:
  
 
* Flutter puede seleccionar automáticamente una variante de una imagen dependiendo del '''devicePixelRatio'''.
 
* Flutter puede seleccionar automáticamente una variante de una imagen dependiendo del '''devicePixelRatio'''.
 +
: Este parámetro indica cuantos píxeles físicos se encuentran dentro de un pixel lógico y viene a ser equivalente al 'pixel independiente de la densidad' de Android.
 +
: Este dato lo obtiene a partir de la densidad del dispositivo.
 +
 +
[[Imagen:Flutter_dart_widget_23.JPG|500px|center]]
 +
<small><center>Imagen obtenida [https://esflutter.dev/docs/get-started/flutter-for/android-devs#d%C3%B3nde-guardo-mis-archivos-de-imagen-dependientes-de-la-resoluci%C3%B3n de este sitio web]</center></small>
 +
 +
: Por lo tanto, cuando en DART damos un tamaño a un Widget, este tamaño va a variar en función del 'Pixel Ratio Device' y será más grande o más pequeño en función de la densidad del dispositivo.
 +
 +
  
 
: Ejemplo:
 
: Ejemplo:
Línea 865: Línea 909:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
  
 
class ImagenesPage extends StatelessWidget {
 
class ImagenesPage extends StatelessWidget {
−
const ImagenesPage({super.key});
+
  const ImagenesPage({super.key});
  
−
@override
+
  @override
−
Widget build(BuildContext context) {
+
  Widget build(BuildContext context) {
−
final size = MediaQuery.sizeOf(context);
+
    final size = MediaQuery.sizeOf(context);
−
final pixelRatio =
+
    final pixelRatio = MediaQuery.devicePixelRatioOf(context);
−
MediaQuery.devicePixelRatioOf(context);
 
  
−
```
+
    debugPrint('DevicePixelRatio: $pixelRatio');
−
debugPrint(
 
−
  'DevicePixelRatio: $pixelRatio',
 
−
);
 
  
−
debugPrint(
+
    debugPrint(
−
  'Ancho físico aproximado: '
+
      'Ancho físico aproximado: '
−
  '${size.width * pixelRatio}px',
+
      '${size.width * pixelRatio}px',
−
);
+
    );
  
−
return Scaffold(
+
    return Scaffold(
−
  appBar: AppBar(
+
      appBar: AppBar(title: const Text('Ejemplos de imágenes')),
−
    title: const Text('Ejemplos de imágenes'),
+
      body: Column(
−
  ),
+
        children: [
−
  body: Column(
+
          SizedBox(
−
    children: [
+
            height: 250,
−
      SizedBox(
+
            width: double.infinity,
−
        height: 250,
+
            child: Image.asset('assets/images/galaxia.jpg', fit: BoxFit.cover),
−
        width: double.infinity,
+
          ),
−
        child: Image.asset(
+
          const SizedBox(height: 100, width: 150),
−
          'assets/images/galaxia.jpg',
+
         ],
−
          fit: BoxFit.cover,
 
−
         ),
 
 
       ),
 
       ),
−
      const SizedBox(
+
    );
−
        height: 100,
+
   }
−
        width: 150,
+
}
−
      ),
 
−
    ],
 
−
   ),
 
−
);
 
−
```
 
  
−
}
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 926: Línea 957:
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
 
FadeInImage(
 
FadeInImage(
−
placeholder: const AssetImage(
+
              placeholder: const AssetImage('assets/images/cargando.gif'),
−
'assets/images/cargando.gif',
+
              image: const NetworkImage(
−
),
+
                'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
−
image: const NetworkImage(
+
              ),
−
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
+
              fadeInDuration: const Duration(milliseconds: 500),
−
),
+
              width: 200,
−
fadeInDuration: const Duration(
+
              height: 200,
−
milliseconds: 500,
+
              fit: BoxFit.cover,
−
),
+
              placeholderFit: BoxFit.contain,
−
width: 200,
+
            )
−
height: 200,
 
−
fit: BoxFit.cover,
 
−
placeholderFit: BoxFit.contain,
 
−
)
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
* '''placeholder''' e '''image''' esperan un '''ImageProvider''', por eso podemos utilizar '''AssetImage''' y '''NetworkImage'''.
 
* '''placeholder''' e '''image''' esperan un '''ImageProvider''', por eso podemos utilizar '''AssetImage''' y '''NetworkImage'''.
  
−
* También continúa disponible '''FadeInImage.assetNetwork''', que resulta más sencillo cuando el placeholder es un asset:
+
* También tenemos la opción de usar '''FadeInImage.assetNetwork''', que resulta más sencillo cuando el placeholder es un asset:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
 
FadeInImage.assetNetwork(
 
FadeInImage.assetNetwork(
−
placeholder: 'assets/images/cargando.gif',
+
  placeholder: 'assets/images/cargando.gif',
−
image:
+
  image: 'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
−
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
+
  width: 200,
−
width: 200,
+
  height: 200,
−
height: 200,
+
  fit: BoxFit.cover,
−
fit: BoxFit.cover,
+
  placeholderFit: BoxFit.contain,
−
placeholderFit: BoxFit.contain,
 
 
)
 
)
 
</syntaxhighlight>
 
</syntaxhighlight>
Línea 989: Línea 1015:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
  
 
class ListasPage extends StatelessWidget {
 
class ListasPage extends StatelessWidget {
−
const ListasPage({super.key});
+
  const ListasPage({super.key});
  
−
@override
+
  @override
−
Widget build(BuildContext context) {
+
  Widget build(BuildContext context) {
−
return Scaffold(
+
    return Scaffold(
−
appBar: AppBar(
+
      appBar: AppBar(title: const Text('Ejemplo de listas')),
−
title: const Text('Ejemplo de listas'),
+
      body: ListView(
−
),
+
        padding: const EdgeInsets.all(8),
−
body: ListView(
+
        children: [
−
padding: const EdgeInsets.all(8),
+
          const Text('Elemento 1'),
−
children: [
+
          const Text('Elemento 2'),
−
const Text('Elemento 1'),
+
          const Text('Elemento 3'),
−
const Text('Elemento 2'),
+
          Image.network(
−
const Text('Elemento 3'),
+
            'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
−
Image.network(
+
            height: 150,
−
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
+
          ),
−
height: 150,
+
          const Text('Elemento 5'),
−
),
+
        ],
−
const Text('Elemento 5'),
+
      ),
−
],
+
    );
−
),
+
  }
−
);
 
−
}
 
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
[[Imagen.JPG|300px|center]]
+
[[Image:Flutter_dart_widget_24.JPG|300px|center]]
 +
 
 +
 
 +
 
  
 
<br />
 
<br />
Línea 1038: Línea 1066:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
ListView(
+
  @override
−
children: [
+
  Widget build(BuildContext context) {
−
ListTile(
+
    return Scaffold(
−
title: const Text('Elemento 1'),
+
      appBar: AppBar(title: const Text('Ejemplo de listas')),
−
subtitle: const Text(
+
      body: ListView(
−
'Texto aclaratorio del elemento 1',
+
        children: [
−
),
+
          ListTile(
−
leading: const Icon(Icons.accessibility),
+
            title: const Text('Elemento 1'),
−
trailing: const Icon(Icons.more_vert),
+
            subtitle: const Text('Texto aclaratorio del elemento 1'),
−
onTap: () {},
+
            leading: const Icon(Icons.accessibility),
−
),
+
            trailing: const Icon(Icons.more_vert),
−
ListTile(
+
            onTap: () {},
−
title: const Text('Elemento 2'),
+
          ),
−
subtitle: const Text(
+
          ListTile(
−
'Texto aclaratorio del elemento 2',
+
            title: const Text('Elemento 2'),
−
),
+
            subtitle: const Text('Texto aclaratorio del elemento 2'),
−
leading: const Icon(Icons.accessibility),
+
            leading: const Icon(Icons.accessibility),
−
trailing: const Icon(Icons.more_vert),
+
            trailing: const Icon(Icons.more_vert),
−
selected: true,
+
            selected: true,
−
onTap: () {},
+
            onTap: () {},
−
),
+
          ),
−
],
+
        ],
−
)
+
      ),
 +
    );
 +
  }
 +
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
'''<u>Nota:</u>'''
 
'''<u>Nota:</u>'''
  
−
* En el código antiguo aparecía:
 
−
 
−
:<syntaxhighlight lang="dart">
 
−
Color.fromARGB(0, 255, 0, 0)
 
−
</syntaxhighlight>
 
−
 
−
: Un valor alfa de '''0''' hace que el color sea completamente transparente.
 
  
 
: Para obtener rojo opaco deberíamos utilizar:
 
: Para obtener rojo opaco deberíamos utilizar:
−
 
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
 
Colors.red
 
Colors.red
Línea 1085: Línea 1108:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
<br />
 
  
−
====Cargando elementos desde una función====
 
  
−
* Los datos de una lista pueden proceder de:
+
 
 +
 
 +
<br />
 +
====Cargando elementos desde una función====
 +
 
 +
* Los datos de una lista pueden proceder de:
 
   :* Variables de la aplicación.
 
   :* Variables de la aplicación.
 
   :* Archivos JSON.
 
   :* Archivos JSON.
Línea 1097: Línea 1123:
 
   :* etc.
 
   :* etc.
  
 +
[[Image:Flutter_dart_widget_26.JPG|300px|center]]
 +
 +
<br />
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
  
 
class ListasPage extends StatelessWidget {
 
class ListasPage extends StatelessWidget {
−
const ListasPage({super.key});
+
  const ListasPage({super.key});
  
−
static const datos = [
+
  static const datos = ['Elem1', 'Elem2', 'Elem3', 'Elem4'];
−
'Elem1',
 
−
'Elem2',
 
−
'Elem3',
 
−
'Elem4',
 
−
];
 
  
−
List<Widget> _obtenerDatos(
+
  List<Widget> _obtenerDatos(List<String> datos) {
−
List<String> datos,
+
    final lista = <Widget>[];
−
) {
 
−
final lista = <Widget>[];
 
  
−
```
+
    for (final dato in datos) {
−
for (final dato in datos) {
+
      lista.add(
−
  lista.add(
+
        ListTile(
−
    ListTile(
+
          title: Text(dato),
−
      title: Text(dato),
+
          subtitle: Text('Texto aclaratorio de $dato'),
−
      subtitle: Text(
+
          leading: const Icon(Icons.accessibility),
−
        'Texto aclaratorio de $dato',
+
          trailing: const Icon(Icons.more_vert),
−
      ),
+
        ),
−
      leading:
+
      );
−
          const Icon(Icons.accessibility),
+
    }
−
      trailing:
 
−
          const Icon(Icons.more_vert),
 
−
    ),
 
−
  );
 
−
}
 
  
−
return lista;
+
    return lista;
−
```
+
  }
  
 +
  @override
 +
  Widget build(BuildContext context) {
 +
    return Scaffold(
 +
      appBar: AppBar(title: const Text('Ejemplo de listas')),
 +
      body: ListView(children: _obtenerDatos(datos)),
 +
    );
 +
  }
 
}
 
}
  
−
@override
 
−
Widget build(BuildContext context) {
 
−
return Scaffold(
 
−
appBar: AppBar(
 
−
title: const Text('Ejemplo de listas'),
 
−
),
 
−
body: ListView(
 
−
children: _obtenerDatos(datos),
 
−
),
 
−
);
 
−
}
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 1159: Línea 1171:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
List<Widget> _obtenerDatos(
+
  List<Widget> _obtenerDatos(List<String> datos) {
−
List<String> datos,
+
    return datos
−
) {
+
        .map(
−
return datos
+
          (dato) => ListTile(
−
.map(
+
            title: Text(dato),
−
(dato) => ListTile(
+
            subtitle: Text('Texto aclaratorio de $dato'),
−
title: Text(dato),
+
            leading: const Icon(Icons.accessibility),
−
subtitle: Text(
+
            trailing: const Icon(Icons.more_vert),
−
'Texto aclaratorio de $dato',
+
          ),
−
),
+
        )
−
leading:
+
        .toList(growable: false);
−
const Icon(Icons.accessibility),
+
  }
−
trailing:
 
−
const Icon(Icons.more_vert),
 
−
),
 
−
)
 
−
.toList(growable: false);
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* En Dart moderno es preferible utilizar:
+
 
 +
 
 +
 
 +
* En Dart moderno es preferible utilizar:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
Línea 1186: Línea 1195:
  
 
: cuando no necesitamos modificar la variable del bucle.
 
: cuando no necesitamos modificar la variable del bucle.
 +
 +
 +
 +
  
 
<br />
 
<br />
Línea 1196: Línea 1209:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
  
 
void main() {
 
void main() {
−
runApp(const MainApp());
+
  runApp(const MainApp());
 
}
 
}
  
 
class MainApp extends StatelessWidget {
 
class MainApp extends StatelessWidget {
−
const MainApp({super.key});
+
  const MainApp({super.key});
  
−
@override
+
  @override
−
Widget build(BuildContext context) {
+
  Widget build(BuildContext context) {
−
return MaterialApp(
+
    return MaterialApp(
−
debugShowCheckedModeBanner: false,
+
      debugShowCheckedModeBanner: false,
−
home: const MainPage(),
+
      home: const MainPage(),
−
);
+
    );
−
}
+
  }
 
}
 
}
  
 
class MainPage extends StatelessWidget {
 
class MainPage extends StatelessWidget {
−
const MainPage({super.key});
+
  const MainPage({super.key});
  
−
@override
+
  @override
−
Widget build(BuildContext context) {
+
  Widget build(BuildContext context) {
−
return Scaffold(
+
    return Scaffold(
−
appBar: AppBar(
+
      appBar: AppBar(title: const Text('Exercicios Listas')),
−
title: const Text('Exercicios Listas'),
+
      body: const SizedBox(),
−
),
+
    );
−
body: const SizedBox(),
+
  }
−
);
 
−
}
 
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
: Substitúe o '''body''' polo widget correspondente a cada exercicio.
 
: Substitúe o '''body''' polo widget correspondente a cada exercicio.
 +
 +
 +
  
 
<br />
 
<br />
Línea 1262: Línea 1277:
  
 
: Crea un '''StatelessWidget''' que devolva unha lista de textos aplicando os datos definidos nos mapas anteriores.
 
: Crea un '''StatelessWidget''' que devolva unha lista de textos aplicando os datos definidos nos mapas anteriores.
 +
 +
 +
  
 
<br />
 
<br />
Línea 1295: Línea 1313:
 
:* '''Padding'''.
 
:* '''Padding'''.
 
:* '''FadeInImage.assetNetwork'''.
 
:* '''FadeInImage.assetNetwork'''.
 +
 +
 +
  
 
<br />
 
<br />
Línea 1331: Línea 1352:
  
 
: Crea un '''StatelessWidget''' que devolva unha lista de '''ListTile''' aplicando os datos anteriores.
 
: Crea un '''StatelessWidget''' que devolva unha lista de '''ListTile''' aplicando os datos anteriores.
 +
 +
  
 
<br />
 
<br />
Línea 1360: Línea 1383:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: Crea unha lista na que cada elemento conteña un texto e unha imaxe.
+
: Crea una lista en la que cada elemento contenga un texto y una imagen.
−
 
+
: La imagen debe ocupar las dimensiones indicadas sin deformarse.
−
: A imaxe debe ocupar as dimensións indicadas sen deformarse.
 
  
 
'''<u>Nota de tipos:</u>'''
 
'''<u>Nota de tipos:</u>'''
 +
:* Los valores '''width''' y '''height''' esperan un '''double?''', que es el tipo de dato que recibe width o height en Image.network.
 +
:<syntaxhighlight lang="dart">
 +
final altura =(e['altura'] as num).toDouble();
 +
final anchura = (e['anchura'] as num).toDouble();
 +
</syntaxhighlight>
 +
 +
 +
:* Si consideramos que los datos vienen de un servicio externo en el que no podemos garantizar la existencia de claves y tipos de valores tendríamos que usar esta forma:
 +
:<syntaxhighlight lang="dart">
 +
final altura = double.tryParse(e['altura']?.toString() ?? '') ?? 0.0;
 +
final anchura = double.tryParse(e['anchura']?.toString() ?? '') ?? 0.0;
 +
</syntaxhighlight>
 +
 +
  
−
* Os valores anteriores son '''int''', mentres que '''width''' e '''height''' esperan un '''double?'''.
 
  
−
* Como sabemos que o valor é numérico, unha conversión máis adecuada que converter primeiro a String é:
 
  
−
:<syntaxhighlight lang="dart">
 
−
final altura =
 
−
(e['altura'] as num).toDouble();
 
  
−
final anchura =
 
−
(e['anchura'] as num).toDouble();
 
−
</syntaxhighlight>
 
  
 
<br />
 
<br />
Línea 1404: Línea 1432:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: Crea unha lista na que cada elemento conteña:
+
: Crea una lista en la que cada elemento contenga:
−
:* O texto principal.
+
:* El texto principal.
 
:* Un '''IconButton'''.
 
:* Un '''IconButton'''.
−
:* O texto almacenado en '''elemento''' coa cor indicada.
+
:* El texto almacenado en '''elemento''' con el color indicado.
  
 
<br />
 
<br />
  
−
=====Solucións Exercicios Propostos=====
+
=====Soluciones a Ejercicios Propuestos=====
  
−
* '''Exercicio 1'''
+
* '''Ejercicio 1'''
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
class Exercicio1Listas
+
import 'package:flutter/material.dart';
−
extends StatelessWidget {
 
−
const Exercicio1Listas({
 
−
super.key,
 
−
required this.datos,
 
−
});
 
  
−
final List<Map<String, dynamic>> datos;
+
class Exercicio1Listas extends StatelessWidget {
 +
  const Exercicio1Listas({super.key, required this.datos});
  
−
@override
+
  final List<Map<String, dynamic>> datos;
−
Widget build(BuildContext context) {
+
 
−
return ListView(
+
  @override
−
children: datos.map((e) {
+
  Widget build(BuildContext context) {
−
return Text(
+
    return ListView(
−
'Texto: ${e['texto'] as String}',
+
      children: datos
−
style: TextStyle(
+
          .map((e) {
−
color: e['color'] as Color,
+
            return Text(
−
fontSize:
+
              'Texto: ${e['texto'] as String}',
−
(e['size'] as num).toDouble(),
+
              style: TextStyle(
−
),
+
                color: e['color'] as Color,
−
);
+
                fontSize: (e['size'] as num).toDouble(),
−
}).toList(growable: false),
+
              ),
−
);
+
            );
 +
          })
 +
          .toList(growable: false),
 +
    );
 +
  }
 
}
 
}
 +
 +
</syntaxhighlight>
 +
 +
<br />
 +
 +
* '''Ejercicio 2'''
 +
 +
:<syntaxhighlight lang="dart">
 +
import 'package:flutter/material.dart';
 +
 +
class Exercicio2Listas extends StatelessWidget {
 +
  const Exercicio2Listas({super.key, required this.datos});
 +
 +
  final List<Map<String, dynamic>> datos;
 +
 +
  @override
 +
  Widget build(BuildContext context) {
 +
    return ListView(
 +
      children: datos
 +
          .map((e) {
 +
            return Padding(
 +
              padding: const EdgeInsets.all(8),
 +
              child: Row(
 +
                children: [
 +
                  Expanded(child: Text(e['texto'] as String)),
 +
                  FadeInImage.assetNetwork(
 +
                    placeholder: 'assets/images/cargando.gif',
 +
                    image: e['url'] as String,
 +
                    width: 100,
 +
                    height: 100,
 +
                    fit: BoxFit.cover,
 +
                  ),
 +
                ],
 +
              ),
 +
            );
 +
          })
 +
          .toList(growable: false),
 +
    );
 +
  }
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
* '''Exercicio 2'''
+
* '''Ejercicio 3'''
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
class Exercicio2Listas
+
import 'package:flutter/material.dart';
−
extends StatelessWidget {
 
−
const Exercicio2Listas({
 
−
super.key,
 
−
required this.datos,
 
−
});
 
  
−
final List<Map<String, dynamic>> datos;
+
class Exercicio3Listas extends StatelessWidget {
 +
  const Exercicio3Listas({super.key, required this.datos});
  
−
@override
+
  final List<Map<String, dynamic>> datos;
−
Widget build(BuildContext context) {
+
 
−
return ListView(
+
  @override
−
children: datos.map((e) {
+
  Widget build(BuildContext context) {
−
return Padding(
+
    return ListView(
−
padding:
+
      children: datos
−
const EdgeInsets.all(8),
+
          .map((e) {
−
child: Row(
+
            return ListTile(
−
children: [
+
              title: Text(e['title'] as String),
−
Expanded(
+
              subtitle: Text(e['subtitle'] as String),
−
child: Text(
+
              leading: FadeInImage.assetNetwork(
−
e['texto'] as String,
+
                placeholder: 'assets/images/cargando.gif',
−
),
+
                image: e['leading'] as String,
−
),
+
                width: 60,
−
FadeInImage.assetNetwork(
+
                height: 60,
−
placeholder:
+
                fit: BoxFit.cover,
−
'assets/images/cargando.gif',
+
              ),
−
image:
+
            );
−
e['url'] as String,
+
          })
−
width: 100,
+
          .toList(growable: false),
−
height: 100,
+
    );
−
fit: BoxFit.cover,
+
  }
−
),
 
−
],
 
−
),
 
−
);
 
−
}).toList(growable: false),
 
−
);
 
−
}
 
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
* '''Exercicio 3'''
+
* '''Ejercicio 4'''
 +
: Partimos de que no podemos asegurar que los datos vengan correctamente del servicio...
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
class Exercicio3Listas
+
import 'package:flutter/material.dart';
−
extends StatelessWidget {
+
 
−
const Exercicio3Listas({
+
class Exercicio4Listas extends StatelessWidget {
−
super.key,
+
  const Exercicio4Listas({super.key, required this.datos});
−
required this.datos,
+
 
−
});
+
  final List<Map<String, dynamic>> datos;
  
−
final List<Map<String, dynamic>> datos;
+
  @override
 +
  Widget build(BuildContext context) {
 +
    return ListView(
 +
      children: datos
 +
          .map((e) {
 +
            final altura = double.tryParse(e['altura']?.toString() ?? '') ?? 0.0;
 +
            final anchura = double.tryParse(e['anchura']?.toString() ?? '') ?? 0.0;
  
−
@override
+
            return Row(
−
Widget build(BuildContext context) {
+
              children: [
−
return ListView(
+
                Expanded(child: Text('Texto: ${e['texto']}')),
−
children: datos.map((e) {
+
                Image.network(
−
return ListTile(
+
                  e['url']?.toString() ?? 'https://picsum.photos/200/300',
−
title: Text(
+
                  width: anchura,
−
e['title'] as String,
+
                  height: altura,
−
),
+
                  fit: BoxFit.cover,
−
subtitle: Text(
+
                ),
−
e['subtitle'] as String,
+
              ],
−
),
+
            );
−
leading:
+
          })
−
FadeInImage.assetNetwork(
+
          .toList(growable: false),
−
placeholder:
+
    );
−
'assets/images/cargando.gif',
+
  }
−
image:
 
−
e['leading'] as String,
 
−
width: 60,
 
−
height: 60,
 
−
fit: BoxFit.cover,
 
−
),
 
−
);
 
−
}).toList(growable: false),
 
−
);
 
−
}
 
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
* '''Exercicio 4'''
+
* '''Exercicio 5'''
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
class Exercicio4Listas
+
import 'package:flutter/material.dart';
−
extends StatelessWidget {
 
−
const Exercicio4Listas({
 
−
super.key,
 
−
required this.datos,
 
−
});
 
  
−
final List<Map<String, dynamic>> datos;
+
class Exercicio5Listas extends StatelessWidget {
 +
  const Exercicio5Listas({super.key, required this.datos});
  
−
@override
+
  final List<Map<String, dynamic>> datos;
−
Widget build(BuildContext context) {
 
−
return ListView(
 
−
children: datos.map((e) {
 
−
final anchura =
 
−
(e['anchura'] as num).toDouble();
 
  
−
```
+
  @override
−
     final altura =
+
  Widget build(BuildContext context) {
−
        (e['altura'] as num).toDouble();
+
     return ListView(
 +
      children: datos
 +
          .map((e) {
 +
            final elemento = e['elemento'] as Map<String, dynamic>;
  
−
    return Row(
+
            return Row(
−
      children: [
+
              children: [
−
        Expanded(
+
                Expanded(child: Text(e['texto'] as String)),
−
          child: Text(
+
                IconButton(
−
            'Texto: ${e['texto']}',
+
                  onPressed: () {},
−
          ),
+
                  icon: Icon(elemento['icon'] as IconData),
−
        ),
+
                ),
−
        Image.network(
+
                Text(
−
          e['url'] as String,
+
                  elemento['texto'] as String,
−
          width: anchura,
+
                  style: TextStyle(color: elemento['cor'] as Color),
−
          height: altura,
+
                ),
−
          fit: BoxFit.cover,
+
              ],
−
        ),
+
            );
−
      ],
+
          })
−
    );
+
          .toList(growable: false),
−
  }).toList(growable: false),
+
    );
−
);
+
  }
−
```
+
}
  
−
}
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
* '''Exercicio 5'''
+
===Listas con elementos cargados dinámicamente: ListView.builder===
 +
 
 +
* Más información:
 +
  :* https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html
  
−
:<syntaxhighlight lang="dart">
+
* '''ListView.builder''' crea los widgets a medida que son necesarios.
−
class Exercicio5Listas
 
−
extends StatelessWidget {
 
−
const Exercicio5Listas({
 
−
super.key,
 
−
required this.datos,
 
−
});
 
  
−
final List<Map<String, dynamic>> datos;
+
: Resulta especialmente útil cuando:
 +
:* La lista tiene muchos elementos.
 +
:* Los datos se generan dinámicamente.
 +
:* No queremos construir inicialmente todos los widgets.
  
−
@override
 
−
Widget build(BuildContext context) {
 
−
return ListView(
 
−
children: datos.map((e) {
 
−
final elemento =
 
−
e['elemento']
 
−
as Map<String, dynamic>;
 
  
−
```
+
[[Image:Flutter_dart_widget_27.JPG|500px|center]]
−
    return Row(
 
−
      children: [
 
−
        Expanded(
 
−
          child: Text(
 
−
            e['texto'] as String,
 
−
          ),
 
−
        ),
 
−
        IconButton(
 
−
          onPressed: () {},
 
−
          icon: Icon(
 
−
            elemento['icon']
 
−
                as IconData,
 
−
          ),
 
−
        ),
 
−
        Text(
 
−
          elemento['texto']
 
−
              as String,
 
−
          style: TextStyle(
 
−
            color:
 
−
                elemento['cor']
 
−
                    as Color,
 
−
          ),
 
−
        ),
 
−
      ],
 
−
    );
 
−
  }).toList(growable: false),
 
−
);
 
−
```
 
  
−
}
 
−
}
 
−
</syntaxhighlight>
 
  
−
<br />
+
* Los dos parámetros principales son:
 +
  :* '''itemCount''': número de elementos disponibles.
 +
  :* '''itemBuilder''': función que construye un widget para una posición determinada.
 +
 
 +
* '''itemCount''' no es obligatorio desde el punto de vista de la API, pero debe indicarse siempre que conozcamos el tamaño de la fuente de datos.
  
−
====Exercicios propostos sen solución====
+
:<syntaxhighlight lang="dart">
 +
import 'package:flutter/material.dart';
  
−
* '''Exercicio 6'''
+
class ListasPage extends StatelessWidget {
 +
  const ListasPage({super.key});
  
−
:<syntaxhighlight lang="dart">
+
  static const datos = [
−
final datosExercicio6 =
+
    'Elem1',
−
[1, 2, 3, 4, 5, 6];
+
    'Elem2',
−
</syntaxhighlight>
+
    'Elem3',
 +
    'Elem4',
 +
    'Elem5',
 +
    'Elem6',
 +
    'Elem7',
 +
    'Elem8',
 +
    'Elem9',
 +
  ];
  
−
: Crea unha lista onde cada elemento mostre:
+
  @override
 +
  Widget build(BuildContext context) {
 +
    return Scaffold(
 +
      appBar: AppBar(title: const Text('ListView.builder')),
 +
      body: ListView.builder(
 +
        itemCount: datos.length,
 +
        itemBuilder: (context, index) {
 +
          debugPrint('Construyendo índice $index');
  
−
:<syntaxhighlight lang="text">
+
          final dato = datos[index];
−
Número: 1 → Resultado: 10
 
−
</syntaxhighlight>
 
  
−
: Cada elemento debe estar dentro dun '''Card''' e separado mediante '''Padding'''.
+
          return ListTile(
 +
            title: Text(dato),
 +
            subtitle: Text('Información de $dato'),
 +
            leading: const Icon(Icons.accessibility),
 +
            trailing: const Icon(Icons.more_vert),
 +
            onTap: () {},
 +
          );
 +
        },
 +
      ),
 +
    );
 +
  }
 +
}
  
−
<br />
+
</syntaxhighlight>
  
−
* '''Exercicio 7'''
+
* No es necesario utilizar:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
final datosExercicio7 = <String>[
+
datos.elementAt(index)
−
'Lugo',
 
−
'Ourense',
 
−
'A Coruña',
 
−
'Pontevedra',
 
−
];
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: Crea unha lista de elementos formados por:
+
: cuando disponemos de una '''List'''. Es más directo utilizar:
−
:* Un '''CircleAvatar'''.
+
 
−
:* O nome da cidade debaixo.
+
:<syntaxhighlight lang="dart">
−
:* Unha '''Column''' para agrupalos.
+
datos[index]
 +
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
* '''Exercicio 8'''
+
===Separando elementos de las listas===
 +
 
 +
====Widget Divider====
 +
 
 +
* Más información:
 +
  :* https://api.flutter.dev/flutter/material/Divider-class.html
 +
 
 +
* Podemos introducir manualmente '''Divider''' dentro de una lista pequeña.
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
final datosExercicio8 =
+
ListView(
−
<Map<String, dynamic>>[
+
children: const [
−
{
+
ListTile(
−
'titulo': 'Aprender Flutter',
+
title: Text('Elemento 1'),
−
'autor': 'Ana',
+
),
−
},
+
Divider(),
−
{
+
ListTile(
−
'titulo': 'Widgets e Layouts',
+
title: Text('Elemento 2'),
−
'autor': 'Brais',
+
),
−
},
+
],
−
{
+
)
−
'titulo': 'Listas e Datos',
 
−
'autor': 'Clara',
 
−
},
 
−
{
 
−
'titulo': 'Eventos e Accións',
 
−
'autor': 'Diego',
 
−
},
 
−
];
 
 
</syntaxhighlight>
 
</syntaxhighlight>
−
 
−
: Crea unha clase '''Libro''' con:
 
−
:* Atributos '''final'''.
 
−
:* Constructor con parámetros obrigatorios e por nome.
 
−
:* Constructor '''factory Libro.fromMap'''.
 
−
 
−
: Converte os mapas en obxectos '''Libro''' e visualízaos mediante '''ListTile'''.
 
  
 
<br />
 
<br />
  
−
* '''Exercicio 9'''
+
====ListView.separated====
 +
 
 +
* Para listas creadas dinámicamente es más apropiado '''ListView.separated'''.
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
final datosExercicio9 =
+
ListView.separated(
−
<Map<String, dynamic>>[
+
itemCount: datos.length,
−
{
+
itemBuilder: (context, index) {
−
'nome': 'Ana',
+
return ListTile(
−
'idade': 25,
+
title: Text(datos[index]),
−
'foto':
+
);
−
'https://randomuser.me/api/portraits/women/1.jpg',
 
 
},
 
},
−
{
+
separatorBuilder: (context, index) {
−
'nome': 'Brais',
+
return const Divider(
−
'idade': 30,
+
thickness: 1,
−
'foto':
+
);
−
'https://randomuser.me/api/portraits/men/2.jpg',
 
 
},
 
},
−
];
+
)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: Crea unha clase '''Persona''' e representa os datos utilizando '''Card''' e '''CircleAvatar'''.
+
* '''separatorBuilder''' recibe el índice correspondiente y puede devolver cualquier widget.
  
−
<br />
+
: Por ejemplo:
−
 
 
−
* '''Exercicio 10'''
 
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
final datosExercicio10 =
+
separatorBuilder: (context, index) {
−
<Map<String, dynamic>>[
+
return const SizedBox(
−
{
+
height: 10,
−
'produto': 'Camiseta',
+
);
−
'prezo': 19.99,
+
}
−
'foto':
 
−
'https://picsum.photos/100?1',
 
−
},
 
−
{
 
−
'produto': 'Pantalón',
 
−
'prezo': 29.99,
 
−
'foto':
 
−
'https://picsum.photos/100?2',
 
−
},
 
−
];
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: Crea unha clase '''Produto''' e unha lista de '''ListTile''' con:
+
<br />
−
:* Imaxe no '''leading'''.
 
−
:* Nome no '''title'''.
 
−
:* Prezo no '''subtitle'''.
 
−
:* '''IconButton''' cunha cesta no '''trailing'''.
 
  
−
<br />
+
==FutureBuilder==
  
−
* '''Exercicio 11'''
+
Más información:
 +
:* https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
  
−
: Define unha clase '''ProductoFitbit''' onde '''prezo''' e '''valoracion''' sexan '''double'''.
+
En algunas ocasiones los datos que queremos mostrar '''no están disponibles inmediatamente'''.
  
−
: No método '''fromMap''', lembra que un valor JSON pode chegar como '''int''' ou como '''double'''.
+
: Por ejemplo:
 +
:* Una petición a una API de Internet.
 +
:* La lectura de un archivo.
 +
:* Una consulta a una base de datos.
 +
:* Una operación que tarda cierto tiempo en finalizar.
  
−
: Para convertelo correctamente:
+
Dart representa este tipo de operaciones mediante objetos de tipo '''Future'''.
  
−
:<syntaxhighlight lang="dart">
+
: Un Future representa un valor que '''estará disponible en algún momento en el futuro'''.
−
prezo:
 
−
(map['prezo'] as num).toDouble(),
 
−
valoracion:
 
−
(map['valoracion'] as num)
 
−
.toDouble(),
 
−
</syntaxhighlight>
 
  
−
: Se a valoración é maior ou igual a 4.5:
+
Por ejemplo, el siguiente método devuelve un Future que dentro de cuatro segundos contendrá una lista de cadenas:
−
:* Mostrar '''Icons.star'''.
 
−
:* Mostrar o prezo en verde.
 
  
−
: En caso contrario:
+
:<syntaxhighlight lang="dart">
−
:* Mostrar '''Icons.star_border'''.
+
Future<List<String>> _obtenerDatosInternet() {
−
:* Mostrar o prezo en vermello.
+
final datos = <String>[
 +
'Valor 1',
 +
'Valor 2',
 +
'Valor 3',
 +
];
  
−
<br />
+
return Future.delayed(
 +
  const Duration(seconds: 4),() => datos,);
 +
}
 +
</syntaxhighlight>
  
−
===Listas con elementos cargados dinámicamente: ListView.builder===
+
También podemos escribirlo utilizando '''async''' y '''await''':
  
−
* Más información:
+
:<syntaxhighlight lang="dart">
−
  :* https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html
+
Future<List<String>> _obtenerDatosInternet() async {
 +
await Future<void>.delayed(
 +
const Duration(seconds: 4),
 +
);
  
−
* '''ListView.builder''' crea los widgets a medida que son necesarios.
+
return [
 +
'Valor 1',
 +
'Valor 2',
 +
'Valor 3',
 +
];
 +
}
 +
</syntaxhighlight>
  
−
: Resulta especialmente útil cuando:
+
: En ambos casos el resultado es el mismo:
−
:* La lista tiene muchos elementos.
 
−
:* Los datos se generan dinámicamente.
 
−
:* No queremos construir inicialmente todos los widgets.
 
  
−
* Los dos parámetros principales son:
+
:<syntaxhighlight lang="dart">
−
  :* '''itemCount''': número de elementos disponibles.
+
Future<List<String>>
−
  :* '''itemBuilder''': función que construye un widget para una posición determinada.
+
</syntaxhighlight>
  
−
* '''itemCount''' no es obligatorio desde el punto de vista de la API, pero debe indicarse siempre que conozcamos el tamaño de la fuente de datos.
+
<br />
 +
 
 +
===El widget FutureBuilder===
 +
 
 +
'''FutureBuilder''' es un widget que permite construir una parte de la interfaz en función del estado de un Future.
 +
 
 +
Entre sus parámetros principales tenemos:
 +
 
 +
:* '''future''': Future cuyo resultado queremos esperar.
 +
:* '''builder''': función que se ejecutará para construir la interfaz.
 +
:* '''initialData''': dato inicial opcional que se puede utilizar mientras todavía no ha terminado el Future.
  
−
:<syntaxhighlight lang="dart">
+
El '''builder''' recibe un objeto denominado '''AsyncSnapshot''' que contiene información acerca del Future.
−
import 'package/material.dart';
 
  
−
class ListasPage extends StatelessWidget {
+
: Entre sus propiedades más utilizadas encontramos:
−
const ListasPage({super.key});
 
  
−
static const datos = [
+
:* '''snapshot.connectionState''': estado de la operación.
−
'Elem1',
+
:* '''snapshot.data''': datos obtenidos.
−
'Elem2',
+
:* '''snapshot.hasData''': indica si existen datos.
−
'Elem3',
+
:* '''snapshot.hasError''': indica si se produjo un error.
−
'Elem4',
+
:* '''snapshot.error''': error producido.
−
'Elem5',
 
−
'Elem6',
 
−
'Elem7',
 
−
'Elem8',
 
−
'Elem9',
 
−
];
 
  
−
@override
+
<br />
−
Widget build(BuildContext context) {
 
−
return Scaffold(
 
−
appBar: AppBar(
 
−
title:
 
−
const Text('ListView.builder'),
 
−
),
 
−
body: ListView.builder(
 
−
itemCount: datos.length,
 
−
itemBuilder: (context, index) {
 
−
debugPrint(
 
−
'Construyendo índice $index',
 
−
);
 
  
−
```
+
===Estados de ConnectionState===
−
      final dato = datos[index];
 
  
−
      return ListTile(
+
La propiedad '''connectionState''' puede tener los siguientes valores:
−
        title: Text(dato),
 
−
        subtitle: Text(
 
−
          'Información de $dato',
 
−
        ),
 
−
        leading: const Icon(
 
−
          Icons.accessibility,
 
−
        ),
 
−
        trailing:
 
−
            const Icon(Icons.more_vert),
 
−
        onTap: () {},
 
−
      );
 
−
    },
 
−
  ),
 
−
);
 
−
```
 
  
−
}
+
:* '''ConnectionState.none''': no existe ninguna operación asíncrona asociada.
−
}
+
:* '''ConnectionState.waiting''': estamos esperando a que termine el Future.
−
</syntaxhighlight>
+
:* '''ConnectionState.active''': se están recibiendo datos de forma activa. Es más habitual con Stream que con Future.
 +
:* '''ConnectionState.done''': la operación ha terminado.
  
−
* No es necesario utilizar:
+
En nuestros ejemplos de FutureBuilder nos interesarán principalmente:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
datos.elementAt(index)
+
ConnectionState.waiting
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: cuando disponemos de una '''List'''. Es más directo utilizar:
+
: y:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
datos[index]
+
ConnectionState.done
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
===Separando elementos de las listas===
+
===Ejemplo sencillo con StatelessWidget===
  
−
====Widget Divider====
+
Como todavía no hemos estudiado '''StatefulWidget''', podemos utilizar FutureBuilder directamente dentro del método '''build''' de un StatelessWidget.
 +
 
 +
Emularemos que estamos obteniendo una lista de datos desde Internet:
 +
 
 +
 
 +
<gallery caption="Empleando un FutureBuilder" widths="600" heights="300px" perrow="2">
 +
Image:Flutter_dart_widget_28.JPG | FutureBuilder cargando los datos iniciales a la espera de que se carguen de Internet el resto de datos.
 +
Image:Flutter_dart_widget_29.JPG | Una vez pasados 4 segundos, aparecen los datos (simulando como si vinieran de Internet)
 +
</gallery>
  
−
* Más información:
 
−
  :* https://api.flutter.dev/flutter/material/Divider-class.html
 
  
−
* Podemos introducir manualmente '''Divider''' dentro de una lista pequeña.
+
<br />
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
ListView(
+
import 'package:flutter/material.dart';
−
children: const [
+
 
−
ListTile(
+
class ListasFuturePage extends StatelessWidget {
−
title: Text('Elemento 1'),
+
  const ListasFuturePage({super.key});
−
),
+
 
−
Divider(),
+
  @override
−
ListTile(
+
  Widget build(BuildContext context) {
−
title: Text('Elemento 2'),
+
    return Scaffold(
−
),
+
      appBar: AppBar(title: const Text('FutureBuilder')),
−
],
+
      body: FutureBuilder<List<String>>(
−
)
+
        future: _obtenerDatosInternet(),
−
</syntaxhighlight>
+
        builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
 +
          // 1. Comprobamos si se produjo un error.
 +
          if (snapshot.hasError) {
 +
            return Center(
 +
              child: Text('Se produjo un error: ${snapshot.error}'),
 +
            );
 +
          }
  
−
<br />
+
          // 2. Comprobamos si estamos esperando los datos.
 +
          if (snapshot.connectionState == ConnectionState.waiting) {
 +
            return const Center(child: CircularProgressIndicator());
 +
          }
  
−
====ListView.separated====
+
          // 3. Obtenemos los datos.
 +
          final datos = snapshot.data ?? const <String>[];
  
−
* Para listas creadas dinámicamente es más apropiado '''ListView.separated'''.
+
          // 4. Comprobamos si la lista está vacía.
 +
          if (datos.isEmpty) {
 +
            return const Center(child: Text('No hay datos disponibles.'));
 +
          }
  
−
:<syntaxhighlight lang="dart">
+
          // 5. Mostramos los datos.
−
ListView.separated(
+
          return ListView.builder(
−
itemCount: datos.length,
+
            itemCount: datos.length,
−
itemBuilder: (context, index) {
+
            itemBuilder: (context, index) {
−
return ListTile(
+
              final dato = datos[index];
−
title: Text(datos[index]),
 
−
);
 
−
},
 
−
separatorBuilder: (context, index) {
 
−
return const Divider(
 
−
thickness: 1,
 
−
);
 
−
},
 
−
)
 
−
</syntaxhighlight>
 
  
−
* '''separatorBuilder''' recibe el índice correspondiente y puede devolver cualquier widget.
+
              return ListTile(
 +
                leading: const Icon(Icons.add),
 +
                title: Text(dato),
 +
                onTap: () {},
 +
              );
 +
            },
 +
          );
 +
        },
 +
      ),
 +
    );
 +
  }
  
−
: Por ejemplo:
+
  Future<List<String>> _obtenerDatosInternet() async {
 +
    await Future<void>.delayed(const Duration(seconds: 4));
  
−
:<syntaxhighlight lang="dart">
+
    return ['Valor 1', 'Valor 2', 'Valor 3'];
−
separatorBuilder: (context, index) {
+
  }
−
return const SizedBox(
 
−
height: 10,
 
−
);
 
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
===FutureBuilder===
+
===¿Qué sucede en el ejemplo?===
  
−
* Más información:
+
Cuando Flutter ejecuta el método '''build''', se crea el Future:
−
  :* https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
 
  
−
* '''FutureBuilder''' permite construir diferentes widgets dependiendo del estado de un '''Future'''.
+
:<syntaxhighlight lang="dart">
 +
future: _obtenerDatosInternet(),
 +
</syntaxhighlight>
  
−
* Un Future representa un resultado que estará disponible en el futuro.
+
En ese momento todavía no tenemos los datos.
  
−
: Es muy habitual utilizarlo para:
+
: Por ello:
−
:* Peticiones HTTP.
 
−
:* Lectura de archivos.
 
−
:* Consultas a bases de datos.
 
−
:* Operaciones asíncronas.
 
  
−
* '''FutureBuilder''' recibe principalmente:
+
:<syntaxhighlight lang="dart">
−
  :* '''future''': el Future que queremos observar.
+
snapshot.connectionState ==
−
  :* '''builder''': función que construye la interfaz dependiendo de su estado.
+
ConnectionState.waiting
−
  :* '''initialData''': dato inicial opcional.
+
</syntaxhighlight>
  
−
* El '''AsyncSnapshot''' recibido por el builder permite consultar:
+
: será verdadero y mostraremos:
−
  :* '''snapshot.connectionState'''.
 
−
  :* '''snapshot.hasData'''.
 
−
  :* '''snapshot.hasError'''.
 
−
  :* '''snapshot.data'''.
 
−
  :* '''snapshot.error'''.
 
  
−
'''<u>IMPORTANTE:</u>'''
+
:<syntaxhighlight lang="dart">
 +
const CircularProgressIndicator()
 +
</syntaxhighlight>
  
−
* No es recomendable crear continuamente un Future nuevo dentro de '''build''' si la operación no debe repetirse en cada reconstrucción.
+
Aproximadamente cuatro segundos después finaliza el Future.
  
−
: Para una operación asíncrona que debe ejecutarse una sola vez podemos guardar el Future en el estado del widget.
+
FutureBuilder recibe el resultado y vuelve a ejecutar su '''builder'''.
  
−
'''Ejemplo actualizado'''
+
Los datos estarán disponibles en:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
snapshot.data
 +
</syntaxhighlight>
 +
 
 +
En nuestro caso tendrán el valor:
  
−
class ListasFuturePage
+
:<syntaxhighlight lang="dart">
−
extends StatefulWidget {
+
[
−
const ListasFuturePage({super.key});
+
'Valor 1',
 +
'Valor 2',
 +
'Valor 3',
 +
]
 +
</syntaxhighlight>
  
−
@override
+
Finalmente construimos un '''ListView.builder''' y mostramos los elementos.
−
State<ListasFuturePage> createState() =>
 
−
_ListasFuturePageState();
 
−
}
 
  
−
class _ListasFuturePageState
+
<br />
−
extends State<ListasFuturePage> {
 
  
−
late final Future<List<String>>
+
===¿Por qué utilizamos snapshot.data ?? []?===
−
_futureDatos;
 
  
−
@override
+
La propiedad '''snapshot.data''' puede ser nula.
−
void initState() {
 
−
super.initState();
 
  
−
```
+
: Su tipo realmente es:
−
_futureDatos =
 
−
    _obtenerDatosInternet();
 
−
```
 
  
−
}
+
:<syntaxhighlight lang="dart">
 +
List<String>?
 +
</syntaxhighlight>
  
−
Future<List<String>>
+
Por ello podemos escribir:
−
_obtenerDatosInternet() async {
 
  
−
```
+
:<syntaxhighlight lang="dart">
−
await Future<void>.delayed(
+
final datos =
−
  const Duration(seconds: 4),
+
snapshot.data ?? const <String>[];
−
);
+
</syntaxhighlight>
  
−
return [
+
El operador:
−
  'Valor 1',
 
−
  'Valor 2',
 
−
  'Valor 3',
 
−
];
 
−
```
 
  
−
}
+
:<syntaxhighlight lang="dart">
 +
??
 +
</syntaxhighlight>
  
−
@override
+
: indica que:
−
Widget build(BuildContext context) {
+
 
−
return Scaffold(
+
:* Si '''snapshot.data''' no es null, se utiliza su valor.
−
appBar: AppBar(
+
:* Si '''snapshot.data''' es null, se utiliza una lista vacía.
−
title:
+
 
−
const Text('FutureBuilder'),
+
: Es equivalente conceptualmente a:
−
),
+
 
−
body: FutureBuilder<List<String>>(
+
:<syntaxhighlight lang="dart">
−
future: _futureDatos,
+
List<String> datos;
−
builder: (
+
 
−
BuildContext context,
+
if (snapshot.data != null) {
−
AsyncSnapshot<List<String>>
+
datos = snapshot.data!;
−
snapshot,
+
} else {
−
) {
+
datos = [];
−
if (snapshot.hasError) {
 
−
return Center(
 
−
child: Text(
 
−
'Houbo un erro: '
 
−
'${snapshot.error}',
 
−
),
 
−
);
 
 
}
 
}
 +
</syntaxhighlight>
  
−
```
+
: pero la primera forma es más compacta.
−
      if (snapshot.connectionState ==
 
−
          ConnectionState.waiting) {
 
−
        return const Center(
 
−
          child:
 
−
              CircularProgressIndicator(),
 
−
        );
 
−
      }
 
  
−
      final datos =
+
<br />
−
          snapshot.data ??
 
−
              const <String>[];
 
  
−
      if (datos.isEmpty) {
+
===Tipar FutureBuilder===
−
        return const Center(
 
−
          child: Text(
 
−
            'Non hai datos dispoñibles.',
 
−
          ),
 
−
        );
 
−
      }
 
  
−
      return ListView.builder(
+
Es recomendable indicar qué tipo de dato va a devolver el Future.
−
        itemCount: datos.length,
 
−
        itemBuilder:
 
−
            (context, index) {
 
−
          final dato =
 
−
              datos[index];
 
  
−
          return ListTile(
+
: En nuestro caso:
−
            title: Text(dato),
 
−
            leading:
 
−
                const Icon(Icons.add),
 
−
            onTap: () {},
 
−
          );
 
−
        },
 
−
      );
 
−
    },
 
−
  ),
 
−
);
 
−
```
 
  
−
}
+
:<syntaxhighlight lang="dart">
−
}
+
FutureBuilder<List<String>>
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* Para listas dinámicas resulta normalmente más eficiente utilizar '''ListView.builder''' directamente dentro del '''FutureBuilder''' que convertir previamente todos los elementos a una lista de widgets.
+
Gracias a ello Flutter sabe que el snapshot contiene:
  
−
<br />
+
:<syntaxhighlight lang="dart">
 +
AsyncSnapshot<List<String>>
 +
</syntaxhighlight>
  
−
====initialData====
+
Esto evita tener que realizar conversiones de tipos posteriormente.
  
−
* '''initialData''' es opcional.
+
'''Ejemplo:'''
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
 
FutureBuilder<List<String>>(
 
FutureBuilder<List<String>>(
−
future: _futureDatos,
+
future: _obtenerDatosInternet(),
 +
builder: (
 +
context,
 +
AsyncSnapshot<List<String>> snapshot,
 +
) {
 +
// ...
 +
},
 +
)
 +
</syntaxhighlight>
 +
 
 +
Dart también puede inferir el tipo del parámetro '''snapshot''', por lo que podemos escribir una versión más corta:
 +
 
 +
:<syntaxhighlight lang="dart">
 +
FutureBuilder<List<String>>(
 +
future: _obtenerDatosInternet(),
 +
builder: (context, snapshot) {
 +
// snapshot.data es List<String>?
 +
},
 +
)
 +
</syntaxhighlight>
 +
 
 +
: Las dos formas son correctas.
 +
 
 +
<br />
 +
 
 +
===Utilizando initialData===
 +
 
 +
FutureBuilder dispone de un parámetro opcional denominado '''initialData'''.
 +
 
 +
Permite indicar unos datos que estarán disponibles antes de que termine el Future.
 +
 
 +
:<syntaxhighlight lang="dart">
 +
FutureBuilder<List<String>>(
 +
future: _obtenerDatosInternet(),
 
initialData: const [
 
initialData: const [
 
'Inicial 1',
 
'Inicial 1',
Línea 2097: Línea 2098:
 
],
 
],
 
builder: (context, snapshot) {
 
builder: (context, snapshot) {
 +
 
final datos =
 
final datos =
−
snapshot.data ??
+
    snapshot.data ?? const <String>[];
−
const <String>[];
 
  
−
```
 
 
return ListView.builder(
 
return ListView.builder(
 
   itemCount: datos.length,
 
   itemCount: datos.length,
 
   itemBuilder: (context, index) {
 
   itemBuilder: (context, index) {
 
     return ListTile(
 
     return ListTile(
−
       title: Text(
+
       title: Text(datos[index]),
−
        datos[index],
 
−
      ),
 
 
     );
 
     );
 
   },
 
   },
 
);
 
);
−
```
 
  
 
},
 
},
Línea 2118: Línea 2115:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
'''<u>Nota:</u>'''
+
En este caso inicialmente veremos:
  
−
* Si proporcionamos '''initialData''', '''snapshot.data''' puede contener esos datos incluso mientras el Future todavía está ejecutándose.
+
:<syntaxhighlight lang="text">
 +
Inicial 1
 +
Inicial 2
 +
</syntaxhighlight>
  
−
: Por ello no debemos utilizar únicamente '''hasData''' para saber si la operación terminó.
+
Cuando finalice el Future serán sustituidos por:
  
−
: Cuando necesitamos distinguir entre carga y datos definitivos debemos comprobar también '''connectionState'''.
+
:<syntaxhighlight lang="text">
 +
Valor 1
 +
Valor 2
 +
Valor 3
 +
</syntaxhighlight>
 +
 
 +
'''Importante:'''
 +
 
 +
Si utilizamos '''initialData''', el hecho de que:
  
−
<br />
+
:<syntaxhighlight lang="dart">
 +
snapshot.hasData
 +
</syntaxhighlight>
  
−
'''<u>Exercicio proposto:</u>'''
+
: sea verdadero '''no implica que el Future haya terminado'''.
  
−
: Crea un novo arquivo Dart e visualiza unha lista de imaxes.
+
: Puede estar mostrando todavía los datos iniciales.
  
−
: Emula que as URLs se descargan de Internet. O Future debe devolver un:
+
Si necesitamos saber si la operación continúa ejecutándose debemos consultar:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
Future<List<String>>
+
snapshot.connectionState
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: Emprega varias URLs de:
+
<br />
 +
 
 +
===Una consideración importante sobre este ejemplo===
 +
 
 +
El ejemplo anterior es válido para aprender el funcionamiento de '''FutureBuilder''', pero hay un detalle que debemos conocer.
  
−
:<syntaxhighlight lang="text">
+
Estamos creando el Future dentro del método '''build''':
−
https://picsum.photos/200/200
+
 
 +
:<syntaxhighlight lang="dart">
 +
future: _obtenerDatosInternet(),
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: Mentres se carga cada imaxe, mostra un placeholder mediante '''FadeInImage.assetNetwork'''.
+
Cada vez que Flutter ejecute de nuevo '''build''', esta llamada puede generar '''un Future nuevo'''.
  
−
<br />
+
Por tanto, una operación como:
  
−
===Cargando datos desde un archivo JSON===
+
:* Descargar información de Internet.
 +
:* Consultar una base de datos.
 +
:* Leer un archivo grande.
  
−
* Más información:
+
: podría iniciarse de nuevo si el widget se reconstruye.
−
  :* https://docs.flutter.dev/data-and-backend/serialization/json
 
−
  :* https://api.flutter.dev/flutter/services/rootBundle.html
 
  
−
* JSON es uno de los formatos más utilizados para intercambiar información.
+
'''Por ahora:'''
  
−
* Dart incluye '''dart''', que permite trabajar con JSON mediante:
+
No vamos a preocuparnos por este problema.
−
  :* '''jsonDecode'''.
 
−
  :* '''jsonEncode'''.
 
  
−
* Para proyectos pequeños resulta adecuado realizar una serialización manual.
+
Estamos utilizando esta forma porque todavía estamos trabajando con '''StatelessWidget''' y nuestro objetivo en esta unidad es comprender:
 +
:* Qué es un Future.
 +
:* Qué hace FutureBuilder.
 +
:* Cómo mostrar un indicador de carga.
 +
:* Cómo mostrar los datos cuando llegan.
  
−
* Para aplicaciones medianas o grandes se suelen emplear generadores como '''json_serializable''', que estudiaremos posteriormente.
+
'''Más adelante:'''
  
−
<br />
+
Cuando estudiemos '''StatefulWidget''' veremos cómo podemos crear el Future una sola vez y conservarlo mientras exista la pantalla.
  
−
====Archivo JSON====
 
  
−
'''Archivo: assets/data/datos_listas.json'''
 
  
−
:<syntaxhighlight lang="json">
+
<br />
−
{
+
'''<u>Exercicio Proposto:</u>''': Crea un novo arquivo Dart e visualiza unha lista de imaxes. Emula que as <u>url´s</u> das imaxes se descargan de Internet (o Future debe devolver unha lista de cadeas). Emprega a url: https://picsum.photos/200/200.
−
"elementos": [
+
: Emprega o Widget que visualiza unha imaxe local mientras se descarga a imaxe de Internet.
−
{
 
−
"titulo": "Elemento 1",
 
−
"texto": "Aclaración elemento 1",
 
−
"imagen": "https://picsum.photos/id/1/200/300"
 
−
},
 
−
{
 
−
"titulo": "Elemento 2",
 
−
"texto": "Aclaración elemento 2",
 
−
"imagen": "https://picsum.photos/id/2/200/300"
 
−
},
 
−
{
 
−
"titulo": "Elemento 3",
 
−
"texto": "Aclaración elemento 3",
 
−
"imagen": "https://picsum.photos/id/3/200/300"
 
−
}
 
−
]
 
−
}
 
−
</syntaxhighlight>
 
  
−
* Registramos el directorio:
 
  
−
:<syntaxhighlight lang="yaml">
 
−
flutter:
 
−
assets:
 
−
- assets/data/
 
−
- assets/images/
 
−
</syntaxhighlight>
 
  
−
<br />
 
  
−
====Modelo====
 
  
−
'''Archivo: lib/models/elemento_lista_model.dart'''
 
  
−
:<syntaxhighlight lang="dart">
+
<br />
−
class ElementoListaModel {
 
−
const ElementoListaModel({
 
−
required this.titulo,
 
−
required this.texto,
 
−
required this.imagen,
 
−
});
 
  
−
final String titulo;
+
===Cargando datos desde un archivo JSON===
−
final String texto;
 
−
final String imagen;
 
  
−
factory ElementoListaModel.fromJson(
+
* Más información:
−
Map<String, dynamic> json,
+
  :* https://docs.flutter.dev/data-and-backend/serialization/json
−
) {
+
  :* https://api.flutter.dev/flutter/services/rootBundle.html
−
return ElementoListaModel(
 
−
titulo: json['titulo'] as String,
 
−
texto: json['texto'] as String,
 
−
imagen: json['imagen'] as String,
 
−
);
 
−
}
 
−
}
 
−
</syntaxhighlight>
 
  
−
'''<u>Nota:</u>'''
+
* JSON es uno de los formatos más utilizados para intercambiar información.
  
−
* Es preferible llamar al constructor:
+
* Dart incluye '''dart''', que permite trabajar con JSON mediante:
 +
  :* '''jsonDecode'''.
 +
  :* '''jsonEncode'''.
  
−
:<syntaxhighlight lang="dart">
+
* Para proyectos pequeños resulta adecuado realizar una serialización manual.
−
fromJson
 
−
</syntaxhighlight>
 
  
−
: cuando el mapa representa directamente datos obtenidos de JSON.
+
* Para aplicaciones medianas o grandes se suelen emplear generadores como '''json_serializable''', que estudiaremos posteriormente.
  
−
* Los atributos del modelo se han declarado '''final''' porque el objeto no necesita modificarse después de su creación.
+
<br />
  
−
<br />
 
  
−
====Provider para cargar el archivo====
 
  
−
'''Archivo: lib/providers/datos_lista_provider.dart'''
+
<center>
 +
{{#ev:youtube|K4mp-MTjAwM}}
 +
</center> 
  
−
:<syntaxhighlight lang="dart">
 
−
import 'dart';
 
  
−
import 'package/services.dart'
 
−
show rootBundle;
 
  
−
import '../models/elemento_lista_model.dart';
+
<br />
 +
====Archivo JSON====
  
−
class DatosListaProvider {
+
'''Archivo: assets/data/datos_listas.json'''
−
const DatosListaProvider();
 
  
−
Future<List<ElementoListaModel>>
+
:<syntaxhighlight lang="json">
−
cargarDatos() async {
+
{
−
 
+
"elementos": [
−
```
+
{
−
final contenido =
+
"titulo": "Elemento 1",
−
    await rootBundle.loadString(
+
"texto": "Aclaración elemento 1",
−
  'assets/data/datos_listas.json',
+
"imagen": "https://picsum.photos/id/1/200/300"
−
);
+
},
−
 
+
{
−
final json =
+
"titulo": "Elemento 2",
−
    jsonDecode(contenido)
+
"texto": "Aclaración elemento 2",
−
        as Map<String, dynamic>;
+
"imagen": "https://picsum.photos/id/2/200/300"
−
 
+
},
−
final elementos =
+
{
−
    json['elementos']
+
"titulo": "Elemento 3",
−
        as List<dynamic>;
+
"texto": "Aclaración elemento 3",
−
 
+
"imagen": "https://picsum.photos/id/3/200/300"
−
return elementos.map((elemento) {
+
}
−
  return ElementoListaModel.fromJson(
+
]
−
    elemento
+
}
−
        as Map<String, dynamic>,
 
−
  );
 
−
}).toList(growable: false);
 
−
```
 
−
 
 
−
}
 
−
}
 
−
 
 
−
const datosListaProvider =
 
−
DatosListaProvider();
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* Ya no utilizamos:
+
* Registramos el directorio:
  
−
:<syntaxhighlight lang="dart">
+
:<syntaxhighlight lang="yaml">
−
new _DatosListaProvider()
+
flutter:
 +
assets:
 +
- assets/data/
 +
- assets/images/
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* Podemos crear directamente:
+
<br />
 +
 
 +
====Modelo====
 +
 
 +
'''Archivo: lib/models/elemento_lista_model.dart'''
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
const DatosListaProvider()
+
class ElementoListaModel {
−
</syntaxhighlight>
+
  const ElementoListaModel({
 +
    required this.titulo,
 +
    required this.texto,
 +
    required this.imagen,
 +
  });
  
−
* En este ejemplo la clase tampoco necesita ser privada.
+
  final String titulo;
 +
  final String texto;
 +
  final String imagen;
  
−
<br />
+
  factory ElementoListaModel.fromJson(Map<String, dynamic> json) {
 +
    return ElementoListaModel(
 +
      titulo: json['titulo'] as String,
 +
      texto: json['texto'] as String,
 +
      imagen: json['imagen'] as String,
 +
    );
 +
  }
 +
}
 +
 
 +
</syntaxhighlight>
  
−
====Visualizando los datos====
+
'''<u>Nota:</u>'''
  
−
'''Archivo: lib/pages/listas_json_page.dart'''
+
* Es preferible llamar al constructor:
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
fromJson
 +
</syntaxhighlight>
  
−
import '../models/elemento_lista_model.dart';
+
: cuando el mapa representa directamente datos obtenidos de JSON.
−
import '../providers/datos_lista_provider.dart';
 
  
−
class ListasJsonPage
+
* Los atributos del modelo se han declarado '''final''' porque el objeto no necesita modificarse después de su creación.
−
extends StatefulWidget {
 
−
const ListasJsonPage({super.key});
 
  
−
@override
+
<br />
−
State<ListasJsonPage> createState() =>
 
−
_ListasJsonPageState();
 
−
}
 
  
−
class _ListasJsonPageState
+
====Provider para cargar el archivo====
−
extends State<ListasJsonPage> {
 
  
−
late final Future<
+
'''Archivo: lib/providers/datos_lista_provider.dart'''
−
List<ElementoListaModel>> _future;
 
  
−
@override
+
:<syntaxhighlight lang="dart">
−
void initState() {
+
import 'dart:convert' show jsonDecode;
−
super.initState();
+
import 'package:flutter/services.dart' show rootBundle;
 +
import '../models/elemento_lista_model.dart';
  
−
```
+
class DatosListaProvider {
−
_future =
+
  const DatosListaProvider();
−
    datosListaProvider.cargarDatos();
 
−
```
 
  
−
}
+
  Future<List<ElementoListaModel>> cargarDatos() async {
 +
    final contenido = await rootBundle.loadString(
 +
      'assets/data/datos_listas.json',
 +
    );
  
−
@override
+
    final json = jsonDecode(contenido) as Map<String, dynamic>;
−
Widget build(BuildContext context) {
 
−
return Scaffold(
 
−
appBar: AppBar(
 
−
title: const Text(
 
−
'Listas con datos desde JSON',
 
−
),
 
−
),
 
−
body: FutureBuilder<
 
−
List<ElementoListaModel>>(
 
−
future: _future,
 
−
builder: (context, snapshot) {
 
  
−
```
+
    final elementos = json['elementos'] as List<dynamic>;
−
      if (snapshot.hasError) {
 
−
        return Center(
 
−
          child: Text(
 
−
            'Error: ${snapshot.error}',
 
−
          ),
 
−
        );
 
−
      }
 
  
−
      if (snapshot.connectionState ==
+
    return elementos
−
          ConnectionState.waiting) {
+
        .map((elemento) {
−
        return const Center(
+
          return ElementoListaModel.fromJson(elemento as Map<String, dynamic>);
−
          child:
+
        })
−
              CircularProgressIndicator(),
+
         .toList(growable: false);
−
         );
+
  }
−
      }
+
}
  
−
      final datos =
+
const datosListaProvider = DatosListaProvider();
−
          snapshot.data ??
+
</syntaxhighlight>
−
              const <
 
−
                  ElementoListaModel>[];
 
  
−
      if (datos.isEmpty) {
+
* Podemos crear directamente:
−
        return const Center(
 
−
          child: Text(
 
−
            'No hay datos.',
 
−
          ),
 
−
        );
 
−
      }
 
  
−
      return ListView.separated(
+
:<syntaxhighlight lang="dart">
−
        itemCount: datos.length,
+
const DatosListaProvider()
−
        separatorBuilder:
+
</syntaxhighlight>
−
            (context, index) =>
 
−
                const Divider(),
 
−
        itemBuilder:
 
−
            (context, index) {
 
  
−
          final modelo =
+
* En este ejemplo la clase tampoco necesita ser privada.
−
              datos[index];
 
  
−
          return ListTile(
+
<br />
−
            leading:
 
−
                FadeInImage.assetNetwork(
 
−
              placeholder:
 
−
                  'assets/images/cargando.gif',
 
−
              image: modelo.imagen,
 
−
              width: 60,
 
−
              height: 60,
 
−
              fit: BoxFit.cover,
 
−
            ),
 
−
            title:
 
−
                Text(modelo.titulo),
 
−
            subtitle:
 
−
                Text(modelo.texto),
 
−
            onTap: () {},
 
−
          );
 
−
        },
 
−
      );
 
−
    },
 
−
  ),
 
−
);
 
−
```
 
  
−
}
+
====Visualizando los datos====
−
}
 
−
</syntaxhighlight>
 
  
−
'''<u>Nota:</u>'''
+
'''Archivo: lib/screens/listas_json_page.dart'''
  
−
* En aplicaciones con muchos modelos JSON puede resultar conveniente utilizar generación automática de código mediante:
+
:<syntaxhighlight lang="dart">
−
   :* '''json_serializable'''.
+
import 'package:flutter/material.dart';
−
   :* '''build_runner'''.
+
 
 +
import '../models/elemento_lista_model.dart';
 +
import '../providers/datos_lista_provider.dart';
 +
 
 +
class ListasJsonPage extends StatefulWidget {
 +
  const ListasJsonPage({super.key});
 +
 
 +
  @override
 +
  State<ListasJsonPage> createState() => _ListasJsonPageState();
 +
}
 +
 
 +
class _ListasJsonPageState extends State<ListasJsonPage> {
 +
  late final Future<List<ElementoListaModel>> _future;
 +
 
 +
  @override
 +
  void initState() {
 +
    super.initState();
 +
 
 +
    _future = datosListaProvider.cargarDatos();
 +
  }
 +
 
 +
  @override
 +
  Widget build(BuildContext context) {
 +
    return Scaffold(
 +
      appBar: AppBar(title: const Text('Listas con datos desde JSON')),
 +
      body: FutureBuilder<List<ElementoListaModel>>(
 +
        future: _future,
 +
        builder: (context, snapshot) {
 +
          if (snapshot.hasError) {
 +
            return Center(child: Text('Error: ${snapshot.error}'));
 +
          }
 +
 
 +
          if (snapshot.connectionState == ConnectionState.waiting) {
 +
            return const Center(child: CircularProgressIndicator());
 +
          }
 +
 
 +
          final datos = snapshot.data ?? const <ElementoListaModel>[];
 +
 
 +
          if (datos.isEmpty) {
 +
            return const Center(child: Text('No hay datos.'));
 +
          }
 +
 
 +
          return ListView.separated(
 +
            itemCount: datos.length,
 +
            separatorBuilder: (context, index) => const Divider(),
 +
            itemBuilder: (context, index) {
 +
              final modelo = datos[index];
 +
 
 +
              return ListTile(
 +
                leading: FadeInImage.assetNetwork(
 +
                  placeholder: 'assets/images/cargando.gif',
 +
                  image: modelo.imagen,
 +
                  width: 60,
 +
                  height: 60,
 +
                  fit: BoxFit.cover,
 +
                ),
 +
                title: Text(modelo.titulo),
 +
                subtitle: Text(modelo.texto),
 +
                onTap: () {},
 +
              );
 +
            },
 +
          );
 +
        },
 +
      ),
 +
    );
 +
  }
 +
}
 +
 
 +
</syntaxhighlight>
 +
 
 +
'''<u>Nota:</u>'''
 +
 
 +
* En aplicaciones con muchos modelos JSON puede resultar conveniente utilizar generación automática de código mediante:
 +
   :* '''json_serializable'''.
 +
   :* '''build_runner'''.
  
 
: Para estos primeros ejemplos continuaremos realizando la conversión manual para comprender el proceso.
 
: Para estos primeros ejemplos continuaremos realizando la conversión manual para comprender el proceso.
Línea 2457: Línea 2452:
 
* El aspecto final depende del tema de la aplicación, por lo que es preferible aprovechar el '''ThemeData''' en lugar de establecer manualmente colores y elevaciones en todos los widgets.
 
* El aspecto final depende del tema de la aplicación, por lo que es preferible aprovechar el '''ThemeData''' en lugar de establecer manualmente colores y elevaciones en todos los widgets.
  
−
'''Ejemplo'''
+
 
 +
 
 +
<br />
 +
'''Ejemplo'''
 +
 
 +
[[Imagen:Flutter_dart_widget_32.png|300px|center]]
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
  
 
class CardsPage extends StatelessWidget {
 
class CardsPage extends StatelessWidget {
−
const CardsPage({super.key});
+
  const CardsPage({super.key});
 +
 
 +
  @override
 +
  Widget build(BuildContext context) {
 +
    return Scaffold(
 +
      appBar: AppBar(title: const Text('Ejemplo de Cards')),
 +
      body: ListView(
 +
        padding: const EdgeInsets.all(12),
 +
        children: [
 +
          _crearTarjeta1(),
 +
          const SizedBox(height: 12),
 +
          _crearTarjeta2(),
 +
          const SizedBox(height: 12),
 +
          _crearTarjeta3(),
 +
        ],
 +
      ),
 +
    );
 +
  }
 +
 
 +
  Widget _crearTarjeta1() {
 +
    return Card(
 +
      clipBehavior: Clip.antiAlias,
 +
      elevation: 4,
 +
      shape: RoundedRectangleBorder(
 +
        side: const BorderSide(width: 2, color: Colors.red),
 +
        borderRadius: BorderRadius.circular(24),
 +
      ),
 +
      child: Column(
 +
        children: [
 +
          Image.network(
 +
            'https://picsum.photos/700/500',
 +
            width: double.infinity,
 +
            height: 200,
 +
            fit: BoxFit.cover,
 +
          ),
 +
          const Padding(
 +
            padding: EdgeInsets.all(16),
 +
            child: Text(
 +
              'Ejemplo de texto '
 +
              'asociado a una tarjeta',
 +
            ),
 +
          ),
 +
        ],
 +
      ),
 +
    );
 +
  }
 +
 
 +
  Widget _crearTarjeta2() {
 +
    return const Card.filled(
 +
      child: ListTile(
 +
        leading: Icon(Icons.accessibility),
 +
        title: Text('Ejemplo de texto'),
 +
        trailing: Icon(Icons.delete),
 +
      ),
 +
    );
 +
  }
  
−
@override
+
  Widget _crearTarjeta3() {
−
Widget build(BuildContext context) {
+
    return const Card.outlined(
−
return Scaffold(
+
      child: Padding(
−
appBar: AppBar(
+
        padding: EdgeInsets.all(24),
−
title:
+
        child: Icon(Icons.access_alarm, size: 80),
−
const Text('Ejemplo de Cards'),
+
      ),
−
),
+
    );
−
body: ListView(
+
  }
−
padding:
 
−
const EdgeInsets.all(12),
 
−
children: [
 
−
_crearTarjeta1(),
 
−
const SizedBox(height: 12),
 
−
_crearTarjeta2(),
 
−
const SizedBox(height: 12),
 
−
_crearTarjeta3(),
 
−
],
 
−
),
 
−
);
 
 
}
 
}
  
−
Widget _crearTarjeta1() {
+
</syntaxhighlight>
−
return Card(
+
 
−
clipBehavior:
 
−
Clip.antiAlias,
 
−
elevation: 4,
 
−
shape:
 
−
RoundedRectangleBorder(
 
−
side: const BorderSide(
 
−
width: 2,
 
−
color: Colors.red,
 
−
),
 
−
borderRadius:
 
−
BorderRadius.circular(24),
 
−
),
 
−
child: Column(
 
−
children: [
 
−
Image.network(
 
−
'https://picsum.photos/700/500',
 
−
width: double.infinity,
 
−
height: 200,
 
−
fit: BoxFit.cover,
 
−
),
 
−
const Padding(
 
−
padding:
 
−
EdgeInsets.all(16),
 
−
child: Text(
 
−
'Ejemplo de texto '
 
−
'asociado a una tarjeta',
 
−
),
 
−
),
 
−
],
 
−
),
 
−
);
 
−
}
 
−
 
 
−
Widget _crearTarjeta2() {
 
−
return const Card.filled(
 
−
child: ListTile(
 
−
leading: Icon(
 
−
Icons.accessibility,
 
−
),
 
−
title: Text(
 
−
'Ejemplo de texto',
 
−
),
 
−
trailing:
 
−
Icon(Icons.delete),
 
−
),
 
−
);
 
−
}
 
−
 
 
−
Widget _crearTarjeta3() {
 
−
return const Card.outlined(
 
−
child: Padding(
 
−
padding: EdgeInsets.all(24),
 
−
child: Icon(
 
−
Icons.access_alarm,
 
−
size: 80,
 
−
),
 
−
),
 
−
);
 
−
}
 
−
}
 
−
</syntaxhighlight>
 
−
 
 
 
* Para recortar otros widgets disponemos, entre otros, de:
 
* Para recortar otros widgets disponemos, entre otros, de:
 
   :* '''ClipRect'''.
 
   :* '''ClipRect'''.
Línea 2572: Línea 2554:
 
   :* '''foregroundColor'''.
 
   :* '''foregroundColor'''.
  
−
'''Ejemplo actualizado'''
+
'''Ejemplo'''
 +
 
 +
 
 +
[[Imagen:Flutter_dart_widget_33.png|300px|center]]
  
 
:<syntaxhighlight lang="dart">
 
:<syntaxhighlight lang="dart">
−
import 'package/material.dart';
+
import 'package:flutter/material.dart';
  
−
class CircleAvatarPage
+
class CircleAvatarPage extends StatelessWidget {
−
extends StatelessWidget {
+
  const CircleAvatarPage({super.key});
−
const CircleAvatarPage({super.key});
 
  
−
static const usuarios =
+
  static const usuarios = <String, String>{
−
<String, String>{
+
    'Luisa Sanz': 'https://randomuser.me/api/portraits/women/1.jpg',
−
'Luisa Sanz':
+
    'Ana Pérez': 'https://randomuser.me/api/portraits/women/2.jpg',
−
'https://randomuser.me/api/portraits/women/1.jpg',
+
    'Miguel López': 'https://randomuser.me/api/portraits/men/3.jpg',
−
'Ana Pérez':
+
    'Carmen San Juan': 'https://randomuser.me/api/portraits/women/4.jpg',
−
'https://randomuser.me/api/portraits/women/2.jpg',
+
  };
−
'Miguel López':
 
−
'https://randomuser.me/api/portraits/men/3.jpg',
 
−
'Carmen San Juan':
 
−
'https://randomuser.me/api/portraits/women/4.jpg',
 
−
};
 
  
−
@override
+
  @override
−
Widget build(BuildContext context) {
+
  Widget build(BuildContext context) {
−
final entradas =
+
    final entradas = usuarios.entries.toList();
−
usuarios.entries.toList();
 
  
−
```
+
    return Scaffold(
−
return Scaffold(
+
      appBar: AppBar(title: const Text('Ejemplo de CircleAvatar')),
−
  appBar: AppBar(
+
      body: ListView.separated(
−
    title: const Text(
+
        itemCount: entradas.length,
−
      'Ejemplo de CircleAvatar',
+
        separatorBuilder: (context, index) => const Divider(),
−
    ),
+
        itemBuilder: (context, index) {
−
  ),
+
          final usuario = entradas[index];
−
  body: ListView.separated(
 
−
    itemCount: entradas.length,
 
−
    separatorBuilder:
 
−
        (context, index) =>
 
−
            const Divider(),
 
−
    itemBuilder: (context, index) {
 
−
      final usuario =
 
−
          entradas[index];
 
  
−
      return ListTile(
+
          return ListTile(
−
        leading: CircleAvatar(
+
            leading: CircleAvatar(backgroundImage: NetworkImage(usuario.value)),
−
          backgroundImage:
+
            title: Text(usuario.key),
−
              NetworkImage(
+
            trailing: CircleAvatar(child: Text(_iniciales(usuario.key))),
−
            usuario.value,
+
            onTap: () {},
−
          ),
+
          );
−
        ),
+
        },
−
        title:
+
      ),
−
            Text(usuario.key),
+
    );
−
        trailing: CircleAvatar(
+
  }
−
          child: Text(
 
−
            _iniciales(
 
−
                usuario.key),
 
−
          ),
 
−
        ),
 
−
        onTap: () {},
 
−
      );
 
−
    },
 
−
  ),
 
−
);
 
−
```
 
  
−
}
+
  String _iniciales(String nombre) {
 +
    final partes = nombre.trim().split(RegExp(r'\s+')); // Devuelve en forma de lista el nombre y apellidos quitando espacios en blanco
  
−
String _iniciales(String nombre) {
+
    if (partes.isEmpty) {
−
final partes = nombre
+
      return '';
−
.trim()
+
    }
−
.split(RegExp(r'\s+'));
 
  
−
```
+
    if (partes.length == 1) {
−
if (partes.isEmpty) {
+
      return partes.first.substring(0, 1).toUpperCase();
−
  return '';
+
    }
−
}
 
  
−
if (partes.length == 1) {
+
    return (partes.first.substring(0, 1) + partes.last.substring(0, 1))
−
  return partes.first
+
        .toUpperCase();
−
      .substring(0, 1)
+
  }
−
      .toUpperCase();
 
 
}
 
}
  
−
return (
 
−
  partes.first.substring(0, 1) +
 
−
  partes.last.substring(0, 1)
 
−
).toUpperCase();
 
−
```
 
−
 
−
}
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 2675: Línea 2624:
 
<br />
 
<br />
  
−
==Resumen de cambios respecto a versiones antiguas==
+
==Ejercicios propuestos sin solución==
  
−
* En código Flutter/Dart moderno debemos prestar especial atención a los siguientes cambios:
+
* '''Ejercicio 1'''
  
−
:* Utilizar '''const''' cuando sea posible.
+
:<syntaxhighlight lang="dart">
−
:* Utilizar '''super.key''' en lugar de:
+
final datosExercicio6 = [1, 2, 3, 4, 5, 6];
−
::<syntaxhighlight lang="dart">
 
−
Constructor({Key? key}) : super(key: key);
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
:* Evitar '''new'''.
+
: Crea unha lista onde cada elemento mostre:
−
:* Utilizar '''WidgetStateProperty''' en lugar del obsoleto '''MaterialStateProperty'''.
+
 
−
:* No utilizar el obsoleto '''window''' de '''dart'''.
+
:<syntaxhighlight lang="text">
−
:* Utilizar '''MediaQuery''' o '''View.of(context)''' para acceder a información de la vista.
+
Número: 1 → Resultado: 10
−
:* '''flutter_html''' ya tiene una versión estable compatible con Dart 3.
 
−
:* '''url_launcher''' utiliza '''Uri''' y '''launchUrl'''.
 
−
:* En Material 3 debemos conocer también '''FilledButton''' y '''FilledButton.tonal'''.
 
−
:* Para listas largas utilizar '''ListView.builder''' o '''ListView.separated'''.
 
−
:* En modelos provenientes de JSON es conveniente realizar conversiones explícitas con '''as'''.
 
−
:* Cuando un JSON pueda contener tanto enteros como decimales, utilizar:
 
−
::<syntaxhighlight lang="dart">
 
−
(valor as num).toDouble()
 
 
</syntaxhighlight>
 
</syntaxhighlight>
−
:* Tipar los '''FutureBuilder''':
+
 
−
::<syntaxhighlight lang="dart">
+
: Cada elemento debe estar dentro de un '''Card''' y separado empleando un '''Padding'''.
−
FutureBuilder<List<MiModelo>>
+
 
 +
<br />
 +
 
 +
* '''Ejercicio 2'''
 +
 
 +
:<syntaxhighlight lang="dart">
 +
final datosExercicio7 = <String>[
 +
'Lugo',
 +
'Ourense',
 +
'A Coruña',
 +
'Pontevedra',
 +
];
 
</syntaxhighlight>
 
</syntaxhighlight>
−
:* No crear repetidamente un Future costoso dentro de '''build'''. Si la operación debe ejecutarse una sola vez, inicializar el Future previamente, por ejemplo en '''initState'''.
+
 
−
:* Para depuración en Flutter es preferible '''debugPrint''' frente a utilizar indiscriminadamente '''print'''.
+
: Crea una lista de elementos formados por:
 +
:* Un '''CircleAvatar'''.
 +
:* El nombre de la ciudad debajo.
 +
:* Un '''Column''' para agruparlos.
 +
 
 +
<br />
 +
 
 +
* '''Ejercicio 3'''
 +
 
 +
:<syntaxhighlight lang="dart">
 +
final datosExercicio8 =
 +
<Map<String, dynamic>>[
 +
{
 +
'titulo': 'Aprender Flutter',
 +
'autor': 'Ana',
 +
},
 +
{
 +
'titulo': 'Widgets e Layouts',
 +
'autor': 'Brais',
 +
},
 +
{
 +
'titulo': 'Listas e Datos',
 +
'autor': 'Clara',
 +
},
 +
{
 +
'titulo': 'Eventos e Accións',
 +
'autor': 'Diego',
 +
},
 +
];
 +
</syntaxhighlight>
 +
 
 +
: Crea una clase '''Libro''' con:
 +
:* Atributos '''final'''.
 +
:* Constructor con parámetros obligatorios y por nombre.
 +
:* Constructor '''factory Libro.fromMap'''.
 +
 
 +
: Convierte los mapas en objetos '''Libro''' y visualízalos empleando '''ListTile'''.
 +
 
 +
<br />
 +
 
 +
* '''Ejercicio 4'''
 +
 
 +
:<syntaxhighlight lang="dart">
 +
final datosExercicio9 =
 +
<Map<String, dynamic>>[
 +
{
 +
'nome': 'Ana',
 +
'idade': 25,
 +
'foto':
 +
'https://randomuser.me/api/portraits/women/1.jpg',
 +
},
 +
{
 +
'nome': 'Brais',
 +
'idade': 30,
 +
'foto':
 +
'https://randomuser.me/api/portraits/men/2.jpg',
 +
},
 +
];
 +
</syntaxhighlight>
 +
 
 +
: Crea una clase '''Persona''' y representa los datos utilizando '''Card''' y un '''CircleAvatar'''.
 +
 
 +
<br />
 +
 
 +
* '''Ejercicio 5'''
 +
 
 +
:<syntaxhighlight lang="dart">
 +
final datosExercicio10 =
 +
<Map<String, dynamic>>[
 +
{
 +
'produto': 'Camiseta',
 +
'prezo': 19.99,
 +
'foto':
 +
'https://picsum.photos/100?1',
 +
},
 +
{
 +
'produto': 'Pantalón',
 +
'prezo': 29.99,
 +
'foto':
 +
'https://picsum.photos/100?2',
 +
},
 +
];
 +
</syntaxhighlight>
 +
 
 +
: Crea una clase '''Producto''' y una lista de '''ListTile''' con:
 +
:* Imagen en el '''leading'''.
 +
:* Nombre en el '''title'''.
 +
:* Precio en el '''subtitle'''.
 +
:* '''IconButton''' con una cesta en el '''trailing'''.
 +
 
 +
<br />
 +
 
 +
* '''Ejercicio 6'''
 +
 
 +
: Define una clase '''ProductoFitbit''' donde '''prezo''' y '''valoracion''' sean '''double'''.
 +
: En el método '''fromMap''', recuerda que un valor JSON puede llegar o no o puede tener el tipo previsto o no.
 +
 
 +
 
 +
: Si la valoración es mayor o igual a 4.5:
 +
:* Mostrar '''Icons.star'''.
 +
:* Mostrar el precio en verde.
 +
 
 +
: En caso contrario:
 +
:* Mostrar '''Icons.star_border'''.
 +
:* Mostrar el precio en rojo.
 +
 
 +
 
  
  

Revisión actual del 07:32 14 oct 2026

Introducción

  • En esta sección vamos a ver algunos de los principales componentes gráficos (widgets) que podemos necesitar para diseñar una pantalla en Flutter:
 :* Textos.
 :* Botones.
 :* Imágenes.
 :* Listas.
 :* Card (tarjetas).
 :* Combos (dropdown).
  • Los ejemplos de esta sección están actualizados para Flutter 3.47 / Dart 3.13.
  • Partimos de lo aprendido en las secciones anteriores, donde tenemos un widget MaterialApp que contiene las diferentes pantallas de nuestra aplicación.
Cada pantalla puede devolver, por ejemplo, un widget Scaffold con los elementos que queramos visualizar.
  • En Dart moderno es recomendable:
 :* Utilizar const siempre que el objeto pueda construirse en tiempo de compilación.
 :* Emplear super.key en los constructores de Widgets.
 :* No utilizar la palabra reservada new. Sigue siendo válida, pero es innecesaria desde Dart 2 y no se utiliza habitualmente en código moderno.
 :* Especificar los tipos siempre que ayuden a mejorar la legibilidad y la seguridad del código.
  • Aún no sabemos cómo colocar los widgets en pantalla. En esta sección aparecerán algunos widgets de diseño, como Column, Row, Padding o SizedBox, que serán explicados con mayor profundidad en la siguiente sección.
  • Cuando ya estéis habituados al uso de widgets, es recomendable consultar la lista Widget of the Week, donde se explica visualmente el funcionamiento de numerosos widgets.


Widget Text

  • Más información:
 :* https://api.flutter.dev/flutter/widgets/Text-class.html
  • Creamos una nueva página en la carpeta pages llamada textos_page.dart.
En nuestros ejemplos:
  • Importaremos package/material.dart.
  • Crearemos una clase que derive de StatelessWidget.
  • Implementaremos el método build.
  • El método build devolverá el árbol de widgets que representa la interfaz.


Textos con un estilo único

  • El widget Text permite visualizar texto.
  • Algunas de sus propiedades más importantes son:
 :* style: permite definir el aspecto del texto mediante un objeto TextStyle.
 :* overflow: determina qué ocurre cuando el contenido no cabe en el espacio disponible. Utiliza valores de TextOverflow, como ellipsis, clip o fade.
 :* maxLines: número máximo de líneas que se pueden visualizar.
 :* textAlign: alineación horizontal de las líneas del texto.
 :* softWrap: indica si el texto puede dividirse automáticamente en varias líneas.

Archivo: textos_page.dart

import 'package:flutter/material.dart';

class TextosPage extends StatelessWidget {
  const TextosPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Textos con diferentes estilos')),
      body: const Column(
        children: [
          Text(
            'Texto con los valores por defecto. '
            'Si no cabe en el espacio disponible, puede pasar '
            'a la siguiente línea.',
          ),
          Text(
            'Texto centrado, rojo y en negrilla.\n'
            'Puede ocupar varias líneas.',
            textAlign: TextAlign.center,
            style: TextStyle(
              color: Colors.red,
              fontWeight: FontWeight.bold,
              fontSize: 16,
            ),
          ),
        ],
      ),
    );
  }
}
Flutter dart widget 13.JPG
  • La propiedad style espera recibir un objeto de tipo TextStyle.
  • Si queremos reutilizar el mismo estilo podemos almacenarlo en una constante:
import 'package:flutter/material.dart';

class TextosPage extends StatelessWidget {
  const TextosPage({super.key});

  static const TextStyle estiloTexto = TextStyle(
    fontSize: 35.3,
    backgroundColor: Colors.red,
    color: Colors.black54,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Textos con diferentes estilos')),
      body: const Column(
        children: [
          Text('Texto con el estilo por defecto'),
          Text('Texto con un estilo reutilizable', style: estiloTexto),
          Text('Otro texto con el mismo estilo', style: estiloTexto),
        ],
      ),
    );
  }
}

NOTA IMPORTANTE:

  • Un StatelessWidget representa una configuración inmutable.
Por este motivo, sus atributos deberían ser final y, cuando sea posible, const o static const.
Esto no significa que un StatelessWidget nunca pueda volver a dibujarse. Su método build puede ejecutarse de nuevo cuando cambia un widget padre o alguna dependencia del contexto.
Lo que no podemos hacer es modificar directamente el estado interno del propio StatelessWidget para provocar el cambio de la interfaz. Para datos que cambian durante la vida del widget estudiaremos posteriormente StatefulWidget y otras técnicas de gestión de estado.


DefaultTextStyle

  • Más información:
 :* https://api.flutter.dev/flutter/widgets/DefaultTextStyle-class.html
  • DefaultTextStyle permite proporcionar un estilo por defecto a los widgets Text que se encuentren debajo de él en el árbol de widgets.
import 'package:flutter/material.dart';

class TextosPage extends StatelessWidget {
  const TextosPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('DefaultTextStyle')),
      body: const DefaultTextStyle(
        style: TextStyle(
          fontSize: 20,
          backgroundColor: Colors.amber,
          color: Colors.blue,
          fontStyle: FontStyle.italic,
        ),
        child: Column(
          children: [
            Text('Este texto utiliza el DefaultTextStyle'),
            Text(
              'Este texto modifica algunas propiedades',
              style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
    );
  }
}
Flutter dart widget 14.JPG

Importante:

  • Si un Text tiene un TextStyle propio, por defecto ese estilo se combina con el DefaultTextStyle.
Esto ocurre porque la propiedad inherit de TextStyle vale true por defecto.
Por ejemplo, si el DefaultTextStyle define un tamaño de 20 y un Text sólo especifica:
style: const TextStyle(
color: Colors.red,
)
el texto conservará el tamaño heredado y cambiará únicamente el color.
  • También podemos utilizar DefaultTextStyle.merge para modificar parcialmente el estilo existente:
DefaultTextStyle(
      style: const TextStyle(fontSize: 20, color: Colors.blue),
      child: Column(
        children: [
          const Text('Texto con el estilo por defecto'),
          DefaultTextStyle.merge(
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            child: const Text('Texto modificado'),
          ),
        ],
      ),
    )


Text.rich

  • Más información:
 :* https://api.flutter.dev/flutter/widgets/RichText-class.html
 :* https://api.flutter.dev/flutter/painting/TextSpan-class.html
  • Cuando diferentes fragmentos de una misma frase necesitan estilos distintos podemos utilizar TextSpan.
  • Una opción sencilla es utilizar el constructor Text.rich:
    const Text.rich(
      TextSpan(
        text: 'Este texto es verde',
        style: TextStyle(color: Colors.green, fontSize: 20),
        children: [
          TextSpan(
            text: ' y esta parte es marrón',
            style: TextStyle(color: Colors.brown),
          ),
          TextSpan(
            text: ' y esta amarilla',
            style: TextStyle(color: Colors.yellow),
          ),
          TextSpan(text: ' y esta parte hereda el estilo del TextSpan padre'),
        ],
      ),
    )



HTML en el interior de textos

  • Flutter no interpreta HTML automáticamente dentro de un widget Text.
  • Para representar HTML podemos emplear paquetes externos.
Una opción es:
o


  • Actualmente flutter_html 3.x es una versión estable compatible con Dart 3.
  • Podemos instalar los paquetes necesarios desde el terminal (recordar que tenéis que saber hacerlo manualmente editando el pubspec.yaml y debéis comprobar si la versión es compatible con la versión de Dart instalada):
flutter pub add flutter_widget_from_html
flutter pub add url_launcher
De esta forma Flutter añadirá automáticamente versiones compatibles al apartado dependencies de pubspec.yaml.
El segundo paquete permite 'lanzar' el navegador web en base a una url dada.

Nota:

  • Los paquetes que utiliza la aplicación en ejecución deben estar en dependencies.
  • dev_dependencies se utiliza para herramientas necesarias durante el desarrollo, pruebas o generación de código, pero que no forman parte directamente de la aplicación en ejecución.

Ejemplo

import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:url_launcher/url_launcher.dart';

class TextosPage extends StatelessWidget {
  const TextosPage({super.key});

  Future<void> _lanzarUrl(String url) async {
    final uri = Uri.parse(url);

    final lanzada = await launchUrl(uri);

    if (!lanzada) {
      throw Exception('No se pudo abrir $url');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('HTML')),
      body: HtmlWidget(
         '''
            <b>Ejemplo de etiqueta HTML</b>
            <br>
            <a href="https://flutter.dev">Página de Flutter</a>
            ''',
        onTapUrl: ((url) {
          _lanzarUrl(url);
          return true;
        }),
      ),
    );
  }
}
  • url_launcher trabaja actualmente con objetos Uri mediante la función launchUrl.


Botones

  • Más información:
 :* https://api.flutter.dev/flutter/material/ButtonStyle-class.html
  • Material 3 dispone de cinco tipos principales de botones:
 :* FilledButton.
 :* FilledButton.tonal.
 :* ElevatedButton.
 :* OutlinedButton.
 :* TextButton.
Además disponemos de botones especializados como IconButton y FloatingActionButton.


FloatingActionButton

  • Más información:
 :* https://api.flutter.dev/flutter/material/FloatingActionButton-class.html
  • Un FloatingActionButton (FAB) se utiliza normalmente para representar una acción importante o frecuente relacionada con la pantalla actual.
Por ejemplo, en una pantalla de contactos puede representar la acción de añadir un nuevo contacto.
  • Scaffold posee el parámetro floatingActionButton:
import 'package:flutter/material.dart';

class BotonesPage extends StatelessWidget {
  const BotonesPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('FloatingActionButton')),
      body: const Center(child: Text('Ejemplo de FAB')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          debugPrint('Botón pulsado');
        },
        tooltip: 'Añadir',
        child: const Icon(Icons.add),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
    );
  }
}
  • onPressed espera una función:
 :* () {} habilita el botón aunque no haga nada.
 :* null deshabilita el botón.
  • Para mensajes de depuración es preferible emplear debugPrint en lugar de utilizar indiscriminadamente print.
  • El parámetro child contiene normalmente un Icon.
  • Podemos emplear floatingActionButtonLocation para cambiar la posición del FAB.


Varios FloatingActionButton

  • Aunque Material Design suele reservar el FAB para una acción principal, técnicamente podemos mostrar varios.
  • Si existen varios FAB en el mismo subárbol, deben tener un heroTag diferente
Un heroTag permite asociar a cada widget FAB un identificador que usan las animaciones para diferenciar un widget de otro:
      floatingActionButton: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            heroTag: 'fabAdd',
            onPressed: () {
              debugPrint('Añadir');
            },
            tooltip: 'Añadir',
            child: const Icon(Icons.add),
          ),
          const SizedBox(width: 12),
          FloatingActionButton(
            heroTag: 'fabDelete',
            onPressed: () {
              debugPrint('Eliminar');
            },
            tooltip: 'Eliminar',
            child: const Icon(Icons.delete),
          ),
  • Los FAB utilizan internamente animaciones Hero. Los identificadores permiten diferenciar los distintos widgets.


FloatingActionButton.extended

  • Si queremos mostrar conjuntamente un icono y un texto podemos utilizar FloatingActionButton.extended:
          FloatingActionButton.extended(
            onPressed: () {
              debugPrint('Añadir');
            },
            icon: const Icon(Icons.add),
            label: const Text('Añadir'),
            tooltip: 'Añadir elemento',
          ),



Flutter dart widget 17.JPG


TextButton

  • Más información:
 :* https://api.flutter.dev/flutter/material/TextButton-class.html
  • TextButton es un botón sin borde ni fondo destacado.
Se utiliza normalmente en:
  • Cuadros de diálogo.
  • Barras de herramientas.
  • Acciones de menor énfasis.
  • Acciones situadas junto a otros contenidos.
      body: Row(
        children: [
          TextButton(onPressed: () {}, child: const Text('Aceptar')),
          TextButton(onPressed: null, child: const Text('Deshabilitado')),
        ],
      ),
  • Para modificar fácilmente el estilo podemos utilizar TextButton.styleFrom:
          TextButton(
            onPressed: () {},
            style: TextButton.styleFrom(
              foregroundColor: Colors.white,
              backgroundColor: Colors.blue,
            ),
            child: const Text('Púlsame'),
          ),




ButtonStyle y WidgetStateProperty

  • Para configuraciones más avanzadas se puede utilizar ButtonStyle.
  • En las versiones actuales de Flutter se utiliza WidgetStateProperty.
El nombre antiguo MaterialStateProperty está obsoleto.
          TextButton(
            onPressed: () {},
            style: ButtonStyle(
              backgroundColor: WidgetStateProperty.all(Colors.blue),
              foregroundColor: WidgetStateProperty.all(Colors.white),
            ),
            child: const Text('PÚLSAME'),
          ),
  • El uso de WidgetStateProperty permite que una propiedad tenga valores diferentes dependiendo del estado del widget:
 :* pressed.
 :* hovered.
 :* focused.
 :* disabled.
 :* selected.
 :* etc.


IconButton

  • Más información:
 :* https://api.flutter.dev/flutter/material/IconButton-class.html
  • IconButton permite que un icono responda a una pulsación.
  • Es muy habitual utilizarlo, por ejemplo, en un AppBar:
      appBar: AppBar(
        title: const Text('Ejemplo'),
        actions: [
          IconButton(
            onPressed: () {
              debugPrint('Buscar');
            },
            tooltip: 'Buscar',
            icon: const Icon(Icons.search),
          ),
        ],
      ),
  • También puede utilizarse en cualquier otra parte de la interfaz:
          IconButton(
            onPressed: () {},
            iconSize: 40,
            color: Colors.blue,
            tooltip: 'Árbol de elementos',
            icon: const Icon(Icons.account_tree),
          ),


Otros botones de Material 3

  • Más información:
 :* https://api.flutter.dev/flutter/material/FilledButton-class.html
 :* https://api.flutter.dev/flutter/material/ElevatedButton-class.html
 :* https://api.flutter.dev/flutter/material/OutlinedButton-class.html
  • FilledButton: botón de alto énfasis. Es apropiado para acciones importantes como guardar, confirmar o continuar.
FilledButton(
onPressed: () {},
child: const Text('Guardar'),
)
  • FilledButton.tonal: variante de menor énfasis visual:
FilledButton.tonal(
onPressed: () {},
child: const Text('Detalles'),
)
  • ElevatedButton: botón con elevación. En Material 3 ya no debe interpretarse simplemente como el antiguo botón principal con fondo de color.
ElevatedButton(
onPressed: () {},
child: const Text('Elevated'),
)
  • OutlinedButton: botón con borde exterior y sin relleno:
OutlinedButton(
onPressed: () {},
child: const Text('Cancelar'),
)


Imágenes

  • Más información:
 :* https://api.flutter.dev/flutter/widgets/Image-class.html
 :* https://docs.flutter.dev/ui/assets/assets-and-images
  • Flutter puede trabajar, entre otros, con JPEG, WebP, GIF, GIF/WebP animados, PNG, BMP y WBMP.
  • Las imágenes pueden proceder:
 :* De Internet.
 :* De los assets de la aplicación.
 :* De memoria.
 :* De un archivo del dispositivo, utilizando las APIs adecuadas.


Cargando imágenes desde Internet

  • La forma más sencilla es utilizar Image.network:
Image.network(
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
)
  • También podemos utilizar un NetworkImage:
const Image(
image: NetworkImage(
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
),
height: 100,
)
  • La diferencia principal es:
 :* Image.network devuelve directamente un widget.
 :* NetworkImage es un ImageProvider que proporciona los datos de la imagen y debe ser utilizado por un widget que pueda representarlos.


Cargando imágenes desde assets

  • Los recursos propios de Flutter deben registrarse normalmente en pubspec.yaml.
  • Es habitual crear un directorio:
assets/
images/
data/
El nombre assets es una convención, no una obligación.
  • Por ejemplo:
flutter:
assets:
- assets/images/
- assets/data/

IMPORTANTE:

  • YAML utiliza la indentación para definir la estructura.
  • Para evitar problemas entre sistemas operativos es recomendable utilizar nombres de archivo:
 :* En minúsculas.
 :* Sin espacios.
 :* Con guiones bajos cuando sea necesario.
Ejemplo:
imagen_usuario.png


  • Normalmente se recomienda crear un directorio con el nombre de assets en el directorio raíz del proyecto Flutter para guardar los recursos locales.
Dentro de dicho directorio podremos crear las carpetas y añadir los recursos de cualquier tipo (audio, imágenes, vídeos, iconos,...) que queramos usar y dichos recursos estarán disponibles para cualquiera de las plataformas sobre las que generemos la aplicación (IOS, Android, PC, Web...)



  • Vamos a descargar una imagen cualquiera de Internet y vamos a guardarla en un directorio /assets/imagenes/ del proyecto Flutter.
En el ejemplo, yo he descargado esta imagen: https://commons.wikimedia.org/wiki/File:El_sol.jpg (Jazalex, CC BY-SA 3.0 <https://creativecommons.org/licenses/by-sa/3.0>, via Wikimedia Commons)


IMPORTANTE: Aunque no es obligatorio, por motivos de compatibilidad con las diferentes plataformas, los nombres de los recursos guardados en assets deberán estar con minúsculas y cada palabra del nombre unida con un guión bajo.


  • Una vez descargada es necesario 'registrarla' en el proyecto.
Para ello es necesario editar el archivo pubspec.yaml y buscar la entrada que pone assets.
Dicha entrada estará comentada. Será necesario descomentarla y debemos de dejarla tabulada en la posición que queda después de descomentar la línea (a dos espacios en blanco del lado izquierdo).



  • Para cargar la imagen localmente podemos hacer uso del constructor Image.asset el cual lleva como parámetro la ruta al recurso asset que queremos cargar.


Image.asset(
'assets/images/sol.jpg',
)
  • Un ejemplo completo:
import 'package:flutter/material.dart';

class ImagenesPage extends StatelessWidget {
  const ImagenesPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Ejemplos de imágenes')),
      body: Column(
        children: [
          Image.network(
            'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
            height: 150,
          ),
          Image.asset('assets/images/sol.jpg'),
        ],
      ),
    );
  }
}


Escalando las imágenes

  • Más información:
 :* https://api.flutter.dev/flutter/painting/BoxFit.html
  • Los widgets Image.asset e Image.network disponen del parámetro fit.
  • Algunos valores habituales son:
 :* BoxFit.contain: muestra toda la imagen sin deformarla. Puede quedar espacio libre.
 :* BoxFit.cover: cubre todo el espacio disponible sin deformar la imagen. Puede recortar una parte.
 :* BoxFit.fill: ocupa todo el espacio, pudiendo deformar la imagen.
 :* BoxFit.fitWidth.
 :* BoxFit.fitHeight.
import 'package:flutter/material.dart';

class ImagenesPage extends StatelessWidget {
  const ImagenesPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Ejemplos de imágenes')),
      body: Column(
        children: [
          Container(
            color: Colors.amber,
            width: 300,
            height: 400,
            child: Image.network(
            'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
            fit: BoxFit.fill,
            ),
          )
        ],
      ),
    );
  }
}



  • También podemos utilizar FittedBox para ajustar cualquier widget a las restricciones de su contenedor.
Por ejemplo, imaginar que tenemos un texto que crece en tamaño hasta desbordar el tamaño de su contenedor.
Si lo rodeamos de un FittedBox podemos indicarle que se adapte al contenedor y disminuya su tamaño...
Container(
  width: 150,
  height: 80,
  color: Colors.blueGrey,
  child: FittedBox(
    fit: BoxFit.contain, // Ajusta el contenido manteniendo la relación de aspecto
    child: Text(
      '999,999,999', // Texto enorme que no cabría en 150px de ancho
      style: TextStyle(
        fontSize: 100.0, // Tamaño base muy grande
        fontWeight: FontWeight.bold,
        color: Colors.white,
      ),
    ),
  ),
)



Resolución en imágenes

  • Más información:
 :* https://api.flutter.dev/flutter/dart-ui/FlutterView/devicePixelRatio.html
 :* https://docs.flutter.dev/ui/assets/assets-and-images
  • Flutter trabaja normalmente con píxeles lógicos.
  • El devicePixelRatio indica cuántos píxeles físicos corresponden a un píxel lógico.
Por ejemplo, si:
devicePixelRatio = 3
un tamaño de 100 píxeles lógicos puede ocupar 300 píxeles físicos.
  • En código de Widgets no debemos utilizar ya:
window.devicePixelRatio
porque el objeto global window está obsoleto.
  • Podemos obtener los datos mediante MediaQuery:
final size = MediaQuery.sizeOf(context);
final devicePixelRatio =
MediaQuery.devicePixelRatioOf(context);

debugPrint('Ancho lógico: ${size.width}');
debugPrint('DevicePixelRatio: $devicePixelRatio');
debugPrint(
'Ancho físico aproximado: '
'${size.width * devicePixelRatio}',
);
  • También podemos acceder a la vista correspondiente al contexto:
final view = View.of(context);
final devicePixelRatio = view.devicePixelRatio;
Para widgets suele ser preferible MediaQuery, porque permite que la interfaz vuelva a construirse automáticamente si cambian las métricas necesarias.


Imágenes adaptadas a la densidad

  • Flutter puede seleccionar automáticamente una variante de una imagen dependiendo del devicePixelRatio.
Este parámetro indica cuantos píxeles físicos se encuentran dentro de un pixel lógico y viene a ser equivalente al 'pixel independiente de la densidad' de Android.
Este dato lo obtiene a partir de la densidad del dispositivo.
Flutter dart widget 23.JPG
Imagen obtenida de este sitio web
Por lo tanto, cuando en DART damos un tamaño a un Widget, este tamaño va a variar en función del 'Pixel Ratio Device' y será más grande o más pequeño en función de la densidad del dispositivo.


Ejemplo:
assets/images/logo.png
assets/images/1.5x/logo.png
assets/images/2.0x/logo.png
assets/images/3.0x/logo.png
assets/images/4.0x/logo.png
  • logo.png se considera la imagen base de resolución 1.0x.
  • Por ejemplo:
 :* Un dispositivo con ratio 1.8 utilizará normalmente la variante 2.0x.
 :* Uno con ratio 2.7 utilizará normalmente la variante 3.0x.
  • Sólo tenemos que registrar la imagen principal o el directorio correspondiente en pubspec.yaml. Flutter detecta automáticamente las variantes de resolución.
flutter:
assets:
- assets/images/

Ejemplo actualizado

import 'package:flutter/material.dart';

class ImagenesPage extends StatelessWidget {
  const ImagenesPage({super.key});

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.sizeOf(context);
    final pixelRatio = MediaQuery.devicePixelRatioOf(context);

    debugPrint('DevicePixelRatio: $pixelRatio');

    debugPrint(
      'Ancho físico aproximado: '
      '${size.width * pixelRatio}px',
    );

    return Scaffold(
      appBar: AppBar(title: const Text('Ejemplos de imágenes')),
      body: Column(
        children: [
          SizedBox(
            height: 250,
            width: double.infinity,
            child: Image.asset('assets/images/galaxia.jpg', fit: BoxFit.cover),
          ),
          const SizedBox(height: 100, width: 150),
        ],
      ),
    );
  }
}


Esperando a descargar una imagen

  • Cuando una imagen procede de Internet puede tardar cierto tiempo en descargarse.
  • FadeInImage permite mostrar una imagen temporal mientras se obtiene la imagen remota.
  • Más información:
 :* https://api.flutter.dev/flutter/widgets/FadeInImage-class.html
FadeInImage(
              placeholder: const AssetImage('assets/images/cargando.gif'),
              image: const NetworkImage(
                'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
              ),
              fadeInDuration: const Duration(milliseconds: 500),
              width: 200,
              height: 200,
              fit: BoxFit.cover,
              placeholderFit: BoxFit.contain,
            )
  • placeholder e image esperan un ImageProvider, por eso podemos utilizar AssetImage y NetworkImage.
  • También tenemos la opción de usar FadeInImage.assetNetwork, que resulta más sencillo cuando el placeholder es un asset:
FadeInImage.assetNetwork(
   placeholder: 'assets/images/cargando.gif',
   image: 'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
   width: 200,
   height: 200,
   fit: BoxFit.cover,
   placeholderFit: BoxFit.contain,
)


ListView

  • Más información:
 :* https://api.flutter.dev/flutter/widgets/ListView-class.html
  • ListView permite visualizar una colección lineal de widgets con desplazamiento.
  • Existen varios constructores importantes:
 :* ListView(...): apropiado cuando tenemos una cantidad pequeña y conocida de widgets. Los elementos de children se construyen directamente.
 :* ListView.builder(...): construye los elementos bajo demanda. Es apropiado para listas largas o generadas dinámicamente.
 :* ListView.separated(...): igual que builder, pero permite definir un separador entre cada elemento.
  • Podemos modificar el sentido del desplazamiento:
scrollDirection: Axis.horizontal
o:
scrollDirection: Axis.vertical


Listas con todos los elementos definidos

import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget {
  const ListasPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Ejemplo de listas')),
      body: ListView(
        padding: const EdgeInsets.all(8),
        children: [
          const Text('Elemento 1'),
          const Text('Elemento 2'),
          const Text('Elemento 3'),
          Image.network(
            'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
            height: 150,
          ),
          const Text('Elemento 5'),
        ],
      ),
    );
  }
}
Flutter dart widget 24.JPG




ListTile

  • Más información:
 :* https://api.flutter.dev/flutter/material/ListTile-class.html
  • ListTile es un widget especialmente útil para representar elementos de listas.
  • Entre sus propiedades habituales encontramos:
 :* leading: widget situado al principio.
 :* title: contenido principal.
 :* subtitle: contenido secundario.
 :* trailing: widget situado al final.
 :* onTap: acción ejecutada al pulsar.
 :* selected: indica si el elemento está seleccionado.
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Ejemplo de listas')),
      body: ListView(
        children: [
          ListTile(
            title: const Text('Elemento 1'),
            subtitle: const Text('Texto aclaratorio del elemento 1'),
            leading: const Icon(Icons.accessibility),
            trailing: const Icon(Icons.more_vert),
            onTap: () {},
          ),
          ListTile(
            title: const Text('Elemento 2'),
            subtitle: const Text('Texto aclaratorio del elemento 2'),
            leading: const Icon(Icons.accessibility),
            trailing: const Icon(Icons.more_vert),
            selected: true,
            onTap: () {},
          ),
        ],
      ),
    );
  }

Nota:


Para obtener rojo opaco deberíamos utilizar:
Colors.red
o:
const Color.fromARGB(255, 255, 0, 0)




Cargando elementos desde una función

  • Los datos de una lista pueden proceder de:
 :* Variables de la aplicación.
 :* Archivos JSON.
 :* Bases de datos.
 :* Servicios web.
 :* APIs REST.
 :* etc.
Flutter dart widget 26.JPG


import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget {
  const ListasPage({super.key});

  static const datos = ['Elem1', 'Elem2', 'Elem3', 'Elem4'];

  List<Widget> _obtenerDatos(List<String> datos) {
    final lista = <Widget>[];

    for (final dato in datos) {
      lista.add(
        ListTile(
          title: Text(dato),
          subtitle: Text('Texto aclaratorio de $dato'),
          leading: const Icon(Icons.accessibility),
          trailing: const Icon(Icons.more_vert),
        ),
      );
    }

    return lista;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Ejemplo de listas')),
      body: ListView(children: _obtenerDatos(datos)),
    );
  }
}


Utilizando map

  • Una forma muy habitual de convertir una colección de datos en widgets consiste en utilizar el método map.
  • map devuelve un Iterable. Cuando necesitamos una List podemos convertirlo mediante toList().
  List<Widget> _obtenerDatos(List<String> datos) {
    return datos
        .map(
          (dato) => ListTile(
            title: Text(dato),
            subtitle: Text('Texto aclaratorio de $dato'),
            leading: const Icon(Icons.accessibility),
            trailing: const Icon(Icons.more_vert),
          ),
        )
        .toList(growable: false);
  }



  • En Dart moderno es preferible utilizar:
for (final dato in datos)
cuando no necesitamos modificar la variable del bucle.




Exercicios propostos

  • Para estes exercicios podemos empregar map ou outras técnicas equivalentes de Dart.
  • Partimos dunha aplicación similar a:
import 'package:flutter/material.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const MainPage(),
    );
  }
}

class MainPage extends StatelessWidget {
  const MainPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Exercicios Listas')),
      body: const SizedBox(),
    );
  }
}
Substitúe o body polo widget correspondente a cada exercicio.




  • EXERCICIO 1
final datosExercicio1 =
<Map<String, dynamic>>[
{
'texto': 'Etiqueta 1',
'color': Colors.blue,
'size': 20.0,
},
{
'texto': 'Etiqueta 2',
'color': Colors.red,
'size': 30.0,
},
{
'texto': 'Etiqueta 3',
'color': Colors.green,
'size': 25.0,
},
{
'texto': 'Etiqueta 4',
'color': Colors.yellow,
'size': 40.0,
},
];
Crea un StatelessWidget que devolva unha lista de textos aplicando os datos definidos nos mapas anteriores.




  • EXERCICIO 2
final datosExercicio2 =
<Map<String, dynamic>>[
{
'texto': 'Imagen 1',
'url': 'https://picsum.photos/100/100',
},
{
'texto': 'Imagen 2',
'url': 'https://picsum.photos/100/101',
},
{
'texto': 'Imagen 3',
'url': 'https://picsum.photos/100/102',
},
{
'texto': 'Imagen 4',
'url': 'https://picsum.photos/100/103',
},
];
Crea unha lista na que cada elemento estea formado por un texto e unha imaxe.
Emprega:
  • Row.
  • Padding.
  • FadeInImage.assetNetwork.




  • EXERCICIO 3
final datosExercicio3 =
<Map<String, dynamic>>[
{
'title': 'Imagen 1',
'subtitle': 'Aclaración imagen1',
'leading':
'https://picsum.photos/100/100',
},
{
'title': 'Imagen 2',
'subtitle': 'Aclaración imagen2',
'leading':
'https://picsum.photos/100/101',
},
{
'title': 'Imagen 3',
'subtitle': 'Aclaración imagen3',
'leading':
'https://picsum.photos/100/102',
},
{
'title': 'Imagen 4',
'subtitle': 'Aclaración imagen4',
'leading':
'https://picsum.photos/100/103',
},
];
Crea un StatelessWidget que devolva unha lista de ListTile aplicando os datos anteriores.



  • EXERCICIO 4
final datosExercicio4 =
<Map<String, dynamic>>[
{
'texto': 'Entrada 1',
'url': 'https://picsum.photos/100/100',
'altura': 100,
'anchura': 120,
},
{
'texto': 'Entrada 2',
'url': 'https://picsum.photos/100/101',
'altura': 50,
'anchura': 70,
},
{
'texto': 'Entrada 3',
'url': 'https://picsum.photos/100/102',
'altura': 80,
'anchura': 90,
},
];
Crea una lista en la que cada elemento contenga un texto y una imagen.
La imagen debe ocupar las dimensiones indicadas sin deformarse.

Nota de tipos:

  • Los valores width y height esperan un double?, que es el tipo de dato que recibe width o height en Image.network.
final altura =(e['altura'] as num).toDouble();
final anchura = (e['anchura'] as num).toDouble();


  • Si consideramos que los datos vienen de un servicio externo en el que no podemos garantizar la existencia de claves y tipos de valores tendríamos que usar esta forma:
final altura = double.tryParse(e['altura']?.toString() ?? '') ?? 0.0;
final anchura = double.tryParse(e['anchura']?.toString() ?? '') ?? 0.0;





  • EXERCICIO 5
final datosExercicio5 =
<Map<String, dynamic>>[
{
'texto': 'Presiona aquí',
'elemento': {
'icon': Icons.map,
'texto': 'Botón 1',
'cor': Colors.yellow,
},
},
{
'texto': 'No presiones aquí',
'elemento': {
'icon': Icons.star,
'texto': 'Botón 2',
'cor': Colors.black,
},
},
];
Crea una lista en la que cada elemento contenga:
  • El texto principal.
  • Un IconButton.
  • El texto almacenado en elemento con el color indicado.


Soluciones a Ejercicios Propuestos
  • Ejercicio 1
import 'package:flutter/material.dart';

class Exercicio1Listas extends StatelessWidget {
  const Exercicio1Listas({super.key, required this.datos});

  final List<Map<String, dynamic>> datos;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: datos
          .map((e) {
            return Text(
              'Texto: ${e['texto'] as String}',
              style: TextStyle(
                color: e['color'] as Color,
                fontSize: (e['size'] as num).toDouble(),
              ),
            );
          })
          .toList(growable: false),
    );
  }
}


  • Ejercicio 2
import 'package:flutter/material.dart';

class Exercicio2Listas extends StatelessWidget {
  const Exercicio2Listas({super.key, required this.datos});

  final List<Map<String, dynamic>> datos;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: datos
          .map((e) {
            return Padding(
              padding: const EdgeInsets.all(8),
              child: Row(
                children: [
                  Expanded(child: Text(e['texto'] as String)),
                  FadeInImage.assetNetwork(
                    placeholder: 'assets/images/cargando.gif',
                    image: e['url'] as String,
                    width: 100,
                    height: 100,
                    fit: BoxFit.cover,
                  ),
                ],
              ),
            );
          })
          .toList(growable: false),
    );
  }
}


  • Ejercicio 3
import 'package:flutter/material.dart';

class Exercicio3Listas extends StatelessWidget {
  const Exercicio3Listas({super.key, required this.datos});

  final List<Map<String, dynamic>> datos;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: datos
          .map((e) {
            return ListTile(
              title: Text(e['title'] as String),
              subtitle: Text(e['subtitle'] as String),
              leading: FadeInImage.assetNetwork(
                placeholder: 'assets/images/cargando.gif',
                image: e['leading'] as String,
                width: 60,
                height: 60,
                fit: BoxFit.cover,
              ),
            );
          })
          .toList(growable: false),
    );
  }
}


  • Ejercicio 4
Partimos de que no podemos asegurar que los datos vengan correctamente del servicio...
import 'package:flutter/material.dart';

class Exercicio4Listas extends StatelessWidget {
  const Exercicio4Listas({super.key, required this.datos});

  final List<Map<String, dynamic>> datos;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: datos
          .map((e) {
            final altura = double.tryParse(e['altura']?.toString() ?? '') ?? 0.0;
            final anchura = double.tryParse(e['anchura']?.toString() ?? '') ?? 0.0;

            return Row(
              children: [
                Expanded(child: Text('Texto: ${e['texto']}')),
                Image.network(
                  e['url']?.toString() ?? 'https://picsum.photos/200/300',
                  width: anchura,
                  height: altura,
                  fit: BoxFit.cover,
                ),
              ],
            );
          })
          .toList(growable: false),
    );
  }
}


  • Exercicio 5
import 'package:flutter/material.dart';

class Exercicio5Listas extends StatelessWidget {
  const Exercicio5Listas({super.key, required this.datos});

  final List<Map<String, dynamic>> datos;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: datos
          .map((e) {
            final elemento = e['elemento'] as Map<String, dynamic>;

            return Row(
              children: [
                Expanded(child: Text(e['texto'] as String)),
                IconButton(
                  onPressed: () {},
                  icon: Icon(elemento['icon'] as IconData),
                ),
                Text(
                  elemento['texto'] as String,
                  style: TextStyle(color: elemento['cor'] as Color),
                ),
              ],
            );
          })
          .toList(growable: false),
    );
  }
}


Listas con elementos cargados dinámicamente: ListView.builder

  • Más información:
 :* https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html
  • ListView.builder crea los widgets a medida que son necesarios.
Resulta especialmente útil cuando:
  • La lista tiene muchos elementos.
  • Los datos se generan dinámicamente.
  • No queremos construir inicialmente todos los widgets.


Flutter dart widget 27.JPG


  • Los dos parámetros principales son:
 :* itemCount: número de elementos disponibles.
 :* itemBuilder: función que construye un widget para una posición determinada.
  • itemCount no es obligatorio desde el punto de vista de la API, pero debe indicarse siempre que conozcamos el tamaño de la fuente de datos.
import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget {
  const ListasPage({super.key});

  static const datos = [
    'Elem1',
    'Elem2',
    'Elem3',
    'Elem4',
    'Elem5',
    'Elem6',
    'Elem7',
    'Elem8',
    'Elem9',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ListView.builder')),
      body: ListView.builder(
        itemCount: datos.length,
        itemBuilder: (context, index) {
          debugPrint('Construyendo índice $index');

          final dato = datos[index];

          return ListTile(
            title: Text(dato),
            subtitle: Text('Información de $dato'),
            leading: const Icon(Icons.accessibility),
            trailing: const Icon(Icons.more_vert),
            onTap: () {},
          );
        },
      ),
    );
  }
}
  • No es necesario utilizar:
datos.elementAt(index)
cuando disponemos de una List. Es más directo utilizar:
datos[index]


Separando elementos de las listas

Widget Divider

  • Más información:
 :* https://api.flutter.dev/flutter/material/Divider-class.html
  • Podemos introducir manualmente Divider dentro de una lista pequeña.
ListView(
children: const [
ListTile(
title: Text('Elemento 1'),
),
Divider(),
ListTile(
title: Text('Elemento 2'),
),
],
)


ListView.separated

  • Para listas creadas dinámicamente es más apropiado ListView.separated.
ListView.separated(
itemCount: datos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(datos[index]),
);
},
separatorBuilder: (context, index) {
return const Divider(
thickness: 1,
);
},
)
  • separatorBuilder recibe el índice correspondiente y puede devolver cualquier widget.
Por ejemplo:
separatorBuilder: (context, index) {
return const SizedBox(
height: 10,
);
}


FutureBuilder

Más información:

En algunas ocasiones los datos que queremos mostrar no están disponibles inmediatamente.

Por ejemplo:
  • Una petición a una API de Internet.
  • La lectura de un archivo.
  • Una consulta a una base de datos.
  • Una operación que tarda cierto tiempo en finalizar.

Dart representa este tipo de operaciones mediante objetos de tipo Future.

Un Future representa un valor que estará disponible en algún momento en el futuro.

Por ejemplo, el siguiente método devuelve un Future que dentro de cuatro segundos contendrá una lista de cadenas:

Future<List<String>> _obtenerDatosInternet() {
final datos = <String>[
'Valor 1',
'Valor 2',
'Valor 3',
];

return Future.delayed(
  const Duration(seconds: 4),() => datos,);
}

También podemos escribirlo utilizando async y await:

Future<List<String>> _obtenerDatosInternet() async {
await Future<void>.delayed(
const Duration(seconds: 4),
);

return [
'Valor 1',
'Valor 2',
'Valor 3',
];
}
En ambos casos el resultado es el mismo:
Future<List<String>>


El widget FutureBuilder

FutureBuilder es un widget que permite construir una parte de la interfaz en función del estado de un Future.

Entre sus parámetros principales tenemos:

  • future: Future cuyo resultado queremos esperar.
  • builder: función que se ejecutará para construir la interfaz.
  • initialData: dato inicial opcional que se puede utilizar mientras todavía no ha terminado el Future.

El builder recibe un objeto denominado AsyncSnapshot que contiene información acerca del Future.

Entre sus propiedades más utilizadas encontramos:
  • snapshot.connectionState: estado de la operación.
  • snapshot.data: datos obtenidos.
  • snapshot.hasData: indica si existen datos.
  • snapshot.hasError: indica si se produjo un error.
  • snapshot.error: error producido.


Estados de ConnectionState

La propiedad connectionState puede tener los siguientes valores:

  • ConnectionState.none: no existe ninguna operación asíncrona asociada.
  • ConnectionState.waiting: estamos esperando a que termine el Future.
  • ConnectionState.active: se están recibiendo datos de forma activa. Es más habitual con Stream que con Future.
  • ConnectionState.done: la operación ha terminado.

En nuestros ejemplos de FutureBuilder nos interesarán principalmente:

ConnectionState.waiting
y:
ConnectionState.done


Ejemplo sencillo con StatelessWidget

Como todavía no hemos estudiado StatefulWidget, podemos utilizar FutureBuilder directamente dentro del método build de un StatelessWidget.

Emularemos que estamos obteniendo una lista de datos desde Internet:




import 'package:flutter/material.dart';

class ListasFuturePage extends StatelessWidget {
  const ListasFuturePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('FutureBuilder')),
      body: FutureBuilder<List<String>>(
        future: _obtenerDatosInternet(),
        builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
          // 1. Comprobamos si se produjo un error.
          if (snapshot.hasError) {
            return Center(
              child: Text('Se produjo un error: ${snapshot.error}'),
            );
          }

          // 2. Comprobamos si estamos esperando los datos.
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          // 3. Obtenemos los datos.
          final datos = snapshot.data ?? const <String>[];

          // 4. Comprobamos si la lista está vacía.
          if (datos.isEmpty) {
            return const Center(child: Text('No hay datos disponibles.'));
          }

          // 5. Mostramos los datos.
          return ListView.builder(
            itemCount: datos.length,
            itemBuilder: (context, index) {
              final dato = datos[index];

              return ListTile(
                leading: const Icon(Icons.add),
                title: Text(dato),
                onTap: () {},
              );
            },
          );
        },
      ),
    );
  }

  Future<List<String>> _obtenerDatosInternet() async {
    await Future<void>.delayed(const Duration(seconds: 4));

    return ['Valor 1', 'Valor 2', 'Valor 3'];
  }
}


¿Qué sucede en el ejemplo?

Cuando Flutter ejecuta el método build, se crea el Future:

future: _obtenerDatosInternet(),

En ese momento todavía no tenemos los datos.

Por ello:
snapshot.connectionState ==
ConnectionState.waiting
será verdadero y mostraremos:
const CircularProgressIndicator()

Aproximadamente cuatro segundos después finaliza el Future.

FutureBuilder recibe el resultado y vuelve a ejecutar su builder.

Los datos estarán disponibles en:

snapshot.data

En nuestro caso tendrán el valor:

[
'Valor 1',
'Valor 2',
'Valor 3',
]

Finalmente construimos un ListView.builder y mostramos los elementos.


¿Por qué utilizamos snapshot.data ?? []?

La propiedad snapshot.data puede ser nula.

Su tipo realmente es:
List<String>?

Por ello podemos escribir:

final datos =
snapshot.data ?? const <String>[];

El operador:

??
indica que:
  • Si snapshot.data no es null, se utiliza su valor.
  • Si snapshot.data es null, se utiliza una lista vacía.
Es equivalente conceptualmente a:
List<String> datos;

if (snapshot.data != null) {
datos = snapshot.data!;
} else {
datos = [];
}
pero la primera forma es más compacta.


Tipar FutureBuilder

Es recomendable indicar qué tipo de dato va a devolver el Future.

En nuestro caso:
FutureBuilder<List<String>>

Gracias a ello Flutter sabe que el snapshot contiene:

AsyncSnapshot<List<String>>

Esto evita tener que realizar conversiones de tipos posteriormente.

Ejemplo:

FutureBuilder<List<String>>(
future: _obtenerDatosInternet(),
builder: (
context,
AsyncSnapshot<List<String>> snapshot,
) {
// ...
},
)

Dart también puede inferir el tipo del parámetro snapshot, por lo que podemos escribir una versión más corta:

FutureBuilder<List<String>>(
future: _obtenerDatosInternet(),
builder: (context, snapshot) {
// snapshot.data es List<String>?
},
)
Las dos formas son correctas.


Utilizando initialData

FutureBuilder dispone de un parámetro opcional denominado initialData.

Permite indicar unos datos que estarán disponibles antes de que termine el Future.

FutureBuilder<List<String>>(
future: _obtenerDatosInternet(),
initialData: const [
'Inicial 1',
'Inicial 2',
],
builder: (context, snapshot) {

final datos =
    snapshot.data ?? const <String>[];

return ListView.builder(
  itemCount: datos.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(datos[index]),
    );
  },
);

},
)

En este caso inicialmente veremos:

Inicial 1
Inicial 2

Cuando finalice el Future serán sustituidos por:

Valor 1
Valor 2
Valor 3

Importante:

Si utilizamos initialData, el hecho de que:

snapshot.hasData
sea verdadero no implica que el Future haya terminado.
Puede estar mostrando todavía los datos iniciales.

Si necesitamos saber si la operación continúa ejecutándose debemos consultar:

snapshot.connectionState


Una consideración importante sobre este ejemplo

El ejemplo anterior es válido para aprender el funcionamiento de FutureBuilder, pero hay un detalle que debemos conocer.

Estamos creando el Future dentro del método build:

future: _obtenerDatosInternet(),

Cada vez que Flutter ejecute de nuevo build, esta llamada puede generar un Future nuevo.

Por tanto, una operación como:

  • Descargar información de Internet.
  • Consultar una base de datos.
  • Leer un archivo grande.
podría iniciarse de nuevo si el widget se reconstruye.

Por ahora:

No vamos a preocuparnos por este problema.

Estamos utilizando esta forma porque todavía estamos trabajando con StatelessWidget y nuestro objetivo en esta unidad es comprender:

  • Qué es un Future.
  • Qué hace FutureBuilder.
  • Cómo mostrar un indicador de carga.
  • Cómo mostrar los datos cuando llegan.

Más adelante:

Cuando estudiemos StatefulWidget veremos cómo podemos crear el Future una sola vez y conservarlo mientras exista la pantalla.



Exercicio Proposto:: Crea un novo arquivo Dart e visualiza unha lista de imaxes. Emula que as url´s das imaxes se descargan de Internet (o Future debe devolver unha lista de cadeas). Emprega a url: https://picsum.photos/200/200.

Emprega o Widget que visualiza unha imaxe local mientras se descarga a imaxe de Internet.





Cargando datos desde un archivo JSON

  • Más información:
 :* https://docs.flutter.dev/data-and-backend/serialization/json
 :* https://api.flutter.dev/flutter/services/rootBundle.html
  • JSON es uno de los formatos más utilizados para intercambiar información.
  • Dart incluye dart, que permite trabajar con JSON mediante:
 :* jsonDecode.
 :* jsonEncode.
  • Para proyectos pequeños resulta adecuado realizar una serialización manual.
  • Para aplicaciones medianas o grandes se suelen emplear generadores como json_serializable, que estudiaremos posteriormente.





Archivo JSON

Archivo: assets/data/datos_listas.json

{
"elementos": [
{
"titulo": "Elemento 1",
"texto": "Aclaración elemento 1",
"imagen": "https://picsum.photos/id/1/200/300"
},
{
"titulo": "Elemento 2",
"texto": "Aclaración elemento 2",
"imagen": "https://picsum.photos/id/2/200/300"
},
{
"titulo": "Elemento 3",
"texto": "Aclaración elemento 3",
"imagen": "https://picsum.photos/id/3/200/300"
}
]
}
  • Registramos el directorio:
flutter:
assets:
- assets/data/
- assets/images/


Modelo

Archivo: lib/models/elemento_lista_model.dart

class ElementoListaModel {
  const ElementoListaModel({
    required this.titulo,
    required this.texto,
    required this.imagen,
  });

  final String titulo;
  final String texto;
  final String imagen;

  factory ElementoListaModel.fromJson(Map<String, dynamic> json) {
    return ElementoListaModel(
      titulo: json['titulo'] as String,
      texto: json['texto'] as String,
      imagen: json['imagen'] as String,
    );
  }
}

Nota:

  • Es preferible llamar al constructor:
fromJson
cuando el mapa representa directamente datos obtenidos de JSON.
  • Los atributos del modelo se han declarado final porque el objeto no necesita modificarse después de su creación.


Provider para cargar el archivo

Archivo: lib/providers/datos_lista_provider.dart

import 'dart:convert' show jsonDecode;
import 'package:flutter/services.dart' show rootBundle;
import '../models/elemento_lista_model.dart';

class DatosListaProvider {
  const DatosListaProvider();

  Future<List<ElementoListaModel>> cargarDatos() async {
    final contenido = await rootBundle.loadString(
      'assets/data/datos_listas.json',
    );

    final json = jsonDecode(contenido) as Map<String, dynamic>;

    final elementos = json['elementos'] as List<dynamic>;

    return elementos
        .map((elemento) {
          return ElementoListaModel.fromJson(elemento as Map<String, dynamic>);
        })
        .toList(growable: false);
  }
}

const datosListaProvider = DatosListaProvider();
  • Podemos crear directamente:
const DatosListaProvider()
  • En este ejemplo la clase tampoco necesita ser privada.


Visualizando los datos

Archivo: lib/screens/listas_json_page.dart

import 'package:flutter/material.dart';

import '../models/elemento_lista_model.dart';
import '../providers/datos_lista_provider.dart';

class ListasJsonPage extends StatefulWidget {
  const ListasJsonPage({super.key});

  @override
  State<ListasJsonPage> createState() => _ListasJsonPageState();
}

class _ListasJsonPageState extends State<ListasJsonPage> {
  late final Future<List<ElementoListaModel>> _future;

  @override
  void initState() {
    super.initState();

    _future = datosListaProvider.cargarDatos();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Listas con datos desde JSON')),
      body: FutureBuilder<List<ElementoListaModel>>(
        future: _future,
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          }

          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          final datos = snapshot.data ?? const <ElementoListaModel>[];

          if (datos.isEmpty) {
            return const Center(child: Text('No hay datos.'));
          }

          return ListView.separated(
            itemCount: datos.length,
            separatorBuilder: (context, index) => const Divider(),
            itemBuilder: (context, index) {
              final modelo = datos[index];

              return ListTile(
                leading: FadeInImage.assetNetwork(
                  placeholder: 'assets/images/cargando.gif',
                  image: modelo.imagen,
                  width: 60,
                  height: 60,
                  fit: BoxFit.cover,
                ),
                title: Text(modelo.titulo),
                subtitle: Text(modelo.texto),
                onTap: () {},
              );
            },
          );
        },
      ),
    );
  }
}

Nota:

  • En aplicaciones con muchos modelos JSON puede resultar conveniente utilizar generación automática de código mediante:
 :* json_serializable.
 :* build_runner.
Para estos primeros ejemplos continuaremos realizando la conversión manual para comprender el proceso.


Card Widget

  • Más información:
 :* https://api.flutter.dev/flutter/material/Card-class.html
  • Card representa una tarjeta de Material Design.
  • Algunos parámetros importantes son:
 :* child: contenido de la tarjeta.
 :* elevation: elevación.
 :* shape: forma del borde.
 :* color: color de fondo.
 :* clipBehavior: controla cómo se recorta el contenido que sobrepasa los límites.
 :* margin: margen exterior.
  • En Material 3 también disponemos de variantes como:
 :* Card.
 :* Card.filled.
 :* Card.outlined.
  • El aspecto final depende del tema de la aplicación, por lo que es preferible aprovechar el ThemeData en lugar de establecer manualmente colores y elevaciones en todos los widgets.



Ejemplo

Flutter dart widget 32.png
import 'package:flutter/material.dart';

class CardsPage extends StatelessWidget {
  const CardsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Ejemplo de Cards')),
      body: ListView(
        padding: const EdgeInsets.all(12),
        children: [
          _crearTarjeta1(),
          const SizedBox(height: 12),
          _crearTarjeta2(),
          const SizedBox(height: 12),
          _crearTarjeta3(),
        ],
      ),
    );
  }

  Widget _crearTarjeta1() {
    return Card(
      clipBehavior: Clip.antiAlias,
      elevation: 4,
      shape: RoundedRectangleBorder(
        side: const BorderSide(width: 2, color: Colors.red),
        borderRadius: BorderRadius.circular(24),
      ),
      child: Column(
        children: [
          Image.network(
            'https://picsum.photos/700/500',
            width: double.infinity,
            height: 200,
            fit: BoxFit.cover,
          ),
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              'Ejemplo de texto '
              'asociado a una tarjeta',
            ),
          ),
        ],
      ),
    );
  }

  Widget _crearTarjeta2() {
    return const Card.filled(
      child: ListTile(
        leading: Icon(Icons.accessibility),
        title: Text('Ejemplo de texto'),
        trailing: Icon(Icons.delete),
      ),
    );
  }

  Widget _crearTarjeta3() {
    return const Card.outlined(
      child: Padding(
        padding: EdgeInsets.all(24),
        child: Icon(Icons.access_alarm, size: 80),
      ),
    );
  }
}
  • Para recortar otros widgets disponemos, entre otros, de:
 :* ClipRect.
 :* ClipRRect.
 :* ClipOval.


CircleAvatar

  • Más información:
 :* https://api.flutter.dev/flutter/material/CircleAvatar-class.html
  • CircleAvatar permite representar un avatar circular.
  • Entre sus propiedades más utilizadas están:
 :* backgroundImage.
 :* foregroundImage.
 :* child.
 :* radius.
 :* backgroundColor.
 :* foregroundColor.

Ejemplo


Flutter dart widget 33.png
import 'package:flutter/material.dart';

class CircleAvatarPage extends StatelessWidget {
  const CircleAvatarPage({super.key});

  static const usuarios = <String, String>{
    'Luisa Sanz': 'https://randomuser.me/api/portraits/women/1.jpg',
    'Ana Pérez': 'https://randomuser.me/api/portraits/women/2.jpg',
    'Miguel López': 'https://randomuser.me/api/portraits/men/3.jpg',
    'Carmen San Juan': 'https://randomuser.me/api/portraits/women/4.jpg',
  };

  @override
  Widget build(BuildContext context) {
    final entradas = usuarios.entries.toList();

    return Scaffold(
      appBar: AppBar(title: const Text('Ejemplo de CircleAvatar')),
      body: ListView.separated(
        itemCount: entradas.length,
        separatorBuilder: (context, index) => const Divider(),
        itemBuilder: (context, index) {
          final usuario = entradas[index];

          return ListTile(
            leading: CircleAvatar(backgroundImage: NetworkImage(usuario.value)),
            title: Text(usuario.key),
            trailing: CircleAvatar(child: Text(_iniciales(usuario.key))),
            onTap: () {},
          );
        },
      ),
    );
  }

  String _iniciales(String nombre) {
    final partes = nombre.trim().split(RegExp(r'\s+')); // Devuelve en forma de lista el nombre y apellidos quitando espacios en blanco

    if (partes.isEmpty) {
      return '';
    }

    if (partes.length == 1) {
      return partes.first.substring(0, 1).toUpperCase();
    }

    return (partes.first.substring(0, 1) + partes.last.substring(0, 1))
        .toUpperCase();
  }
}
  • En este ejemplo:
 :* backgroundImage recibe un NetworkImage.
 :* Se utiliza ListView.separated para separar los usuarios.
 :* Los datos se mantienen en una constante.
 :* La función _iniciales obtiene las iniciales sin asumir que el nombre siempre tenga dos caracteres disponibles.




Ejercicios propuestos sin solución

  • Ejercicio 1
final datosExercicio6 = [1, 2, 3, 4, 5, 6];
Crea unha lista onde cada elemento mostre:
Número: 1 → Resultado: 10
Cada elemento debe estar dentro de un Card y separado empleando un Padding.


  • Ejercicio 2
final datosExercicio7 = <String>[
'Lugo',
'Ourense',
'A Coruña',
'Pontevedra',
];
Crea una lista de elementos formados por:
  • Un CircleAvatar.
  • El nombre de la ciudad debajo.
  • Un Column para agruparlos.


  • Ejercicio 3
final datosExercicio8 =
<Map<String, dynamic>>[
{
'titulo': 'Aprender Flutter',
'autor': 'Ana',
},
{
'titulo': 'Widgets e Layouts',
'autor': 'Brais',
},
{
'titulo': 'Listas e Datos',
'autor': 'Clara',
},
{
'titulo': 'Eventos e Accións',
'autor': 'Diego',
},
];
Crea una clase Libro con:
  • Atributos final.
  • Constructor con parámetros obligatorios y por nombre.
  • Constructor factory Libro.fromMap.
Convierte los mapas en objetos Libro y visualízalos empleando ListTile.


  • Ejercicio 4
final datosExercicio9 =
<Map<String, dynamic>>[
{
'nome': 'Ana',
'idade': 25,
'foto':
'https://randomuser.me/api/portraits/women/1.jpg',
},
{
'nome': 'Brais',
'idade': 30,
'foto':
'https://randomuser.me/api/portraits/men/2.jpg',
},
];
Crea una clase Persona y representa los datos utilizando Card y un CircleAvatar.


  • Ejercicio 5
final datosExercicio10 =
<Map<String, dynamic>>[
{
'produto': 'Camiseta',
'prezo': 19.99,
'foto':
'https://picsum.photos/100?1',
},
{
'produto': 'Pantalón',
'prezo': 29.99,
'foto':
'https://picsum.photos/100?2',
},
];
Crea una clase Producto y una lista de ListTile con:
  • Imagen en el leading.
  • Nombre en el title.
  • Precio en el subtitle.
  • IconButton con una cesta en el trailing.


  • Ejercicio 6
Define una clase ProductoFitbit donde prezo y valoracion sean double.
En el método fromMap, recuerda que un valor JSON puede llegar o no o puede tener el tipo previsto o no.


Si la valoración es mayor o igual a 4.5:
  • Mostrar Icons.star.
  • Mostrar el precio en verde.
En caso contrario:
  • Mostrar Icons.star_border.
  • Mostrar el precio en rojo.





Enlace a la página principal de la UD3

Enlace a la página principal del curso