latest

Introducción al lenguaje Dart. Intermedio. Parte 1

Siguiendo un poco la filosofía de este curso de Flutter para personas que ya saben programar en lenguajes orientados a objetos como Java o C#, no vamos a detallar en qué consiste dicho paradigma, pasando a explicar directamente las diferencias en las definiciones de clases, constructores, funciones/métodos, etc.

Clases en Flutter

La definición básica de una clase en Flutter apenas varía nada de cómo estamos acostumbrados a hacerlo en Java.

void main(){  
  Jugador jug1 = new Jugador();
  jug1.nombre= "Gilmour";
  print(jug1.toString());
}

class Jugador{
  String nombre;
  num puntos;

  toString() => "$nombre tiene $puntos puntos";
}

Vamos a destacar algunos matices diferenciadores que no se aprecian a simple vista. Al igual que en Java, en Dart existe un constructor por defecto (Jugador()) que se emplea cuando no se ha definido explícitamente otro constructor. Es el caso del código anterior, la clase Jugador no tiene definido explícitamente su constructor y por ese motivo se utiliza uno por defecto.

El constructor (Jugador()) reserva espacio en memoria para un objeto de tipo Jugador con todas sus propiedades, si bien, no las inicializa con valores, siendo todas null en ese momento. En el ejemplo de código anterior, el nombre del objeto de tipo Jugador jug1 se modifica a posteriori con el literal "Gilmour", por tanto, cuando imprimimos el resultado del método toString() que, como se puede comprobar ha sido sobreescrito, se obtiene que la propiedad puntos (de tipo num) es null (no fue inicializada a posteriori). Esto difiere de Java donde un tipo básico entero no es tratado como un objeto y se inicializa con valor 0 por defecto.

En Dart, como ya vimos con anterioridad, todos los tipos básicos se tratan como objetos y se inicializan a null, por defecto. No obstante, esta aseveración de "ser tratados como objetos" no es del todo cierta dado que los tipos básicos de Dart, usados como argumentos en funciones/métodos, se pasan "por valor" y no "por referencia" como lo haría un objeto,  por ejemplo, nuestro objeto Jugador en el ejemplo. El siguiente código ilustra lo que aquí se ha comentado.

void main(){   
  Jugador jug1 = new Jugador();
  jug1.nombre= "Gilmour";
  print(jug1.toString());
  
  String nombreReal ="Juan";
  jug1.metodoPasoPorValor(nombreReal);
  print(nombreReal);  
  
  jug1.metodoPasoPorReferencia(jug1);
  print(jug1.toString());
}

class Jugador{
  String nombre;
  num puntos;
  
  toString() => "$nombre tiene $puntos puntos";
  
  void metodoPasoPorValor(String nombreReal){
    nombreReal = "nombre_" + nombreReal;
  }
  
  void metodoPasoPorReferencia(Jugador jugador){
    jugador.nombre="Waters";
  }
}

Cuando jug1 invoca el método metodoPasoPorValor()la variable de tipo cadena que se pasa como argumento no ha modificado su contenido tras la invocación. Sigue siendo el literal "Juan". Contrariamente, cuando jug1 invoca el método metodoPasoPorReferencia(), pasando por referencia el propio objeto de tipo Jugador jug1, éste sí permanece modificado tras la invocación. En este segundo caso no manipulamos una copia de la variable en otra zona de memoria, sino que trabajamos con la misma referencia, es decir, con el mismo espacio de memoria reservado.

Además de la puntualización anterior, cabe mencionar otra diferencia con Java. En el instante en el que se define explícitamente un constructor, el contructor por defecto deja de poder utilizarse. Lo ilustramos en el siguiente ejemplo.

void main(){  
  Jugador jug1 = new Jugador("Gilmour",100);
  print(jug1.toString()); 
  
  // La siguiente sentencia daría un error de compilación
  // Jugador jug2 = Jugador();
}

class Jugador{
  String nombre;
  num puntos;
  
  // Constructor definido explícitamente:
  Jugador(this.nombre,this.puntos);
  
  toString() => "$nombre tiene $puntos puntos";
}

Prestad atención en el ejemplo anterior al constructor definido explícitamente y como usa el operador this, seguido del nombre de la variable para lograr reducir el tamaño del constructor sin necesidad de usar un bloque de llaves. Si no tenemos que realizar ningún procesamiento de los parámetros de entrada antes de asignárselo a las propiedades del objeto de tipo Jugador esta forma reducida para definir explícitamente un constructor es muy adecuada.

Constructores en Flutter

Con lo que se ha comentado anteriormente queda clara esta diferencia en la definición de constructores entre Java y Dart. Al igual que en la definición de métodos en Dart, los constructores también pueden tener parámetros obligatorios y parámetros opcionales (posicionales y nombrados) en su definición. A continuación os enseñamos un ejemplo que os animamos a probar en DartPad.

void main(){  
  Jugador jug1 = new Jugador("Gilmour",100,["LoL","WoW","Dota 2"]);
  print(jug1.toString()); 
  
  jug1.tresJuegosPreferidos[0] = "CoD: WWII";
  print(jug1.toString() + "\n"); 
  
  Jugador jug2 = Jugador("Wright",80,
                 ["WoW","Monster Hunter:World","Dota 2"],nombreReal:'Richard');
  print(jug2.toString()); 
  
  jug2.tresJuegosPreferidos[0] = "Half-Life 2";
  print(jug2.toString()); 
}

class Jugador{
  String nombre;
  num puntos;
  List tresJuegosPreferidos;
  String nombreReal;
  
  // Constructor definido explícitamente
  Jugador(this.nombre,this.puntos,this.tresJuegosPreferidos,{this.nombreReal=""});
  
  toString() => "$nombre de nombre real: $nombreReal tiene $puntos puntos" 
    " y estos son sus 3 juegos preferidos: $tresJuegosPreferidos";
}

Como podéis comprobar en este código, se implementa una clase Jugador de videojuegos que tiene, además de su nombre en el juego y puntuación, una lista con sus tres juegos preferidos en orden y un parámetro opcional nombrado para indicar su nombre real. Dicho parámetro opcional nombreReal será por defecto una cadena vacía "", que no es igual que una variable cadena con valor null.  

Probadlo en DartPad y fijaros también en lo fácil que es en Dart pasar un List a String, simplemente con el operador $ de interpolación de cadenas y sin indicar ningún índice de la lista. Os proporcionamos también la siguiente variación del código anterior.

void main(){  
  Jugador jug1 = new Jugador("Gilmour",100);
  print(jug1.toString()); 
  
  jug1.tresJuegosPreferidos[0] = "CoD: WWII";
  print(jug1.toString() + "\n"); 
  
  Jugador jug2 = Jugador("Wright",80,
                 tresJuegosPreferidos:["WoW","Monster Hunter:World",
                                      "Dota 2"],nombreReal:'Richard');
  print(jug2.toString()); 
  
  jug2.tresJuegosPreferidos[0] = "Half-Life 2";
  print(jug2.toString()); 
}

class Jugador{
  String nombre;
  num puntos;
  List tresJuegosPreferidos;
  String nombreReal;
  
  // Constructor definido explícitamente
  Jugador(this.nombre, this.puntos,{this.nombreReal="", List tresJuegosPreferidos}){
    if(tresJuegosPreferidos==null){
      this.tresJuegosPreferidos = ["GoW","LoL","WoW"];
    } else {
      this.tresJuegosPreferidos = tresJuegosPreferidos;
    }
  }
  
  toString() => "$nombre de nombre real: $nombreReal tiene $puntos puntos" 
    " y estos son sus 3 juegos preferidos: $tresJuegosPreferidos";
}

En esta variación, tenemos los dos argumentos obligatorios y dos argumentos opcionales nombrados que son nombreReal y la lista tresJuegosPreferidos. En la definición se entremezcla parámetros asignados con this, con una asignación tradicional de la lista donde se define su contenido por defecto (["GoW","LoL","WoW"]); o se asigna a la propiedad de Jugador el contenido de la List tresJuegosPreferidosque se pasa como parámetro.

Una diferencia relativamente importante entre Java y Dart, en lo relacionado con la definición de constructores, es que Dart no permite la sobrecarga de éstos del mismo modo que Java. Es decir, sólo podemos definir un constructor y no sobrecargarlo. Afortunadamente, proporciona otro mecanismo compatible que nos permite instanciar objetos de la misma clase de diferentes formas, son los constructores nombrados (named constructors, en inglés). Veámoslo con un ejemplo.

void main(){
	
    var xml = "<juego><nombre>FIFA 2019</nombre>" +
    "<genero>deporte</genero></juego>";
    
    var json = '{"juego":{"nombre":"FIFA 2019", "genero":"deporte"}}';
    
    var juego1 = new Juego.fromXML(xml);
    Juego juego2 = Juego.fromJSON(json);
}
class Juego{
	// Propiedades del objeto Juego...
    // ...
    
    Juego.fromXML(String XML){
    	// ...
    }
    
    Juego.fromJSON(String JSON){
    	// ...
    }
}

Esta característica de constructores nombrados no sólo facilita la sobrecarga del constructor, además podemos tener varios constructores definidos con los mismos argumentos (en tipo y número) y que realicen diferentes procesamientos, es el caso del ejemplo anterior.

En el ejemplo podemos ver una clase Juego con dos constructores nombrados para extraer las características del juego a partir del análisis de dos cadenas de naturaleza diferente. Una guarda las propiedades del juego empleando el lenguaje de marcado XML; mientras que la otra cadena utiliza la sintaxis del formato JSON.

Hasta aquí la primera parte de conceptos intermedios de la Introducción al lenguaje Dart. En la siguiente parte del curso de Flutter nos adentraremos en como se implementan los mecanismos de herencia y sobreescritura con Dart.

Author image
Iván González is postdoctoral researcher at the Castilla-La Mancha University.
Ciudad Real (Spain)