DEV Community

Rubén Aguilera Díaz-Heredero
Rubén Aguilera Díaz-Heredero

Posted on • Updated on

Empezando con Dart

Resumen

En este artículo vas a encontrar una guía práctica sobre Dart orientada a su uso en aplicaciones flutter.

Entorno

  • Slimbook Prox15 32 Gb RAM i7
  • SlimbookOS (Ubuntu 20.04)
  • Dart 2.8.4
  • Visual Studio Code

Instalación en Ubuntu 20.04 y Hola mundo

Para la instalación de Dart en Ubuntu 20.04 tenemos que seguir los siguientes pasos:

$> sudo apt-get update
$> sudo apt-get install apt-transport-https
$> sudo sh -c 'wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -'
$> sudo sh -c 'wget -qO- https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list'
$> sudo apt-get update
$> sudo apt-get install dart
$> dart --version
Enter fullscreen mode Exit fullscreen mode

Lo primero que tenemos que hacer cuando empezamos en cualquier lenguaje es conseguir el Hola Mundo!. Para el caso de Dart vamos a crear el fichero holamundo.dart con el siguiente contenido:

main(List<String> args) {
  print('Hola Mundo! ' + args[0]);
}
Enter fullscreen mode Exit fullscreen mode

Y para ejecutarlo vamos a abrir el terminal y ejecutar:

$> dart holamundo.dart Dart
Enter fullscreen mode Exit fullscreen mode

De forma que por consola tenemos que ver el texto "Hola Mundo! Dart" siendo la garantía de que todo está correctamente instalado y configurado.

Creación de un proyecto completo

La creación del proyecto inicial se puede hacer desde Visual Studio Code con la opción "Dart: New Project" (siempre que se tenga instalada la extensión de Dart para este IDE).

Al seleccionar esta opción, el asistente nos dará a elegir entre las siguiente opciones:

  • AngularDart Web App: A web app with material design components.
  • Bare-bones Web App: A web app thah uses only core Dart libraries.
  • Console Application: A command-line application sample.
  • Dart Package: A starting point for Dart libraries or applications.
  • Simple Console Application: A simple command-line application.
  • StageXL Web App: A starting point for 2D animation and games.
  • Web Server: A web server build using the shelf package.

Una vez seleccionado, nos pedirá donde queremos crear los archivos, en caso de crear un proyecto con la opción "Console Application" nos creará una estructura de directorios parecida a esta:

Depuración de programas

Con la extensión de Visual Studio Code propuesta anteriormente la compatibilidad con el Debug es total, por lo que solo vamos a tener que pulsar en F5 para activar el modo de debug y que la ejecución se pare en cualquier punto de ruptura que hayamos puesto en el código.

Tenemos que tener un archivo .vscode/launch.json con el siguiente contenido:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Dart",
            "program": "bin/main.dart",
            "request": "launch",
            "type": "dart"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Tipos de datos

Dart es un lenguaje de tipado estricto por lo que permite definir las variables de un tipo especifico, haciendo que el IDE nos pueda ayudar a gestionarlas mejor.

Números

En Dart podemos declarar variables de tipo int y de tipo double.

int a = 11;
double b = 18.9;
Enter fullscreen mode Exit fullscreen mode

Cuando no se le da valor simplemente toma el valor null por defecto.

Strings

Las variables de tipo string se pueden declarar con comillas simples o con comillas dobles, siendo preferible la comilla simple.

String cad = 'Hola Mundo';
Enter fullscreen mode Exit fullscreen mode

También es posible definir String en multilínea con la siguiente sintaxis:

String multilinea = '''
  Esto es una cadena
  multilínea
'''
Enter fullscreen mode Exit fullscreen mode

Booleans

Una variable también podemos definir la tipo boolean de esta forma:

bool loading = true
bool loading = false
Enter fullscreen mode Exit fullscreen mode

Hay que tener en cuenta que las variables de tipo bool que no se inicializan tienen el valor null.

Listas

También podemos definir variables de tipo lista de valores que definimos de las siguientes formas:

List<String> colores = ['Red', 'Blue'];

List<String> colores = new List();
colores.add('Red');
colores.add('Blue');
colores.addall(['Orange', 'Purple']);

Enter fullscreen mode Exit fullscreen mode

En caso de declarar la lista con final:

final List<String> colores = ['Red', 'Blue'];
Enter fullscreen mode Exit fullscreen mode

Podemos seguir agregando valores con el método add y addAll, pero no podemos asignarle una nueva lista de valores a la misma variable.

Si se define como const de cualquiera de estas formas:

const List<String> colores = ['Red', 'Blue'];
List<String> colores = const['Red', 'Blue'];
Enter fullscreen mode Exit fullscreen mode

Al utilizar el método add o addAll vamos a recibir un error en tiempo de ejecución indicando que la lista no puede ser modificada.

Sets

La diferencia con las listas es que no admite valores duplicados y los valores se establecen con {}. De forma que podemos declarar un Set de Strings de esta forma:

Set<String> colores = {'Red', 'Blue'};
colores.add('Blue');
Enter fullscreen mode Exit fullscreen mode

Pero si añado un elemento que ya existe a este conjunto, simplemente lo ignora sin dar error.

Maps

También podemos definir el tipo de dato Map que no es más que una estructura de datos que almacena la información ordenada en clave - valor.

Map<String, String> colores = {
    'RED': 'red',
    'BLUE': 'blue'
}
Enter fullscreen mode Exit fullscreen mode

En caso de querer acceder al primer valor lo podríamos hacer con la síntaxis:

print(colores['RED']);
Enter fullscreen mode Exit fullscreen mode

También podemos definir un mapa que contenga cualquier tipo de valor, lo que se conoce en dart como "dynamic".

Map<String, dynamic> cajon = new Map();
cajon.addAll({'RED': 'red', 'BLUE': 'blue', 'YELLOW': 190});
Enter fullscreen mode Exit fullscreen mode

Comentarios

Los comentarios de una sola línea se hace con // y los comentarios de multilinea con /* */

Podemos hacer uso de dartDoc para añadir documentación a nuestras funciones:

/**
* Esta función no hace nada
* [arg1] Argumento 1
* [arg2] Argumento 2
*/
void noop(Strign arg1, String arg2) {}
Enter fullscreen mode Exit fullscreen mode

Operadores aritméticos

En Dart se pueden utilizar los operadores aritméticos para realizar operaciones básicas como:

int a = 10 + 5       //  15
int b = 20 - 10      //  10
int c = 10 * 2       //  20
double d = 10 / 2    // 5

double e = 10.0 % 3  // Devuelve el módulo, el sobrante de la división
double f = 10.0 ~/ 3 // Devuelve la parte entera de la división.
f = -f               // Cambia el signo de la variable
Enter fullscreen mode Exit fullscreen mode

Operadores de asignación

En Dart se utiliza el = para asignar valor a una variable, como ocurre en muchos otros lenguajes. Lo realmente característico de Dart es la forma de darle valor a una variable solo en el caso de que se null, de esta forma:

int a;

a ??= 11;
Enter fullscreen mode Exit fullscreen mode

El resultado será 11, pero si hacemos lo siguiente:

int a;

a = 3;

a ??= 11;
Enter fullscreen mode Exit fullscreen mode

Entonces el resultado será 3.

Operadores condicionales

Podemos utilizar el ?? para asignar valor en función de una condición, por ejemplo:

int a = b ?? c ?? d ?? 11;
Enter fullscreen mode Exit fullscreen mode

En caso de que b, c y d sean nulos, entonces el valor de a será 11. Si alguno de los anteriores no es nulo, a tomará el valor del primero de ellos de izquierda a derecha.

Operadores relacionales

Son los operadores que retornas valores booleanos, nunca devuelven null, nos permiten evaluar ciertas condiciones. En Dart tenemos:

  • > -- Mayor que
  • < -- Menor que
  • >= -- Mayor o igual que
  • <= -- Menor o igual que
  • == -- Igual
  • != -- Distintos
  • is -- Para saber si es de un tipo
  • is! -- Para saber que no es de un tipo

Control de flujo

If else

En Dart tenemos la posibilidad de hacer un if else con la siguiente sintaxis:

if (edad < 18) {
    print('No tienes la edad suficiente');
} else {
    print('Edad valida');
}
Enter fullscreen mode Exit fullscreen mode

Switch

Para el caso donde tengamos que anidar varios if else nos puede ser útil el switch, que tiene la siguiente sintaxis:

switch ( value ) {

    case case1:
      print('case1');
      break;

    case case2:
      print('case2');
      break;

    default:
     print('default');

}
Enter fullscreen mode Exit fullscreen mode

For

El bucle for nos sirve para recorrer colecciones, podemos hacerlo con un índice:

const List<String> colores = ['Red', 'Blue'];
for (int i = 0; i < colores.length; i++) {
    print(colores[i]);
}
Enter fullscreen mode Exit fullscreen mode

O podemos hacerlo con la sintaxis for..in:

const List<String> colores = ['Red', 'Blue'];
for (String color in colores) {
    print(color);
}
Enter fullscreen mode Exit fullscreen mode

También podemos hacer uso de "continue" para saltar un elemento de la colección o "break" para salir prematuramente del bucle.

While

El while es un bucle que se puede ejecutar de 0 a n veces, dependiendo de la condición. La sintaxis sería:

while (cond) {
    // algo
}
Enter fullscreen mode Exit fullscreen mode

Do while

El do while es un bucle que se puede ejecutar de 1 a n veces, dependiendo de la condición. La sintaxis sería:

do {
    // algo
} while (cond);
Enter fullscreen mode Exit fullscreen mode

Funciones

Las funciones en Dart como en cualquier otro lenguaje nos permiten agrupar código que vamos a llamar más de una vez. Para un caso donde tengo una función que recibe un String obligatorio, un String opcional y devuelve un String está sería la sintaxis:

String noop(String arg, [String arg2 = 'default']){
    //algo
    return 'algo';
}
Enter fullscreen mode Exit fullscreen mode

En caso de querer que los argumentos fueran por nombre simplemente los englobo en {} llamando a la función pasando los nombres, de esta forma:

String noop({String arg, String arg2}){
    //algo
    return 'algo';
}
noop(arg: 'value', arg2: 'value2');
Enter fullscreen mode Exit fullscreen mode

Los argumentos primitivos siempre se pasan por valor, es decir, no pueden ser modificados; mientras que las colecciones se pasan por referencia, de forma que cuando se modifican los valores se está haciendo de forma global.

Para evitar está situación en el paso por referencia se recomienda hacer una copia del objeto con la sintaxis:

collection = { ... collection }
Enter fullscreen mode Exit fullscreen mode

Las funciones se pueden simplificar con la notación fecha => , como en este ejemplo:

int sumar(int a, int b) {
    return a + b;
}

int sumar(int a, int b) => a + b;
Enter fullscreen mode Exit fullscreen mode

También se utiliza mucho esta notación para añadir las funciones que requieren algunos iterables como el de este ejemplo:

List<int> listado = [1,2,3,4,5,6];
List<int> newList = listado.where((n) => n > 2);
Enter fullscreen mode Exit fullscreen mode

En Dart también tenemos el concepto de callback, que no es más que una función que se llama dentro de otra función. Este sería un ejemplo de utilización.

main() {
    obtenerPersona('1', (Map persona) {
        print(persona);
    })
}

void obtenerPersona(String id, Function callback) {

    Map persona = {
        'id': '1',
        'nombre': 'Mateo'
    }

    callback(persona);

}
Enter fullscreen mode Exit fullscreen mode

Otros tipos de datos

Queue

en Dart tenemos el tipo de colección Queue que nos permite mantener la colección en una estructura de datos de cola que se puede iterar con un Iterator de Dart de la siguiente forma:

import 'dart:collection';

main() {
    Queue<int> cola = new Queue();
    cola.addAll([10,20,30,40,50]);

    Iterator i = cola.iterator;
    while(i.moveNext) {
        print(i.current);
    }
}

Enter fullscreen mode Exit fullscreen mode

Enumeraciones

En Dart también podemos definir enumeraciones para darle mayor significado y semántica a ciertos valores constantes de la aplicación. Este sería un ejemplo de utilización:

void main() {

    Audio volumen = Audio.bajo;

    switch(volumen) {

        case Audio.bajo : print('Volumen bajo'); break;
        case Audio.medio : print('Volumen medio'); break;
        case Audio.alto : print('Volumen alto'); break;
    }

}

enum Audio {
    bajo, 
    medio, 
    alto
}
Enter fullscreen mode Exit fullscreen mode

Future

Es el tipo de dato de Dart que se define para las tareas asíncronas, se podría asemejar a Promise de JavaScript. Este sería un ejemplo de uso:

void main() {

    Future<String> timeout = Future.delayed(Duration(seconds: 2), () {
      print('Después de 2 segundos');
      return 'Cadena que devuelve el future';
    });

    timeout.then(print).catchError(print);

    print('Fin método main');

}
Enter fullscreen mode Exit fullscreen mode

La salida de esta ejecución sería:

Fin método main
Después de 2 segundos
Cadena que devuelve el future
Enter fullscreen mode Exit fullscreen mode

Como en el caso de las promesas también podemos hacer uso de async / await con Future:

void main() async {

    Future<String> timeout = Future.delayed(Duration(seconds: 2), () {
      print('Después de 2 segundos');
      return 'Cadena que devuelve el future';
    });

    String resultado = await timeout;
    print(resultado);

    print('Fin método main');

}
Enter fullscreen mode Exit fullscreen mode

Pero hay que hacer notar que ahora la salida difiere del tratamiento anterior sin async/await en que el código se vuelve más secuencial y también más fácil de entender y mantener.

Después de 2 segundos
Cadena que devuelve el future
Fin método main
Enter fullscreen mode Exit fullscreen mode

En caso de producirse un error podemos capturarlo con un bloque try / catch de esta forma:

void main() async {
  Future<String> timeout = Future.delayed(Duration(seconds: 2), () {
    print('Después de 2 segundos');
    if (1 == 1) throw 'Error';
    return 'Cadena que devuelve el future';
  });

  try {
    String resultado = await timeout;
    print(resultado);
  } catch (err) {
    print(err);
  }

  print('Fin método main');
}
Enter fullscreen mode Exit fullscreen mode

Streams

Los Streams en Dart son muy parecidos a los Observables de RxJS y representan un flujo de información. Este sería un ejemplo sencillo:

import 'dart:async';

void main() async {
  final streamController = new StreamController<String>.broadcast();

  streamController.stream.listen(
      (event) => print(event),
      onError: (err) => print(err),
      onDone: () => print('Fin del stream'),
      cancelOnError: false
  );


  streamController.sink.add('info');
  streamController.sink.addError('Error info 2');
  streamController.sink.add('info 3');

  streamController.sink.close();

  print('Fin método main');
}
Enter fullscreen mode Exit fullscreen mode

El broadcast es para permitir más de un listen, sin establecerlo, al conectar un segundo listen tendríamos un error.

Es importante resaltar que para hacer uso de los Streams tenemos que importar el paquete 'async' de Dart; el cual nos permite definir un stream con la clase StreamController, donde con el método listen (lo que es el subscribe en RxJS) podemos definir que se tiene que hacer con la información que fuya en ese stream y con el método sink.add podemos inyectar información válida en el stream, y con sink.addError podemos inyectar un error en el stream el cual será gestionado por el bloque onError, de modo que cuando ejecutemos este código esta será la salida:

Fin método main
info
Error info 2
Enter fullscreen mode Exit fullscreen mode

Como se ve el último mensaje "info 3" y el OnDone no se muestra, si queremos que se muestren tendremos que establecer la propiedad "cancelOnError" a false en el listen del stream, de forma que ahora la salida sería:

Fin método main
info
Error info 2
info 3
Fin del stream
Enter fullscreen mode Exit fullscreen mode

Clases

En Dart también existe el concepto de clase, que nos permite modelar las distintas entidades de mi aplicación en atributos, métodos y constructores y ser utilizado para la definición de un tipo de dato.

Este es un ejemplo de creación de una clase básica, quizá lo más novedoso de Dart es que nos permite tener constructores por nombre como ocurre con Persona.withName.

class Persona {
  String _nombre;
  String _apellidos;

  String get nombre => _nombre;
  String get apellidos => _apellidos;

  set nombre(String nombre) => _nombre = nombre;
  set apellidos(String apellidos) => _apellidos = apellidos;

  Persona(this._nombre, this._apellidos);

  Persona.withName(this._nombre);

  @override
  String toString() => '$_nombre $_apellidos';
}
Enter fullscreen mode Exit fullscreen mode

Existe también una forma de crear las clases para que sean inmutables y siempre que todos los valores de los atributos sean iguales, no se creen más instancias en memoria.

class Persona {
  final String _nombre;
  final String _apellidos;

  String get nombre => _nombre;
  String get apellidos => _apellidos;


  const Persona(this._nombre, this._apellidos);

  @override
  String toString() => '$_nombre $_apellidos';
}
Enter fullscreen mode Exit fullscreen mode

Pero es importante que cuando se instancien los objetos se haga con la palabra reservada "const", de esta forma pintaremos true por consola:

Persona p = const Persona('Ruben', 'Aguilera');
Persona p2 = const Persona('Ruben', 'Aguilera');

print(p == p2);
Enter fullscreen mode Exit fullscreen mode

En Dart también contamos con los constructores facotry que devuelven una instancia de la clase en base al resultado de cierta lógica que debe devolver la instancia a partir de un constructor con nombre, nunca del constructor por defecto ya que se crea un bucle infinito.

class Persona {
  String _nombre = '';
  String _apellidos = '';

  String get nombre => _nombre;
  String get apellidos => _apellidos;


  factory Persona(String nombre, String apellidos) {
    if (nombre != apellidos) {
      return Persona.distinta(nombre, apellidos);
    }else {
      return Persona.igual(nombre, apellidos);
    }
  }

  Persona.distinta(String nombre, String apellidos) {
    this._nombre = 'distinta';
    this._apellidos = 'dintinta';
  }

  Persona.igual(String nombre, String apellidos) {
    this._nombre = 'igual';
    this._apellidos = 'igual';
  }

  @override
  String toString() => '$_nombre $_apellidos';
}

Enter fullscreen mode Exit fullscreen mode

En las clases de Dart también podemos definir propiedades y métodos estáticos que pertenezcan a la clase y no la instancia de la misma. Para definir una propiedad estática, me interesa utilizar el modificador const junto con static a fin de hacer que sea solo de lectura; y de igual forma se crean los métodos estáticos para ser llamados sin hacer el new de la clase, hay que tener en cuenta que estos métodos o manejan elementos estáticos o son funciones puras que no requieren de atributos de las clases. Por ejemplo:

class Paleta {

    static const List<String> colores = ['Red', 'Blue', 'Orange', 'Purple'];

    static void mostrarColores() => colores.forEach((print));

}
Enter fullscreen mode Exit fullscreen mode

Y sería llamado de esta forma, mostrando los colores establecidos en la lista.

Paleta.mostrarColores();
Enter fullscreen mode Exit fullscreen mode

Patrón singleton

Para implementar el patrón Singleton con Dart tenemos que hacer:

class EjemploSingleton {

    static final EjemploSingleton _singleton = new EjemploSingleton._internal();

    factory EjemploSingleton {
      return _singleton;
    }

    EjemploSingleton._internal();

    String prop = 'Propiedad 1';

}
Enter fullscreen mode Exit fullscreen mode

De esta forma cuando se modifique el valor de prop se hará para toda la aplicación al tener una única instancia.

Como en todo lenguaje orientado a objetos en Dart tenemos clases y la posibilidad de herencia, utilizando la palabra reservada "extends".

abstract class Animal {

    String nombre;
    String tipo;

    Animal(this.nombre, this.tipo);

    imprimirInfo() => print('''
    $nombre 
    $tipo
    ${this.calculoTiempoVida().toString()}''');

    double calculoTiempoVida();
}

class Gato extends Animal {

    Gato(String nombre) : super(nombre, 'Gato');

    @override
    imprimirInfo() => print(super.imprimirInfo() );

  @override
  double calculoTiempoVida() {
    return 7.0;
  }

}
Enter fullscreen mode Exit fullscreen mode

Mixins

Este es un concepto propio de Dart que hace que los métodos de las clases anotadas con la palabra reservada "mixin" puedan ser utilizados por cualquier otra clase que las quiera utilizar con la palabra reservada "with".

Una clase anotada con "mixin" tiene las siguientes restricciones:

  • No pueden ser instanciados.
  • No puede tener constructor.
  • No puede ser utilizada con "extends"

Este sería un ejemplo:

mixin Logger {

  void imprimirLog(String texto) {
    final fecha = DateTime.now();
    print('$fecha ----- $texto');
  }

}

class Gato with Logger {

  Gato() {
    imprimirLog('He creado un Gato');
  }

}
Enter fullscreen mode Exit fullscreen mode

Uso de Http

Para poder conectar con servicios remotos a través de Http, necesitamos immportar el paquete http, que como cualquier paquete de Dart lo podemos encontrar en el repositorio oficial https://pub.dev, una vez localizado lo damos de alta en la sección "dev_dependencies" de nuestro fichero "pubspec.yaml"

name: paquetes
description: A sample command-line application.
# version: 1.0.0
# homepage: https://www.example.com

environment:
  sdk: '>=2.8.1 <3.0.0'

#dependencies:
#  path: ^1.7.0

dev_dependencies:
  pedantic: ^1.9.0
  test: ^1.14.4
  http: 0.12.2
Enter fullscreen mode Exit fullscreen mode

y al guardar tenemos que observar que se dispara el proceso pub get de forma automática.

Ahora en el fichero main.dart o donde lo vayamos a utilizar vamos a declarar el paquete http y hacer una petición get, de esta forma:

import 'package:http/http.dart' as http;

void main(List<String> arguments) {
  final url = 'https://api.github.com/users';
  http.get(url).then((res) => print(res.body));
}
Enter fullscreen mode Exit fullscreen mode

Recetas

Permitir introducir información por consola

Lo tenemos que hacer con la librería "dart:io" de esta forma:

import 'dart:io';

main(List<String> args) {

  stdout.writeln('Introduce la edad');

  int edad = int.parse(stdin.readLineSync());

  stdout.writeln('Has introducido $edad');

}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)