Empezando con Dart
Rubén Aguilera Díaz-Heredero
Posted on July 29, 2020
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
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]);
}
Y para ejecutarlo vamos a abrir el terminal y ejecutar:
$> dart holamundo.dart Dart
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"
}
]
}
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;
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';
También es posible definir String en multilínea con la siguiente sintaxis:
String multilinea = '''
Esto es una cadena
multilínea
'''
Booleans
Una variable también podemos definir la tipo boolean de esta forma:
bool loading = true
bool loading = false
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']);
En caso de declarar la lista con final:
final List<String> colores = ['Red', 'Blue'];
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'];
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');
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'
}
En caso de querer acceder al primer valor lo podríamos hacer con la síntaxis:
print(colores['RED']);
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});
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) {}
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
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;
El resultado será 11, pero si hacemos lo siguiente:
int a;
a = 3;
a ??= 11;
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;
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');
}
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');
}
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]);
}
O podemos hacerlo con la sintaxis for..in:
const List<String> colores = ['Red', 'Blue'];
for (String color in colores) {
print(color);
}
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
}
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);
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';
}
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');
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 }
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;
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);
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);
}
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);
}
}
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
}
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');
}
La salida de esta ejecución sería:
Fin método main
Después de 2 segundos
Cadena que devuelve el future
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');
}
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
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');
}
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');
}
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
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
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';
}
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';
}
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);
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';
}
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));
}
Y sería llamado de esta forma, mostrando los colores establecidos en la lista.
Paleta.mostrarColores();
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';
}
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;
}
}
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');
}
}
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
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));
}
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');
}
Posted on July 29, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.