Diferencia entre revisiones de «Flutter Widget Componentes»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 75 ediciones intermedias del mismo usuario)
Línea 1: Línea 1:
 
==Introducción==
 
==Introducción==
  
−
* En esta sección vamos a ver los principales componentes gráficos (widget) que podemos necesitar para diseñar una pantalla en Flutter.
+
* 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
+
  :* Textos.
−
:* Botones
+
  :* Botones.
−
:* Imágenes
+
  :* Imágenes.
−
:* Listas
+
  :* Listas.
−
:* Card (tarjetas)
+
  :* Card (tarjetas).
−
:* Combos (dropdown)
+
  :* 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, en las que tenemomos un widget MaterialApp el cual tiene como contenido un Scaffold.
+
* Partimos de lo aprendido en las secciones anteriores, donde tenemos un widget '''MaterialApp''' que contiene las diferentes pantallas de nuestra aplicación.
−
: Los dos se encuentran en ficheros diferentes.
 
−
: Lo que vamos a hacer es crear nuevas páginas que devuelve un scaffold con los contenidos que queramos visualizar.
 
  
−
* Aún no sabemos cómo 'colocar' los widget, por lo que en esta sección se empezará a hablar de alguno de los widget´s que nos permiten hacer esto, pero que serán explicados en profundidad [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget_Dise%C3%B1o_de_pantallas en la siguiente secció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 [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget_Dise%C3%B1o_de_pantallas en la siguiente sección].
 +
 
 +
* Cuando ya estéis habituados al uso de widgets, es recomendable consultar la [https://www.youtube.com/playlist?list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG lista Widget of the Week], donde se explica visualmente el funcionamiento de numerosos widgets.
  
 
<br />
 
<br />
Línea 20: Línea 29:
 
==Widget Text==
 
==Widget Text==
  
−
* Más información en: https://flutter.dev/docs/development/ui/widgets/text
+
* Más información:
 +
  :* https://api.flutter.dev/flutter/widgets/Text-class.html
  
−
* Creamos una nueva página en la [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget#Estructura_de_p.C3.A1ginas carpeta pages] de nombre '''textos_page.dart'''
+
* Creamos una nueva página en la carpeta '''pages''' llamada '''textos_page.dart'''.
−
: En todas las páginas haremos lo mismo:
+
 
−
:* Importamos la librería de 'material' (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
: En nuestros ejemplos:
−
:* Creamos una clase que derive de StatelessWidget (al hacerlo será necesario implementar el método build, ya os da un aviso el Android Studio).
+
:* Importaremos '''package/material.dart'''.
−
:* Dentro del método build, retornamos un widget Scaffold (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
:* 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.
  
 
<br />
 
<br />
 +
 
===Textos con un estilo único===
 
===Textos con un estilo único===
  
−
* Más información en: https://api.flutter.dev/flutter/widgets/Text-class.html
+
* El widget [https://api.flutter.dev/flutter/widgets/Text-class.html Text] permite visualizar texto.
  
−
* Este widget como su nombre indica, visualiza un texto. Entre sus propiedades nos puede ser útiles:
+
* Algunas de sus propiedades más importantes son:
−
:* [https://api.flutter.dev/flutter/widgets/Text/style.html style]: Para darle un formato.
+
  :* '''style''': permite definir el aspecto del texto mediante un objeto '''TextStyle'''.
−
:* [https://api.flutter.dev/flutter/widgets/Text/overflow.html overflow]: Para cuando no quepa el texto dentro del contenedor. Debemos usar una de las constantes definidas en [https://api.flutter.dev/flutter/painting/TextOverflow-class.html TextOverFlow].
+
  :* '''overflow''': determina qué ocurre cuando el contenido no cabe en el espacio disponible. Utiliza valores de '''TextOverflow''', como '''ellipsis''', '''clip''' o '''fade'''.
−
:* [https://api.flutter.dev/flutter/widgets/Text/maxLines.html maxLines]: Número de líneas de texto que podrán visualizarse con respecto a su contenedor.
+
  :* '''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'''
  
−
* El estilo hace referencia a las características visuales del texto, como puedan ser el color, negrilla, tamaño,...
+
:<syntaxhighlight lang="dart" enclose="div">
−
: Veamos ejemplo de código con alguna de sus propiedades aplicadas.
+
import 'package:flutter/material.dart';
−
: Indicar que en el ejemplo vamos a 'envolver' el texto en otro widget denominado [https://api.flutter.dev/flutter/widgets/Column-class.html Column]. Este widget es uno de los que veremos en la siguiente sección, y nos permite colocar varios widget´s uno debajo de otro, formando filas. Para ello hace uso de la propiedad 'children' la cual espera recibir un conjunto de widget que conformarán cada una de las filas.
 
  
−
'''Archivo: textos_pages.dart'''
+
class TextosPage extends StatelessWidget {
−
<u>Nota:</u> Recuerda cambiar el archivo app.dart para que cargue la nueva página.
+
  const TextosPage({super.key});
−
:<syntaxhighlight lang="java" enclose="div" highlight="13-20" >
 
−
import 'package:flutter/material.dart';
 
  
−
class TextosPage extends StatelessWidget{
 
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('Textos con diferentes estilos')),
−
          title: Text('Textos con diferentes estilos'),
+
      body: const Column(
−
        ),
+
        children: [
−
        body: Column(
+
          Text(
−
          children: const<Widget> [
+
            'Texto con los valores por defecto. '
−
            Text('Texto con los valores por defecto. Ocupa lo que ocupe de ancho del texto escrito. Si no cabe, pasa a la siguiente línea.'),
+
            'Si no cabe en el espacio disponible, puede pasar '
−
            Text(
+
            'a la siguiente línea.',
−
              'Texto centrado en color rojo y negrilla\n Con varias líneas. Lo centra con respecto al ancho mayor de los widget´s que estén en el Column',
+
          ),
−
              textAlign: TextAlign.center,
+
          Text(
−
              style: TextStyle(color: Colors.red,
+
            'Texto centrado, rojo y en negrilla.\n'
−
                              fontWeight: FontWeight.bold,
+
            'Puede ocupar varias líneas.',
−
                              fontSize: 10.2),
+
            textAlign: TextAlign.center,
 +
            style: TextStyle(
 +
              color: Colors.red,
 +
              fontWeight: FontWeight.bold,
 +
              fontSize: 16,
 
             ),
 
             ),
−
 
+
          ),
−
          ],
+
        ],
−
        )
+
      ),
 
     );
 
     );
 
   }
 
   }
−
 
 
}
 
}
−
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
[[Imagen:Flutter_dart_widget_13.JPG|300px|center]]
 
[[Imagen:Flutter_dart_widget_13.JPG|300px|center]]
  
 +
* La propiedad '''style''' espera recibir un objeto de tipo '''TextStyle'''.
  
 +
* Si queremos reutilizar el mismo estilo podemos almacenarlo en una constante:
  
−
* Como podemos ver en el ejemplo anterior, la clase Text tiene la propiedad style, que nos va a servir para dar un estilo al texto.
+
:<syntaxhighlight lang="dart" enclose="div">
−
: Veremos en el siguiente punto como podemos crear un 'estilo por defecto' y aplicarlo a varios Text.
 
−
: Flutter nos permite también declarar una variable local a la clase (un atributo) que sea una instancia de la clase TextStyle, en la que podemos definir el aspecto que tendrán los textos, y después aplicar dicha instancia de clase a todos los textos que lo necesiten.
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="4-6,18,19" >
 
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
−
class TextosPage extends StatelessWidget{
+
class TextosPage extends StatelessWidget {
−
   const estiloTexto = new TextStyle(fontSize: 35.3, // Recordar que el new no es obligatorio en Flutter
+
   const TextosPage({super.key});
 +
 
 +
  static const TextStyle estiloTexto = TextStyle(
 +
    fontSize: 35.3,
 
     backgroundColor: Colors.red,
 
     backgroundColor: Colors.red,
−
     color: Colors.black12);
+
     color: Colors.black54,
 +
  );
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('Textos con diferentes estilos')),
−
          title: Text('Textos con diferentes estilos'),
+
      body: const Column(
−
        ),
+
        children: [
−
        body: Column(
+
          Text('Texto con el estilo por defecto'),
−
          children: <Widget> [
+
          Text('Texto con un estilo reutilizable', style: estiloTexto),
−
            Text('Texto con el estilo predefinido'),
+
          Text('Otro texto con el mismo estilo', style: estiloTexto),
−
            Text('Texto con el estilo definido en un atributo de la clase', style: estiloTexto),
+
        ],
−
            Text('Otro Texto con el estilo definido en un atributo de la clase', style: estiloTexto), // Se puede dejar la comilla puesta por si queremos añadir nuevos textos
+
      ),
−
          ],
 
−
        )
 
 
     );
 
     );
 
   }
 
   }
−
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* <u>'''NOTA IMPORTANTE:'''</u> Fijarse que el atributo está marcado como <u>const</u> (podría ser final, pero mejor const [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_DART_variables_-_tipos_de_datos#Constantes como ya vimos en DART]) ya que estamos en un StatelessWidget y por lo tanto todo lo que esté definido dentro del mismo debe ser 'inmutable'. Es decir, en este tipo de Widget no podemos 'redibujar' la interface, de tal forma que si empleamos una variable para visualizar un texto, dicho texto nunca podría redibujarse si por programación cambiamos el valor de la variable.
+
'''<u>NOTA IMPORTANTE:</u>'''
−
: Si no definimos como final, el AndroidStudio nos avisará de que no tiene sentido.
 
  
−
[[Imagen:Flutter_dart_widget_16.JPG|300px|center]]
+
* Un '''StatelessWidget''' representa una configuración inmutable.
  
−
<br />
+
: Por este motivo, sus atributos deberían ser '''final''' y, cuando sea posible, '''const''' o '''static const'''.
  
−
===DefaultTextStyle===
+
: 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.
  
−
* Más información en: https://api.flutter.dev/flutter/widgets/DefaultTextStyle-class.html
+
: 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.
  
 +
<br />
  
−
* Es posible 'envolver' un conjunto de widget con un estilo por defecto y hacer que todos los Text que se encuentren dentro de ese estilo 'hereden' las características definidas en el mismo.
+
===DefaultTextStyle===
  
 +
* Más información:
 +
  :* https://api.flutter.dev/flutter/widgets/DefaultTextStyle-class.html
  
−
:'''<u>Nota:</u>''' Recordar que en AndroidStudio es posible 'envolver' un Widget con otro, pulsando las teclas '''Alt+Enter''' y escogiendo la opción '''wrap'''.
+
* '''DefaultTextStyle''' permite proporcionar un estilo por defecto a los widgets '''Text''' que se encuentren debajo de él en el árbol de widgets.
−
: En el ejemplo envolvemos el Widget Column con un Widget DefaultTextStyle al cual al que pasarle dos parámatros de forma obligatoria (required):
 
−
:* child: Donde iré el widget que va a visualizar (en nuestro caso el Column)
 
−
:* style: Donde se instancia la clase TextStyle y donde se define el estile que van a 'heredar' los textos que estén dentro de DefaultTextStyle.
 
  
−
:<syntaxhighlight lang="java" enclose="div" highlight="11-15" >
+
:<syntaxhighlight lang="dart" enclose="div">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
−
class TextosPage extends StatelessWidget{
+
class TextosPage extends StatelessWidget {
 +
  const TextosPage({super.key});
 +
 
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('DefaultTextStyle')),
−
           title: Text('Textos con diferentes estilos'),
+
      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),
 +
            ),
 +
          ],
 
         ),
 
         ),
−
        body: DefaultTextStyle(
+
      ),
−
          style: TextStyle(fontSize: 20.3,
 
−
                          backgroundColor: Colors.amber,
 
−
                          color: Colors.blue,
 
−
                          fontStyle: FontStyle.italic),
 
−
          child: Column(
 
−
            children: <Widget> [
 
−
              Text('Texto con el estilo definido en DefaultTextStyle'),
 
−
              Text(
 
−
                'Texto centrado en color rojo y negrilla\n Con varias líneas. Lo centra con respecto al ancho mayor de los widget´s que estén en el Column',
 
−
                textAlign: TextAlign.center,
 
−
                style: TextStyle(color: Colors.red,
 
−
                    fontWeight: FontWeight.bold,
 
−
                    fontSize: 10.2),
 
−
              ),
 
−
 
 
−
            ],
 
−
          ),
 
−
        )
 
 
     );
 
     );
 
   }
 
   }
 +
}
  
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
−
 
  
 
[[Imagen:Flutter_dart_widget_14.JPG|300px|center]]
 
[[Imagen:Flutter_dart_widget_14.JPG|300px|center]]
  
 +
'''<u>Importante:</u>'''
  
 +
* Si un '''Text''' tiene un '''TextStyle''' propio, por defecto ese estilo se '''combina''' con el '''DefaultTextStyle'''.
  
−
<u>'''Nota:'''</u>
+
: Esto ocurre porque la propiedad '''inherit''' de '''TextStyle''' vale '''true''' por defecto.
−
:* Fijarse como los widget´s Text que tengan un estilo propio, sobreescribirán el DefaultTextStyle. En principio esta forma de definir un estilo está pensado para aquellos Widget que no tengan un estilo propio previamente definido.
 
−
:: Si queremos que no 'sobreescriba' el estilo que pueda tener podemos hacer uso del [https://api.flutter.dev/flutter/widgets/DefaultTextStyle/merge.html método static merge].
 
−
:: En el ejercicio anterior podéis probar a cambiar DefaultTextStyle por DefaultTextStyle.merge y quitar la línea que le da un color al texto. Podéis comprobar que el estilo por defecto con merge es negro, pero el que sobreescribe lo pone en blanco....
 
  
 +
: Por ejemplo, si el DefaultTextStyle define un tamaño de 20 y un Text sólo especifica:
  
−
<br />
+
:<syntaxhighlight lang="dart">
 +
style: const TextStyle(
 +
color: Colors.red,
 +
)
 +
</syntaxhighlight>
  
−
===RichText===
+
: el texto conservará el tamaño heredado y cambiará únicamente el color.
  
−
* Más información en: https://api.flutter.dev/flutter/widgets/RichText-class.html
+
* También podemos utilizar '''DefaultTextStyle.merge''' para modificar parcialmente el estilo existente:
  
 +
:<syntaxhighlight lang="dart">
 +
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'),
 +
          ),
 +
        ],
 +
      ),
 +
    )
 +
</syntaxhighlight>
  
−
* Permite que un mismo texto tenga diferentes estilos.
+
<br />
−
: Para ello hace uso de la [https://api.flutter.dev/flutter/painting/TextSpan-class.html clase TextSpan] que representa un texto inmutable.
 
−
: Cada TextSpan posee la propiedad 'children' que espera recibir una lista de TextSpan, cada uno de ellos con su propio TextStyle. Si no se pone un estilo que lo sobreescriba, el TextSpan 'heredará' el estilo del TextSpan que lo contenga.
 
  
−
:<syntaxhighlight lang="java" enclose="div" highlight="26-33" >
+
===Text.rich===
−
import 'package:flutter/material.dart';
 
  
−
class TextosPage extends StatelessWidget{
+
* Más información:
−
   @override
+
   :* https://api.flutter.dev/flutter/widgets/RichText-class.html
−
  Widget build(BuildContext context) {
+
  :* https://api.flutter.dev/flutter/painting/TextSpan-class.html
−
    // TODO: implement build
 
−
    return Scaffold(
 
−
        appBar: AppBar(
 
−
          title: Text('Textos con diferentes estilos'),
 
−
        ),
 
−
        body: DefaultTextStyle(
 
−
          style: TextStyle(fontSize: 20.3,
 
−
                          backgroundColor: Colors.amber,
 
−
                          color: Colors.blue,
 
−
                          fontStyle: FontStyle.italic),
 
−
          child: Column(
 
−
            children: <Widget> [
 
−
              Text('Texto con el estilo definido en DefaultTextStyle'),
 
−
              Text(
 
−
                'Texto centrado en color rojo y negrilla\n Con varias líneas. Lo centra con respecto al ancho mayor de los widget´s que estén en el Column',
 
−
                textAlign: TextAlign.center,
 
−
                style: TextStyle(color: Colors.red,
 
−
                    fontWeight: FontWeight.bold,
 
−
                    fontSize: 10.2),
 
−
              ),
 
−
              RichText(
 
−
                text: TextSpan(text: 'Este es un texto TextSpan',style: TextStyle(color: Colors.green),
 
−
                      children: [
 
−
                        TextSpan(text: ' con un estilo diferente', style: TextStyle(color:Colors.brown)),
 
−
                        TextSpan(text: ' en cada parte del texto.', style: TextStyle(color:Colors.yellow)),
 
−
                        TextSpan(text: ' Si no se pone nada, hereda el estilo del TextSpan que lo contenga'),
 
−
                      ]),
 
−
              )
 
  
−
            ],
+
* Cuando diferentes fragmentos de una misma frase necesitan estilos distintos podemos utilizar '''TextSpan'''.
−
          ),
 
−
        )
 
−
    );
 
−
  }
 
  
−
}
+
* Una opción sencilla es utilizar el constructor '''Text.rich''':
  
 +
:<syntaxhighlight lang="dart">
 +
    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'),
 +
        ],
 +
      ),
 +
    )
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
[[Imagen:Flutter_dart_widget_15.JPG|300px|center]]
 
  
  
 
<br />
 
<br />
−
===Html en el interior de textos===
 
  
−
<br />
+
===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:
 +
 
 +
:* 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.
 +
 
 +
* 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):
  
−
<center>
+
:<syntaxhighlight lang="bash">
−
{{#ev:youtube|Ao3eQbjYDH8}}
+
flutter pub add flutter_widget_from_html
−
</center>
+
flutter pub add url_launcher
 +
</syntaxhighlight>
  
−
'''<u>NOTA IMPORTANTE:</u>''' En el vídeo explico que se debe de poner el paquete a usar en la sección '''dependences''' del pubspec.yaml, pero lo pongo en la sección dev_dependences. Eso está mal. Dicha sección se emplea para añadir paquetes (nuevas funcionalidades) pero que sólo están disponibles mientras hagamos la aplicación (estemos en depuración). Si generamos la aplicación para ser instalada (lo veremos en puntos posteriores) dichos paquetes no son instalados si están en dev_dependences. Por lo tanto, <u>debéis de poner la referencia a los paquetes en la sección '''dependences''' del pubspec.yaml</u>.
+
: 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>'''
  
 +
* 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'''
  
−
<br />
+
:<syntaxhighlight lang="dart">
−
:<syntaxhighlight lang="java" enclose="div" highlight="3-4,21-24,32-34">
 
 
import 'package:flutter/material.dart';
 
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});
  
−
import 'package:flutter_html/flutter_html.dart' as flutterhtml;
+
  Future<void> _lanzarUrl(String url) async {
−
import 'package:url_launcher/url_launcher.dart';
+
    final uri = Uri.parse(url);
  
−
class TextosPage extends StatelessWidget{
+
    final lanzada = await launchUrl(uri);
−
  TextStyle _estiloTexto = new TextStyle(fontSize: 35.3,  // Recordar que el new no es obligatorio en Flutter
 
−
    backgroundColor: Colors.red,
 
−
    color: Colors.black12);
 
  
 +
    if (!lanzada) {
 +
      throw Exception('No se pudo abrir $url');
 +
    }
 +
  }
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('HTML')),
−
          title: Text('Textos con diferentes estilos'),
+
      body: HtmlWidget(
−
        ),
+
        '''
−
        body: Column(
+
             <b>Ejemplo de etiqueta HTML</b>
−
          children: <Widget> [
+
            <br>
−
             flutterhtml.Html(data: "<b>Ejemplo de etiqueta</b> html <a href = 'https://www.google.es'>Enlace</a>",
+
            <a href="https://flutter.dev">Página de Flutter</a>
−
                onLinkTap: (String? url, _,__,___) {
+
            ''',
−
                  _lanzar(url!);
+
        onTapUrl: ((url) {
−
                }),
+
          _lanzarUrl(url);
−
            Text('Texto con el estilo predefinido'),
+
          return true;
−
            Text('Texto con el estilo definido en un atributo de la clase', style: _estiloTexto),
+
        }),
−
            Text('Otro Texto con el estilo definido en un atributo de la clase', style: _estiloTexto), // Se puede dejar la comilla puesta por si queremos añadir nuevos textos
+
      ),
−
          ],
 
−
        )
 
 
     );
 
     );
 
   }
 
   }
−
  _lanzar(String url) async{
 
−
    await launch(url);
 
−
  }
 
−
 
 
}
 
}
  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* '''url_launcher''' trabaja actualmente con objetos '''Uri''' mediante la función '''launchUrl'''.
  
 
<br />
 
<br />
Línea 298: Línea 324:
 
==Botones==
 
==Botones==
  
−
* Más información en: https://flutter.dev/docs/development/ui/widgets/material#Buttons
+
* 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'''.
  
−
* En esta sección vamos a ver como añadir los diferentes tipos de botones que podemos emplear en Flutter.
+
: Además disponemos de botones especializados como '''IconButton''' y '''FloatingActionButton'''.
−
: No vamos a ver como gestionar el evento de Click sobre los mismos. Esto lo veremos en secciones  posteriores.
 
−
 
 
−
* Creamos una nueva página en la [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget#Estructura_de_p.C3.A1ginas carpeta pages] de nombre '''botones_page.dart'''
 
−
: En todas las páginas haremos lo mismo:
 
−
:* Importamos la librería de 'material' (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
 
−
:* Creamos una clase (BotonesPage) que derive de StatelessWidget (al hacerlo será necesario implementar el método build, ya os da un aviso el Android Studio).
 
−
 
 
−
[[Imagen:Flutter_dart_widget_18.JPG|300px|center]]
 
  
 
<br />
 
<br />
−
===Float Action Button===
 
  
−
* Más información en: https://api.flutter.dev/flutter/material/FloatingActionButton-class.html
+
===FloatingActionButton===
  
−
* Los FAB son botones con un aspecto circular que se sitúa 'por encima' del contenido de la página y que normalmente son reservados para implementar 'la acción principal' de la página en la que nos encontramos.
+
* Más información:
 +
  :* https://api.flutter.dev/flutter/material/FloatingActionButton-class.html
  
−
: Por ejemplo, en una pantalla de contactos, dicho botón sería empleado para dar de alta a nuevos contactos.
+
* 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.
  
−
* El Widget Scaffold ya dispone, [https://api.flutter.dev/flutter/material/Scaffold/Scaffold.html entre sus parámetros], la forma de añadir un FAB.
+
* '''Scaffold''' posee el parámetro '''floatingActionButton''':
−
: <u>Nota:</u> De hecho tenéis un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] para crear un Scaffold junto con un FAB.
 
−
:* Dentro del método build, de la página 'botones_page.dart', retornamos un widget Scaffold con un Float Action Button (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
 
  
 +
:<syntaxhighlight lang="dart">
 +
import 'package:flutter/material.dart';
  
−
* Al implementar el Snippet aparecerá una serie de código que tendréis que eliminar, ya que está pensado para ejecutarse en un StatefulWidget.
+
class BotonesPage extends StatelessWidget {
−
: <u>Nota:</u> Recordar cambiar el código del fichero '''app.dart''' e importar el fichero 'botones_page.dart' y cambiar la clase que se instancia en la propiedad home del widget MaterialApp.
+
  const BotonesPage({super.key});
−
 
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="11-16">
 
−
import 'package:flutter/material.dart';
 
  
−
class BotonesPage extends StatelessWidget{
 
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
 
     return Scaffold(
 
     return Scaffold(
−
       appBar: AppBar(
+
       appBar: AppBar(title: const Text('FloatingActionButton')),
−
        title: Text('appbarTitle'),
+
       body: const Center(child: Text('Ejemplo de FAB')),
−
      ),
 
−
       body: Text('Ejemplo de FAB'),
 
 
       floatingActionButton: FloatingActionButton(
 
       floatingActionButton: FloatingActionButton(
−
         onPressed: null, // Equivalente a disable. Tendríamos que enviar una función anónima para que aparezca el efecto de click
+
         onPressed: () {
−
         tooltip: 'Presiona para hacer algo',
+
          debugPrint('Botón pulsado');
−
         child: Icon(Icons.add),
+
        },
 +
         tooltip: 'Añadir',
 +
         child: const Icon(Icons.add),
 
       ),
 
       ),
 
       floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
 
       floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
Línea 351: Línea 373:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
:* Estamos instanciando la clase FloationActionButton.
+
* '''onPressed''' espera una función:
−
:* Es necesario implementar el método onPressed (mirar la definición del constructor donde el parámetro aparece definido como 'required').
+
  :* '''() {}''' habilita el botón aunque no haga nada.
−
::* Normalmente empleamos una función (puede ser anónima) donde escribimos el código que queremos que se ejecute cuando presionamos el botón. Por ejemplo, podemos poner '''() {}'''. De esta forma no hace nada pero podemos visualizar el efecto de Click sobre el botón.
+
  :* '''null''' deshabilita el botón.
−
::* Si escribimos null (como en el ejemplo) tiene el mismo efecto que si inhabilitármos el botón.
 
−
:* Tiene un parámetro child que espera recibir un Widget. Puede ser cualquier Widget, pero normalmente se pone un icono. Para ello podemos emplear la clase Icons.
 
−
:: Tenéis [https://fonts.google.com/icons?selected=Material+Icons en este enlace] todos los iconos predefinidos de la clase Icons.
 
−
:* Mediante el parámetro floatingActionButtonLocation especificamos donde colocar el botón. Para ello empleamos una serie de constantes definidas [https://api.flutter.dev/flutter/material/FloatingActionButtonLocation-class.html en la clase FloatingActionButtonLocation].
 
  
 +
* Para mensajes de depuración es preferible emplear '''debugPrint''' en lugar de utilizar indiscriminadamente '''print'''.
  
−
* <u>Nota:</u> Recordar que un FloatActionButton no es más que un Widget que podemos colocar como hicimos con los textos en la sección anterior, pero que normalmente se suele emplear el Scaffold para hacelo.
+
* El parámetro '''child''' contiene normalmente un '''Icon'''.
  
−
* Si necesitamos colocar varios FAB empleando las propiedades del Scaffold, podemos 'envolver' el Widget FAB del ejemplo anterior en un Row (o Column):
+
* Podemos emplear '''floatingActionButtonLocation''' para cambiar la posición del FAB.
−
:<syntaxhighlight lang="java" enclose="div" highlight="11">
+
 
−
import 'package:flutter/material.dart';
+
<br />
 +
 
 +
====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:
  
−
class BotonesPage extends StatelessWidget{
+
:<syntaxhighlight lang="dart">
−
  @override
 
−
  Widget build(BuildContext context) {
 
−
    return Scaffold(
 
−
      appBar: AppBar(
 
−
        title: Text('appbarTitle'),
 
−
      ),
 
−
      body: Text('Ejemplo de FAB'),
 
 
       floatingActionButton: Row(
 
       floatingActionButton: Row(
−
         mainAxisAlignment: MainAxisAlignment.center,
+
         mainAxisSize: MainAxisSize.min,
 
         children: [
 
         children: [
 
           FloatingActionButton(
 
           FloatingActionButton(
−
             onPressed: () { print ('Presionamos el botón ADD'); },
+
            heroTag: 'fabAdd',
−
             tooltip: 'Presiona para añadir',
+
             onPressed: () {
−
             child: Icon(Icons.add),
+
              debugPrint('Añadir');
 +
            },
 +
             tooltip: 'Añadir',
 +
             child: const Icon(Icons.add),
 
           ),
 
           ),
 +
          const SizedBox(width: 12),
 
           FloatingActionButton(
 
           FloatingActionButton(
−
             onPressed: () { print ('Presionamos el botón DELETE'); },
+
            heroTag: 'fabDelete',
−
             tooltip: 'Presiona para borrar',
+
             onPressed: () {
−
             child: Icon(Icons.delete),
+
              debugPrint('Eliminar');
 +
            },
 +
             tooltip: 'Eliminar',
 +
             child: const Icon(Icons.delete),
 
           ),
 
           ),
−
        ],
+
 
−
      ),
 
−
    );
 
−
  }
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* Los FAB utilizan internamente animaciones '''Hero'''. Los identificadores permiten diferenciar los distintos widgets.
  
−
* También podemos asociar un texto al icono con el parámetro 'label', pero para ello debemos de hacer uso de un FloatActionButton extendido de la forma:
+
<br />
−
:: FloatActionButton.extended
+
 
−
::* Hace uso del parámetro 'label' para asociar un texto al botón.
+
====FloatingActionButton.extended====
−
::* No tiene el parámetro child para asociar un icono. Ya dispone de un parámetro 'icon' para indicar el icono asociado.
+
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="14-20">
+
* Si queremos mostrar conjuntamente un icono y un texto podemos utilizar '''FloatingActionButton.extended''':
−
import 'package:flutter/material.dart';
 
  
−
class BotonesPage extends StatelessWidget{
+
:<syntaxhighlight lang="dart">
−
  @override
 
−
  Widget build(BuildContext context) {
 
−
    return Scaffold(
 
−
      appBar: AppBar(
 
−
        title: Text('appbarTitle'),
 
−
      ),
 
−
      body: Text('Ejemplo de FAB'),
 
−
      floatingActionButton: Row(
 
−
        mainAxisAlignment: MainAxisAlignment.center,
 
−
        children: [
 
 
           FloatingActionButton.extended(
 
           FloatingActionButton.extended(
−
            icon: Icon(Icons.add),
+
             onPressed: () {
−
             onPressed: () { print ('Presionamos el botón ADD'); },
+
              debugPrint('Añadir');
−
             tooltip: 'Presiona para añadir',
+
            },
−
             label: Text('Añadir'),
+
             icon: const Icon(Icons.add),
−
             backgroundColor: Colors.pink,
+
             label: const Text('Añadir'),
 +
             tooltip: 'Añadir elemento',
 
           ),
 
           ),
−
          FloatingActionButton(
+
 
−
            onPressed: () { print ('Presionamos el botón DELETE'); },
 
−
            tooltip: 'Presiona para borrar',
 
−
            child: Icon(Icons.delete),
 
−
          ),
 
−
        ],
 
−
      ),
 
−
    );
 
−
  }
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
  
 +
<br />
 
[[Imagen:Flutter_dart_widget_17.JPG|300px|center]]
 
[[Imagen:Flutter_dart_widget_17.JPG|300px|center]]
−
 
  
 
<br />
 
<br />
Línea 440: Línea 444:
 
===TextButton===
 
===TextButton===
  
−
* Más información en: https://api.flutter.dev/flutter/material/TextButton-class.html
+
* 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.
 +
 
 +
:<syntaxhighlight lang="dart">
 +
      body: Row(
 +
        children: [
 +
          TextButton(onPressed: () {}, child: const Text('Aceptar')),
 +
          TextButton(onPressed: null, child: const Text('Deshabilitado')),
 +
        ],
 +
      ),
 +
</syntaxhighlight>
  
−
* Son botones que no tienen borde y suelen usarse en cajas de diálogo, toobar o junto con otros contenidos pero separados de ellos para que se note que no forman parte del contenido que se está visualizando.
+
* Para modificar fácilmente el estilo podemos utilizar '''TextButton.styleFrom''':
−
: En las cajas de diálogos y cards (tarjetas) deberían estar todos agrupados en una esquina inferior.
 
  
−
: Fijarse que entre los parámetros que podemos emplear estarían la gestión de eventos con onPressed y onLongPress.
+
:<syntaxhighlight lang="dart">
−
: De forma obligatoria hay que enviarle un widget (child) que será lo que visualice. Normalmente un texto o un icono.
+
          TextButton(
 +
            onPressed: () {},
 +
            style: TextButton.styleFrom(
 +
              foregroundColor: Colors.white,
 +
              backgroundColor: Colors.blue,
 +
            ),
 +
            child: const Text('Púlsame'),
 +
          ),
  
−
: Para modificar el estilo se recomienda hacer uso del [https://api.flutter.dev/flutter/material/TextButton/styleFrom.html método static styleFrom].
+
</syntaxhighlight>
  
−
* Veamos un ejemplo:
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="18-20">
 
−
import 'package:flutter/material.dart';
 
  
−
class BotonesPage extends StatelessWidget{
 
−
  @override
 
−
  Widget build(BuildContext context) {
 
−
    return Scaffold(
 
−
      appBar: AppBar(
 
−
        title: Text('appbarTitle'),
 
−
      ),
 
−
      floatingActionButton: FloatingActionButton(
 
−
        onPressed: () { print ('Presionamos el botón DELETE'); },
 
−
        tooltip: 'Presiona para borrar',
 
−
        child: Icon(Icons.delete),
 
−
      ),
 
  
−
        body: Row(
 
−
        children: [
 
−
          TextButton(onPressed: () => {}, child: const Text('Botón sin borders')),
 
−
          TextButton(onPressed: null, child: const Icon (Icons.access_alarm), // Botón inhabilitado
 
−
                    style: TextButton.styleFrom(backgroundColor: Colors.red)),
 
−
        ]
 
−
      )
 
−
    );
 
−
  }
 
−
}</syntaxhighlight>
 
  
 +
<br />
 +
 +
====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.
 +
 +
:<syntaxhighlight lang="dart">
 +
          TextButton(
 +
            onPressed: () {},
 +
            style: ButtonStyle(
 +
              backgroundColor: WidgetStateProperty.all(Colors.blue),
 +
              foregroundColor: WidgetStateProperty.all(Colors.white),
 +
            ),
 +
            child: const Text('PÚLSAME'),
 +
          ),
 +
</syntaxhighlight>
  
 +
* El uso de '''WidgetStateProperty''' permite que una propiedad tenga valores diferentes dependiendo del estado del widget:
 +
  :* pressed.
 +
  :* hovered.
 +
  :* focused.
 +
  :* disabled.
 +
  :* selected.
 +
  :* etc.
  
 
<br />
 
<br />
 +
 
===IconButton===
 
===IconButton===
  
−
* Más información en: https://api.flutter.dev/flutter/material/IconButton-class.html
+
* Más información:
−
 
+
  :* https://api.flutter.dev/flutter/material/IconButton-class.html
−
* Son iconos que reaccionan al hacer click sobre ellos.
 
−
: Visualmente se tintan de un color al hacer click sobre ellos.
 
  
−
: Normalmente se utilizan en el AppBar (barra superior de iconos que conforman el menú principal).
+
* '''IconButton''' permite que un icono responda a una pulsación.
−
: Posee un parámetro icon donde se indica el icono que se va a utilizar.
 
  
−
* Veamos un ejemplo:
+
* Es muy habitual utilizarlo, por ejemplo, en un '''AppBar''':
−
:<syntaxhighlight lang="java" enclose="div" highlight="18-19">
 
−
import 'package:flutter/material.dart';
 
  
−
class BotonesPage extends StatelessWidget{
+
:<syntaxhighlight lang="dart">
−
  @override
 
−
  Widget build(BuildContext context) {
 
−
    return Scaffold(
 
 
       appBar: AppBar(
 
       appBar: AppBar(
−
         title: Text('appbarTitle'),
+
         title: const Text('Ejemplo'),
−
      ),
+
        actions: [
−
      floatingActionButton: FloatingActionButton(
+
          IconButton(
−
        onPressed: () { print ('Presionamos el botón DELETE'); },
+
            onPressed: () {
−
        tooltip: 'Presiona para borrar',
+
              debugPrint('Buscar');
−
        child: Icon(Icons.delete),
+
            },
 +
            tooltip: 'Buscar',
 +
            icon: const Icon(Icons.search),
 +
          ),
 +
        ],
 
       ),
 
       ),
  
−
        body: Row(
 
−
        children: [
 
−
          IconButton(icon: const Icon(Icons.account_tree),onPressed: () => {},color: Colors.yellow,
 
−
                                      iconSize: 50.5),
 
−
          TextButton(onPressed: () => {}, child: const Text('Botón sin borders')),
 
−
          TextButton(onPressed: null, child: const Icon (Icons.access_alarm), // Botón inhabilitado
 
−
                    style: TextButton.styleFrom(backgroundColor: Colors.red)),
 
−
        ]
 
−
      )
 
−
    );
 
−
  }
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* También puede utilizarse en cualquier otra parte de la interfaz:
 +
 +
:<syntaxhighlight lang="dart">
 +
          IconButton(
 +
            onPressed: () {},
 +
            iconSize: 40,
 +
            color: Colors.blue,
 +
            tooltip: 'Árbol de elementos',
 +
            icon: const Icon(Icons.account_tree),
 +
          ),
 +
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
===Otro botones===
+
===Otros botones de Material 3===
  
−
* Más información en:  
+
* Más información:
−
:* ElevatedButton: https://api.flutter.dev/flutter/material/ElevatedButton-class.html
+
  :* https://api.flutter.dev/flutter/material/FilledButton-class.html
−
:* OutlinedButton: https://api.flutter.dev/flutter/material/OutlinedButton-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.
  
−
* El '''ElevatedButton''' es un botón con un fondo rodeando el contenido. Se suele emplear para quitar el aspecto 'plano' que quedan con los TextButton, dando una cierta impresión de dimensión (parace como si estuvieran 'por encima' del contenido.
+
:<syntaxhighlight lang="dart">
 +
FilledButton(
 +
onPressed: () {},
 +
child: const Text('Guardar'),
 +
)
 +
</syntaxhighlight>
  
−
* El '''OutlinedButton''' es un TextButton con un border exterior. Se suele emplear para indicar una acción a realizar en la pantalla, pero no la principal.
+
* '''FilledButton.tonal''': variante de menor énfasis visual:
  
 +
:<syntaxhighlight lang="dart">
 +
FilledButton.tonal(
 +
onPressed: () {},
 +
child: const Text('Detalles'),
 +
)
 +
</syntaxhighlight>
  
 +
* '''ElevatedButton''': botón con elevación. En Material 3 ya no debe interpretarse simplemente como el antiguo botón principal con fondo de color.
  
−
[[Imagen:Flutter_dart_widget_18B.JPG|500px|center]]
+
:<syntaxhighlight lang="dart">
−
 
+
ElevatedButton(
−
+
onPressed: () {},
−
<small><center>Imagen obtenida de [https://docs.google.com/document/d/1yohSuYrvyya5V1hB6j9pJskavCdVq9sVeTqSoEPsWH0/edit este enlace]</center></small>
+
child: const Text('Elevated'),
 +
)
 +
</syntaxhighlight>
  
 +
* '''OutlinedButton''': botón con borde exterior y sin relleno:
  
 +
:<syntaxhighlight lang="dart">
 +
OutlinedButton(
 +
onPressed: () {},
 +
child: const Text('Cancelar'),
 +
)
 +
</syntaxhighlight>
  
 
<br />
 
<br />
Línea 549: Línea 598:
 
==Imágenes==
 
==Imágenes==
  
−
* Más información en:
+
* Más información:
−
:* https://api.flutter.dev/flutter/widgets/Image-class.html
+
  :* https://api.flutter.dev/flutter/widgets/Image-class.html
−
:* https://flutter.dev/docs/development/ui/assets-and-images
+
  :* 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.
  
−
* Creamos una nueva página en la [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget#Estructura_de_p.C3.A1ginas carpeta pages] de nombre '''imagenes_page.dart'''
+
<br />
−
: En todas las páginas haremos lo mismo:
 
−
:* Importamos la librería de 'material' (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
 
−
:* Creamos una clase de nombre ImagenesPage que derive de StatelessWidget (al hacerlo será necesario implementar el método build, ya os da un aviso el Android Studio).
 
−
:* Dentro del método build, retornamos un widget Scaffold (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
 
  
 +
===Cargando imágenes desde Internet===
  
 +
* La forma más sencilla es utilizar '''Image.network''':
  
 +
:<syntaxhighlight lang="dart">
 +
Image.network(
 +
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
 +
width: 200,
 +
height: 200,
 +
fit: BoxFit.cover,
 +
)
 +
</syntaxhighlight>
  
−
* Flutter suporta los siguientes tipos de imágenes: JPEG, WebP, GIF, WebP/GIF animados, PNG, BMP y WBMP
+
* También podemos utilizar un '''NetworkImage''':
−
: Las imágenes pueden ser cargadas localmente o desde un recurso de Internet.
+
 
 +
:<syntaxhighlight lang="dart">
 +
const Image(
 +
image: NetworkImage(
 +
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
 +
),
 +
height: 100,
 +
)
 +
</syntaxhighlight>
  
 +
* 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.
  
 
<br />
 
<br />
−
===Cargando imágenes desde Internet===
 
  
−
* En el siguiente ejemplo veremos como cargar una imagen de Internet de dos formas diferentes:
+
===Cargando imágenes desde assets===
−
:<syntaxhighlight lang="java" enclose="div" highlight="13,14">
+
 
−
import 'package:flutter/material.dart';
+
* Los recursos propios de Flutter deben registrarse normalmente en '''pubspec.yaml'''.
  
−
class ImagenesPage extends StatelessWidget{
+
* Es habitual crear un directorio:
−
  @override
+
:<syntaxhighlight lang="text">
−
  Widget build(BuildContext context) {
+
assets/
−
    // TODO: implement build
+
images/
−
    return Scaffold(
+
data/
−
        appBar: AppBar(
 
−
          title: Text('Ejemplos de imágenes'),
 
−
        ),
 
−
        body: Column(
 
−
          children: <Widget>[
 
−
            Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',scale: 3,),
 
−
            Image(image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),height: 100,)
 
−
          ],
 
−
        )
 
−
    );
 
−
  }
 
−
 
 
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
: El nombre '''assets''' es una convención, no una obligación.
  
−
* Las dos son equivalentes (menos en el tamaño que uno hace uso del parámetro scale y otro de height).
+
* Por ejemplo:
−
: La diferencia estriba en que Image.network devuelve un Widget mientras que [https://api.flutter.dev/flutter/painting/NetworkImage-class.html NetworkImage] devuelve un objeto que no se visualiza en pantalla, por eso debemos pasarlo como dato al parámetro 'image' de la clase Image.
 
  
−
<br />
+
:<syntaxhighlight lang="yaml">
−
===Cargando imágenes localmente===
+
flutter:
 +
assets:
 +
- assets/images/
 +
- assets/data/
 +
</syntaxhighlight>
 +
 
 +
'''<u>IMPORTANTE:</u>'''
  
−
* A nivel interno, las aplicaciones Android (y también IOS) disponen de unos 'directorios especiales' donde guardan los recursos que son 'compilados' con el proyecto y se referencian internamente mediante un identificador numérico.
+
* YAML utiliza la indentación para definir la estructura.
−
: En el caso de Android, estos recursos se guardan en '''/android/app/src/main/res'''
 
−
: En el caso de IOS, estos recursos se guardan en '''/ios/Runner/Assets.xcasetts'''
 
  
−
<gallery caption="Comparando datos numéricos" widths="600" heights="300px" perrow="2">
+
* Para evitar problemas entre sistemas operativos es recomendable utilizar nombres de archivo:
−
Image:Flutter_dart_widget_19.JPG | Directorio de recursos para Android.
+
  :* En minúsculas.
−
Image:Flutter_dart_widget_20.JPG | Directorio de recursos para IOS.
+
  :* Sin espacios.
−
</gallery>
+
  :* Con guiones bajos cuando sea necesario.
  
 +
: Ejemplo:
  
 +
:<syntaxhighlight lang="text">
 +
imagen_usuario.png
 +
</syntaxhighlight>
  
−
* Normalmente se guardan los iconos de la aplicación, como el icono que representa la aplicación en el teléfono, iconos de la barra de acción (ActionBar, lo que viene a ser el menú superior de opciones) e imágenes.
 
−
: El problema que tiene utilizar estos directorios es que:
 
−
:* No deja establecer una estructura de directorios para 'organizar' los recursos como queramos.
 
−
:* Son recursos que sólamente son visibles para el S.O. donde se encuentren (IOS / Android) por lo que nos obligaría a tener una copia de los recursos en los dos directorios.
 
  
−
* Flutter resuelve este problema permitiendo crear un directorio '''assets''' en el directorio raíz del proyecto Flutter.
+
* 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...)
 
: 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="Comparando datos numéricos" widths="600" heights="300px" perrow="2">
+
<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_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.
 
Image:Flutter_dart_widget_22.JPG | Dentro del directorio assets podemos crear las carpetas que queramos y copiar ahí nuestro recursos.
Línea 630: Línea 692:
 
* Vamos a descargar una imagen cualquiera de Internet y vamos a guardarla en un directorio /assets/imagenes/ del proyecto Flutter.
 
* 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)
 
: 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.
 
* Una vez descargada es necesario 'registrarla' en el proyecto.
Línea 643: Línea 709:
 
* 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.
 
* 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="java" enclose="div" highlight="15">
+
 
 +
:<syntaxhighlight lang="dart">
 +
Image.asset(
 +
'assets/images/sol.jpg',
 +
)
 +
</syntaxhighlight>
 +
 
 +
* Un ejemplo completo:
 +
 
 +
:<syntaxhighlight lang="dart">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
−
class ImagenesPage extends StatelessWidget{
+
class ImagenesPage extends StatelessWidget {
 +
  const ImagenesPage({super.key});
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('Ejemplos de imágenes')),
−
          title: Text('Ejemplos de imágenes'),
+
      body: Column(
−
        ),
+
        children: [
−
        body: Column(
+
          Image.network(
−
          children: <Widget>[
+
            'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
−
            Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',scale: 3),
+
            height: 150,
−
            Image.asset('assets/imagenes/sol.jpg')
+
          ),
−
          ],
+
          Image.asset('assets/images/sol.jpg'),
−
        )
+
        ],
 +
      ),
 
     );
 
     );
 
   }
 
   }
−
 
 
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Línea 671: Línea 747:
 
===Escalando las imágenes===
 
===Escalando las imágenes===
  
−
* Más información: https://api.flutter.dev/flutter/painting/BoxFit.html
+
* 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'''.
 +
 
 +
:<syntaxhighlight lang="dart">
 +
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,
 +
            ),
 +
          )
 +
        ],
 +
      ),
 +
    );
 +
  }
 +
}
 +
 
 +
</syntaxhighlight>
 +
 
 +
 
 +
 
 +
<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...
 +
 
 +
:<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>
  
  
Línea 678: Línea 819:
 
===Resolución en imágenes===
 
===Resolución en imágenes===
  
−
* Más información en:  
+
* Más información:
−
:* https://api.flutter.dev/flutter/dart-ui/FlutterView/devicePixelRatio.html
+
  :* https://api.flutter.dev/flutter/dart-ui/FlutterView/devicePixelRatio.html
−
:* https://www.mydevice.io/#compare-devices
+
  :* https://docs.flutter.dev/ui/assets/assets-and-images
−
:* https://esflutter.dev/docs/get-started/flutter-for/android-devs#d%C3%B3nde-guardo-mis-archivos-de-imagen-dependientes-de-la-resoluci%C3%B3n
 
  
 +
* Flutter trabaja normalmente con '''píxeles lógicos'''.
  
 +
* El '''devicePixelRatio''' indica cuántos píxeles físicos corresponden a un píxel lógico.
  
−
* Las imágenes deberían ajustarse a la resolución del dispositivo.
+
: Por ejemplo, si:
−
: Deberíamos tener diferentes versiones de la misma imagen (a no ser que trabajemos con imágenes vectoriales) con diferentes resoluciones y que Flutter cargue la imagen en función del dispositivo.
 
  
−
* Información de cómo se consigue en Android:
+
:<syntaxhighlight lang="text">
−
:* Tenemos que hablar de un concepto que es el de [https://wiki.cifprodolfoucha.es/index.php?title=PDM_Activities,_Layouts_e_M%C3%BAltiples_pantallas._Introduci%C3%B3n#P.C3.ADxeles_independentes_da_densidade.28dp_ou_dip.29_e_Escala_independente.28sp.29 densidad de pantalla y del concepto pixel independiente de la densidad].
+
devicePixelRatio = 3
−
:: La densidad es la cantidad de puntos por pulgada que tiene un dispositivo y esto depende de la resolución y del tamaño del dispositivo.
+
</syntaxhighlight>
−
:: Fijarse que un dispositivo tenga  un tamaño grande no implica que tenga una densidad grande, ya que puede tener una resolución pequeña, y lo mismo a la inversa.
 
−
:: Un pixel independiente de la densidad es un pixel virtual que se transforma en un pixel real en función de la densidad.
 
−
:: Así, cuando defino que algo tiene 3dp (o dip, pixel independiente de la densidad):
 
−
::* Si el dispositivo tiene una densidad de 160dpi (pixel por pulgada) => tiene 3 px de ancho
 
−
::* Si el dispositivo tiene una densidad de 320dpi (pixel por pulgada) => tiene 6 px de ancho (2x3)
 
−
::: ....
 
  
−
* Flutter hace algo parecido a Android y usa un parámetro que es el 'Pixel Ratio Device'. 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.
+
: un tamaño de 100 píxeles lógicos puede ocupar 300 píxeles físicos.
−
: Este dato lo obtiene a partir de la densidad del dispositivo.
+
 
 +
* En código de Widgets no debemos utilizar ya:
 +
 
 +
:<syntaxhighlight lang="dart">
 +
window.devicePixelRatio
 +
</syntaxhighlight>
 +
 
 +
: porque el objeto global '''window''' está obsoleto.
 +
 
 +
* Podemos obtener los datos mediante '''MediaQuery''':
  
−
[[Imagen:Flutter_dart_widget_23.JPG|500px|center]]
+
:<syntaxhighlight lang="dart">
−
<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>
+
final size = MediaQuery.sizeOf(context);
 +
final devicePixelRatio =
 +
MediaQuery.devicePixelRatioOf(context);
  
−
: 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.
+
debugPrint('Ancho lógico: ${size.width}');
 +
debugPrint('DevicePixelRatio: $devicePixelRatio');
 +
debugPrint(
 +
'Ancho físico aproximado: '
 +
'${size.width * devicePixelRatio}',
 +
);
 +
</syntaxhighlight>
  
−
: Flutter permite tener en la carpeta assets una imagen con el siguiente formato:
+
* También podemos acceder a la vista correspondiente al contexto:
−
:* /assets/carpeta/Mx/image.png
 
−
:* /assets/carpeta/Nx/image.png
 
−
:* /assets/carpeta/Ox/image.png
 
−
.......
 
−
:: Siendo M,N,O,...números que empiezan en 2.0 (3.0,4.0,...)
 
  
−
: Un dispositivo con una densidad de 1.8 cogerá la imagen que se encuentre en la carpeta /assets/2.0x/imagen.png
+
:<syntaxhighlight lang="dart">
−
: Vamos a ver un ejemplo utilizando esta imagen: https://pxhere.com/es/photo/741629
+
final view = View.of(context);
−
: He hecho tres versiones de la misma imagen para que se carguen en función de si la densidad es '1.0', '2.0' o tiene '2.5':
+
final devicePixelRatio = view.devicePixelRatio;
 +
</syntaxhighlight>
  
 +
: Para widgets suele ser preferible '''MediaQuery''', porque permite que la interfaz vuelva a construirse automáticamente si cambian las métricas necesarias.
  
−
<center>
+
<br />
−
{{#ev:youtube|5ECwN_GJ79g}}
 
−
</center>
 
  
 +
===Imágenes adaptadas a la densidad===
  
−
<syntaxhighlight lang="java" enclose="div" highlight="">
+
* Flutter puede seleccionar automáticamente una variante de una imagen dependiendo del '''devicePixelRatio'''.
−
import 'dart:ui';
+
: 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.
  
−
import 'package:flutter/material.dart';
+
[[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>
  
−
class ImagenesPage extends StatelessWidget{
+
: 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.
  
−
  @override
 
−
  Widget build(BuildContext context) {
 
−
    MediaQueryData queryData;
 
−
    queryData = MediaQuery.of(context);
 
−
    print('DevicePixelRatio:${window.devicePixelRatio}'); // Puedo obtenerlo de esta forma o haciendo uso del MediaQueryData: queryData.devicePixelRatio
 
−
    print('Ancho del dispositivo px:${queryData.size.width*queryData.devicePixelRatio}px');
 
−
    print('Imagen Pixeles con 150 de pixel ratio:${queryData.devicePixelRatio*150}px');
 
−
    print('Ancho del dispositivo pixel ratio:${queryData.size.width}');
 
−
    // TODO: implement build
 
−
    return Scaffold(
 
−
        appBar: AppBar(
 
−
          title: Text('Ejemplos de imágenes'),
 
−
        ),
 
−
        body:  Column(
 
−
          children: [
 
−
            Container(
 
−
                height: 250,
 
−
                width:queryData.size.width,  // Ocupa todo el ancho del dispositivo
 
−
                child: Image.asset('assets/imagenes/galaxia.jpg',fit: BoxFit.fill,)
 
−
            ),
 
−
            Container(
 
−
              height: 100,
 
−
              width: 150,
 
−
              color: Colors.red
 
−
            )
 
−
          ],
 
−
        ),
 
−
    );
 
−
  }
 
−
 
 
−
}
 
−
</syntaxhighlight>
 
  
  
 +
: Ejemplo:
  
−
<br />
+
:<syntaxhighlight lang="text">
 +
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
 +
</syntaxhighlight>
  
−
===Esperando a descargar===
+
* '''logo.png''' se considera la imagen base de resolución '''1.0x'''.
  
−
* Cuando las imágenes a descargar 'tardan' debido a que son obtenidas de Internet, visualmente no queda bien ya que los recursos locales aparecerán instantáneamente mientras que los recursos que vengan de Internet irán cargándose poco a poco y ocupando su espacio.
+
* 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.
  
−
: Flutter nos ofrece un Widget que permite mostrar un recurso local mientras se carga el recurso de Internet. Normalmente tendremos en pantalla algún gif animado indicando que se está descargando el recurso y cuando ya está descargado se sustituye por la imagen.
+
* Sólo tenemos que registrar la imagen principal o el directorio correspondiente en '''pubspec.yaml'''. Flutter detecta automáticamente las variantes de resolución.
  
−
* El widget es el FadeInImage: https://api.flutter.dev/flutter/widgets/FadeInImage-class.html
+
:<syntaxhighlight lang="yaml">
−
: Necesita obligatoriamente dos parámetros:
+
flutter:
−
:* placeholder: Sería el recurso local que queremos mostrar mientras se cargar el remoto.
+
assets:
−
:* image: La image a visualizar cuando ha terminado de descargarse.
+
- assets/images/
 +
</syntaxhighlight>
  
−
* Vamos a registrar una imagen animada que represente un tiempo de espera mientras se descarga la imagen de internet.
+
'''Ejemplo actualizado'''
−
: Sitio web para descarga de gif´s animados: https://www.gifsanimados.org/
 
−
: En el ejemplo que voy a explicar he descargado este gif: https://www.gifsanimados.org/img-reloj-imagen-animada-0141-82234.htm
 
−
: Dicha imagen está copiada en un directorio /assets/gifanimados/RelojCarga.gif
 
−
: <u>Recordar registrar dicho directorio en el archivo pubspec.yaml y reinstalar la aplicación con un restart.</u> commo vimos en la sección anterior.
 
  
−
:<syntaxhighlight lang="java" enclose="div" highlight="14-18">
+
:<syntaxhighlight lang="dart">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
−
class ImagenesPage extends StatelessWidget{
+
class ImagenesPage extends StatelessWidget {
 +
  const ImagenesPage({super.key});
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
     // TODO: implement build
+
     final size = MediaQuery.sizeOf(context);
 +
    final pixelRatio = MediaQuery.devicePixelRatioOf(context);
 +
 
 +
    debugPrint('DevicePixelRatio: $pixelRatio');
 +
 
 +
    debugPrint(
 +
      'Ancho físico aproximado: '
 +
      '${size.width * pixelRatio}px',
 +
    );
 +
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('Ejemplos de imágenes')),
−
          title: Text('Ejemplos de imágenes'),
+
      body: Column(
−
        ),
+
        children: [
−
        body: Column(
+
          SizedBox(
−
          children: <Widget>[
+
             height: 250,
−
             FadeInImage(placeholder: AssetImage('assets/gifsanimados/RelojCarga.gif'),
+
            width: double.infinity,
−
                        image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',scale: 2),
+
            child: Image.asset('assets/images/galaxia.jpg', fit: BoxFit.cover),
−
                        fadeInDuration: Duration(seconds: 2),
+
          ),
−
                        height: 200.0,
+
          const SizedBox(height: 100, width: 150),
−
            ),
+
        ],
−
            Text('Este texto no debería moverse al cambiar el tamaño de la imagen')
+
      ),
−
          ],
 
−
        )
 
 
     );
 
     );
 
   }
 
   }
−
 
 
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
−
* <u>Aclaraciones:</u>
+
===Esperando a descargar una imagen===
−
:* Tenemos que hacer uso de la [https://api.flutter.dev/flutter/painting/AssetImage-class.html clase AssetImage] y de la clase [https://api.flutter.dev/flutter/painting/NetworkImage-class.html NetworkImage] ya que el constructor de FadeInImage espera recibir objetos donde estén cargadas las imágenes, no Widget´s que es lo que obteníamos con Image.asset y Image.network.
 
−
:* El parámetro fadeInDuration espera recibir una instancia de la clase Duration. Este parámetro sirve para que la imagen remota vaya apareciendo poco a poco después de que sea descargada de Internet.
 
−
:* El parámetro height indica el alto de la imagen. Si no lo ponemos, la imagen animada no mide lo mismo que la imagen que se va a descargar, por lo que se produce un efecto de 'salto' del texto que está debajo de la imagen. Estableciendo un tamaño, la imagen, por defecto, se amplía hasta cubrir dicho espacio, por lo que no hay ese salto.
 
  
 +
* Cuando una imagen procede de Internet puede tardar cierto tiempo en descargarse.
  
−
* Disponemos de un constructor con nombre [https://api.flutter.dev/flutter/widgets/FadeInImage/FadeInImage.assetNetwork.html FadeInImage.assetNetwork] el cual espera recibir como parámetro de imagen local y imagen a descargar de Internet, un '''String''', en vez de un provider, siendo más cómodo de usar que el anterior.
+
* '''FadeInImage''' permite mostrar una imagen temporal mientras se obtiene la imagen remota.
  
−
<br />
+
* Más información:
 +
  :* https://api.flutter.dev/flutter/widgets/FadeInImage-class.html
  
−
==ListView==
+
:<syntaxhighlight lang="dart">
 +
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,
 +
            )
 +
</syntaxhighlight>
  
−
* Más información en: https://api.flutter.dev/flutter/widgets/ListView-class.html
+
* '''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:
  
−
* Creamos una nueva página en la [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget#Estructura_de_p.C3.A1ginas carpeta pages] de nombre '''listas_page.dart'''
+
:<syntaxhighlight lang="dart">
−
: En todas las páginas haremos lo mismo:
+
FadeInImage.assetNetwork(
−
:* Importamos la librería de 'material' (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
  placeholder: 'assets/images/cargando.gif',
−
:* Creamos una clase que derive de StatelessWidget (al hacerlo será necesario implementar el método build, ya os da un aviso el Android Studio).
+
  image: 'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
−
:* Dentro del método build, retornamos un widget Scaffold (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
  width: 200,
 +
  height: 200,
 +
  fit: BoxFit.cover,
 +
  placeholderFit: BoxFit.contain,
 +
)
 +
</syntaxhighlight>
  
 +
<br />
  
 +
==ListView==
  
−
* Permite mostrar un conjunto de Widget en forma de lista con scroll.
+
* Más información:
−
: Si consultáis los constructores podéis ver que existen varias posibilidades.
+
  :* https://api.flutter.dev/flutter/widgets/ListView-class.html
−
:* [https://api.flutter.dev/flutter/widgets/ListView/ListView.html ListView] => Sería equivalente a [https://wiki.cifprodolfoucha.es/index.php?title=ListView un ListView de Android]: Se recomienda su uso cuando tenemos un número limitado de elementos a mostrar, ya que lo que hace es cargar todos los elementos en memoria.
 
−
:* [https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html ListView.builder] => Sería equivalente a [https://wiki.cifprodolfoucha.es/index.php?title=PDM_RecyclerView_e_CardView RecyclerView] de Android: Se recomienda su uso cuando tengamos un número muy alto de elementos a mostrar, ya que sólo carga en memoria aquellos que visualmente se ven en pantalla.
 
−
:* ListView.separated => Igual que el ListView.builder pero permite establecer un Widget entre los elementos de la lista para separarlos unos de otros (normalmente se pone una línea horizontal). 
 
−
 
  
−
* La propiedad principal de un Widget ListView es el de '''children''' que espera recibir una lista de Widgets que serán lo que se van a visualizar.
+
* '''ListView''' permite visualizar una colección lineal de widgets con desplazamiento.
−
: Por lo tanto, como todo en Flutter es un Widget, podríamos poner cualquier cosa, como Column, Row, Image, y todos los Widget que iremos viendo posteriormente.
 
  
 +
* 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.
  
−
* <u>Nota:</u> Podemos cambiar fácilmente el sentido del scroll (horizontal / vertical) con la [https://api.flutter.dev/flutter/widgets/ScrollView/scrollDirection.html propiedad scrollDirection].
+
* Podemos modificar el sentido del desplazamiento:
  
 +
:<syntaxhighlight lang="dart">
 +
scrollDirection: Axis.horizontal
 +
</syntaxhighlight>
  
 +
: o:
 +
 +
:<syntaxhighlight lang="dart">
 +
scrollDirection: Axis.vertical
 +
</syntaxhighlight>
  
 
<br />
 
<br />
−
===Listas con todos los elementos cargados===
 
  
−
* Se crea haciendo uso del [https://api.flutter.dev/flutter/widgets/ListView/ListView.html constructor ListView].
+
===Listas con todos los elementos definidos===
  
 +
:<syntaxhighlight lang="dart">
 +
import 'package:flutter/material.dart';
  
−
: Veamos un ejemplo.
+
class ListasPage extends StatelessWidget {
−
[[Imagen:Flutter_dart_widget_24.JPG|300px|center]]
+
  const ListasPage({super.key});
  
−
<br />
 
−
:''' Archivo listas_page.dart''':
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="11-19">
 
−
import 'package:flutter/material.dart';
 
−
 
−
class ListasPage extends StatelessWidget{
 
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('Ejemplo de listas')),
−
          title: Text('Ejemplo de listas'),
+
      body: ListView(
−
        ),
+
        padding: const EdgeInsets.all(8),
−
        body: ListView(
+
        children: [
−
          children: [
+
          const Text('Elemento 1'),
−
            Text('Elemento 1'),
+
          const Text('Elemento 2'),
−
            Text('Elemento 2'),
+
          const Text('Elemento 3'),
−
            Text('Elemento 3'),
+
          Image.network(
−
            Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',scale: 3,),
+
            'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
−
            Text('Elemento 5'),
+
            height: 150,
−
            Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',scale: 3,),
+
          ),
−
          ],
+
          const Text('Elemento 5'),
−
        )
+
        ],
 +
      ),
 
     );
 
     );
 
   }
 
   }
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: <u>Nota:</u>
+
[[Image:Flutter_dart_widget_24.JPG|300px|center]]
−
:* Fijarse que en Android, al estar utilizando los Widget de tipo Material, las listas tienen un efecto al hacer scroll arriba-abajo de los elementos en la parte superior e inferior.
+
 
−
:* Veremos en la sección de diseño el uso de [https://api.flutter.dev/flutter/widgets/Padding-class.html padding] y marging, pero ya podéis probar en el ejemplo el parámetro padding.
+
 
  
  
 
<br />
 
<br />
 +
 
====ListTile====
 
====ListTile====
  
−
* Más información en: https://api.flutter.dev/flutter/material/ListTile-class.html
+
* Más información:
−
 
+
  :* https://api.flutter.dev/flutter/material/ListTile-class.html
  
−
* Asociado a las listas tenemos otro Widget que se suele usar, que es el ListTile.
+
* '''ListTile''' es un widget especialmente útil para representar elementos de listas.
−
: Este Widget tiene un alto fijo y dentro del mismo se suele visualizar un texto (puede llevar otro subtexto asociado), un icono en la parte izquierda ('''leading''' Widget) y otro icono en la parte derecha ('''trailing''' Widget). Indicar que esto es lo 'normal' pero cada una de estas propiedades espera recibir un Widget por lo que podríamos poner cualquier cosa :)
 
  
−
: Veamos un ejemplo.
+
* Entre sus propiedades habituales encontramos:
−
[[Imagen:Flutter_dart_widget_25.JPG|300px|center]]
+
  :* '''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.
  
−
<br />
+
:<syntaxhighlight lang="dart">
−
:''' Archivo listas_page.dart''':
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="13-25">
 
−
import 'package:flutter/material.dart';
 
−
 
 
−
class ListasPage extends StatelessWidget{
 
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('Ejemplo de listas')),
−
          title: Text('Ejemplo de listas'),
+
      body: ListView(
−
        ),
+
        children: [
−
        body: ListView(
+
          ListTile(
−
          children: [
+
            title: const Text('Elemento 1'),
−
            ListTile(
+
            subtitle: const Text('Texto aclaratorio del elemento 1'),
−
              title: Text('Elemento 1'), tileColor: Color.fromARGB(0, 255, 0, 0),
+
            leading: const Icon(Icons.accessibility),
−
              subtitle: Text('Texto aclaratorio elemento 1'),
+
            trailing: const Icon(Icons.more_vert),
−
              leading: Icon(Icons.accessibility),
+
             onTap: () {},
−
              trailing: Icon(Icons.more_vert),
+
          ),
−
             ),
+
          ListTile(
−
            ListTile(
+
            title: const Text('Elemento 2'),
−
              title: Text('Elemento 2'), tileColor: Color.fromARGB(0, 255, 0, 0),
+
            subtitle: const Text('Texto aclaratorio del elemento 2'),
−
              subtitle: Text('Texto aclaratorio elemento 2'),
+
            leading: const Icon(Icons.accessibility),
−
              leading: Icon(Icons.accessibility),
+
            trailing: const Icon(Icons.more_vert),
−
              trailing: Icon(Icons.more_vert),
+
            selected: true,
−
              selected: true,
+
             onTap: () {},
−
             ),
+
           ),
−
           ],
+
        ],
−
        )
+
      ),
 
     );
 
     );
 
   }
 
   }
−
}
+
 
 +
</syntaxhighlight>
 +
 
 +
'''<u>Nota:</u>'''
 +
 
 +
 
 +
: Para obtener rojo opaco deberíamos utilizar:
 +
:<syntaxhighlight lang="dart">
 +
Colors.red
 +
</syntaxhighlight>
 +
 
 +
: o:
 +
 
 +
:<syntaxhighlight lang="dart">
 +
const Color.fromARGB(255, 255, 0, 0)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
  
−
<u>Nota:</u> Dentro del ListTile podemos gestionar el evento de click sobre el ListTile. Para ello debemos emplear el parámetro onTap. Si no queremos añadir código pero queremos ver el efecto de click sobre el elemento, podemos poner: '''onTap: () {}'''
+
 
 +
 
 +
 
 
<br />
 
<br />
−
 
 
====Cargando elementos desde una función====
 
====Cargando elementos desde una función====
  
−
* Normalmente los datos de las listas vienen de diferentes fuentes de datos:
+
* Los datos de una lista pueden proceder de:
−
:* JSON
+
  :* Variables de la aplicación.
−
:* Archivos locales
+
  :* Archivos JSON.
−
:* Bases de datos
+
  :* Bases de datos.
 +
  :* Servicios web.
 +
  :* APIs REST.
 +
  :* etc.
  
−
: En el ejemplo siguiente voy a explicar como podríamos programar el código necesario.
+
[[Image:Flutter_dart_widget_26.JPG|300px|center]]
−
: Partimos que disponemos de unos datos. En el ejemplo los tenemos guardados en una variable local privada de nombre _datos, el cual tiene una lista de datos de tipo String (podría ser cualquier tipo de dato, como objetos de una clase definida por nosotros).
 
−
: Lo que haré será crear una función privada que devuelva una lista de Widgets. Cada elemento de la lista será un Widget con lo que va a visualizar la lista y cuyos datos son obtenidos de _datos.
 
−
 
 
−
[[Imagen:Flutter_dart_widget_26.JPG|300px|center]]
 
  
 
<br />
 
<br />
−
:<syntaxhighlight lang="java" enclose="div" highlight="4,14,19-32">
+
:<syntaxhighlight lang="dart">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
−
class ListasPage extends StatelessWidget{
+
class ListasPage extends StatelessWidget {
−
   final _datos = ['Elem1','Elem2','Elem3','Elem4']; // Tiene que ser final ya que estamos en un StateLessWidget
+
   const ListasPage({super.key});
  
−
   @override
+
   static const datos = ['Elem1', 'Elem2', 'Elem3', 'Elem4'];
−
  Widget build(BuildContext context) {
 
−
    // TODO: implement build
 
−
    return Scaffold(
 
−
        appBar: AppBar(
 
−
          title: Text('Ejemplo de listas'),
 
−
        ),
 
−
        body: ListView(
 
−
          children: _obtenerDatos(_datos)
 
−
        )
 
−
    );
 
−
  }
 
  
−
   List<Widget> _obtenerDatos(List<String>datos){ // Método privado
+
   List<Widget> _obtenerDatos(List<String> datos) {
−
     final lista = <Widget>[];                    // Es equivalente a new List<Widget>();
+
     final lista = <Widget>[];
  
−
     for (String dato in datos){
+
     for (final dato in datos) {
−
       lista.add(ListTile(
+
       lista.add(
−
        title: Text(dato), tileColor: Colors.blue,
+
        ListTile(
−
        subtitle: Text('Texto aclaratorio de $dato'),
+
          title: Text(dato),
−
        leading: Icon(Icons.accessibility),
+
          subtitle: Text('Texto aclaratorio de $dato'),
−
        trailing: 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)),
 +
    );
 
   }
 
   }
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
−
* Una mejora al código anterior es utilizar el [https://api.flutter.dev/flutter/dart-core/Iterable/map.html método map] asociado a las listas.
+
====Utilizando map====
−
: Este método es parecido a las funciones anónimas que vimos al trabajar con listas y mapas.
 
−
:* Llevará como parámetro un String ya que se corresponde con el tipo de dato de cada uno de los elementos de la lista
 
−
:* <u>Por cada elemento de la lista</u> ejecutará la función que definamos. Lo que hará dichoa función en el ejemplo es devolver un ListTile que representa el Widget de cada una de las filas.
 
−
:* El resultado de la llamada al métood map será un objecto de la [https://api.flutter.dev/flutter/dart-core/Iterable-class.html clase Iterable<ListTile>] (de ListTile porque va a ser lo que devolvamos en la función anónima del método map).
 
−
:* Como nuestra función tiene que devolver una lista de Widget (recordar que un ListTile es un Widget) necesitamos convertir la colección Iterable a una lista. Para ello sólo tenemos que llamar al [https://api.flutter.dev/flutter/dart-core/Iterable/toList.html método toList()]
 
  
−
:<syntaxhighlight lang="java" enclose="div" highlight="14,19-30">
+
* Una forma muy habitual de convertir una colección de datos en widgets consiste en utilizar el método '''map'''.
−
import 'package:flutter/material.dart';
 
  
−
class ListasPage extends StatelessWidget{
+
* '''map''' devuelve un '''Iterable'''. Cuando necesitamos una '''List''' podemos convertirlo mediante '''toList()'''.
−
  final _datos = ['Elem1','Elem2','Elem3','Elem4']; // Tiene que ser final ya que estamos en un StateLessWidget
 
  
−
  @override
+
:<syntaxhighlight lang="dart">
−
   Widget build(BuildContext context) {
+
   List<Widget> _obtenerDatos(List<String> datos) {
−
    // TODO: implement build
+
     return datos
−
     return Scaffold(
+
         .map(
−
         appBar: AppBar(
+
           (dato) => ListTile(
−
           title: Text('Ejemplo de listas'),
+
            title: Text(dato),
−
        ),
+
            subtitle: Text('Texto aclaratorio de $dato'),
−
        body: ListView(
+
            leading: const Icon(Icons.accessibility),
−
           children: _obtenerDatosConMap(_datos)
+
            trailing: const Icon(Icons.more_vert),
 +
           ),
 
         )
 
         )
−
    );
+
        .toList(growable: false);
 
   }
 
   }
 +
</syntaxhighlight>
  
−
  List<Widget> _obtenerDatosConMap(List<String>datos){  // Método privado
 
−
    final lista = <Widget>[];                          // Es equivalente a new List<Widget>();
 
  
−
    var datositerables = (datos.map((dato) => ListTile(  // map devuelve un objeto de la clase Iterable
 
−
      title: Text(dato), tileColor: Colors.blue,
 
−
      subtitle: Text('Texto aclaratorio de $dato con map'),
 
−
      leading: Icon(Icons.accessibility),
 
−
      trailing: Icon(Icons.more_vert),
 
−
    )));
 
  
−
    return datositerables.toList(growable: false);        // Convertimos el objeto de la clase Iterable a un objeto de la clase List e indicamos que no podemos añadir nuevos elementos a la lista
+
 
−
  }
+
* En Dart moderno es preferible utilizar:
−
}
+
 
 +
:<syntaxhighlight lang="dart">
 +
for (final dato in datos)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
: cuando no necesitamos modificar la variable del bucle.
  
−
<br />
 
−
====Exercicios propostos====
 
  
−
* Todos os exercicios se poden solucionar empregando forEach no conxunto de datos que conforman o contido das listas, pero é mellor empregar o método map.
 
  
  
−
* Partindo do seguinte arquivo dart:
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="38">
 
  
 +
<br />
  
−
import 'package:flutter/material.dart';
+
====Exercicios propostos====
  
 +
* Para estes exercicios podemos empregar '''map''' ou outras técnicas equivalentes de Dart.
  
 +
* Partimos dunha aplicación similar a:
  
−
void main(){
+
:<syntaxhighlight lang="dart">
−
 
+
import 'package:flutter/material.dart';
−
  runApp(
 
−
      MaterialApp(
 
−
        home: _MainPage(),
 
−
        debugShowCheckedModeBanner: false,
 
−
      )
 
−
  );
 
  
 +
void main() {
 +
  runApp(const MainApp());
 
}
 
}
  
−
class _MainPage extends StatelessWidget {
+
class MainApp extends StatelessWidget {
−
   const _MainPage({Key? key}) : super(key: key);
+
   const MainApp({super.key});
  
 
   @override
 
   @override
Línea 1069: Línea 1222:
 
     return MaterialApp(
 
     return MaterialApp(
 
       debugShowCheckedModeBanner: false,
 
       debugShowCheckedModeBanner: false,
−
       home: _MiScaffold(),
+
       home: const MainPage(),
 
     );
 
     );
 
   }
 
   }
 
}
 
}
  
−
 
+
class MainPage extends StatelessWidget {
−
class _MiScaffold extends StatelessWidget {
+
   const MainPage({super.key});
−
   const _MiScaffold({Key? key}) : super(key: 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: Text('Exercicios Listas'),
+
      body: const SizedBox(),
−
        ),
 
−
        body:
 
−
         
 
−
 
 
−
 
 
 
     );
 
     );
 
   }
 
   }
Línea 1094: Línea 1241:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
:Modifica a liña 42 e fai que body devolva un ListView definido nun arquivo dart diferente de nome exercicioX_listas.dart sendo X = 1, 2 ou 3
+
: Substitúe o '''body''' polo widget correspondente a cada exercicio.
 +
 
 +
 
  
−
* '''EXERCICIO 1''':
 
  
−
:Definindo no arquivo exercicio1_listas.dart un conxunto de datos:
+
<br />
  
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
+
* '''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},
 
  
 +
:<syntaxhighlight lang="dart">
 +
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,
 +
},
 
];
 
];
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
:Crea un StatelessWidget que devolva unha lista de textos, no que se apliquen os datos definidos na constante anterior.
+
: Crea un '''StatelessWidget''' que devolva unha lista de textos aplicando os datos definidos nos mapas anteriores.
 +
 
  
  
  
 
<br />
 
<br />
−
* '''EXERCICIO 2''':
 
  
−
:Definindo no arquivo exercicio2_listas.dart un conxunto de datos:
+
* '''EXERCICIO 2'''
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
−
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'},
 
  
 +
:<syntaxhighlight lang="dart">
 +
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',
 +
},
 
];
 
];
 +
</syntaxhighlight>
  
−
</syntaxhighlight>
+
: Crea unha lista na que cada elemento estea formado por un texto e unha imaxe.
  
 +
: Emprega:
 +
:* '''Row'''.
 +
:* '''Padding'''.
 +
:* '''FadeInImage.assetNetwork'''.
  
  
−
:Crea un StatelessWidget que devolva unha lista no que cada elemento da lista está formado por un texto e unha imaxe, no que se apliquen os datos definidos na constante anterior.
 
−
:Para crear o conxunto de Widget que conforman cada elemento da lista, podes empregar o Widget Row.
 
−
:Fai que a imaxe se cargue dentro dun FadeInImage (podes empregar o constructor FadeInImage.assetNetwork para non ten que empregar providers)
 
−
:Fai que estean separados os elementos da lista, empregando o Widget Padding (terás que envolver o Widget Row con el).
 
  
  
 
<br />
 
<br />
−
* '''EXERCICIO 3''':
 
  
−
:Definindo no arquivo exercicio1_listas.dart un conxunto de datos:
+
* '''EXERCICIO 3'''
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
−
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/100'},
 
−
  {'title' : 'Imagen 3', 'subtitle' : 'Aclaración imagen3',  'leading' : 'https://picsum.photos/100/100'},
 
−
  {'title' : 'Imagen 4', 'subtitle' : 'Aclaración imagen4',  'leading' : 'https://picsum.photos/100/100'},
 
  
 +
:<syntaxhighlight lang="dart">
 +
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',
 +
},
 
];
 
];
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
: Crea un '''StatelessWidget''' que devolva unha lista de '''ListTile''' aplicando os datos anteriores.
  
−
 
−
:Crea un StatelessWidget que devolva unha lista de ListTiles, no que se apliquen os datos definidos na constante anterior. Para a imaxe emprega un FadeInImage.
 
  
  
 
<br />
 
<br />
−
* '''EXERCICIO 4''':
 
−
 
−
:Definindo no arquivo exercicio4_listas.dart un conxunto de datos:
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
−
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/100', 'altura' : 50, 'anchura' : 70},
 
−
  { 'texto': 'Entrada 3',  'url' : 'https://picsum.photos/100/100', 'altura' : 80, 'anchura' : 90},
 
−
  { 'texto': 'Entrada 4',  'url' : 'https://picsum.photos/100/100', 'altura' : 120, 'anchura' : 60},
 
−
  { 'texto': 'Entrada 5',  'url' : 'https://picsum.photos/100/100', 'altura' : 70, 'anchura' : 80},
 
  
 +
* '''EXERCICIO 4'''
  
 +
:<syntaxhighlight lang="dart">
 +
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,
 +
},
 
];
 
];
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
: Crea una lista en la que cada elemento contenga un texto y una imagen.
 +
: La imagen debe ocupar las dimensiones indicadas sin deformarse.
 +
 +
'''<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>
  
  
−
:Crea un StatelessWidget que devolva unha lista no que cada elemento da lista está formado por un texto no que aparece a cadea: 'Entrada:texto_do_mapa', e unha imaxe ao seu carón (emprega a Widget row) coa URL indicada e o tamaño indicado (altura e anchura). A imaxe debe cubrir todo o espazo indicado, pero sen distorsionarse.
+
:* 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:
−
: '''Nota importante:''' Cando intentedes asinarlle o width e height á imaxe, vos dará un erro de tipo. Lembrada que a clase double [https://api.flutter.dev/flutter/dart-core/double/tryParse.html ten un método] para convertir algo a double. No caso de que non poida, deberá devolver o valor 10 (mirar [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_DART_variables_-_tipos_de_datos#Valores_nulos o visto neste curso cando traballamos con valores nulos].
+
:<syntaxhighlight lang="dart">
 +
final altura = double.tryParse(e['altura']?.toString() ?? '') ?? 0.0;
 +
final anchura = double.tryParse(e['anchura']?.toString() ?? '') ?? 0.0;
 +
</syntaxhighlight>
  
  
−
:'''<u>Variante:</u>''': Modifica o exercicio anterior e fai que a imaxe se cargue empregando un FadeInImage.
 
  
  
−
<br />
 
−
* '''EXERCICIO 5''':
 
  
−
:Definindo no arquivo exercicio5_listas.dart un conxunto de datos:
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
−
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}
 
−
  },
 
−
  {
 
−
    'texto' : 'Un botón bonito',
 
−
    'elemento' : {'icon' : Icons.clear, 'texto' : 'Botón 3', 'cor' : Colors.blue}
 
−
  },
 
−
  {
 
−
    'texto' : 'Otro botón más feo',
 
−
    'elemento' : {'icon' : Icons.error, 'texto' : 'Botón 4', 'cor' : Colors.red}
 
−
  }
 
−
];
 
−
</syntaxhighlight>
 
  
  
 +
<br />
  
−
:Crea un StatelessWidget que devolva unha lista no que cada elemento da lista está formado por texto coa cadea texto do mapa, ao seu carón (emprega un row) un Icon Button no que se cargue a icona indicada no mapa e ao seu carón, un texto có texto indicado no mapa e cunha cor indicada no mapa.
+
* '''EXERCICIO 5'''
−
[[Imagen:Flutter_componentes_listas_ex_5.jpg|400px|center]]
 
  
 +
:<syntaxhighlight lang="dart">
 +
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,
 +
},
 +
},
 +
];
 +
</syntaxhighlight>
  
 +
: Crea una lista en la que cada elemento contenga:
 +
:* El texto principal.
 +
:* Un '''IconButton'''.
 +
:* El texto almacenado en '''elemento''' con el color indicado.
  
 
<br />
 
<br />
−
=====Solucións Exercicios Propostos=====
 
  
−
* '''Exercicio 1'''
+
=====Soluciones a Ejercicios Propuestos=====
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
−
import 'package:flutter/material.dart';
 
  
−
final datosExercicio1 = <Map<String,dynamic>>[
+
* '''Ejercicio 1'''
−
  {'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},
 
  
−
];
+
:<syntaxhighlight lang="dart">
 +
import 'package:flutter/material.dart';
  
 
class Exercicio1Listas extends StatelessWidget {
 
class Exercicio1Listas extends StatelessWidget {
−
   Exercicio1Listas({Key? key, required this.datos}) : super(key: key);
+
   const Exercicio1Listas({super.key, required this.datos});
  
−
   final List<Map<String,dynamic>>datos;
+
   final List<Map<String, dynamic>> datos;
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
 
     return ListView(
 
     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),
 +
    );
 +
  }
 +
}
 +
 +
</syntaxhighlight>
 +
 +
<br />
 +
 +
* '''Ejercicio 2'''
 +
 +
:<syntaxhighlight lang="dart">
 +
import 'package:flutter/material.dart';
 +
 +
class Exercicio2Listas extends StatelessWidget {
 +
  const Exercicio2Listas({super.key, required this.datos});
  
−
      children: datos.map((e) => Text(
+
  final List<Map<String, dynamic>> datos;
−
          'Texto:${e['texto']}', style: TextStyle(color: e['color'],fontSize: e['size']),
 
−
        )
 
−
      ).toList(growable: false)
 
  
 +
  @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 />
  
 +
* '''Ejercicio 3'''
 +
 +
:<syntaxhighlight lang="dart">
 +
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),
 +
    );
 +
  }
 +
}
 +
 +
</syntaxhighlight>
  
 
<br />
 
<br />
−
* '''Exercicio 2'''
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
−
import 'package:flutter/material.dart';
 
  
−
final datosExercicio2 = <Map<String,dynamic>>[
+
* '''Ejercicio 4'''
−
  {'texto' : 'Imagen 1', 'url' : 'https://picsum.photos/100/100'},
+
: Partimos de que no podemos asegurar que los datos vengan correctamente del servicio...
−
  {'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'},
 
  
−
];
+
:<syntaxhighlight lang="dart">
 +
import 'package:flutter/material.dart';
  
−
class Exercicio2Listas extends StatelessWidget {
+
class Exercicio4Listas extends StatelessWidget {
−
   Exercicio2Listas({Key? key, required this.datos}) : super(key: key);
+
   const Exercicio4Listas({super.key, required this.datos});
  
−
   final List<Map<String,dynamic>>datos;
+
   final List<Map<String, dynamic>> datos;
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
 
     return ListView(
 
     return ListView(
 +
      children: datos
 +
          .map((e) {
 +
            final altura = double.tryParse(e['altura']?.toString() ?? '') ?? 0.0;
 +
            final anchura = double.tryParse(e['anchura']?.toString() ?? '') ?? 0.0;
  
−
      children: datos.map((e) =>
+
             return Row(
−
          Padding(
 
−
             padding: const EdgeInsets.all(8.0),
 
−
            child: Row(
 
 
               children: [
 
               children: [
−
                 Text(e['texto']),
+
                 Expanded(child: Text('Texto: ${e['texto']}')),
−
                 FadeInImage.assetNetwork(
+
                 Image.network(
−
                    placeholder: 'assets/images/centro.jpg',
+
                  e['url']?.toString() ?? 'https://picsum.photos/200/300',
−
                    image: e['url'],
+
                  width: anchura,
−
                    width: 100,
+
                  height: altura,
−
                    height: 100,
+
                  fit: BoxFit.cover,
−
                 )
+
                 ),
 
               ],
 
               ],
−
             ),
+
             );
−
           )
+
           })
−
      ).toList(growable: false)
+
          .toList(growable: false),
−
 
 
 
     );
 
     );
 
   }
 
   }
−
 
 
}
 
}
  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
 +
* '''Exercicio 5'''
  
−
<br />
+
:<syntaxhighlight lang="dart">
−
* '''Exercicio 3'''
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
−
final datosExercicio3 = <Map<String,dynamic>>[
+
class Exercicio5Listas extends StatelessWidget {
−
  {'title' : 'Imagen 1', 'subtitle' : 'Aclaración imagen1', 'leading' : 'https://picsum.photos/100/100'},
+
   const Exercicio5Listas({super.key, required this.datos});
−
   {'title' : 'Imagen 2', 'subtitle' : 'Aclaración imagen2',  'leading' : 'https://picsum.photos/100/100'},
 
−
  {'title' : 'Imagen 3', 'subtitle' : 'Aclaración imagen3',  'leading' : 'https://picsum.photos/100/100'},
 
−
  {'title' : 'Imagen 4', 'subtitle' : 'Aclaración imagen4',  'leading' : 'https://picsum.photos/100/100'},
 
−
 
 
−
];
 
  
−
class Exercicio3Listas extends StatelessWidget {
+
   final List<Map<String, dynamic>> datos;
−
  Exercicio3Listas({Key? key, required this.datos}) : super(key: key);
 
−
 
 
−
   final List<Map<String,dynamic>>datos;
 
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
 
     return ListView(
 
     return ListView(
 +
      children: datos
 +
          .map((e) {
 +
            final elemento = e['elemento'] as Map<String, dynamic>;
  
−
      children: datos.map((e) =>
+
            return Row(
−
          ListTile(
+
              children: [
−
            title: Text('Título ${e['title']}'),
+
                Expanded(child: Text(e['texto'] as String)),
−
            subtitle: Text('Título ${e['subtitle']}'),
+
                IconButton(
−
            leading:
+
                  onPressed: () {},
−
                 FadeInImage
+
                  icon: Icon(elemento['icon'] as IconData),
−
                  (
+
                ),
−
                   placeholder: AssetImage('assets/images/sol.jpg'),
+
                 Text(
−
                   image: NetworkImage(e['leading']),
+
                   elemento['texto'] as String,
 +
                   style: TextStyle(color: elemento['cor'] as Color),
 
                 ),
 
                 ),
−
           )
+
              ],
−
 
+
            );
−
      ).toList(growable: false)
+
           })
−
 
+
          .toList(growable: false),
 
     );
 
     );
 
   }
 
   }
−
 
 
}
 
}
  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
 +
 +
===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.
  
−
<br />
+
: Resulta especialmente útil cuando:
−
* '''Exercicio 4'''
+
:* La lista tiene muchos elementos.
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
+
:* Los datos se generan dinámicamente.
−
import 'package:flutter/material.dart';
+
:* No queremos construir inicialmente todos los widgets.
 +
 
 +
 
 +
[[Image:Flutter_dart_widget_27.JPG|500px|center]]
  
−
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/100', 'altura' : 50, 'anchura' : 70},
 
−
  { 'texto': 'Entrada 3',  'url' : 'https://picsum.photos/100/100', 'altura' : 80, 'anchura' : 90},
 
−
  { 'texto': 'Entrada 4',  'url' : 'https://picsum.photos/100/100', 'altura' : 120, 'anchura' : 60},
 
−
  { 'texto': 'Entrada 5',  'url' : 'https://picsum.photos/100/100', 'altura' : 70, 'anchura' : 80},
 
  
 +
* 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.
  
−
class Exercicio4Listas extends StatelessWidget {
+
:<syntaxhighlight lang="dart">
−
Exercicio4Listas({Key? key, required this.datos}) : super(key: key);
+
import 'package:flutter/material.dart';
  
−
final List<Map<String,dynamic>>datos;
+
class ListasPage extends StatelessWidget {
 +
  const ListasPage({super.key});
  
−
@override
+
  static const datos = [
−
Widget build(BuildContext context) {
+
    'Elem1',
−
   return ListView(
+
    'Elem2',
 +
    'Elem3',
 +
    'Elem4',
 +
    'Elem5',
 +
    'Elem6',
 +
    'Elem7',
 +
    'Elem8',
 +
    'Elem9',
 +
   ];
  
−
    children: datos.map((e) =>
+
  @override
−
        Row(
+
  Widget build(BuildContext context) {
−
          children: [
+
    return Scaffold(
−
            Text('Texto:${e['texto']}'),
+
      appBar: AppBar(title: const Text('ListView.builder')),
−
            Image.network(
+
      body: ListView.builder(
−
              e['url'],
+
        itemCount: datos.length,
−
              width: double.tryParse(e['anchura'].toString()) ?? 10,
+
        itemBuilder: (context, index) {
−
              height: double.tryParse(e['altura'].toString()) ?? 10,
+
           debugPrint('Construyendo índice $index');
−
              fit: BoxFit.cover,
 
−
            ),
 
−
           ],
 
−
        )
 
  
−
    ).toList(growable: false)
+
          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: () {},
 +
          );
 +
        },
 +
      ),
 +
    );
 +
  }
 
}
 
}
  
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* No es necesario utilizar:
 +
 +
:<syntaxhighlight lang="dart">
 +
datos.elementAt(index)
 +
</syntaxhighlight>
 +
 +
: cuando disponemos de una '''List'''. Es más directo utilizar:
  
 +
:<syntaxhighlight lang="dart">
 +
datos[index]
 +
</syntaxhighlight>
  
 
<br />
 
<br />
−
* '''Exercicio 5'''
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
−
import 'package:flutter/material.dart';
 
  
−
final datosExercicio5 = <Map<String,dynamic>>[
+
===Separando elementos de las listas===
−
  {
 
−
    '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}
 
−
  },
 
−
  {
 
−
    'texto' : 'Un botón bonito',
 
−
    'elemento' : {'icon' : Icons.clear, 'texto' : 'Botón 3', 'cor' : Colors.blue}
 
−
  },
 
−
  {
 
−
    'texto' : 'Otro botón más feo',
 
−
    'elemento' : {'icon' : Icons.error, 'texto' : 'Botón 4', 'cor' : Colors.red}
 
−
  }
 
−
];
 
  
−
class Exercicio5Listas extends StatelessWidget {
+
====Widget Divider====
−
Exercicio5Listas({Key? key, required this.datos}) : super(key: key);
 
  
−
final List<Map<String,dynamic>>datos;
+
* Más información:
 +
  :* https://api.flutter.dev/flutter/material/Divider-class.html
  
−
@override
+
* Podemos introducir manualmente '''Divider''' dentro de una lista pequeña.
−
Widget build(BuildContext context) {
 
−
  return ListView(
 
  
−
    children: datos.map((e) =>
+
:<syntaxhighlight lang="dart">
−
        Row(
+
ListView(
−
          children: [
+
children: const [
−
            Text('Texto:${e['texto']}'),
+
ListTile(
−
            IconButton(
+
title: Text('Elemento 1'),
−
                onPressed: () => {},
+
),
−
                icon: Icon(e['elemento']['icon'])
+
Divider(),
−
            ),
+
ListTile(
−
            Text('${e['texto']}',
+
title: Text('Elemento 2'),
−
              style: TextStyle(
+
),
−
                color: e['elemento']['cor']
+
],
−
              ),
+
)
−
            )
+
</syntaxhighlight>
−
          ],
 
−
        )
 
  
−
    ).toList(growable: false)
+
<br />
  
−
  );
+
====ListView.separated====
−
}
 
  
−
}
+
* Para listas creadas dinámicamente es más apropiado '''ListView.separated'''.
  
 +
:<syntaxhighlight lang="dart">
 +
ListView.separated(
 +
itemCount: datos.length,
 +
itemBuilder: (context, index) {
 +
return ListTile(
 +
title: Text(datos[index]),
 +
);
 +
},
 +
separatorBuilder: (context, index) {
 +
return const Divider(
 +
thickness: 1,
 +
);
 +
},
 +
)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* '''separatorBuilder''' recibe el índice correspondiente y puede devolver cualquier widget.
 +
 +
: Por ejemplo:
  
 +
:<syntaxhighlight lang="dart">
 +
separatorBuilder: (context, index) {
 +
return const SizedBox(
 +
height: 10,
 +
);
 +
}
 +
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
===Listas con los elementos cargados dinámicamente. ListView.builder===
+
==FutureBuilder==
  
−
* Se consiguen empleando el constructor [https://api.flutter.dev/flutter/widgets/ListView/ListView.builder.html ListView.builder].
+
Más información:
 +
:* https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
  
 +
En algunas ocasiones los datos que queremos mostrar '''no están disponibles inmediatamente'''.
  
−
* <u>Se debe aplicar siempre que el número de elementos de la lista sea elevado</u>.
+
: Por ejemplo:
−
: Básicamente son necesarios dos parámetros en su constructor:
+
:* Una petición a una API de Internet.
−
:* itemcount: Indicamos el número de elementos de la lista o de la fuente de datos que empleemos. <u>No es obligatorio ponerlo</u> pero el View no tendría información de cuantos elementos tiene la lista y intentaría mostrar elementos (por su posición) que no existen en la fuente de datos.
+
:* La lectura de un archivo.
−
:* itembuilder: Es una función que espera recibir dos parámetros, un BuildContext y un número que representa la posición de la lista que se va a visualizar. Esta función debe devolver el Widget que queremos visualizar para la posición indicada.
+
:* Una consulta a una base de datos.
 +
:* Una operación que tarda cierto tiempo en finalizar.
  
−
: Es decir, este Widget va 'construyendo' los elementos de la lista a medida que son visualizados en la pantalla.
+
Dart representa este tipo de operaciones mediante objetos de tipo '''Future'''.
−
: Veamos un ejemplo:
 
  
−
[[Imagen:Flutter_dart_widget_27.JPG|500px|center]]
+
: Un Future representa un valor que '''estará disponible en algún momento en el futuro'''.
  
−
:<syntaxhighlight lang="java" enclose="div" highlight="4-6,15,19-28">
+
Por ejemplo, el siguiente método devuelve un Future que dentro de cuatro segundos contendrá una lista de cadenas:
−
import 'package:flutter/material.dart';
 
  
−
class ListasPage extends StatelessWidget{
+
:<syntaxhighlight lang="dart">
−
  final _datos = ['Elem1','Elem2','Elem3','Elem4','Elem5','Elem6',    // Tiene que ser final ya que estamos en un StateLessWidget
+
Future<List<String>> _obtenerDatosInternet() {
−
                  'Elem7','Elem8','Elem9','Elem10','Elem11','Elem12',
+
final datos = <String>[
−
                  'Elem13','Elem14','Elem15','Elem16','Elem17','Elem18',];
+
'Valor 1',
 +
'Valor 2',
 +
'Valor 3',
 +
];
  
−
  @override
+
return Future.delayed(
−
  Widget build(BuildContext context) {
+
  const Duration(seconds: 4),() => datos,);
−
    // TODO: implement build
+
}
−
    return Scaffold(
+
</syntaxhighlight>
−
        appBar: AppBar(
 
−
          title: Text('Ejemplo de listas'),
 
−
        ),
 
−
        body: _cargarListaBuilder()
 
−
    );
 
−
  }
 
  
−
  Widget _cargarListaBuilder() {
+
También podemos escribirlo utilizando '''async''' y '''await''':
  
−
    return ListView.builder(
+
:<syntaxhighlight lang="dart">
−
        itemCount: _datos.length,
+
Future<List<String>> _obtenerDatosInternet() async {
−
        itemBuilder: (buildcontext, pos){
+
await Future<void>.delayed(
−
          print (pos);    // Por consola podemos ver como se van cargando los nuevos elementos a medida que son visualizados
+
const Duration(seconds: 4),
−
          return _elementoLista(pos);
+
);
−
        }
 
−
    );
 
−
  }
 
−
 
 
−
  Widget _elementoLista(pos) {
 
−
    var elemLista = _datos.elementAt(pos);
 
−
    return ListTile(
 
−
      title: Text(elemLista),
 
−
      subtitle: Text('Texto aclaratorio de $elemLista con map'),
 
−
      leading: Icon(Icons.accessibility),
 
−
      trailing: Icon(Icons.more_vert),
 
−
      onTap: () {},
 
−
    );
 
−
  }
 
  
 +
return [
 +
'Valor 1',
 +
'Valor 2',
 +
'Valor 3',
 +
];
 
}
 
}
 +
</syntaxhighlight>
  
 +
: En ambos casos el resultado es el mismo:
  
 +
:<syntaxhighlight lang="dart">
 +
Future<List<String>>
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
−
* Otro parámetro que podemos utilizar es [https://api.flutter.dev/flutter/widgets/ScrollPhysics-class.html physics] el cual determina la forma en cómo se va a realizar el scroll.
+
===El widget FutureBuilder===
−
: Podéis [https://medium.com/flutter-community/flutter-listview-and-scrollphysics-a-detailed-look-7f0912df2754 consultar en este enlace] los diferentes tipos de scroll de que disponemos.
 
  
 +
'''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.
  
 
<br />
 
<br />
−
====Exercicios Propostos====
 
  
−
* Crea unha nova versión dos exercicios feitos no apartado anterior, empregando un ListView.builder no seu lugar.
+
===Estados de ConnectionState===
−
:Para facer que os elementos da lista ocupen máis alto e poder ter un scroll, podes envolver cada compoñente da lista có Widget SizedBox e darlle un valor á propiedade height.
+
 
 +
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:
 +
 
 +
:<syntaxhighlight lang="dart">
 +
ConnectionState.waiting
 +
</syntaxhighlight>
 +
 
 +
: y:
  
−
+
:<syntaxhighlight lang="dart">
 +
ConnectionState.done
 +
</syntaxhighlight>
  
 
<br />
 
<br />
  
−
===Separando elementos de las listas===
+
===Ejemplo sencillo con StatelessWidget===
  
−
* En el caso de las listas creadas con [https://api.flutter.dev/flutter/widgets/ListView/ListView.html ListView] en el que se van a cargar todos los elementos, se puede emplear el [https://api.flutter.dev/flutter/material/Divider-class.html widget Divider].
+
Como todavía no hemos estudiado '''StatefulWidget''', podemos utilizar FutureBuilder directamente dentro del método '''build''' de un StatelessWidget.
−
* Si los elementos se van a crear a medida que son visualizados se puede hacer uso del constructor [https://api.flutter.dev/flutter/widgets/ListView/ListView.separated.html ListView.separated].
 
  
 +
Emularemos que estamos obteniendo una lista de datos desde Internet:
  
−
<br />
 
−
====Widget Divider====
 
  
−
* [https://api.flutter.dev/flutter/material/Divider-class.html widget Divider]
+
<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>
  
−
: Veamos un ejemplo:
 
−
[[Imagen:Flutter_dart_widget_30.JPG|300px|center]]
 
  
 +
<br />
  
−
:<syntaxhighlight lang="java" enclose="div" highlight="29">
+
:<syntaxhighlight lang="dart">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
−
class ListasPage extends StatelessWidget{
+
class ListasFuturePage extends StatelessWidget {
−
   final _datos = ['Elem1','Elem2','Elem3','Elem4']; // Tiene que ser final ya que estamos en un StateLessWidget
+
   const ListasFuturePage({super.key});
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('FutureBuilder')),
−
          title: Text('Ejemplo de listas'),
+
      body: FutureBuilder<List<String>>(
−
         ),
+
         future: _obtenerDatosInternet(),
−
         body: ListView(
+
         builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
−
             children: _obtenerDatos(_datos)
+
          // 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: () {},
 +
              );
 +
            },
 +
          );
 +
         },
 +
      ),
 
     );
 
     );
 
   }
 
   }
  
−
   List<Widget> _obtenerDatos(List<String>datos){ // Método privado
+
   Future<List<String>> _obtenerDatosInternet() async {
−
     final lista = <Widget>[];                    // Es equivalente a new List<Widget>();
+
     await Future<void>.delayed(const Duration(seconds: 4));
−
 
 
−
    for (String dato in datos){
 
−
      lista.add(ListTile(
 
−
        title: Text(dato), tileColor: Colors.red,
 
−
        subtitle: Text('Texto aclaratorio de $dato'),
 
−
        leading: Icon(Icons.accessibility),
 
−
        trailing: Icon(Icons.more_vert),
 
−
      ));
 
−
      lista.add(Divider(color: Colors.blue, thickness: 5, height: 15.0,));
 
−
    }
 
  
−
     return lista;
+
     return ['Valor 1', 'Valor 2', 'Valor 3'];
 
   }
 
   }
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
<br />
 
<br />
−
====ListView.separated====
 
  
−
* Constructor [https://api.flutter.dev/flutter/widgets/ListView/ListView.separated.html ListView.separated].
+
===¿Qué sucede en el ejemplo?===
−
: Básicamente es igual que el ListView.builder, pero además permite definir un Widget en el parámetro separatorBuilder que visualiza un widget de separación entre elementos de la lista.
+
 
−
: Dentro del parámetro, definimos una función anónima que recibe entre sus parámetros la posición del elemento de la lista. Por lo tanto, con este constructor podríamos tener diferentes separadores en función de la posición del elemento a visualizar.
+
Cuando Flutter ejecuta el método '''build''', se crea el Future:
  
 +
:<syntaxhighlight lang="dart">
 +
future: _obtenerDatosInternet(),
 +
</syntaxhighlight>
  
−
: Veamos un ejemplo:
+
En ese momento todavía no tenemos los datos.
  
−
[[Imagen:Flutter_dart_widget_31.JPG|300px|center]]
+
: Por ello:
  
−
:<syntaxhighlight lang="java" enclose="div" highlight="21-25">
+
:<syntaxhighlight lang="dart">
−
import 'package:flutter/material.dart';
+
snapshot.connectionState ==
 +
ConnectionState.waiting
 +
</syntaxhighlight>
 +
 
 +
: será verdadero y mostraremos:
  
−
class ListasPage extends StatelessWidget{
+
:<syntaxhighlight lang="dart">
−
  final _datos = ['Elem1','Elem2','Elem3','Elem4','Elem5','Elem6',    // Tiene que ser final ya que estamos en un StateLessWidget
+
const CircularProgressIndicator()
−
    'Elem7','Elem8','Elem9','Elem10','Elem11','Elem12',
+
</syntaxhighlight>
−
    'Elem13','Elem14','Elem15','Elem16','Elem17','Elem18',];
 
  
−
  @override
+
Aproximadamente cuatro segundos después finaliza el Future.
−
  Widget build(BuildContext context) {
 
−
    // TODO: implement build
 
−
    return Scaffold(
 
−
        appBar: AppBar(
 
−
          title: Text('Ejemplo de listas con ListView.separated'),
 
−
        ),
 
−
        body: _obtenerLista()
 
−
    );
 
−
  }
 
  
−
  Widget _obtenerLista(){
+
FutureBuilder recibe el resultado y vuelve a ejecutar su '''builder'''.
  
−
    return ListView.separated(
+
Los datos estarán disponibles en:
−
        itemBuilder: (buildContext, index) => _elementoLista(index),
 
−
        separatorBuilder: (buildContext, index) => Divider(thickness: 10,color: Colors.green,),
 
−
        itemCount: _datos.length);
 
−
  }
 
  
−
  Widget _elementoLista(pos) {
+
:<syntaxhighlight lang="dart">
−
    var elemLista = _datos.elementAt(pos);
+
snapshot.data
−
    return ListTile(
+
</syntaxhighlight>
−
      title: Text(elemLista),
 
−
      subtitle: Text('Texto aclaratorio de $elemLista'),
 
−
      leading: Icon(Icons.accessibility),
 
−
      trailing: Icon(Icons.more_vert),
 
−
      onTap: () {},
 
−
    );
 
−
  }
 
  
 +
En nuestro caso tendrán el valor:
  
−
}
+
:<syntaxhighlight lang="dart">
 +
[
 +
'Valor 1',
 +
'Valor 2',
 +
'Valor 3',
 +
]
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
Finalmente construimos un '''ListView.builder''' y mostramos los elementos.
  
 
<br />
 
<br />
−
====Exercicios propostos====
 
  
−
* Modifica os exercicios do punto '''5.1.3 Exercicios propostos''' e fai que cada elemento da lista esté separado por:
+
===¿Por qué utilizamos snapshot.data ?? []?===
−
:* Opción 1: Un Divider de cor azul, cun grosor de 2 e unha altura de 4.
+
 
−
:* Opción 2: Un SizedBox cunha altura de 10.
+
La propiedad '''snapshot.data''' puede ser nula.
−
:* Opción 3: Un texto coa cadea 'texto elemento X' e unha icona (h t t ps://icons.iconarchive.com/icons/fatcow/farm-fresh/32/link-break-icon.png) ao seu carón (emprega un row)
+
 
 +
: Su tipo realmente es:
 +
 
 +
:<syntaxhighlight lang="dart">
 +
List<String>?
 +
</syntaxhighlight>
 +
 
 +
Por ello podemos escribir:
 +
 
 +
:<syntaxhighlight lang="dart">
 +
final datos =
 +
snapshot.data ?? const <String>[];
 +
</syntaxhighlight>
 +
 
 +
El operador:
  
 +
:<syntaxhighlight lang="dart">
 +
??
 +
</syntaxhighlight>
  
−
<br />
+
: indica que:
  
−
===FutureBuilder===
+
:* Si '''snapshot.data''' no es null, se utiliza su valor.
 +
:* Si '''snapshot.data''' es null, se utiliza una lista vacía.
  
−
* Más información en: https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
+
: Es equivalente conceptualmente a:
  
 +
:<syntaxhighlight lang="dart">
 +
List<String> datos;
  
−
* Creamos una nueva página en la [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget#Estructura_de_p.C3.A1ginas carpeta pages] de nombre '''listas_future_page.dart'''
+
if (snapshot.data != null) {
−
: En todas las páginas haremos lo mismo:
+
datos = snapshot.data!;
−
:* Importamos la librería de 'material' (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
} else {
−
:* Creamos una clase que derive de StatelessWidget (al hacerlo será necesario implementar el método build, ya os da un aviso el Android Studio) de nombre ListasFuturePage
+
datos = [];
−
:* Dentro del método build, retornamos un widget Scaffold (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
}
 +
</syntaxhighlight>
  
 +
: pero la primera forma es más compacta.
  
 +
<br />
  
−
* Cuando los datos vienen a través de un Future (es decir, cargados de forma asíncrona, normalmente a través de Internet) tendría que ser necesario esperar a que dichos datos fueran obtenidos para poder mostrarlos gráficamente.
+
===Tipar FutureBuilder===
−
: Si lo hacemos de la forma 'tradicional' el hilo principal de la aplicación tendría que 'esperar' dando la impresión de que la aplicación está bloqueada.
 
−
: Para evitarlo se hace uso del FutureBuilder.
 
−
: Básicamente lo que hace es esperar a que vayan llegando los datos desde un Future y una vez llegan retorna un Widget (en nuestro ejemplo debe retorna un ListView, pero podría ser cualquier Widget) con los datos cargados.
 
  
 +
Es recomendable indicar qué tipo de dato va a devolver el Future.
  
−
* Un FutureBuilder va a tener 3 estados:
+
: En nuestro caso:
−
:* Cuando está obteniendo los datos.
 
−
:* Cuando ya obtuvo los datos.
 
−
:* Cuando se ha producido algún error.
 
  
 +
:<syntaxhighlight lang="dart">
 +
FutureBuilder<List<String>>
 +
</syntaxhighlight>
  
−
* Dentro del FutureBuilder vamos a emplear una serie de parámetros, algunos de ellos son:
+
Gracias a ello Flutter sabe que el snapshot contiene:
−
:* '''future''': Aquí debemos especificar la función que devuelve un Future. Normalmente será la función que accede a Internet a buscar los datos.
 
−
:* '''initialData''': Parámetro opcional en el que podemos indicar los valores iniciales que va a tener la 'fuente de datos' a partir de la cual vamos a construir los widget´s. Es una lista ([]) y cada elemento de la lista tiene que ser del mismo tipo de elemento que devuelve el future...Es decir, si la función del future va a devolver una lista de String, los elementos que van a ir en initialData son de tipo String.
 
−
:* '''builder''': Este es el más importante y es donde se va a llamar cuabndo ya dispone de los datos. Dependiendo de los tipos de datos que vayamos a buscar a Internet, tendremos un objeto de la clase [https://api.flutter.dev/flutter/widgets/AsyncSnapshot-class.html AsyncSnapshot] asociado al mismo tipo de dato.
 
−
:: Dentro de este objeto AsyncSnapshot tendremos acceso a las siguientes propiedades:
 
−
::* bool hasData: Tenemos datos.
 
−
::* bool hasError: Error en la obtención de datos.
 
  
 +
:<syntaxhighlight lang="dart">
 +
AsyncSnapshot<List<String>>
 +
</syntaxhighlight>
  
 +
Esto evita tener que realizar conversiones de tipos posteriormente.
  
−
* Veamos un ejemplo:
+
'''Ejemplo:'''
  
−
<gallery caption="Empleando un FutureBuilder" widths="600" heights="300px" perrow="2">
+
:<syntaxhighlight lang="dart">
−
Image:Flutter_dart_widget_28.JPG | FutureBuilder cargando los datos iniciales a la espera de que se carguen de Internet el resto de datos.
+
FutureBuilder<List<String>>(
−
Image:Flutter_dart_widget_29.JPG | Una vez pasados 4 segundos, aparecen los datos (simulando como si vinieran de Internet)
+
future: _obtenerDatosInternet(),
−
</gallery>
+
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:
  
−
'''Archivo listas_future_page.dart'''
+
:<syntaxhighlight lang="dart">
−
:<syntaxhighlight lang="java" enclose="div" highlight="23-34">
+
FutureBuilder<List<String>>(
−
import 'package:flutter/material.dart';
+
future: _obtenerDatosInternet(),
 +
builder: (context, snapshot) {
 +
// snapshot.data es List<String>?
 +
},
 +
)
 +
</syntaxhighlight>
  
−
class ListasFuturePage extends StatelessWidget {
+
: Las dos formas son correctas.
  
−
  @override
+
<br />
−
  Widget build(BuildContext context) {
 
−
    return _listaWidget();
 
−
  }
 
−
  Widget _listaWidget(){
 
  
−
    return Scaffold(
+
===Utilizando initialData===
−
        appBar: AppBar(
 
−
          title: Text('Ejemplo de listas con FutureBuilder'),
 
−
        ),
 
−
        body: _cargarLista()
 
−
    );
 
  
−
  }
+
FutureBuilder dispone de un parámetro opcional denominado '''initialData'''.
−
  Widget _cargarLista() {
 
  
−
//    _obtenerDatosInternet().then((value) { print(value); _datos.addAll(value); }); // Si lo hacemos de esta forma, se muestra la lista sin datos ya que no han sido descargados
+
Permite indicar unos datos que estarán disponibles antes de que termine el Future.
  
−
    return FutureBuilder(               // FutureBuilder es un Widget
+
:<syntaxhighlight lang="dart">
−
      future: _obtenerDatosInternet(),  // En este parámetro llamamos a algo que devuelva un Future,
+
FutureBuilder<List<String>>(
−
      initialData: ['Inicial1','Inicial2'] ,                 // Opcional. Son los datos iniciales. Se reeemplazarían por los traídos de Internet
+
future: _obtenerDatosInternet(),
−
      builder: (buildContext, AsyncSnapshot<List<dynamic>> snapShoot) {
+
initialData: const [
 +
'Inicial 1',
 +
'Inicial 2',
 +
],
 +
builder: (context, snapshot) {
  
−
        print (snapShoot.data);      // Podemos imprimir la información que llega al builder del Future
+
final datos =
−
        return ListView(
+
    snapshot.data ?? const <String>[];
−
          children: _cargarElementosLista(snapShoot.data!),    // Si queremos convertirlo a List<String> => snapShoot.data!.cast<String>()
 
−
        );
 
−
      },
 
  
 +
return ListView.builder(
 +
  itemCount: datos.length,
 +
  itemBuilder: (context, index) {
 +
    return ListTile(
 +
      title: Text(datos[index]),
 
     );
 
     );
 +
  },
 +
);
 +
 +
},
 +
)
 +
</syntaxhighlight>
 +
 +
En este caso inicialmente veremos:
 +
 +
:<syntaxhighlight lang="text">
 +
Inicial 1
 +
Inicial 2
 +
</syntaxhighlight>
  
−
  }
+
Cuando finalice el Future serán sustituidos por:
 +
 
 +
:<syntaxhighlight lang="text">
 +
Valor 1
 +
Valor 2
 +
Valor 3
 +
</syntaxhighlight>
  
−
  List<Widget> _cargarElementosLista(List<dynamic> datosLista){
+
'''Importante:'''
−
    final List<Widget> elemLista = [];
 
  
−
    for(String dato in datosLista){
+
Si utilizamos '''initialData''', el hecho de que:
−
      elemLista..add(ListTile(title: Text(dato),leading: Icon (Icons.add),onTap: () {},))
 
−
                ..add(Divider());
 
−
    }
 
  
−
    return elemLista;
+
:<syntaxhighlight lang="dart">
 +
snapshot.hasData
 +
</syntaxhighlight>
  
−
  }
+
: sea verdadero '''no implica que el Future haya terminado'''.
  
−
  Future<List<String>> _obtenerDatosInternet() {
+
: Puede estar mostrando todavía los datos iniciales.
−
    var datosLocales = <String>[];
 
−
    datosLocales.add("Valor 1");
 
−
    datosLocales.add("Valor 2");
 
−
    datosLocales.add("Valor 3");
 
  
−
    return Future.delayed(Duration(seconds: 4),() => datosLocales);
+
Si necesitamos saber si la operación continúa ejecutándose debemos consultar:
  
−
  }
+
:<syntaxhighlight lang="dart">
−
}
+
snapshot.connectionState
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<br />
  
−
*'''Nota:''' En el ejemplo anterior, si no ponemos el '''initialData''' o si lo ponemos e indicamos el tipo de dato, podemos definir el AsyncSnapshot con el tipo de dato que va a recibir sin necesidad de realizar un cast:
+
===Una consideración importante sobre este ejemplo===
−
:<syntaxhighlight lang="java" enclose="div" highlight="23-34">
 
−
    return FutureBuilder(              // FutureBuilder es un Widget
 
−
      future: _obtenerDatosInternet(),  // En este parámetro llamamos a algo que devuelva un Future,
 
−
      initialData: <String>['Inicial1','Inicial2'] ,                  // Opcional. Si no lo ponemos, ya sabe por el future, que va a recibir un List<String>. Si lo ponemos hasData() siempre devolverá true. Podríamos poner también initialData: <String>[]
 
−
      builder: (buildContext, AsyncSnapshot<List<String>> snapShoot) {
 
  
−
        print (snapShoot.data);      // Podemos imprimir la información que llega al builder del Future
+
El ejemplo anterior es válido para aprender el funcionamiento de '''FutureBuilder''', pero hay un detalle que debemos conocer.
−
        return ListView(
 
−
          children: _cargarElementosLista(snapShoot.data!),   
 
−
        );
 
−
      },
 
  
−
    );
+
Estamos creando el Future dentro del método '''build''':
 +
 
 +
:<syntaxhighlight lang="dart">
 +
future: _obtenerDatosInternet(),
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
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.
 +
 +
 +
 +
<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.
 
'''<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.
 
: Emprega o Widget que visualiza unha imaxe local mientras se descarga a imaxe de Internet.
 
: Emprega o Widget que visualiza unha imaxe local mientras se descarga a imaxe de Internet.
 +
 +
 +
  
  
Línea 1782: Línea 2199:
 
===Cargando datos desde un archivo JSON===
 
===Cargando datos desde un archivo JSON===
  
−
* Más información en:
+
* Más información:
−
:* https://flutter.dev/docs/development/data-and-backend/json
+
  :* https://docs.flutter.dev/data-and-backend/serialization/json
−
:* https://api.flutter.dev/flutter/services/rootBundle.html
+
  :* 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.
 +
 +
<br />
  
−
* Los archivos [https://es.wikipedia.org/wiki/JSON#:~:text=JSON%20(acr%C3%B3nimo%20de%20JavaScript%20Object,para%20el%20intercambio%20de%20datos. JSON] son archivos de texto que suelen ser empleados para intercambio de información entre sistemas operativos heterogéneos, siendo usados en la actualidad para obtener información a través de Internet de diferentes servicios.
 
−
: Esta parte está relacionada con el [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_DART_Clases#Empleando_Mapas empleo de Mapas para crear instancias de una clase].
 
  
  
Línea 1794: Línea 2220:
 
{{#ev:youtube|K4mp-MTjAwM}}
 
{{#ev:youtube|K4mp-MTjAwM}}
 
</center>   
 
</center>   
 +
 +
  
 
<br />
 
<br />
−
'''Archivo /data/datos_listas.json'''
+
====Archivo JSON====
−
:<u>Nota:</u> Recordar registrar el archivo en '''pubspec.yaml''' y reiniciar la aplicación.
+
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
+
'''Archivo: assets/data/datos_listas.json'''
 +
 
 +
:<syntaxhighlight lang="json">
 +
{
 +
"elementos": [
 
{
 
{
−
  "elementos": [
+
"titulo": "Elemento 1",
−
    {
+
"texto": "Aclaración elemento 1",
−
      "titulo" : "Elemento 1",
+
"imagen": "https://picsum.photos/id/1/200/300"
−
      "texto" : "Aclaración elemento 1",
+
},
−
      "imagen" : "https://picsum.photos/id/1/200/300"
+
{
−
    },
+
"titulo": "Elemento 2",
−
    {
+
"texto": "Aclaración elemento 2",
−
      "titulo" : "Elemento 2",
+
"imagen": "https://picsum.photos/id/2/200/300"
−
      "texto" : "Aclaración elemento 2",
+
},
−
      "imagen" : "https://picsum.photos/id/2/200/300"
+
{
−
    },
+
"titulo": "Elemento 3",
−
    {
+
"texto": "Aclaración elemento 3",
−
      "titulo" : "Elemento 3",
+
"imagen": "https://picsum.photos/id/3/200/300"
−
      "texto" : "Aclaración elemento 3",
+
}
−
      "imagen" : "https://picsum.photos/id/3/200/300"
+
]
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 4",
 
−
      "texto" : "Aclaración elemento 4",
 
−
      "imagen" : "https://picsum.photos/id/4/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 5",
 
−
      "texto" : "Aclaración elemento 5",
 
−
      "imagen" : "https://picsum.photos/id/5/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 6",
 
−
      "texto" : "Aclaración elemento 6",
 
−
      "imagen" : "https://picsum.photos/id/6/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 7",
 
−
      "texto" : "Aclaración elemento 7",
 
−
      "imagen" : "https://picsum.photos/id/7/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 8",
 
−
      "texto" : "Aclaración elemento 8",
 
−
      "imagen" : "https://picsum.photos/id/8/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 9",
 
−
      "texto" : "Aclaración elemento 9",
 
−
      "imagen" : "https://picsum.photos/id/9/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 10",
 
−
      "texto" : "Aclaración elemento 10",
 
−
      "imagen" : "https://picsum.photos/id/10/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 11",
 
−
      "texto" : "Aclaración elemento 11",
 
−
      "imagen" : "https://picsum.photos/id/11/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 12",
 
−
      "texto" : "Aclaración elemento 12",
 
−
      "imagen" : "https://picsum.photos/id/12/200/300"
 
−
    },
 
−
    {
 
−
      "titulo" : "Elemento 13",
 
−
      "texto" : "Aclaración elemento 13",
 
−
      "imagen" : "https://picsum.photos/id/13/200/300"
 
−
    }
 
−
  ]
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* Registramos el directorio:
  
 +
:<syntaxhighlight lang="yaml">
 +
flutter:
 +
assets:
 +
- assets/data/
 +
- assets/images/
 +
</syntaxhighlight>
  
−
'''Archivo /lib/models/elemento_lista_model.dart'''
+
<br />
−
<u>Nota:</u> Fijarse que hago uso de un [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_DART_Clases#Constructores_Factory constructor '''factory'''].
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
−
class ElementoListaModel{
 
  
−
  String titulo;
+
====Modelo====
−
  String texto;
 
−
  String imagen;
 
  
−
  ElementoListaModel({required this.titulo,required this.texto,required this.imagen});
+
'''Archivo: lib/models/elemento_lista_model.dart'''
  
−
  factory ElementoListaModel.fromMap(Map<String,dynamic>mapa) =>
+
:<syntaxhighlight lang="dart">
−
    ElementoListaModel(titulo: mapa['titulo'],texto:  mapa['texto'],imagen:  mapa['imagen']);
+
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,
 +
    );
 +
  }
 
}
 
}
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
'''<u>Nota:</u>'''
  
 +
* Es preferible llamar al constructor:
  
−
'''Archivo /lib/providers/datos_lista_provider.dart'''
+
:<syntaxhighlight lang="dart">
−
:<u>Nota:</u> Fijarse que la clase es privada. Para acceder a ella hago uso de un objeto (datosListaProvider) definido en el mismo archivo.
+
fromJson
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
+
</syntaxhighlight>
−
import 'dart:convert';
 
  
 +
: 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.
 +
 +
<br />
 +
 +
====Provider para cargar el archivo====
 +
 +
'''Archivo: lib/providers/datos_lista_provider.dart'''
 +
 +
:<syntaxhighlight lang="dart">
 +
import 'dart:convert' show jsonDecode;
 
import 'package:flutter/services.dart' show rootBundle;
 
import 'package:flutter/services.dart' show rootBundle;
−
import 'package:holamundo/models/elemento_lista_model.dart';   // De todo el paquete sólo queremos usar la clase rootBundle
+
import '../models/elemento_lista_model.dart';
  
−
class _DatosListaProvider{       // Creamos la clase privada. Usamos una instancia concreta en este mismo archivo
+
class DatosListaProvider {
 +
  const DatosListaProvider();
  
 +
  Future<List<ElementoListaModel>> cargarDatos() async {
 +
    final contenido = await rootBundle.loadString(
 +
      'assets/data/datos_listas.json',
 +
    );
  
−
  Future<List<ElementoListaModel>> cargarDatos() async {
+
     final json = jsonDecode(contenido) as Map<String, dynamic>;
−
     final resp = await rootBundle.loadString('data/datos_listas.json');    // Devuelve un Future
 
−
    Map<String,dynamic> mapa = json.decode(resp);
 
  
−
     List<dynamic> datosLista = mapa['elementos'];
+
     final elementos = json['elementos'] as List<dynamic>;
−
    List<ElementoListaModel> datosDevolver = [];
 
−
    for (Map<String,dynamic> elem in datosLista){
 
−
      datosDevolver.add(ElementoListaModel.fromMap(elem));
 
−
    }
 
  
−
     return datosDevolver;
+
     return elementos
 +
        .map((elemento) {
 +
          return ElementoListaModel.fromJson(elemento as Map<String, dynamic>);
 +
        })
 +
        .toList(growable: false);
 
   }
 
   }
 
}
 
}
−
final datosListaProvider = new _DatosListaProvider();
 
  
 +
const datosListaProvider = DatosListaProvider();
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* Podemos crear directamente:
  
−
<u>'''Nota:'''</u> Recordar que vimos [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_DART_Clases#Empleando_Mapas en la UD2 Clases : Empleando mapas] como crear un objeto a partir de un Map y comenté que [https://quicktype.io/ existe un sitio Web: quicktype.io] que permite generar el código para poder crear un mapa a partir de un JSON, <u>QUE ES LO QUE ESTÁ PROGRAMADO EN EL CÓDIGO ANTERIOR</u>.
+
:<syntaxhighlight lang="dart">
 +
const DatosListaProvider()
 +
</syntaxhighlight>
  
 +
* En este ejemplo la clase tampoco necesita ser privada.
  
 +
<br />
  
 +
====Visualizando los datos====
  
−
'''Archivo /lib/src/listas_json_page.dart'''
+
'''Archivo: lib/screens/listas_json_page.dart'''
−
:<syntaxhighlight lang="java" enclose="div" highlight="">
+
 
 +
:<syntaxhighlight lang="dart">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
−
import 'package:holamundo/models/elemento_lista_model.dart';
 
−
import 'package:holamundo/providers/datos_lista_provider.dart';
 
  
−
class ListasJsonPage extends StatelessWidget{
+
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
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
−
    // TODO: implement build
 
−
    // datosListaProvider.cargarDatos();  -- Para probar que funcionaba....
 
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('Listas con datos desde JSON')),
−
          title: Text('Listas con datos desde JSON'),
+
      body: FutureBuilder<List<ElementoListaModel>>(
−
         ),
+
         future: _future,
−
         body: _listaWidget()
+
         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>[];
−
  }
 
  
−
  Widget _listaWidget(){
+
          if (datos.isEmpty) {
 +
            return const Center(child: Text('No hay datos.'));
 +
          }
  
−
    return FutureBuilder(
+
          return ListView.separated(
−
        future: datosListaProvider.cargarDatos(),
+
            itemCount: datos.length,
−
        initialData: [],
+
            separatorBuilder: (context, index) => const Divider(),
−
        builder: (buildContext, AsyncSnapshot<List<dynamic>> data){
+
            itemBuilder: (context, index) {
 +
              final modelo = datos[index];
  
−
          List<Widget>elementosListaWidget = [];
+
              return ListTile(
−
          List<dynamic>datosasync = data.data ?? [];
+
                leading: FadeInImage.assetNetwork(
−
          datosasync.forEach((element) {
+
                  placeholder: 'assets/images/cargando.gif',
−
            elementosListaWidget.add(_elementoListaWidget(element));
+
                  image: modelo.imagen,
−
          });
+
                  width: 60,
−
          return ListView(
+
                  height: 60,
−
             children: elementosListaWidget,
+
                  fit: BoxFit.cover,
 +
                ),
 +
                title: Text(modelo.titulo),
 +
                subtitle: Text(modelo.texto),
 +
                onTap: () {},
 +
              );
 +
             },
 
           );
 
           );
−
         }
+
         },
 +
      ),
 
     );
 
     );
 
   }
 
   }
 +
}
  
−
  Widget _elementoListaWidget(ElementoListaModel modelo){
+
</syntaxhighlight>
−
    return Column(
 
−
      children: [
 
−
        ListTile(
 
−
          leading: FadeInImage(
 
−
            placeholder: AssetImage('assets/gifsanimados/RelojCarga.gif'),
 
−
            image: NetworkImage(modelo.imagen)
 
−
          ,),
 
−
          title: Text(modelo.titulo),
 
−
          subtitle: Text(modelo.texto),
 
−
          onTap: (){},
 
−
        ),
 
−
        Divider()
 
−
      ],
 
−
    );
 
−
 
 
−
  }
 
  
 +
'''<u>Nota:</u>'''
  
−
}
+
* En aplicaciones con muchos modelos JSON puede resultar conveniente utilizar generación automática de código mediante:
−
</syntaxhighlight>
+
  :* '''json_serializable'''.
 +
  :* '''build_runner'''.
  
 +
: Para estos primeros ejemplos continuaremos realizando la conversión manual para comprender el proceso.
  
 
<br />
 
<br />
Línea 1992: Línea 2432:
 
==Card Widget==
 
==Card Widget==
  
−
* Más información en: https://api.flutter.dev/flutter/material/Card-class.html
+
* Más información:
 +
  :* https://api.flutter.dev/flutter/material/Card-class.html
  
 +
* '''Card''' representa una tarjeta de Material Design.
  
−
* Creamos una nueva página en la [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget#Estructura_de_p.C3.A1ginas carpeta pages] de nombre '''cards_page.dart'''
+
* Algunos parámetros importantes son:
−
: En todas las páginas haremos lo mismo:
+
  :* '''child''': contenido de la tarjeta.
−
:* Importamos la librería de 'material' (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
  :* '''elevation''': elevación.
−
:* Creamos una clase que derive de StatelessWidget (al hacerlo será necesario implementar el método build, ya os da un aviso el Android Studio).
+
  :* '''shape''': forma del borde.
−
:* Dentro del método build, retornamos un widget Scaffold (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
  :* '''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'''.
  
−
* Gráficamente es un panel con cierta elevación sobre el fondo y que puede tener las esquinas redondeadas.
+
* 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.
−
: Dentro del panel podemos añadir lo que queramos.
 
−
: Parámetros más importantes:
 
−
:* '''child''': Un widget que conforma el contenido del Card.
 
−
:* '''elevation''': Un double para que visualmente parezca que está 'encima' del contenido haciendo aparecer una sombra sobre el fondo. Podéis consultar en [https://material.io/design/environment/elevation.html este enlace] las elevaciones recomendadas para cada tipo de componente.
 
−
:* '''shape''': El tipo de borde que conforma la tarjeta. Podéis consultar [https://api.flutter.dev/flutter/painting/ShapeBorder-class.html en este enlace] las diferentes clases que se pueden emplear.
 
−
:* '''[https://api.flutter.dev/flutter/dart-ui/Clip-class.html clipBehavior]''': Sirve para recortar el contenido de la tarjeta cuando este sale por fuera de los borders de la misma. Muy útil con imágenes que visualizamos en tarjetas con el borde redondeado.
 
  
  
−
<u>'''Nota:'''</u> Si quisiéramos 'recortar' cualquier widget (no sólo aplicado al Card Widget) podríamos hacer uso del [https://api.flutter.dev/flutter/widgets/ClipRect-class.html widget ClipRect] o cualquiera de los relacionados con él como [https://api.flutter.dev/flutter/widgets/ClipRRect-class.html ClipRRect] que recorta los bordes de forma redondeada.
 
−
 
  
 +
<br />
 +
'''Ejemplo'''
  
−
* Veamos un ejemplo:
+
[[Imagen:Flutter_dart_widget_32.png|300px|center]]
−
 
 
−
[[Imagen:Flutter_dart_widget_32.JPG|300px|center]]
 
−
 
 
  
−
'''Archivo cards_page.dart'''
+
:<syntaxhighlight lang="dart">
−
:<syntaxhighlight lang="java" enclose="div" highlight="25,48,64">
 
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
 
class CardsPage extends StatelessWidget {
 
class CardsPage extends StatelessWidget {
 +
  const CardsPage({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 Cards')),
−
          title: Text('Ejemplo de uso de Cards'),
+
      body: ListView(
−
        ),
+
        padding: const EdgeInsets.all(12),
−
        body: ListView(
+
        children: [
−
          children: <Widget>[
+
          _crearTarjeta1(),
−
            _crearTarjeta1(),
+
          const SizedBox(height: 12),
−
            Divider(),
+
          _crearTarjeta2(),
−
            _crearTarjeta2(),
+
          const SizedBox(height: 12),
−
            Divider(),
+
          _crearTarjeta3(),
−
            _crearTarjeta3(),
+
        ],
−
            Divider(),
+
      ),
−
          ],
 
−
        )
 
 
     );
 
     );
 
   }
 
   }
Línea 2047: Línea 2484:
 
   Widget _crearTarjeta1() {
 
   Widget _crearTarjeta1() {
 
     return Card(
 
     return Card(
−
       clipBehavior: Clip.hardEdge,   // Usado para que la imagen no salga por fuera del border redondeado
+
       clipBehavior: Clip.antiAlias,
−
       elevation: 8.0,
+
       elevation: 4,
−
       shape: const RoundedRectangleBorder(
+
       shape: RoundedRectangleBorder(
−
          side: BorderSide(
+
        side: const BorderSide(width: 2, color: Colors.red),
−
          width: 2.0, color: Colors.red, style: BorderStyle.solid),
+
        borderRadius: BorderRadius.circular(24),
−
          borderRadius: BorderRadius.all(Radius.circular(30.0))
 
 
       ),
 
       ),
 
       child: Column(
 
       child: Column(
 
         children: [
 
         children: [
−
           Center(
+
           Image.network(
−
              child: Image.network('https://picsum.photos/700/500')
+
            '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',
 +
            ),
 
           ),
 
           ),
−
          Padding(
 
−
            padding: const EdgeInsets.all(8.0),
 
−
            child: Text('Ejemplo de texto asociado a una tarjeta'),
 
−
          )
 
 
         ],
 
         ],
 
       ),
 
       ),
Línea 2069: Línea 2511:
  
 
   Widget _crearTarjeta2() {
 
   Widget _crearTarjeta2() {
−
     return const Card(
+
     return const Card.filled(
−
      color: Colors.green,
 
−
      elevation: 5.0,
 
 
       child: ListTile(
 
       child: ListTile(
−
          leading: Icon (Icons.accessibility),
+
        leading: Icon(Icons.accessibility),
−
          title: Text('Ejemplo de texto',
+
        title: Text('Ejemplo de texto'),
−
                      style: TextStyle(color: Colors.yellow,
+
        trailing: Icon(Icons.delete),
−
                          fontWeight: FontWeight.bold,
 
−
                          fontSize: 20.2),
 
−
                  ),
 
−
          trailing: Icon(Icons.delete),
 
 
       ),
 
       ),
 
     );
 
     );
 
   }
 
   }
  
−
   Widget _crearTarjeta3(){
+
   Widget _crearTarjeta3() {
−
     return const Card(
+
     return const Card.outlined(
−
       shape: const CircleBorder(
+
       child: Padding(
−
         side: BorderSide(width: 10.0, color: Colors.amber, style: BorderStyle.solid),
+
         padding: EdgeInsets.all(24),
 +
        child: Icon(Icons.access_alarm, size: 80),
 
       ),
 
       ),
−
      color: Colors.cyanAccent,
 
−
      elevation: 5.0,
 
−
      child: Icon (Icons.access_alarm, size: 80.0,),
 
 
     );
 
     );
 
   }
 
   }
 +
}
  
−
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* Para recortar otros widgets disponemos, entre otros, de:
 +
  :* '''ClipRect'''.
 +
  :* '''ClipRRect'''.
 +
  :* '''ClipOval'''.
  
 
<br />
 
<br />
Línea 2103: Línea 2541:
 
==CircleAvatar==
 
==CircleAvatar==
  
−
* Más información en: https://api.flutter.dev/flutter/material/CircleAvatar-class.html
+
* Más información:
 +
  :* https://api.flutter.dev/flutter/material/CircleAvatar-class.html
  
 +
* '''CircleAvatar''' permite representar un avatar circular.
  
−
* Creamos una nueva página en la [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_Widget#Estructura_de_p.C3.A1ginas carpeta pages] de nombre '''circleavatar_page.dart'''
+
* Entre sus propiedades más utilizadas están:
−
: En todas las páginas haremos lo mismo:
+
  :* '''backgroundImage'''.
−
:* Importamos la librería de 'material' (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
  :* '''foregroundImage'''.
−
:* Creamos una clase que derive de StatelessWidget (al hacerlo será necesario implementar el método build, ya os da un aviso el Android Studio) de nombre CircleAvatarPage
+
  :* '''child'''.
−
:* Dentro del método build, retornamos un widget Scaffold (recordar que existe un [https://github.com/marius-h/flutter_enhancement_suite/blob/main/SNIPPETS.md#flutter-widget-snippets snippet] que lo hace automático).
+
  :* '''radius'''.
 +
  :* '''backgroundColor'''.
 +
  :* '''foregroundColor'''.
  
−
* Este widget visualiza un texto o una imagen con un fondo de forma redondeada. Las valores más importantes a enviar al constructor son:
+
'''Ejemplo'''
−
:* backgroundImage: Imagen a visualizar.
 
−
:* child: Widget a visualizar. Normalmente se pone un texto, pero podríamos poner cualquier Widget.
 
−
:* radius: Tamaño del círculo que conforma el Widget expresado en radio de una circunferencia.
 
−
:* backgroundColor: Color de fondo.
 
  
  
−
* En este ejemplo hago uso del [https://api.flutter.dev/flutter/material/Ink-class.html Widget Ink] que permite poner un color de fondo y establecer un efecto de color al pulsar sobre el Widget.
+
[[Imagen:Flutter_dart_widget_33.png|300px|center]]
  
−
 
+
:<syntaxhighlight lang="dart">
−
* Veamos un ejemplo de uso:
 
−
 
 
−
[[Imagen:Flutter_dart_widget_33.JPG|300px|center]]
 
−
 
 
−
 
 
−
'''Archivo circleavatar_page.dart'''
 
−
:<syntaxhighlight lang="java" enclose="div" highlight="45-49,60-64">
 
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
  
 
class CircleAvatarPage extends StatelessWidget {
 
class CircleAvatarPage extends StatelessWidget {
 +
  const CircleAvatarPage({super.key});
  
−
   final _usuarios = {
+
   static const usuarios = <String, String>{
−
     'Luisa Sanz Ter' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRFogB3c0aNtnWrl9WPR9VHd4RZXjx5ZAT4Dw&usqp=CAU',
+
     'Luisa Sanz': 'https://randomuser.me/api/portraits/women/1.jpg',
−
     'Ana Perez Lopez' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRnHDgxesYDWDD9P9vCUVhG14rqT7KlBrGDhA&usqp=CAU',
+
     'Ana Pérez': 'https://randomuser.me/api/portraits/women/2.jpg',
−
     'Miguel Losz Sanz' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTz0SRPEC1Yd1ImLu_xearePHoYeYctbahgyQ&usqp=CAU',
+
     'Miguel López': 'https://randomuser.me/api/portraits/men/3.jpg',
−
     'Carmen San Juan Lopez' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmbiE5DKrFYHW5O8lC46tsuOlrthYNk3rFGw&usqp=CAU'
+
     'Carmen San Juan': 'https://randomuser.me/api/portraits/women/4.jpg',
 
   };
 
   };
  
 
   @override
 
   @override
 
   Widget build(BuildContext context) {
 
   Widget build(BuildContext context) {
 +
    final entradas = usuarios.entries.toList();
 +
 
     return Scaffold(
 
     return Scaffold(
−
        appBar: AppBar(
+
      appBar: AppBar(title: const Text('Ejemplo de CircleAvatar')),
−
          title: Text('Ejemplo de Circle Avatar'),
+
      body: ListView.separated(
−
         ),
+
         itemCount: entradas.length,
−
         body: _lista()
+
        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: () {},
 +
          );
 +
        },
 +
      ),
 
     );
 
     );
 
   }
 
   }
−
 
 
−
  Widget _lista(){
 
−
    return ListView(
 
−
      children: _elementosLista()
 
−
    );
 
−
  }
 
−
  List<Widget> _elementosLista(){
 
−
    List<Widget> lista = [];
 
−
    for (MapEntry<String,String> dato in _usuarios.entries) {
 
−
      lista..add(SizedBox(height: 5.0,)) // Para separar los elementos de la lista
 
−
          ..add(_circleAvatarLista1(dato))
 
−
          ..add(SizedBox(height: 5.0,))
 
−
          ..add(_circleAvatarLista2(dato));
 
−
    };
 
  
−
     return lista;
+
  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 '';
 +
    }
  
−
  Widget _circleAvatarLista1(MapEntry dato){
+
    if (partes.length == 1) {
−
    return Ink(
+
      return partes.first.substring(0, 1).toUpperCase();
−
      color: Colors.yellow,
+
     }
−
      child: ListTile(
 
−
        title: Text(dato.key),
 
−
        trailing: CircleAvatar(
 
−
          backgroundColor: Colors.blue,
 
−
          backgroundImage: NetworkImage(dato.value),
 
−
          radius: 20.0,
 
−
        ),
 
−
        onTap: () {},
 
−
      ),
 
−
     );
 
−
  }
 
  
−
  Widget _circleAvatarLista2(MapEntry dato){
+
     return (partes.first.substring(0, 1) + partes.last.substring(0, 1))
−
     return Ink(
+
        .toUpperCase();
−
      color: Colors.yellow,
 
−
      child: ListTile(
 
−
        title: Text(dato.key),
 
−
        trailing: CircleAvatar(
 
−
          backgroundColor: Colors.red,
 
−
          child: Text(dato.key.toString().substring(0,2).toUpperCase()),
 
−
          radius: 20.0,
 
−
        ),
 
−
        onTap: () {},
 
−
      ),
 
−
    );
 
 
   }
 
   }
 
}
 
}
  
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
* 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.
 +
 +
 +
 +
 +
<br />
 +
 +
==Ejercicios propuestos sin solución==
 +
 +
* '''Ejercicio 1'''
 +
 +
:<syntaxhighlight lang="dart">
 +
final datosExercicio6 = [1, 2, 3, 4, 5, 6];
 +
</syntaxhighlight>
 +
 +
: Crea unha lista onde cada elemento mostre:
 +
 +
:<syntaxhighlight lang="text">
 +
Número: 1 → Resultado: 10
 +
</syntaxhighlight>
 +
 +
: Cada elemento debe estar dentro de un '''Card''' y separado empleando un '''Padding'''.
 +
 +
<br />
 +
 +
* '''Ejercicio 2'''
 +
 +
:<syntaxhighlight lang="dart">
 +
final datosExercicio7 = <String>[
 +
'Lugo',
 +
'Ourense',
 +
'A Coruña',
 +
'Pontevedra',
 +
];
 +
</syntaxhighlight>
 +
 +
: 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 />
 
<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.
 +
 +
  
  
−
[https://wiki.cifprodolfoucha.es/index.php?title=Programaci%C3%B3n_de_dispositivos_m%C3%B3viles_con_FLUTTER#Unidad_3%3A_Flutter._Dise.C3.B1o_gr.C3.A1fico Enlace a la página principal de la UD3]
 
  
−
[https://wiki.cifprodolfoucha.es/index.php?title=Programaci%C3%B3n_de_dispositivos_m%C3%B3viles_con_FLUTTER Enlace a la página principal del curso]
 
  
  
 +
<br />
  
 +
[https://wiki.cifprodolfoucha.es/index.php?title=Programaci%C3%B3n_de_dispositivos_m%C3%B3viles_con_FLUTTER#Unidad_3%3A_Flutter._Dise.C3.B1o_gr.C3.A1fico Enlace a la página principal de la UD3]
  
−
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2021).
+
[https://wiki.cifprodolfoucha.es/index.php?title=Programaci%C3%B3n_de_dispositivos_m%C3%B3viles_con_FLUTTER Enlace a la página principal del curso]

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