Hablemos (de) Dart, el idioma de Flutter

ene. 1, 2024

Este artículo forma parte de una serie:

En el mundo del desarrollo de aplicaciones móviles, Flutter se ha destacado como uno de los frameworks más innovadores y eficientes. Sin embargo, el verdadero héroe detrás de su éxito es el lenguaje de programación que lo impulsa: Dart. Anteriormente hemos visto los conceptos básicos tras los Widgets de Flutter y un poco de su historia, ahora nos sumergiremos en sus detalles, explorando sus orígenes, características y cómo se utiliza en el contexto de Flutter para crear aplicaciones móviles, web y pronto videojuegos.

Orígenes

Dart, un lenguaje de programación moderno y escalable, fue desarrollado por Google y presentado al mundo en 2011. Surgió como una alternativa a JavaScript, con el objetivo de abordar algunas de las limitaciones que los desarrolladores enfrentaban al utilizarlo, especialmente en términos de estructura y rendimiento. Dart se diseñó para ser tanto familiar como fácil de aprender para los programadores con experiencia en otros lenguajes orientados a objetos como C++ o Java, ofreciendo una sintaxis clara y concisa.

Desde su concepción, Dart fue ideado para ser un lenguaje versátil, adecuado para el desarrollo tanto en el cliente como en el servidor. Su naturaleza compilada a código máquina lo hace especialmente potente para aplicaciones móviles, donde el rendimiento y la fluidez son críticos. Además, Dart se optimizó para trabajar de manera eficiente en la web, permitiendo a los desarrolladores compilar aplicaciones de alto rendimiento que pueden ejecutarse en cualquier navegador moderno.

Características como el compilado Just-In-Time (JIT) para un desarrollo rápido y el compilado Ahead-Of-Time (AOT) para un rendimiento óptimo en tiempo de ejecución, determinaron la elección de Dart como la opción ideal para el framework. Dart facilita la creación de aplicaciones de alta calidad con animaciones suaves y una interfaz de usuario receptiva, elementos clave en la experiencia del usuario móvil.

Conceptos básicos

Estamos hablando de un lenguaje de programación orientado a objetos, con una sintaxis similar a la de Java o C++. En Dart, como en muchos otros lenguajes de programación, las variables son un componente básico. Puedes declarar variables de diferentes tipos de datos, incluyendo: int, double, String, bool, List, Map, Set y muchos otros.


    int age = 25;
    double height = 1.75;
    String name = 'Dart';
    bool isDeveloper = true;
    List<String> hobbies = ['programming', 'reading', 'running'];
    Map<String, String> languages = {'es': 'Spanish', 'en': 'English'};
    Set<String> skills = {'Dart', 'Flutter', 'Java', 'C++'};
    

Para ejecutar código Dart no necesitamos un IDE completo aunque existan plugins y herramientas para los más comunes, puede utilizarse como lenguaje de scripting y ejecutarse directamente desde la línea de comandos con tenerlo instalado en nuestro sistema. Incluso podemos ejecutar su código fuente directamente desde el navegador web antes que nada si sólo queremos probarlo.

Tipado de datos, constantes y variables

Es de tipado estático, lo que significa que el tipo de una variable se define en el momento de su declaración y no puede cambiar. Esto ayuda a detectar errores en tiempo de compilación y mejora el rendimiento del código. Sin embargo, Dart también ofrece cierta flexibilidad con el tipado dinámico cuando es necesario.

  • Tipado Estático: Declaras explícitamente el tipo de cada variable.
  • Inferencia de Tipo: Es capaz de inferir el tipo de una variable basándose en el valor asignado.
  • Tipado Dinámico: Puedes usar el tipo especial dynamic si necesitas una variable que pueda cambiar de tipo.

    dynamic dynamicVariable = 'Hola Dart';
    
    void main(){
      dynamicVariable = 100; // Permite cambiar el tipo
      print(dynamicVariable); // Salida: 100
    }
    

Aun siendo un lenguaje de tipado estático, el compilador es capaz de inferir el tipo de una variable basándose en el valor que se le asigna al momento de su declaración, permitiendo el uso de var. Esto significa que, aunque no especifiques explícitamente el tipo de una variable al usar var, el compilador determina y fija el tipo de esa variable basándose en el valor inicial que recibe.


    var name = 'Dart'; // Dart infiere que 'name' es de tipo String
    name = 123; // Esto resultaría en un error porque 'name' es un String
    

De todas maneras es recomendable no abusar del uso de var y especificar el tipo de las variables siempre que sea posible ya que esto facilita la lectura del código y ayuda a detectar errores. En su documentación tienes más información sobre el tipado en Dart.

El uso de constantes es una práctica común en la programación, y Dart no es una excepción. Éstas, como en cualquier otro lenguaje no pueden cambiar durante la ejecución del programa. Puedes declarar constantes usando la palabra clave const o final. Suele utilizarse final cuando necesites una variable que solo se asigne una vez y pueda ser calculada en tiempo de ejecución. En cambio, usa const para valores que nunca cambian y pueden ser determinados en tiempo de compilación, asegurando que todas las instancias de ese valor en el programa sean idénticas y no mutables.


    const String name = 'Dart';
    final DateTime now = DateTime.now();
        

Funciones

Las funciones en Dart son bloques de código que realizan una tarea específica y pueden ser reutilizadas en diferentes partes de tu programa. Puedes definir funciones con o sin parámetros y especificar el tipo de retorno. Si no especificas el tipo de retorno, el tipo de retorno predeterminado es dynamic.


    // Función sin parámetros y sin retorno
    void sayHello() {
      print('Hello Dart!');
    }
    
    // Función con parámetros y sin retorno
    void sayHelloTo(String name) {
      print('Hello $name!');
    }
    
    // Función con parámetros y con retorno
    String sayHelloTo(String name) {
      return 'Hello $name!';
    }
    

Dart ofrece la posibilidad de definir funciones anónimas, que son funciones sin nombre y que pueden ser útiles en varias situaciones, como para ser asignadas a variables o pasadas como parámetros a otras funciones. Son un recurso poderoso y flexible, se utilizan comúnmente en situaciones donde se requieren funciones breves o de uso único, como en los callbacks.


    // Función anónima asignada a una variable
    var sayHello = () {
      print('Hello Dart!');
    };
    
    // Función anónima pasada como parámetro
    void sayHelloTo(String name, Function callback) {
      callback(name);
    }
    
    // Llamada a la función 'sayHelloTo' pasando una función anónima como parámetro
    sayHelloTo('Dart', (name) {
      print('Hello $name!');
    });
    
    // Función anónima con parámetros y retorno
    var numbersList = [1, 2, 3, 4, 5];
    var squares = numbersList.map((number) {
        return number * number;
    });
    
    // Printing the squares of the numbers
    print("Squares of the numbers: $squares");
    

Utilizar return o un callback cambia la forma en que se devuelven los valores de una función. Cuando se utiliza return, estás devolviendo un valor directamente desde una función al punto en el que se llamó a esa función. Esto es útil cuando deseas obtener un resultado inmediato y sincrónico de una función.


    int sum(int a, int b) {
      return a + b;
    }
    
    void main() {
      var result = sum(5, 3);
      print("La suma es: $result");
    }
    

Un mecanismo de callback implica pasar una función como argumento a otra función para que sea ejecutada en un momento posterior o en respuesta a un evento específico. Esto es especialmente útil en situaciones asincrónicas o cuando deseas realizar ciertas acciones después de que se complete una operación. Aquí hay un ejemplo:


    void performOperation(int a, int b, void Function(int) callback) {
      int result = a + b;
      // Simula una operación asincrónica, como una solicitud de red o un temporizador.
      Future.delayed(Duration(seconds: 2), () {
        callback(result);
      });
    }
    
    void main() {
      performOperation(5, 3, (result) {
        print("La suma es: $result");
      });
    }
    

En este caso, la respuesta nos llegaría dos segundos después de haber ejecutado la función. Como podemos observar, la diferencia a la hora de obtener el resultado de cada función es que con return obtenemos el valor de la función de forma inmediata, mientras que con un callback obtenemos el valor de la función en un momento concreto. De esta forma podemos realizar operaciones asincrónicas y obtener el resultado cuando esté disponible, pero no es la única. En Dart también podemos utilizar async y await, además de Future para realizar operaciones asíncronas.

Asincronía

Existen varios mecanismos en Dart para realizar operaciones asíncronas, los más utilizados además de async/await son los Future. Un Future es un objeto que representa un valor potencial o un error que estará disponible en algún momento en el futuro. Son utilizados para realizar operaciones asíncronas, como solicitudes de red o consultas a una base de datos, y obtener el resultado cuando estuviera disponible.


    // Define una función asíncrona que toma dos números y devuelve un resultado tras 2 segundos.
    Future<int> performOperation(int a, int b) async {
      int result = a + b;
      // Simula una operación asincrónica, como una solicitud de red o un temporizador.
      await Future.delayed(Duration(seconds: 2));
      return result;
    }
    
    void main() async {
      print("Inicio del programa");
    
      // Llamada a performOperation y espera a que se complete.
      int result = await performOperation(5, 3);
    
      print("El resultado es: $result");
      print("Final del programa");
    }
    

Es común que las aplicaciones móviles necesiten realizar operaciones asincrónicas, como solicitudes de red o consultas a una base de datos. En estos casos, es recomendable utilizar async y await para realizar estas operaciones de forma asincrónica. También podemos utilizar Future para solicitar datos sin conocer de antemano el tiempo que tardará en estar disponible.

La combinación de cada una de las técnicas mencionadas es decisión última del desarrollador y dependerá de las necesidades y contexto de cada aplicación. En la documentación de Dart tienes más información sobre la asincronía que te recomiendo visitar para profundizar en el tema.

Estructuras de control

Dart acepta todo tipo de estructuras de control como cualquier otro lenguaje de programación, incluyendo if, else, switch, for, while, etc. Como estamos en una serie de artículos sobre Flutter, vamos a dar por supuesto que ya se conocen las estructuras de control básicas de cualquier lenguaje de programación. Mencionaremos algunos de los más utilizados y específicos de Dart, aun a pesar de que también puedan existir en otros lenguajes.

Operador ternario

El operador ternario es una forma abreviada de la estructura if-else y se utiliza para asignar un valor a una variable o para realizar una operación en función de una condición.

    
    // Asigna el valor 'Dart' a la variable 'name' si 'isDart' es true, de lo contrario asigna 'Flutter'.
    String name = isDart ? 'Dart' : 'Flutter';
    

Operador de cascada

El operador de cascada (..) es una forma abreviada de llamar a varios métodos en un objeto. Esto es especialmente útil cuando necesitas realizar varias operaciones de una vez, como agregar varios elementos a una lista o establecer algunas de las propiedades de un widget.


    // Crea una lista y agrega varios elementos a la vez.
    var numbersList = []
      ..add(1)
      ..add(2)
      ..add(3)
      ..add(4)
      ..add(5);
    
    // Crea un widget y establece varias propiedades a la vez.
    var widget = Container()
      ..color = Colors.blue
      ..width = 100
      ..height = 100;
    

Operador de propagación

El operador de propagación es una forma abreviada de agregar los elementos de una lista a otra lista. De esta manera no es necesario recorrer la lista para agregar cada elemento a la nueva lista.


    var numbersList = [1, 2, 3, 4, 5];
    var newNumbersList = [0, ...numbersList];
    

Operador de null-aware

El operador null-aware es una forma abreviada de verificar si una variable es nula y asignar un valor predeterminado en caso de que lo sea.


    String? predefinedName = 'Alice';
    // Asigna el valor 'Alice' a la variable 'name' si 'predefinedName' no es nulo, como este caso, de lo contrario asigna 'Bob'.
    String name = predefinedName ?? 'Bob';
    

Operador de null-aware de acceso condicional (?.)

El operador de null-aware de acceso condicional es una forma abreviada de acceder a una propiedad de un objeto si ese objeto no es nulo. De serlo no se accede a la propiedad.


    User? user = User();
    // Accede a la propiedad 'name' del objeto 'user' si 'user' no es nulo.
    String name = user?.name ?? 'Dart';
    

Clases y objetos

Con las clases y objetos sucede más de lo mismo, Dart es un lenguaje de programación orientado a objetos y por tanto dispone de ellos como cualquier otro lenguaje de este tipo. Para crear una clase en Dart, se utiliza la palabra clave class, seguida del nombre de la clase y su definición. Veamos un ejemplo de una clase llamada Person que tiene atributos y métodos básicos:


    class Person {
      String name;
      int age;
    
      Person(this.name, this.age);
    
      void introduceYourself() {
        print('Hola, me llamo $name y tengo $age años.');
      }
    }
    
    void main() {
      var person = Person('Alice', 30);
      person.introduceYourself(); // Salida: Hola, me llamo Alice y tengo 30 años.
    }
    

Puedes recibir parámetros opcionales utilizando llaves {} en la definición del constructor para crear un constructor más flexible que permite enviar bajo demanda los valores que en él se definen.


    class Person {
      String name;
      int age;
      String? hairColor; // Parámetro opcional
    
      Person(this.name, this.age, {this.hairColor});
    
      void introduceYourself() {
        if (hairColor != null) {
          print('Hola, mi nombre es $name, tengo $age años y mi pelo es de color $hairColor.');
        } else {
          print('Hola, mi nombre es $name, tengo $age años.');
        }
      }
    }
    
    void main() {
      var person1 = Person('Bob', 30);
      var person2 = Person('Alice', 25, hairColor: 'Azul');
    
      person1.introduceYourself(); // Salida: Hola, mi nombre es Bob y tengo 30 años.
      person2.introduceYourself(); // Salida: Hola, mi nombre es Alice, tengo 25 años y mi pelo es de color Marrón.
    }
    

Securización de nulos

La evolución de este lenguaje ha sido muy pronunciada desde su creación y ha ido incorporando nuevas características y mejoras con el paso del tiempo. Una de las más importantes es la Null Safety, que se introdujo en la versión 2.12 de Dart y que es una de las principales razones por las que Flutter ha decidido migrar a la versión 2.12 de Dart. Permite a los desarrolladores evitar errores de tiempo de ejecución relacionados con el valor null.


    String? name = null;
    print(name.length); // Error: La propiedad 'length' no puede ser accedida de forma incondicional porque el receptor puede ser 'null'.
    

En el ejemplo anterior, la variable name es de tipo String?, lo que significa que puede ser String o null. Si la variable name es null, el programa fallará en tiempo de ejecución porque no se puede acceder a la propiedad length de un valor null. Para evitar este error, podemos utilizar el operador de null-aware de acceso condicional ?. para acceder a la propiedad length solo si la variable name no es null.


    String? name = null;
    print(name?.length); // Sin error
    

En la documentación de Dart tienes más información sobre la Null Safety que te recomiendo visitar para profundizar en el tema.

Conclusión y ejemplo

Hemos visto los conceptos básicos de Dart, los tipos de datos que existen, las funciones y lo fundamental para entender el lenguaje. Te invito a poner en práctica lo aprendido en este artículo con un pequeño ejercicio creando una sencilla aplicación para gestionar tareas por consola de comandos que permita crear, listar y eliminar. Una vez terminado puedes compararlo con este ejemplo de lo que podría ser el código fuente final dónde podrás identificar muchos de los conceptos aquí explicados.


    import 'dart:io';
    
    class Task {
      String description;
      bool isCompleted;
    
      Task(this.description, this.isCompleted);
    
      // Método para convertir una tarea a una cadena de texto que se puede guardar en un archivo.
      String toFileString() {
        return '$description|$isCompleted';
      }
    
      // Método estático para crear una tarea desde una cadena de texto recuperada de un archivo.
      static Task fromFileString(String line) {
        final parts = line.split('|');
        final description = parts[0];
        final isCompleted = parts[1] == 'true';
        return Task(description, isCompleted);
      }
    }
    
    class TaskList {
      List<Task> tasks = [];
    
      void addTask(String description) {
        final task = Task(description, false);
        tasks.add(task);
      }
    
      void completeTask(int index) {
        if (index >= 0 && index < tasks.length) {
          tasks[index].isCompleted = true;
        }
      }
    
      void removeTask(int index) {
        if (index >= 0 && index < tasks.length) {
          tasks.removeAt(index);
        }
      }
    
      void displayTasks() {
        print("Tasks:");
        for (var i = 0; i < tasks.length; i++) {
          final task = tasks[i];
          final status = task.isCompleted ? "Completed" : "Pending";
          print("$i. ${task.description} ($status)");
        }
      }
    
      // Método para guardar la lista de tareas en un archivo.
      void saveToFile(String filename) {
        final file = File(filename);
        final lines = tasks.map((task) => task.toFileString()).toList();
        file.writeAsStringSync(lines.join('\n'));
      }
    
      // Método para cargar la lista de tareas desde un archivo.
      void loadFromFile(String filename) {
        final file = File(filename);
        if (file.existsSync()) {
          final lines = file.readAsLinesSync();
          tasks = lines.map((line) => Task.fromFileString(line)).toList();
        }
      }
    }
    
    void main() {
      final taskList = TaskList();
      final filename = 'tasks.txt';
    
      // Cargar tareas desde el archivo (si existe).
      taskList.loadFromFile(filename);
    
      while (true) {
        print("Elige una acción:");
        print("1. Añadir tarea");
        print("2. Mostrar tareas");
        print("3. Completar tarea");
        print("4. Eliminar tarea");
        print("5. Guardar y Salir");
    
        var choice = int.tryParse(stdin.readLineSync() ?? '');
    
        switch (choice) {
          case 1:
            print("Introduce la descripción de la tarea:");
            var description = stdin.readLineSync() ?? '';
            taskList.addTask(description);
            break;
          case 2:
            taskList.displayTasks();
            break;
          case 3:
            taskList.displayTasks();
            print("Introduce el número de tarea para marcarla como `completada`:");
            var index = int.tryParse(stdin.readLineSync() ?? '');
            taskList.completeTask(index ?? -1);
            break;
          case 4:
            taskList.displayTasks();
            print("Introduce el número de tarea a eliminar:");
            var index = int.tryParse(stdin.readLineSync() ?? '');
            taskList.removeTask(index ?? -1);
            break;
          case 5:
            // Guardar tareas en el archivo y salir.
            taskList.saveToFile(filename);
            exit(0);
            break;
          default:
            print("Elección inválida. Inténtalo de nuevo.");
        }
      }
    }
    

En este artículo, hemos explorado las bases de Dart, el lenguaje de programación que impulsa el desarrollo de aplicaciones en Flutter. Hemos cubierto conceptos esenciales, como tipos de datos, funciones, clases y objetos, así como características un poco más avanzadas, incluyendo la programación asincrónica.

Hemos demostrado cómo crear programas interactivos en Dart, como una lista de tareas con capacidad de agregar, mostrar, completar y eliminar tareas, y cómo persistir los datos en archivos para hacer que las tareas sean accesibles a lo largo del tiempo. Para el próximo continuaremos indagando en Flutter y empezaremos a ver algunos ejemplos.

¡Happy Coding!

¿Te ha servido esta información?

Tu apoyo nos permite seguir compartiendo conocimiento de calidad. Si alguno de nuestros artículos te han aportado valor, considera hacer una donación para mantener este espacio en marcha y actualizado. ¡Gracias por tu contribución! 😊

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