Creando nuestra primera app multiplataforma, un verificador de palíndromos

mar. 15, 2024

Este artículo forma parte de una serie:

Introducción

En los capítulos anteriores de nuestra serie sobre Flutter, hemos establecido una sólida base teórica y práctica que nos permite adentrarnos más profundamente en el fascinante mundo del desarrollo de aplicaciones multiplataforma. Comenzamos nuestra aventura con una Introducción a Flutter, donde descubrimos qué es Flutter y por qué ha ganado tanta popularidad entre los desarrolladores. Luego, en nuestro segundo capítulo, exploramos el concepto de Widgets, los bloques de construcción fundamentales de cualquier aplicación Flutter, aprendiendo cómo se organizan en un árbol para construir interfaces de usuario interactivas y atractivas. Más recientemente, en el tercer capítulo, profundizamos en Dart, el lenguaje de programación optimizado por Google para el desarrollo con este framework, familiarizándonos con su sintaxis, características y cómo -aprovecha este poderoso lenguaje para crear aplicaciones eficientes y compiladas de forma nativa.

Ahora, en este capítulo, estamos listos para dar un paso significativo: crear nuestro primer proyecto Flutter completo. Juntos, no sólo entenderemos la estructura jerárquica que subyace en un proyecto Flutter y cómo organizar nuestros archivos y código para maximizar la eficiencia y la escalabilidad, sino que también pondremos en práctica todo lo aprendido mediante el desarrollo de un proyecto básico: un verificador de palíndromos.

A través de él, experimentaremos con la creación de interfaces de usuario, manejaremos la entrada de datos del usuario y aplicaremos lógica en Dart para determinar si una palabra o frase es un palíndromo, es decir, se lee igual hacia adelante que hacia atrás. Es una excelente oportunidad para ver en acción los Widgets interactivos, aprender más sobre el manejo de estados en Flutter y entender cómo una idea se transforma en una aplicación funcional que puedes ejecutar en tu propio dispositivo.

Preparativos

Antes de sumergirnos en el desarrollo de nuestra aplicación, es crucial tener nuestro entorno de desarrollo configurado correctamente. Utilizaremos Android Studio, complementado con los plugins de Flutter y Dart, para ofrecernos una experiencia de desarrollo integral. Estos plugins están disponibles en el Marketplace de Android Studio (accesible desde su panel de configuración) y son esenciales para trabajar con Flutter.

Cuando se trata de iniciar un nuevo proyecto Flutter, tienes la flexibilidad de elegir el método que prefieras. Puedes optar por la rapidez y simplicidad de la línea de comandos, donde un simple comando establece todo lo necesario para comenzar:


  flutter create palindrome_validator
  

O también podemos utilizar el mismo Android Studio, que en sus versiones más recientes nos permite nos permite crear nuestro proyecto Flutter utilizando una interfaz gráfica intuitiva desde de la opción File > New... > New Flutter Project..., que te guiará a través de la configuración inicial. Ahora que hemos establecido las bases para comenzar nuestro proyecto Flutter, estamos listos para estudiar, antes que nada, la estructura del proyecto.

Estructura del proyecto

Podemos observar una estructura de directorios y ficheros básica que se asemeja a la siguiente:

  • android: Contiene los archivos específicos de Android para tu proyecto, configuraciones de compilación, manifiesto y recursos.

    • app: Carpeta principal para el código específico de la aplicación Android, incluidos el código fuente y los recursos.
    • gradle: Alberga los scripts y configuraciones de Gradle necesarios para compilar la aplicación.
  • ios: Almacena los archivos específicos de iOS, incluidos los archivos de proyecto de Xcode y el código fuente de Objective-C/Swift.

    • Flutter: Usada por el plugin de Flutter para Xcode, contiene configuraciones para compilar la parte Flutter de tu app.
    • Runner: Contiene el código fuente específico de iOS y archivos de configuración de la aplicación.
    • RunnerTests: Archivos de prueba para tu aplicación iOS.
    • Runner.xcodeproj: Archivo de proyecto de Xcode.
    • Runner.xcworkspace: Espacio de trabajo de Xcode que incluye tu proyecto y dependencias.
  • lib: Directorio para el código fuente Dart de tu aplicación Flutter.

  • linux: Contiene archivos específicos para compilar tu aplicación en escritorio Linux.

    • flutter: Configuraciones y scripts específicos para la compilación de tu aplicación Flutter en Linux.
  • macos: Similar a linux e ios, pero para aplicaciones de escritorio MacOS.

    • Flutter: Configuraciones para compilar la parte Flutter de tu aplicación MacOS.
    • Runner: Archivos de configuración y código fuente específico de MacOS.
    • RunnerTests: Archivos de prueba para el código específico de MacOS de tu app.
    • Runner.xcodeproj y Runner.xcworkspace: Archivos de proyecto y espacio de trabajo de Xcode para MacOS.
  • test: Directorio para archivos de prueba Dart de tu aplicación. Añadiremos una pequeña batería de tests al final

  • web: Archivos específicos para la versión web de tu aplicación Flutter, incluido el archivo de entrada HTML y recursos.

    • icons: Recursos gráficos como íconos utilizados en la versión web.
  • windows: Archivos específicos de Windows para compilar tu aplicación de escritorio.

    • flutter: Configuraciones y scripts para la compilación en Windows.
    • runner: Archivos de proyecto de Visual Studio para Windows.
  • pubspec.yaml: Archivo de configuración de tu proyecto, donde se especifican las dependencias, el nombre de la aplicación, la versión y otros metadatos.

Ahora que ya conocemos cada una de las partes que componen un proyecto Flutter, estamos listos para comenzar a construir nuestra super aplicación verificadora de palíndromos. Como veíamos antes, la carpeta principal del código fuente de nuestra aplicación es la de lib, por lo que es aquí donde vamos a comenzar a trabajar, tendremos ya almacenada en ella una aplicación de ejemplo que se añade al crear el proyecto, ubicado en el fichero main.dart que tiene un Widget sin estado (Stateless Widget) como principal y nos permitirá mostrar el número de pulsaciones de un botón que podremos ejecutar desde el mismo IDE o desde la consola con:


  flutter run
  

Código fuente de la aplicación

El primer paso, sin contar la parte de investigación y planificación, para construir nuestra aplicación verificadora de palíndromos es crear la interfaz de usuario y el código. En ella mostraremos un campo de texto para que los usuarios ingresen la palabra o frase a verificar, un botón para que los usuarios puedan enviar la palabra o frase y un área de texto para mostrar el resultado de la verificación. Podríamos empezar modificando la clase MyApp que se encuentra en el fichero main.dart pero para hacerlo más modular vamos a separar las diferentes partes en múltiples ficheros. Al final nuestro fichero main quedará tal que así:


  import 'package:flutter/material.dart';
  import 'package:palindrome_validator/views/my_home_page.dart';
  
  void main() {
    runApp(const MyApp());
  }
  
  class MyApp extends StatelessWidget {
    final String appTitle = 'Verificador de Palíndromos';
  
    const MyApp({super.key});
  
    // Este Widget es la raíz de la aplicación.
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: appTitle,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightGreen),
          useMaterial3: true,
        ),
        home: MyHomePage(title: appTitle),
      );
    }
  }
  

Este Widget es el principal de nuestra aplicación, el punto de entrada, normalmente pocas cosas se modifican en esta parte ya que es la que se encarga de inicializar la aplicación junto a sus librerías relacionadas y mostrar el primer Widget que se va a ver. Inicia una aplicación Flutter que utiliza los principios de diseño de Material 3, la última actualización del lenguaje de diseño de Material Design, ofreciendo una apariencia más moderna que las anteriores. La aplicación arranca con la función main() que ejecuta el widget MyApp, el cual actúa como la raíz de la aplicación. Dentro de MyApp, un widget MaterialApp se utiliza para configurar aspectos visuales y de navegación global de la aplicación, incluyendo el título y el tema.

El tema de la aplicación se define mediante ThemeData, donde se especifica el uso de Material 3 con el parámetro useMaterial3, y se establece un esquema de colores personalizado con ColorScheme.fromSeed(seedColor: Colors.lightGreen). Esta forma de definir el tema aprovecha una funcionalidad de Material 3 que permite generar una paleta de colores coherente a partir de un color semilla, en este caso, Colors.lightGreen. Finalmente, apunta a MyHomePage` como la página de inicio, pasando el título de la aplicación a esta página para su uso.

En nuestro caso, el Widget MyHomePage se encuentra en el fichero my_home_page.dart, dentro del directorio views, que sería el encargado de mostrar la interfaz de usuario:


  import 'package:flutter/material.dart';
  import '../controllers/palindrome_checker.dart';
  
  class MyHomePage extends StatefulWidget {
    const MyHomePage({super.key, required this.title});
  
    final String title;
  
    @override
    State<MyHomePage> createState() => _HomePageState();
  }
  
  class _HomePageState extends State<MyHomePage> {
    final TextEditingController _controller = TextEditingController();
    String _result = '';
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextField(
                controller: _controller,
                decoration: InputDecoration(
                  labelText: 'Ingresa un texto',
                ),
              ),
              SizedBox(height: 10),
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    _result = PalindromeChecker().isPalindrome(_controller.text)
                        ? 'Es un palíndromo'
                        : 'No es un palíndromo';
                  });
                },
                child: Text('Verificar'),
              ),
              SizedBox(height: 20),
              Text(_result, style: TextStyle(fontSize: 20)),
            ],
          ),
        ),
      );
    }
  }
   

Este fragmento implementa una página principal MyHomePage, ésta hereda de StatefulWidget, un Widget con estado que nos permitirá que la interfaz de usuario reaccione a cambios en el mismo, como la actualización del resultado tras analizar la frase. El estado se maneja a través de _HomePageState, donde se inicializa un TextEditingController _controller para capturar la entrada del usuario y una variable _result para almacenar el veredicto sobre si el texto ingresado es un palíndromo o no.

Dentro del método build de _HomePageState, se estructura la interfaz de usuario usando un Scaffold, que proporciona una barra de aplicaciones AppBar para mostrar el título y un cuerpo que organiza los widgets internos con un Padding y un Column. Este último alinea verticalmente los widgets en el centro, incluyendo un TextField para la entrada del usuario, seguido de un ElevatedButton que, al ser presionado, invoca la verificación del palíndromo a través de PalindromeChecker().isPalindrome. El resultado de esta verificación se asigna a _result y se muestra debajo del botón en un widget Text.

El uso de setState dentro del onPressed del botón asegura que la interfaz de usuario se actualice con el nuevo resultado cada vez que el usuario presiona “Verificar”. Este código demuestra efectivamente cómo se pueden construir interfaces de usuario interactivas en Flutter, procesando y mostrando la entrada del usuario de manera dinámica.

Como comentábamos en el primer artículo de introducción a Flutter, los Widgets sin estado son aquellos que no cambian a lo largo del tiempo, mientras que los que tienen estado sí lo hacen, en este caso, el estado de la aplicación cambia cada vez que el usuario pulsa el botón de verificar para actualizar el resultado en el texto correspondiente.

La clase PalindromeChecker que vemos a continuación se encarga de realizar la lógica relativa al análisis del texto introducido, básicamente, si se lee igual hacia adelante que hacia atrás:


  import 'package:diacritic/diacritic.dart';
  
  class PalindromeChecker {
    bool isPalindrome(String string) {
      String formattedString = removeDiacritics(string.toLowerCase()).replaceAll(RegExp(r'[^a-zA-Z0-9]'), '').replaceAll(' ', '');
      return formattedString == formattedString.split('').reversed.join('');
    }
  }
   

Este código fuente define una clase PalindromeChecker con un método isPalindrome que determina si una cadena de texto (string) es un palíndromo. Ignorando mayúsculas, minúsculas, espacios, signos de puntuación y tildes. El método primero convierte la cadena a minúsculas y utiliza la función removeDiacritics del paquete diacritic. Luego, elimina todos los caracteres que no sean alfanuméricos y los espacios. Finalmente, compara la cadena formateada con su versión invertida para verificar si es un palíndromo, retornando true si lo es, o false en caso contrario.

Hacemos uso del paquete diacritic para eliminar los acentos de las palabras y de las frases, de esta forma sólo tendremos en cuenta los caracteres en sí para determinar si la palabra introducida es realmente un palíndromo. Para añadir este paquete a nuestro proyecto, podemos hacer uso de la línea de comandos o del propio Android Studio, en este caso, utilizamos la línea de comandos:


  flutter pub add diacritic
  

Podríamos modificar directamente el fichero pubspec.yaml para añadir el paquete, pero es más seguro hacerlo a través del comando pub de Flutter ya que éste automáticamente se encarga de sincronizar el proyecto con las nuevas librerías añadidas mientras que si lo modificamos a mano tendremos que hacer la sincronización después a mano, además también determinará la versión actual de forma automática y se encargará de añadirlo a la lista de dependencias.

Una vez integradas las partes de nuestro proyecto podemos ejecutarlo y ver cómo funciona, el uso es simple, introducimos una palabra o frase en el campo de texto y tras pulsar el botón de verificar nos mostrará si es un palíndromo o no. Aunque es una aplicación muy simple, nos ha servido para ver cómo se estructura un proyecto Flutter, cómo se organizan los ficheros y cómo se pueden separar las diferentes partes de la aplicación para hacerla más modular y fácil de mantener.

Tests unitarios

Ya tenemos nuestra aplicación creada pero vamos a avanzar un paso más y añadirle una batería de tests para comprobar que la lógica de nuestra aplicación es correcta, para ello podríamos añadir un fichero palindrome_checker_test.dart (el nombre del fichero ha de ser el mismo que su correspondiente en el proyecto seguido del sufijo _test y almacenado en una carpeta en el mismo lugar de la jerarquía) en el directorio test (si hemos creado la carpeta controllers también tenemos que crearla en la parte de los tests) con el siguiente contenido:


  import 'package:flutter_test/flutter_test.dart';
  import 'package:palindrome_validator/controllers/palindrome_checker.dart';
  
  void main() {
    final palindromeChecker = PalindromeChecker();
  
    group('PalindromeChecker', () {
      test('should return true for a palindrome', () {
        expect(palindromeChecker.isPalindrome('A man, a plan, a canal: Panama'), true);
      });
  
      test('should return false for a non-palindrome', () {
        expect(palindromeChecker.isPalindrome('Not a palindrome'), false);
      });
  
      test('should return true for a palindrome with mixed case', () {
        expect(palindromeChecker.isPalindrome('Able , was I saw eLba'), true);
      });
  
      test('should return true for a palindrome with symbols', () {
        expect(palindromeChecker.isPalindrome('Madam, in Eden, I’m Adam.'), true);
      });
  
      test('should return true for single word palindromes', () {
        var words = ['Ana', 'Anilina', 'Arenera', 'Menem', 'Oso', 'Radar', 'Reconocer', 'Salas', 'Somos'];
        for (var word in words) {
          expect(palindromeChecker.isPalindrome(word), true);
        }
      });
  
      test('should return true for phrase palindromes', () {
        var phrases = [
          'Anita lava la tina',
          'A mamá Roma le aviva el amor a papá y a papá Roma le aviva el amor a mamá',
          'Amo la pacífica paloma',
          'La ruta nos aportó otro paso natural',
          'No subas, abusón'
        ];
        for (var phrase in phrases) {
          expect(palindromeChecker.isPalindrome(phrase), true);
        }
      });
    });
  }
  

Una pequeña batería de tests básicos con palabras y frases en castellano e inglés en la que, de dos formas diferentes, comprobamos si la palabra o frase es un palíndromo o no, lo de hacerlo de dos distintas tiene mera función ilustrativa. Podemos ejecutar nuestra batería de tests pulsando con el botón derecho de nuestro ratón sobre la carpeta test y seleccionando la opción Run tests in test. Si todo ha terminado correctamente nos debería aparecer un mensaje al final de la ejecución de los tests indicando que todos se han ejecutado correctamente. De no ser así nos indicará cuáles han fallado y por qué motivo. No entraremos en mayor detalle en el tema de tests ya que es un tema que merece un capítulo propio, pero es importante tener en cuenta que es una parte fundamental del desarrollo de software.

Conclusión

En este capítulo, hemos dado un paso significativo en nuestra serie sobre Flutter, hemos creado nuestra primera aplicación, que aun no siendo la más útil del mundo nos ha permitido explorar la estructura de un proyecto Flutter, aprendido cómo organizar nuestros archivos y código para maximizar la eficiencia y la escalabilidad. Hemos experimentado con la creación de interfaces de usuario, manejado la entrada de datos del usuario y aplicado lógica en Dart para determinar si una palabra o frase es un palíndromo. Hemos visto en acción los Widgets interactivos, aprendido más sobre el manejo de estados en Flutter y entendido cómo una idea se transforma en una aplicación funcional que puedes ejecutar en tu propio dispositivo.

En el próximo capítulo, continuaremos nuestra aventura por los mundos de Flutter, profundizando en el desarrollo de aplicaciones multiplataforma y explorando cómo podemos aprovechar las características que nos proporciona para crear aplicaciones más complejas y atractivas. Hasta entonces, te animo a que juegues con la aplicación que acabamos de crear, experimentes con su código y compartas tus ideas y preguntas en la sección de comentarios. Y recuerda, ¡Happy Coding!

Artículos relacionados

Quizá te puedan interesar

September 15, 2023

Introducción a Flutter

Flutter es un framework de desarrollo de aplicaciones móviles creado por Google, utiliza el motor de …

leer más