Enmilocalfunciona

Thoughts, stories and ideas.

Flutter & Dart: Hagamos una App (Parte 6)

Publicado por Javier Moreno Valle el

MobileFlutterDart

En este sexto artículo de la serie "Flutter & Dart: Hagamos una App" vamos a mejorar la lista de Pokémons y preparar nuestra app para poder consultar el detalle de cada pokémon y mostrar los mismos con un poco de estilo. Vamos a ello!

Mejorando la lista

Ahora mismo nuestra lista de Pokémons no llama nada la atención mostrando solo el nombre de los mismos. ¿Qué tal si hacemos que se muestre el nombre y la imagen de cada uno de ellos? Vamos a ello!

Lo primero que vamos a hacer es cambiar el modelo y vamos a agregarle otra propiedad que será la imagen.

Posteriormente tenemos que cambiar el servicio para crear este nuevo modelo ya que la respuesta que tenemos al consultar la lista no trae la imagen. Si consultamos la API y vemos la respuesta que obtenemos al consultar el detalle de un Pokémon podemos comprobar que contiene múltiples imágenes que podemos usar. Como posteriormente vamos a desarrollar la vista para mostrar los detalles de cada Pokémon, vamos a crear este modelo.

Creando el modelo

Antes de empezar a implementar la lógica, vamos a crear el modelo a partir de la respuesta que nos devuelve la API. He de decir que es algo más complejo que el otro modelo que ya tenemos y por tanto, para no perder mucho el tiempo, os dejo la clase en este gist para que solamente creéis un nuevo archivo dentro de la carpeta models y peguéis el contenido.

Como podéis ver, es una clase bastante extensa. Por supuesto, no vamos a usar todos esos detalles, pero he querido dejar el modelo completo por si os animáis a jugar y editar la app a vuestro gusto añadiendo nuevos detalles.

Actualizando el servicio

Cambiaremos el valor de la propiedad url del servicio ya que vamos a reutilizarla cambiando solamente el path en cada llamada.

String url = 'https://pokeapi.co/api/v2';

Crearemos una nueva llamada en nuestro servicio para que nos devuelva la imagen de cada Pokémon. En el modelo podemos ver que tenemos varios recursos que podemos usar para mostrar la imagen. Como siempre digo, todo depende de vuestra imaginación.

La nueva llamada, con la imagen que yo he querido usar, quedaría de la siguiente forma:

Ahora, tenemos 2 llamadas: la primera que nos devuelve la lista de Pokémons y esta última que nos devuelve la imagen de un Pokémon (pasamos la url del mismo como parámetro). Necesitamos crear el modelo a partir de ambas llamadas. Existen métodos más avanzados y óptimos para hacerlo, pero como este tutorial está pensado para todos los niveles, voy a hacerlo de la forma más fácil de entender.

Como la primera llamada nos devuelve una lista de Pokémons, ¿Qué tal si parseamos esa lista buscando la imagen de cada uno de ellos?

Para ello, en el mismo servicio creamos un método que reciba una lista de Pokémons y nos devuelva esa misma lista, pero con la imagen.

Por último, actualizamos la primera llamada para que devuelva la lista con el modelo actualizado.

Actualizando la vista

Ahora que ya tenemos cada Pokémon con su imagen, vamos a mejorar el diseño de nuestra vista.

Estaréis cansado de leerlo, pero todo queda a vuestra imaginación.

Antes de nada, vamos a crear varios widgets que vamos a reutilizar en varias pantallas de la app.

Dentro de la carpeta lib, crearemos otra llamada widgets. Dentro de la misma crearemos un fichero llamado custom_image.dart. Este widget contendrá lo necesario para poder mostrar una imagen con el efecto drop-shadow de CSS y otra propiedades para tener un mejor control de la carga de la imagen. Os dejo el código del mismo en el siguiente gist:

Image with backdrop shadow and loading handler
Image with backdrop shadow and loading handler. GitHub Gist: instantly share code, notes, and snippets.

Podréis ver que hago uso de la clase Image. Esta clase tiene varios constructores que nos permiten cargar imágenes desde una url, desde la memoria del dispositivo, desde los assets, etc.

Ahora crearemos otro widget llamado pokemon_card.dart el cual será una tarjeta que mostrará el nombre del Pokémon junto con su imagen.

Aquí hemos hecho uso del widget Card que, a parte de la propiedad child, tiene otras propiedades que usaremos más adelante para darle un poco de estilo a la misma. Entre ellas:

  • color: para establecer el color de fondo
  • shape: para darle forma a la tarjeta como por ejemplo bordes redondeados
  • elevation: para mostrar una sombra debajo de la tarjeta
  • shadowColor: para cambiar el color del sombreado

Con los widgets necesarios, solo queda actualizar la home.

En mi caso he optado por cambiar el ListView que teníamos por un GridView el cual nos permite crear un sistema de rejillas indicando cuantas columnas queremos. Este widget tiene diferentes constructores y para este caso he usado GridView.builder().

Este widget tiene multitud de propiedades. Vamos a comentar algunas de ellas.

La propiedad gridDelegate nos permite elegir en particular el número de columnas en las que se distribuirán los ítems. En el código que servirá de ejemplo, siempre con los coches, el parámetro gridDelegate recibirá una instancia de SliverGridDelegateWithFixedCrossAxisCount que permite definir un número fijo de columnas y la propiedad childAspectRadio que nos permite mostrar cada rejilla del Grid dependiendo del alto y ancho de la pantalla haciendo que esta rejilla sea completamente responsive . En este caso concreto, se establecerá en 2. Por otro lado, tenemos la propiedad itemCount que recibirá el número de ítems que queremos mostrar, itemBuilder que será la función que nos permitirá mostrar cada ítem de la lista.

El resultado del cambio sería el siguiente:

Si en este momento, volvemos a ejecutar la aplicación, podemos comprobar que ahora se muestra cada Pokémon con su imagen correspondiente.

Obteniendo el detalle

Si os habéis dado cuenta, con la llamada que hemos añadido para obtener la imagen de un Pokémon, ya nos está devolviendo todos los detalles.

Añadiremos otra llamada a nuestro servicio para que nos devuelva todos los detalles de un Pokémon.

Creando la vista

Ahora que tenemos todo listo para consultar los detalles de cada Pokémon, empecemos a crear la vista. Dentro de la carpeta pages, crearemos el fichero pokemon_details_page.dart.

Crearemos un StatelessWidget al igual que en la home con el añadido que tendrá un parámetro requerido el cual será el nombre del Pokémon. Para añadir un parámetro basta con declararlo con el modificador final y añadirlo al constructor de la clase.

Pero... ¿Cómo llegamos a esta nueva página?

Bien, ahora es el momento de hablar sobre la navegación y enrutamiento en Flutter.

Otro punto bueno y muy destacado de Flutter es que tiene una documentación excelente, hasta tal punto que en raras ocasiones necesitarás consultar otras fuentes.

Como ejemplo de ello, si vamos a la documentación, veremos que el primer ejemplo que nos muestra es exactamente lo que necesitamos.

Podemos copiar y pegar ese código y modificarlo para lo que necesitamos, pero claro, os preguntaréis dónde ponemos ese código y cómo hacemos que navegue a la página detalle al seleccionar un Pokémon de la lista.

Como comenté al principio de esta serie, en Flutter, todo son widgets y por tanto, tenemos otro widget para detectar las interacciones del usuario con la vista. El widget se llama GestureDetector.

Este widget tiene la propiedad child, que será el elemento sobre el cual queremos detectar la interacción y por otro lado que interacción queremos detectar. En la documentación podremos ver que hay multitud de gestos que podemos detectar y en nuestro caso, necesitamos detectar el evento onTap en los elementos de la lista. Una vez realizado el cambio y añadida la navegación, quedaría así:

Ahora, si corremos nuestra app y clicamos sobre cualquier elemento de la lista, nos llevará a la página de detalles y podremos ver tanto en el AppBar como en el body el nombre del Pokémon seleccionado.

También podremos ver que, de manera automática, Flutter nos muestra el icono en el ApppBar para volver atrás.

Mostrando los detalles

Antes de nada, en esta vista, también haremos uso de la función _capitalizeString por lo que para evitar malas prácticas y no tener código duplicacdo, crearemos una nueva carpeta llamada utils dentro de lib y dentro de la misma crearemos un fichero llamado strings.dart. Movemos la función a este nuevo archivo, pero sin el guion bajo ya que queremos que sea pública.

Ahora solo tenemos que importar este archivo para usar el método en cuestión.

Ya tenemos todo listo para poder empezar a mostrar los detalles del Pokémon. Para ello, como debemos realizar una llamada a la API, la forma de empezar a construir la vista será mediante un FutureBuilder al igual que en la home.

Como siempre, os invito a que juguéis un poco con los datos de la API y con lo que ya sabéis sobre los widget, que hagáis la vista a vuestro gusto. Además, de esta forma, cogeréis práctica y soltura con Flutter.

No obstante, dejaré el repositorio actualizado con la vista tal y como la he hecho yo pero antes aprovecharé para comentaros sobre widgets que he usado en mi código.

He decidido mostrar primero la imagen del Pokémon seleccionado. Para ello, usaremos el widget que creamos anteriormente.

Por otro lado, he querido dividir la información en secciones haciendo uso de tarjetas (Cards).

Junto con las tarjetas y con los widgets Row y Column, podemos conseguir el siguiente resultado:

También he usado de nuevo el widget GridView pero en este caso con el constructor GridView.count().

Este widget tiene una propiedad llamada shrinkWrap y es bastante útil si, por ejemplo, como en mi caso, estoy intentando crear un GridView dentro de un Column. Por defecto, esta propiedad tiene el valor de false y por lo tanto, el Grid intentará expandirse al tamaño máximo por lo que tendremos un error en la vista. Al cambiar el valor de esta propiedad a true, lo que estamos haciendo es decirle que adapte la altura a los widgets secundarios.

También, para este caso, jugaremos con la propiedad childAspectRatio pero de una forma diferente. Os dejaré un pequeño tip que podemos usar para este propósito.

MediaQuery

Con la clase MediaQuery podemos obtener información sobre las dimensiones de la pantalla del dispositivo. Por tanto, podemos conocer en tiempo real el ancho y el alto de la misma simplemente con MediaQuery.of(context).size.width y MediaQuery.of(context).size.heigth

Sabiendo esto, podemos hacer que el childAspectRatio sea dinámico calculando el valor de la siguiente forma:

El resultado de este Grid sería el siguiente:

Para darle un poco mas de estilo a estos valores podemos usar el widget Slider sobrescribiendo un poco los estilos con la ayuda de SliderTheme para darle un toque personal. De nuevo os invito a leer la documentación y usar la imaginación para dejarlo como más os guste. Aún así, os dejo como lo he hecho; tanto el código como el resultado.

Si vamos añadiendo una tarjeta tras otra, veremos que quedan demasiado juntas. De nuevo, en Flutter hay varios métodos para poder dejar espacios entre widgets. Podemos usar un Container con una altura específica y un child vacío, podemos envolver el widget con el widget Padding o, para mi gusto, es mejor usar SizedBox, al cual solo le indicamos la altura o la anchura que queremos que ocupe el espacio en blanco.

A modo de ejemplo y para que veáis como va quedando según los cambios que he aplicado y haciendo uso de la librería palette_generator...

Con lo aprendido en este post y sabiendo que hay cientos de widget y una muy buena documentación, el límite para crear vista, es vuestra imaginación.

En el siguiente post, refactorizaremos nuestro código para que se mantenible y escalable, veremos como mostrar alertas, diálogos y aprenderemos a gestionar los posibles errores en las llamadas de nuestro servicio a la API. Además, este widget para la vista de detalles ha quedado demasiado extenso (250+ líneas). Como buena práctica, refactorizaremos la vista para que sea más escalable y óptima.

Enlace al repositorio

Bibliografía

widgets library - Dart API
widgets library API docs, for the Dart programming language.