Diferencia entre revisiones de «Flutter Widget Componentes»

De MediaWiki
Ir a la navegación Ir a la búsqueda
Línea 1159: Línea 1159:
 
=====Solucións Exercicios Propostos=====
 
=====Solucións Exercicios Propostos=====
  
* '''Exercicio 1''''
+
* '''Exercicio 1'''
 
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
Línea 1195: Línea 1195:
  
 
<br />
 
<br />
* '''Exercicio 2''''
+
* '''Exercicio 2'''
 
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';
Línea 1243: Línea 1243:
  
 
<br />
 
<br />
* '''Exercicio 3''''
+
* '''Exercicio 3'''
 
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
:<syntaxhighlight lang="java" enclose="div" highlight="">
 
import 'package:flutter/material.dart';
 
import 'package:flutter/material.dart';

Revisión del 17:47 4 oct 2022

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.
  • Textos
  • Botones
  • Imágenes
  • Listas
  • Card (tarjetas)
  • Combos (dropdown)


  • Partimos de lo aprendido en las secciones anteriores, en las que tenemomos un widget MaterialApp el cual tiene como contenido un Scaffold.
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 en la siguiente sección.


Widget Text

  • Creamos una nueva página en la carpeta pages de nombre textos_page.dart
En todas las páginas haremos lo mismo:
  • Importamos la librería de 'material' (recordar que existe un snippet que lo hace automático).
  • Creamos una clase 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 snippet que lo hace automático).


Textos con un estilo único

  • Este widget como su nombre indica, visualiza un texto. Entre sus propiedades nos puede ser útiles:
  • style: Para darle un formato.
  • overflow: Para cuando no quepa el texto dentro del contenedor. Debemos usar una de las constantes definidas en TextOverFlow.
  • maxLines: Número de líneas de texto que podrán visualizarse con respecto a su contenedor.


  • El estilo hace referencia a las características visuales del texto, como puedan ser el color, negrilla, tamaño,...
Veamos ejemplo de código con alguna de sus propiedades aplicadas.
Indicar que en el ejemplo vamos a 'envolver' el texto en otro widget denominado 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 Nota: Recuerda cambiar el archivo app.dart para que cargue la nueva página.

import 'package:flutter/material.dart';

class TextosPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Textos con diferentes estilos'),
        ),
        body: Column(
          children: const<Widget> [
            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.'),
            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),
            ),

          ],
        )
    );
  }

}
Flutter dart widget 13.JPG


  • 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.
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.
import 'package:flutter/material.dart';

class TextosPage extends StatelessWidget{
  const estiloTexto = new TextStyle(fontSize: 35.3,  // Recordar que el new no es obligatorio en Flutter
    backgroundColor: Colors.red,
    color: Colors.black12);

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Textos con diferentes estilos'),
        ),
        body: Column(
          children: <Widget> [
            Text('Texto con el estilo predefinido'),
            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
          ],
        )
    );
  }

}
  • NOTA IMPORTANTE: Fijarse que el atributo está marcado como const (podría ser final, pero mejor const 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.
Si no definimos como final, el AndroidStudio nos avisará de que no tiene sentido.
Flutter dart widget 16.JPG


DefaultTextStyle


  • 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.


Nota: Recordar que en AndroidStudio es posible 'envolver' un Widget con otro, pulsando las teclas Alt+Enter y escogiendo la opción wrap.
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.
import 'package:flutter/material.dart';

class TextosPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    // 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),
              ),

            ],
          ),
        )
    );
  }

}


Flutter dart widget 14.JPG


Nota:

  • 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 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....



RichText


  • Permite que un mismo texto tenga diferentes estilos.
Para ello hace uso de la 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.
import 'package:flutter/material.dart';

class TextosPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    // 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'),
                      ]),
              )

            ],
          ),
        )
    );
  }

}
Flutter dart widget 15.JPG



Html en el interior de textos


NOTA IMPORTANTE: 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, debéis de poner la referencia a los paquetes en la sección dependences del pubspec.yaml.




import 'package:flutter/material.dart';

import 'package:flutter_html/flutter_html.dart' as flutterhtml;
import 'package:url_launcher/url_launcher.dart';

class TextosPage extends StatelessWidget{
  TextStyle _estiloTexto = new TextStyle(fontSize: 35.3,  // Recordar que el new no es obligatorio en Flutter
    backgroundColor: Colors.red,
    color: Colors.black12);


  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Textos con diferentes estilos'),
        ),
        body: Column(
          children: <Widget> [
            flutterhtml.Html(data: "<b>Ejemplo de etiqueta</b> html <a href = 'https://www.google.es'>Enlace</a>",
                onLinkTap: (String? url, _,__,___) {
                  _lanzar(url!);
                }),
            Text('Texto con el estilo predefinido'),
            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);
  }

}



Botones


  • En esta sección vamos a ver como añadir los diferentes tipos de botones que podemos emplear en Flutter.
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 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 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).
Flutter dart widget 18.JPG


Float Action Button

  • 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.
Por ejemplo, en una pantalla de contactos, dicho botón sería empleado para dar de alta a nuevos contactos.


Nota: De hecho tenéis un 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 snippet que lo hace automático).


  • Al implementar el Snippet aparecerá una serie de código que tendréis que eliminar, ya que está pensado para ejecutarse en un StatefulWidget.
Nota: 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.
import 'package:flutter/material.dart';

class BotonesPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('appbarTitle'),
      ),
      body: Text('Ejemplo de FAB'),
      floatingActionButton: FloatingActionButton(
        onPressed: null,  // Equivalente a disable. Tendríamos que enviar una función anónima para que aparezca el efecto de click
        tooltip: 'Presiona para hacer algo',
        child: Icon(Icons.add),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
    );
  }
}
  • Estamos instanciando la clase FloationActionButton.
  • Es necesario implementar el método onPressed (mirar la definición del constructor donde el parámetro aparece definido como 'required').
  • 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.
  • 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 en este enlace todos los iconos predefinidos de la clase Icons.


  • Nota: 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.
  • Si necesitamos colocar varios FAB empleando las propiedades del Scaffold, podemos 'envolver' el Widget FAB del ejemplo anterior en un Row (o Column):
import 'package:flutter/material.dart';

class BotonesPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('appbarTitle'),
      ),
      body: Text('Ejemplo de FAB'),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          FloatingActionButton(
            onPressed: () { print ('Presionamos el botón ADD'); },
            tooltip: 'Presiona para añadir',
            child: Icon(Icons.add),
          ),
          FloatingActionButton(
            onPressed: () { print ('Presionamos el botón DELETE'); },
            tooltip: 'Presiona para borrar',
            child: Icon(Icons.delete),
          ),
        ],
      ),
    );
  }
}


  • 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:
FloatActionButton.extended
  • Hace uso del parámetro 'label' para asociar un texto al botón.
  • No tiene el parámetro child para asociar un icono. Ya dispone de un parámetro 'icon' para indicar el icono asociado.
import 'package:flutter/material.dart';

class BotonesPage extends StatelessWidget{
  @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(
            icon: Icon(Icons.add),
            onPressed: () { print ('Presionamos el botón ADD'); },
            tooltip: 'Presiona para añadir',
            label: Text('Añadir'),
            backgroundColor: Colors.pink,
          ),
          FloatingActionButton(
            onPressed: () { print ('Presionamos el botón DELETE'); },
            tooltip: 'Presiona para borrar',
            child: Icon(Icons.delete),
          ),
        ],
      ),
    );
  }
}


Flutter dart widget 17.JPG



TextButton

  • 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.
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.
De forma obligatoria hay que enviarle un widget (child) que será lo que visualice. Normalmente un texto o un icono.
Para modificar el estilo se recomienda hacer uso del método static styleFrom.
  • Veamos un ejemplo:
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)),
        ]
      )
    );
  }
}



IconButton

  • 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).
Posee un parámetro icon donde se indica el icono que se va a utilizar.
  • Veamos un ejemplo:
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: [
          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)),
        ]
      )
    );
  }
}



Otro botones

  • Más información en:


  • 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.
  • 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.


Flutter dart widget 18B.JPG


Imagen obtenida de este enlace



Imágenes

  • Más información en:


  • Creamos una nueva página en la carpeta pages de nombre imagenes_page.dart
En todas las páginas haremos lo mismo:
  • Importamos la librería de 'material' (recordar que existe un 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 snippet que lo hace automático).



  • Flutter suporta los siguientes tipos de imágenes: JPEG, WebP, GIF, WebP/GIF animados, PNG, BMP y WBMP
Las imágenes pueden ser cargadas localmente o desde un recurso de Internet.



Cargando imágenes desde Internet

  • En el siguiente ejemplo veremos como cargar una imagen de Internet de dos formas diferentes:
import 'package:flutter/material.dart';

class ImagenesPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        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,)
          ],
        )
    );
  }
  
}


  • Las dos son equivalentes (menos en el tamaño que uno hace uso del parámetro scale y otro de height).
La diferencia estriba en que Image.network devuelve un Widget mientras que NetworkImage devuelve un objeto que no se visualiza en pantalla, por eso debemos pasarlo como dato al parámetro 'image' de la clase Image.


Cargando imágenes localmente

  • 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.
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


  • 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.
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)
  • 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.
import 'package:flutter/material.dart';

class ImagenesPage extends StatelessWidget{

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        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.asset('assets/imagenes/sol.jpg')
          ],
        )
    );
  }
  
}


Escalando las imágenes



Resolución en imágenes

  • Más información en:


  • Las imágenes deberían ajustarse a la resolución del dispositivo.
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:
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.
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.
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.
Flutter permite tener en la carpeta assets una imagen con el siguiente formato:
  • /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
Vamos a ver un ejemplo utilizando esta imagen: https://pxhere.com/es/photo/741629
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':



import 'dart:ui';

import 'package:flutter/material.dart';

class ImagenesPage extends StatelessWidget{

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



Esperando a descargar

  • 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.
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.
Necesita obligatoriamente dos parámetros:
  • placeholder: Sería el recurso local que queremos mostrar mientras se cargar el remoto.
  • image: La image a visualizar cuando ha terminado de descargarse.
  • Vamos a registrar una imagen animada que represente un tiempo de espera mientras se descarga la imagen de internet.
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
Recordar registrar dicho directorio en el archivo pubspec.yaml y reinstalar la aplicación con un restart. commo vimos en la sección anterior.
import 'package:flutter/material.dart';

class ImagenesPage extends StatelessWidget{

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplos de imágenes'),
        ),
        body: Column(
          children: <Widget>[
            FadeInImage(placeholder: AssetImage('assets/gifsanimados/RelojCarga.gif'),
                        image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',scale: 2),
                        fadeInDuration: Duration(seconds: 2),
                        height: 200.0,
            ),
            Text('Este texto no debería moverse al cambiar el tamaño de la imagen')
          ],
        )
    );
  }
  
}


  • Aclaraciones:
  • Tenemos que hacer uso de la clase AssetImage y de la clase 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.


  • Disponemos de un constructor con nombre 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.


ListView


  • Creamos una nueva página en la carpeta pages de nombre listas_page.dart
En todas las páginas haremos lo mismo:
  • Importamos la librería de 'material' (recordar que existe un snippet que lo hace automático).
  • Creamos una clase 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 snippet que lo hace automático).


  • Permite mostrar un conjunto de Widget en forma de lista con scroll.
Si consultáis los constructores podéis ver que existen varias posibilidades.
  • ListView => Sería equivalente a 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.
  • ListView.builder => Sería equivalente a 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.
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.




Listas con todos los elementos cargados


Veamos un ejemplo.
Flutter dart widget 24.JPG


Archivo listas_page.dart:
import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de listas'),
        ),
        body: ListView(
          children: [
            Text('Elemento 1'),
            Text('Elemento 2'),
            Text('Elemento 3'),
            Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',scale: 3,),
            Text('Elemento 5'),
            Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',scale: 3,),
          ],
        )
    );
  }
}
Nota:
  • 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 padding y marging, pero ya podéis probar en el ejemplo el parámetro padding.



ListTile


  • Asociado a las listas tenemos otro Widget que se suele usar, que es el ListTile.
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.
Flutter dart widget 25.JPG


Archivo listas_page.dart:
import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de listas'),
        ),
        body: ListView(
          children: [
            ListTile(
              title: Text('Elemento 1'), tileColor: Color.fromARGB(0, 255, 0, 0),
              subtitle: Text('Texto aclaratorio elemento 1'),
              leading: Icon(Icons.accessibility),
              trailing: Icon(Icons.more_vert),
            ),
            ListTile(
              title: Text('Elemento 2'), tileColor: Color.fromARGB(0, 255, 0, 0),
              subtitle: Text('Texto aclaratorio elemento 2'),
              leading: Icon(Icons.accessibility),
              trailing: Icon(Icons.more_vert),
              selected: true,
            ),
          ],
        )
    );
  }
}


Nota: 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: () {}

Cargando elementos desde una función

  • Normalmente los datos de las listas vienen de diferentes fuentes de datos:
  • JSON
  • Archivos locales
  • Bases de datos
En el ejemplo siguiente voy a explicar como podríamos programar el código necesario.
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.
Flutter dart widget 26.JPG


import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget{
  final _datos = ['Elem1','Elem2','Elem3','Elem4']; // Tiene que ser final ya que estamos en un StateLessWidget

  @override
  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
    final lista = <Widget>[];                     // Es equivalente a new List<Widget>();

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

    return lista;
  }
}


  • Una mejora al código anterior es utilizar el método map asociado a las listas.
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
  • Por cada elemento de la lista 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 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 método toList()
import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget{
  final _datos = ['Elem1','Elem2','Elem3','Elem4']; // Tiene que ser final ya que estamos en un StateLessWidget

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de listas'),
        ),
        body: ListView(
          children: _obtenerDatosConMap(_datos)
        )
    );
  }

  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
  }
}



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:
import 'package:flutter/material.dart';



void main(){

  runApp(
      MaterialApp(
        home: _MainPage(),
        debugShowCheckedModeBanner: false,
      )
  );

}

class _MainPage extends StatelessWidget {
  const _MainPage({Key? key}) : super(key: key);

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


class _MiScaffold extends StatelessWidget {
  const _MiScaffold({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Exercicios Listas'),
        ),
        body:
          


    );
  }
}
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
  • EXERCICIO 1:
Definindo no arquivo exercicio1_listas.dart un conxunto de datos:
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, no que se apliquen os datos definidos na constante anterior.



  • EXERCICIO 2:
Definindo no arquivo exercicio2_listas.dart un conxunto de datos:
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 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).



  • EXERCICIO 3:
Definindo no arquivo exercicio1_listas.dart un conxunto de datos:
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'},

];


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.



Solucións Exercicios Propostos
  • Exercicio 1
import 'package:flutter/material.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},

];

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

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

  @override
  Widget build(BuildContext context) {
    return ListView(

      children: datos.map((e) => Text(
          'Texto:${e['texto']}', style: TextStyle(color: e['color'],fontSize: e['size']),
        )
      ).toList(growable: false)

    );
  }

}



  • Exercicio 2
import 'package:flutter/material.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'},

];

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

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

  @override
  Widget build(BuildContext context) {
    return ListView(

      children: datos.map((e) =>
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: [
                Text(e['texto']),
                FadeInImage.assetNetwork(
                    placeholder: 'assets/images/centro.jpg',
                    image: e['url'],
                    width: 100,
                    height: 100,
                )
              ],
            ),
          )
      ).toList(growable: false)

    );
  }

}



  • Exercicio 3
import 'package:flutter/material.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/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 {
  Exercicio3Listas({Key? key, required this.datos}) : super(key: key);

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

  @override
  Widget build(BuildContext context) {
    return ListView(

      children: datos.map((e) =>
          ListTile(
            title: Text('Título ${e['title']}'),
            subtitle: Text('Título ${e['subtitle']}'),
            leading:
                FadeInImage
                  (
                  placeholder: AssetImage('assets/images/sol.jpg'),
                  image: NetworkImage(e['leading']),
                ),
          )

      ).toList(growable: false)

    );
  }

}



Listas con los elementos cargados dinámicamente. ListView.builder


  • Se debe aplicar siempre que el número de elementos de la lista sea elevado.
Básicamente son necesarios dos parámetros en su constructor:
  • itemcount: Indicamos el número de elementos de la lista o de la fuente de datos que empleemos. No es obligatorio ponerlo 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.
  • 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.
Es decir, este Widget va 'construyendo' los elementos de la lista a medida que son visualizados en la pantalla.
Veamos un ejemplo:
Flutter dart widget 27.JPG
import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget{
  final _datos = ['Elem1','Elem2','Elem3','Elem4','Elem5','Elem6',    // Tiene que ser final ya que estamos en un StateLessWidget
                  'Elem7','Elem8','Elem9','Elem10','Elem11','Elem12',
                  'Elem13','Elem14','Elem15','Elem16','Elem17','Elem18',];

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de listas'),
        ),
        body: _cargarListaBuilder()
    );
  }

  Widget _cargarListaBuilder() {

    return ListView.builder(
        itemCount: _datos.length,
        itemBuilder: (buildcontext, pos){
          print (pos);    // Por consola podemos ver como se van cargando los nuevos elementos a medida que son visualizados
          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: () {},
    );
  }

}


  • Otro parámetro que podemos utilizar es physics el cual determina la forma en cómo se va a realizar el scroll.
Podéis consultar en este enlace los diferentes tipos de scroll de que disponemos.


Separando elementos de las listas

  • En el caso de las listas creadas con ListView en el que se van a cargar todos los elementos, se puede emplear el widget Divider.
  • Si los elementos se van a crear a medida que son visualizados se puede hacer uso del constructor ListView.separated.



Widget Divider

Veamos un ejemplo:
Flutter dart widget 30.JPG


import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget{
  final _datos = ['Elem1','Elem2','Elem3','Elem4']; // Tiene que ser final ya que estamos en un StateLessWidget

  @override
  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
    final lista = <Widget>[];                     // Es equivalente a new List<Widget>();

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


ListView.separated

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.


Veamos un ejemplo:
Flutter dart widget 31.JPG
import 'package:flutter/material.dart';

class ListasPage extends StatelessWidget{
  final _datos = ['Elem1','Elem2','Elem3','Elem4','Elem5','Elem6',    // Tiene que ser final ya que estamos en un StateLessWidget
    'Elem7','Elem8','Elem9','Elem10','Elem11','Elem12',
    'Elem13','Elem14','Elem15','Elem16','Elem17','Elem18',];

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de listas con ListView.separated'),
        ),
        body: _obtenerLista()
    );
  }

  Widget _obtenerLista(){

    return ListView.separated(
        itemBuilder: (buildContext, index) => _elementoLista(index),
        separatorBuilder: (buildContext, index) => Divider(thickness: 10,color: Colors.green,),
        itemCount: _datos.length);
  }

  Widget _elementoLista(pos) {
    var elemLista = _datos.elementAt(pos);
    return ListTile(
      title: Text(elemLista),
      subtitle: Text('Texto aclaratorio de $elemLista'),
      leading: Icon(Icons.accessibility),
      trailing: Icon(Icons.more_vert),
      onTap: () {},
    );
  }


}






FutureBuilder


  • Creamos una nueva página en la carpeta pages de nombre listas_future_page.dart
En todas las páginas haremos lo mismo:
  • Importamos la librería de 'material' (recordar que existe un snippet que lo hace automático).
  • 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
  • Dentro del método build, retornamos un widget Scaffold (recordar que existe un snippet que lo hace automático).


  • 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.
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.


  • Un FutureBuilder va a tener 3 estados:
  • Cuando está obteniendo los datos.
  • Cuando ya obtuvo los datos.
  • Cuando se ha producido algún error.


  • Dentro del FutureBuilder vamos a emplear una serie de parámetros, algunos de ellos son:
  • 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 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.


  • Veamos un ejemplo:


Archivo listas_future_page.dart

import 'package:flutter/material.dart';

class ListasFuturePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return _listaWidget();
  }
  Widget _listaWidget(){

    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de listas con FutureBuilder'),
        ),
        body: _cargarLista()
    );

  }
  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

    return FutureBuilder(               // FutureBuilder es un Widget
      future: _obtenerDatosInternet(),  // En este parámetro llamamos a algo que devuelva un Future,
      initialData: ['Inicial1','Inicial2'] ,                  // Opcional. Son los datos iniciales. Se reeemplazarían por los traídos de Internet
      builder: (buildContext, AsyncSnapshot<List<dynamic>> snapShoot) {

        print (snapShoot.data);       // Podemos imprimir la información que llega al builder del Future
        return ListView(
          children: _cargarElementosLista(snapShoot.data!),    // Si queremos convertirlo a List<String> => snapShoot.data!.cast<String>()
        );
      },

    );

  }

  List<Widget> _cargarElementosLista(List<dynamic> datosLista){
    final List<Widget> elemLista = [];

    for(String dato in datosLista){
       elemLista..add(ListTile(title: Text(dato),leading: Icon (Icons.add),onTap: () {},))
                ..add(Divider());
    }

    return elemLista;

  }

  Future<List<String>> _obtenerDatosInternet() {
    var datosLocales = <String>[];
    datosLocales.add("Valor 1");
    datosLocales.add("Valor 2");
    datosLocales.add("Valor 3");

    return Future.delayed(Duration(seconds: 4),() => datosLocales);

  }
}


  • 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:
    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>
      builder: (buildContext, AsyncSnapshot<List<String>> snapShoot) {

        print (snapShoot.data);       // Podemos imprimir la información que llega al builder del Future
        return ListView(
          children: _cargarElementosLista(snapShoot.data!),    
        );
      },

    );



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 en:


  • Los archivos 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 empleo de Mapas para crear instancias de una clase.



Archivo /data/datos_listas.json

Nota: Recordar registrar el archivo en pubspec.yaml y reiniciar la aplicación.
{
  "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"
    },
    {
      "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"
    }
  ]
}


Archivo /lib/models/elemento_lista_model.dart Nota: Fijarse que hago uso de un constructor factory.

class ElementoListaModel{

  String titulo;
  String texto;
  String imagen;

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

  factory ElementoListaModel.fromMap(Map<String,dynamic>mapa) =>
     ElementoListaModel(titulo: mapa['titulo'],texto:  mapa['texto'],imagen:  mapa['imagen']);


}


Archivo /lib/providers/datos_lista_provider.dart

Nota: Fijarse que la clase es privada. Para acceder a ella hago uso de un objeto (datosListaProvider) definido en el mismo archivo.
import 'dart:convert';

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

class _DatosListaProvider{        // Creamos la clase privada. Usamos una instancia concreta en este mismo archivo


  Future<List<ElementoListaModel>> cargarDatos() async {
    final resp = await rootBundle.loadString('data/datos_listas.json');    // Devuelve un Future
    Map<String,dynamic> mapa = json.decode(resp);

    List<dynamic> datosLista = mapa['elementos'];
    List<ElementoListaModel> datosDevolver = [];
    for (Map<String,dynamic> elem in datosLista){
      datosDevolver.add(ElementoListaModel.fromMap(elem));
    }

    return datosDevolver;
  }
}
final datosListaProvider = new _DatosListaProvider();


Nota: Recordar que vimos en la UD2 Clases : Empleando mapas como crear un objeto a partir de un Map y comenté que existe un sitio Web: quicktype.io que permite generar el código para poder crear un mapa a partir de un JSON, QUE ES LO QUE ESTÁ PROGRAMADO EN EL CÓDIGO ANTERIOR.



Archivo /lib/src/listas_json_page.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{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    // datosListaProvider.cargarDatos();  -- Para probar que funcionaba....
    return Scaffold(
        appBar: AppBar(
          title: Text('Listas con datos desde JSON'),
        ),
        body: _listaWidget()

    );
  }

  Widget _listaWidget(){

    return FutureBuilder(
        future: datosListaProvider.cargarDatos(),
        initialData: [],
        builder:  (buildContext, AsyncSnapshot<List<dynamic>> data){

          List<Widget>elementosListaWidget = [];
          List<dynamic>datosasync = data.data ?? [];
          datosasync.forEach((element) {
            elementosListaWidget.add(_elementoListaWidget(element));
          });
          return ListView(
            children: elementosListaWidget,
          );
        }
    );
  }

  Widget _elementoListaWidget(ElementoListaModel modelo){
    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()
      ],
    );

  }


}



Card Widget


  • Creamos una nueva página en la carpeta pages de nombre cards_page.dart
En todas las páginas haremos lo mismo:
  • Importamos la librería de 'material' (recordar que existe un snippet que lo hace automático).
  • Creamos una clase 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 snippet que lo hace automático).


  • Gráficamente es un panel con cierta elevación sobre el fondo y que puede tener las esquinas redondeadas.
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 este enlace las elevaciones recomendadas para cada tipo de componente.
  • shape: El tipo de borde que conforma la tarjeta. Podéis consultar en este enlace las diferentes clases que se pueden emplear.
  • 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.


Nota: Si quisiéramos 'recortar' cualquier widget (no sólo aplicado al Card Widget) podríamos hacer uso del widget ClipRect o cualquiera de los relacionados con él como ClipRRect que recorta los bordes de forma redondeada.


  • Veamos un ejemplo:
Flutter dart widget 32.JPG


Archivo cards_page.dart

import 'package:flutter/material.dart';

class CardsPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de uso de Cards'),
        ),
        body: ListView(
          children: <Widget>[
            _crearTarjeta1(),
            Divider(),
            _crearTarjeta2(),
            Divider(),
            _crearTarjeta3(),
            Divider(),
          ],
        )
    );
  }

  Widget _crearTarjeta1() {
    return Card(
      clipBehavior: Clip.hardEdge,   // Usado para que la imagen no salga por fuera del border redondeado
      elevation: 8.0,
      shape: const RoundedRectangleBorder(
          side: BorderSide(
          width: 2.0, color: Colors.red, style: BorderStyle.solid),
          borderRadius: BorderRadius.all(Radius.circular(30.0))
      ),
      child: Column(
        children: [
          Center(
              child: Image.network('https://picsum.photos/700/500')
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text('Ejemplo de texto asociado a una tarjeta'),
          )
        ],
      ),
    );
  }

  Widget _crearTarjeta2() {
    return const Card(
      color: Colors.green,
      elevation: 5.0,
      child: ListTile(
          leading: Icon (Icons.accessibility),
          title: Text('Ejemplo de texto',
                      style: TextStyle(color: Colors.yellow,
                          fontWeight: FontWeight.bold,
                          fontSize: 20.2),
                  ),
          trailing: Icon(Icons.delete),
      ),
    );
  }

  Widget _crearTarjeta3(){
    return const Card(
      shape: const CircleBorder(
        side: BorderSide(width: 10.0, color: Colors.amber, style: BorderStyle.solid),
      ),
      color: Colors.cyanAccent,
      elevation: 5.0,
      child: Icon (Icons.access_alarm, size: 80.0,),
    );
  }

}



CircleAvatar


  • Creamos una nueva página en la carpeta pages de nombre circleavatar_page.dart
En todas las páginas haremos lo mismo:
  • Importamos la librería de 'material' (recordar que existe un snippet que lo hace automático).
  • 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
  • Dentro del método build, retornamos un widget Scaffold (recordar que existe un snippet que lo hace automático).
  • Este widget visualiza un texto o una imagen con un fondo de forma redondeada. Las valores más importantes a enviar al constructor son:
  • 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 Widget Ink que permite poner un color de fondo y establecer un efecto de color al pulsar sobre el Widget.


  • Veamos un ejemplo de uso:
Flutter dart widget 33.JPG


Archivo circleavatar_page.dart

import 'package:flutter/material.dart';

class CircleAvatarPage extends StatelessWidget {

  final _usuarios = {
    'Luisa Sanz Ter' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRFogB3c0aNtnWrl9WPR9VHd4RZXjx5ZAT4Dw&usqp=CAU',
    'Ana Perez Lopez' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRnHDgxesYDWDD9P9vCUVhG14rqT7KlBrGDhA&usqp=CAU',
    'Miguel Losz Sanz' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTz0SRPEC1Yd1ImLu_xearePHoYeYctbahgyQ&usqp=CAU',
    'Carmen San Juan Lopez' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmbiE5DKrFYHW5O8lC46tsuOlrthYNk3rFGw&usqp=CAU'
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Ejemplo de Circle Avatar'),
        ),
        body: _lista()
    );
  }
  
  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;

  }

  Widget _circleAvatarLista1(MapEntry dato){
    return Ink(
      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 Ink(
      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: () {},
      ),
    );
  }
}



Enlace a la página principal de la UD3

Enlace a la página principal del curso




-- Ángel D. Fernández González -- (2021).