Flutter DART Clases

De MediaWiki
Revisión del 10:53 4 oct 2026 de Angelfg (discusión | contribuciones) (→‎Solución Ejercicio 3)
(dif) ← Revisión anterior | Revisión actual (dif) | Revisión siguiente → (dif)
Ir a la navegación Ir a la búsqueda

Introducción

  • Información: En VS Code, para que aparezcan las sugerencias de métodos, propiedades y otros miembros disponibles podemos emplear la combinación de teclas: Control + Barra Espaciadora.


  • Como comenté anteriormente, este curso está pensado para alumnos que ya tienen conocimientos previos en algún lenguaje de programación orientado a objetos.
 : En esta sección se van a explicar las diferencias y características de Dart con respecto a otros lenguajes orientados a objetos en lo que se refiere al uso de clases y objetos.
  • En Dart no es necesario utilizar `new` cuando instanciamos una clase.
 : Aunque la palabra clave `new` sigue siendo válida al invocar un constructor se aconseja omitirla.
 : Esto resulta especialmente útil en Flutter, donde se crean objetos y widgets de forma continua y el uso repetido de `new` haría el código más redundante.
Veamos un ejemplo, empleando conceptos habituales de otros lenguajes de programación orientados a objetos, como la redefinición del método `toString`:
class Vehiculo {
int? anoCompra; // Admite null porque inicialmente no le asignamos ningún valor.
String? matricula;
String? marca;

@override
String toString() =>
'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
final veh1 = Vehiculo();
veh1.marca = 'Opel';
veh1.matricula = '1234GHZ';
veh1.anoCompra = 2018;

print(veh1);

final veh2 = Vehiculo();
veh2.marca = 'Peugeot';
veh2.matricula = '6543HZZ';
veh2.anoCompra = 2020;

print(veh2);
}


Constructores

  • Los constructores tienen una finalidad similar a los de Java: permiten inicializar un objeto en el momento de crearlo.
 Sin embargo, Dart incorpora algunas características propias, como los constructores con nombre, las listas de inicialización, los constructores `factory` y los súper parámetros.
 Veamos un ejemplo de uso:
class Vehiculo {
int? anoCompra;
String? matricula;
String? marca;

Vehiculo(int anoCompra, String matricula, String marca) {
this.anoCompra = anoCompra;
this.marca = marca;
this.matricula = matricula;
}

@override
String toString() =>
'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
final veh1 = Vehiculo(2018, '1234GHZ', 'Opel');
print(veh1);

final veh2 = Vehiculo(2020, '6543HZZ', 'Peugeot');
print(veh2);
}
Un constructor es una función especial asociada a la creación e inicialización de objetos. No debe confundirse con un método ordinario.
Los constructores pueden recibir parámetros posicionales, opcionales o con nombre, al igual que otras funciones de Dart.
Ejercicio propuesto: Modifica el ejercicio anterior y haz que los parámetros puedan ser enviados en cualquier orden especificando el nombre del parámetro en la llamada. Haz que todos ellos sean obligatorios.




  • En el ejemplo anterior las propiedades admiten valores nulos porque no se inicializan en el momento de declararlas.
 : Sin embargo, si siempre vamos a proporcionar sus valores al crear el objeto, podemos hacer que sean no anulables e inicializarlas directamente desde el constructor.
 : Dart proporciona una sintaxis abreviada para realizar esta asignación:
class Vehiculo {
int anoCompra;
String matricula;
String marca;

Vehiculo(this.anoCompra, this.marca, this.matricula);

@override
String toString() =>
'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
final veh1 = Vehiculo(2018, 'Opel', '1234GHZ');
print(veh1);

final veh2 = Vehiculo(2020, 'Peugeot', '6543HZZ');
print(veh2);
}
En `Vehiculo(this.anoCompra, this.marca, this.matricula)`, Dart asigna automáticamente cada parámetro recibido a la propiedad correspondiente del objeto.



  • Si queremos emplear parámetros con nombre debemos utilizar las llaves { }.
Si además queremos que sea obligatorio indicar esos parámetros al llamar al constructor, emplearemos la palabra reservada required, como ya vimos anteriormente.
class Vehiculo {
int anoCompra;
String matricula;
String marca;

Vehiculo({
required this.anoCompra,
required this.marca,
required this.matricula,
});

@override
String toString() =>
'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
final veh1 = Vehiculo(
anoCompra: 2018,
marca: 'Opel',
matricula: '1234GHZ',
);
print(veh1);

final veh2 = Vehiculo(
anoCompra: 2020,
marca: 'Peugeot',
matricula: '6543HZZ',
);
print(veh2);
}



NOTAS IMPORTANTES:

  • En Dart los constructores no se heredan.
  • Para inicializar la parte correspondiente a una clase superior podemos llamar a uno de sus constructores mediante `super`.




Múltiples constructores. Constructores con nombre

  • Dart no permite la sobrecarga tradicional de métodos o constructores, es decir, no podemos declarar varias veces un método o constructor con el mismo nombre y diferenciarlos únicamente por el número o tipo de sus parámetros.
 : Para proporcionar diferentes formas de crear un objeto, Dart permite utilizar constructores con nombre.
 : Su forma general es:

NombreClase.nombreConstructor(parámetros)

Veamos un ejemplo:
class Vehiculo {
  int anoCompra;
  String matricula;
  String marca;

  Vehiculo({
    required this.anoCompra,
    required this.marca,
    required this.matricula,
  });

  Vehiculo.sinAnho({required this.marca, required this.matricula})
    : anoCompra = 2000;

  Vehiculo.sinAnhoMarca({required this.matricula})
    : anoCompra = 2000,
      marca = 'Opel';

  @override
  String toString() =>
      'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
  final veh1 = Vehiculo(anoCompra: 2018, marca: 'Opel', matricula: '1234GHZ');
  print(veh1);

  final veh2 = Vehiculo.sinAnho(marca: 'Opel', matricula: '2234GHZ');
  print(veh2);

  final veh3 = Vehiculo.sinAnhoMarca(matricula: '3234GHZ');
  print(veh3);
}
  • Nota importante: En los constructores anteriores no recibimos todos los valores necesarios para inicializar las propiedades del objeto.
 : Como las propiedades `anoCompra` y `marca` no admiten valores nulos, debemos inicializarlas antes de que finalice la construcción del objeto.
 : Para hacerlo hemos empleado una característica de Dart denominada lista de inicialización.
La lista de inicialización aparece después de los parámetros del constructor, precedida por dos puntos (`:`):
Vehiculo.sinAnhoMarca({required this.matricula,}) : anoCompra = 2000, marca = 'Opel';
Las expresiones de la lista de inicialización se ejecutan antes del cuerpo del constructor.
Más información: Constructores en Dart.


Constructores Factory

  • Los constructores `factory` se utilizan cuando la creación de un objeto necesita un comportamiento diferente al de un constructor generativo convencional.


  • Por ejemplo, pueden utilizarse cuando:
  • queremos devolver una instancia creada anteriormente, como ocurre al utilizar una caché;
  • queremos devolver una instancia de una subclase compatible;
  • queremos ejecutar lógica previa antes de decidir qué objeto devolver;
  • queremos transformar o interpretar información antes de delegar la construcción a otro constructor.
Se definen anteponiendo la palabra reservada factory al nombre del constructor.
Un constructor `factory` debe devolver un objeto compatible con el tipo de la clase. Puede hacerlo mediante `return` o utilizando la sintaxis de función flecha (`=>`).
Un uso habitual consiste en delegar la creación del objeto al constructor principal.
Veamos un ejemplo:
class Vehiculo {
  int anoCompra;
  String matricula;
  String marca;

  Vehiculo({
    required this.anoCompra,
    required this.marca,
    required this.matricula,
  });

  factory Vehiculo.sinAnho({
    required String marca,
    required String matricula,
  }) => Vehiculo(anoCompra: 2020, marca: marca, matricula: matricula);

  @override
  String toString() =>
      'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
  final veh2 = Vehiculo.sinAnho(marca: 'Opel', matricula: '2234GHZ');

  print(veh2);
}


Empleando Mapas

  • En esta sección vamos a ver cómo crear un objeto utilizando como fuente de datos un mapa.
 :* Debéis recordar lo explicado acerca de los valores nulos al emplear mapas.
 :* También utilizaremos las listas de inicialización vistas anteriormente.
Partimos de un mapa que almacena la información de un vehículo.
Es habitual recibir datos procedentes de servicios web en formato JSON y, una vez decodificados, trabajar con estructuras del tipo `Map<String, dynamic>`.


class Vehiculo {
  int anoCompra;
  String matricula;
  String marca;

  Vehiculo({
    required this.anoCompra,
    required this.marca,
    required this.matricula,
  });

  Vehiculo.fromMap(Map<String, dynamic> dato)
    : anoCompra = int.parse(dato['anoCompra'].toString()),
      matricula = dato['matricula'] as String,
      marca = dato['marca'] as String;

  @override
  String toString() =>
      'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
  final vehiculoJson = <String, dynamic>{
    'anoCompra': '2020',
    'matricula': '1234GHZ',
    'marca': 'Opel',
  };

  final veh1 = Vehiculo.fromMap(vehiculoJson);

  print(veh1);
}

}
El tipo `Map<String, dynamic>` indica que las claves del mapa son cadenas de caracteres, mientras que sus valores pueden ser de diferentes tipos.



  • Importante: utilizar `dynamic` no garantiza que los datos sean correctos ni evita errores en tiempo de ejecución.
Por ejemplo:
  • `dato['matricula'] as String` producirá un error si el valor existente no es un `String`.
  • `int.parse(...)` producirá un `FormatException` si el contenido no puede convertirse a un número entero.
  • Que la clave no exista en los datos recibidos de internet.
Por tanto, al trabajar con información externa conviene comprobar y validar los valores recibidos antes de utilizarlos.
USAREMOS ESTA FORMA PARA PROCESAR LOS DATOS QUE PROVENGAN DE UNA FUENTE DE DATOS EXTERNA
class Vehiculo {
  int anoCompra;
  String matricula;
  String marca;

  Vehiculo({
    required this.anoCompra,
    required this.marca,
    required this.matricula,
  });

  Vehiculo.fromMap(Map<String, dynamic> dato)
    : anoCompra = int.tryParse(dato['anoCompra']?.toString() ?? '') ?? 1990,
      matricula = dato['matricula']?.toString() ?? '',
      marca = dato['marca']?.toString() ?? '';

  @override
  String toString() =>
      'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
  final vehiculoJson = <String, dynamic>{
    'anoCompra': '2020',
    'matricula': '1234GHZ',
    'marca': 'Opel',
  };

  final veh1 = Vehiculo.fromMap(vehiculoJson);

  print(veh1);
}

}




  • Podéis emplear Quicktype para generar automáticamente modelos a partir de una estructura JSON.
  • También podríamos utilizar un constructor `factory`, como vimos en el apartado anterior:
class Vehiculo {
int anoCompra;
String matricula;
String marca;

Vehiculo({
required this.anoCompra,
required this.marca,
required this.matricula,
});

factory Vehiculo.fromJson(Map<String, dynamic> dato) => Vehiculo( 
      anoCompra : int.tryParse(dato['anoCompra']?.toString() ?? '') ?? 1990,
      matricula : dato['matricula']?.toString() ?? '',
      marca : dato['marca']?.toString() ?? '');

@override
String toString() =>
'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra';
}

void main() {
final vehiculoJson = <String, dynamic>{
'anoCompra': '2020',
'matricula': '1234GHZ',
'marca': 'Opel',
};

final veh1 = Vehiculo.fromJson(vehiculoJson);

print(veh1);
}


Herencia

  • Para indicar que una clase hereda de otra empleamos la palabra reservada extends.
 : La palabra reservada `super` permite acceder a miembros de la superclase y llamar a sus constructores.
Veamos un ejemplo utilizando súper parámetros, disponibles desde Dart 2.17.
Los súper parámetros permiten enviar directamente un parámetro al constructor de la superclase sin tener que declararlo de nuevo y llamar explícitamente a `super(...)`.
class Personal {
String nombreCompleto;

Personal({required this.nombreCompleto});
}

class Alumno extends Personal {
String numCarnet;

Alumno({
required this.numCarnet,
required super.nombreCompleto,
});

@override
String toString() =>
'Alumna/o: $nombreCompleto con número de carnet: $numCarnet';
}

class Profesor extends Personal {
String nrp;

Profesor({
required this.nrp,
required super.nombreCompleto,
});

@override
String toString() =>
'Profesor/a: $nombreCompleto con número de registro personal: $nrp';
}

void main() {
final alum1 = Alumno(
numCarnet: 'C1A',
nombreCompleto: 'Juan Luis Rodriguez',
);

final prof1 = Profesor(
nrp: '1234534',
nombreCompleto: 'Luisa Sanchez Dragó',
);

print(alum1);
print(prof1);
}


Overriding - Sobrescritura

  • La sobrescritura (overriding) permite que una subclase proporcione su propia implementación de un miembro que ya existe en la superclase.
 : Es uno de los mecanismos que permiten aplicar polimorfismo en una jerarquía de herencia.
Ya hemos utilizado este concepto al sobrescribir el método `toString`, heredado de la clase `Object`.
  • Es recomendable emplear la anotación `@override` cuando sobrescribimos un miembro.
 : No es obligatoria para que se produzca la sobrescritura, pero permite que el analizador de Dart compruebe nuestra intención y detecte posibles errores.
  • Nota: No debemos confundir sobrescritura (overriding) con sobrecarga (overloading).
 : La sobrecarga consiste en definir varios métodos con el mismo nombre pero diferentes listas de parámetros.
 : Dart no soporta este tipo de sobrecarga tradicional.
Veamos un ejemplo en el que definimos el método `imprimirDatos` en la superclase.
Este método es heredado por las subclases.
La clase `Alumno` lo sobrescribe y, además, llama desde su nueva implementación al método existente en la clase padre mediante `super.imprimirDatos()`.
class Personal {
String nombreCompleto;

Personal(this.nombreCompleto);

void imprimirDatos() {
print('Nombre completo: $nombreCompleto');
}
}

class Alumno extends Personal {
String numCarnet;

Alumno({
required this.numCarnet,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
void imprimirDatos() {
super.imprimirDatos();
print('Carnet: $numCarnet');
}

@override
String toString() =>
'Alumna/o: $nombreCompleto con número de carnet: $numCarnet';
}

class Profesor extends Personal {
String nrp;

Profesor({
required this.nrp,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
String toString() =>
'Profesor/a: $nombreCompleto con número de registro personal: $nrp';
}

void main() {
final alum1 = Alumno(
numCarnet: 'C1A',
nombreCompleto: 'Juan Luis Rodriguez',
);

final prof1 = Profesor(
nrp: '1234534',
nombreCompleto: 'Luisa Sanchez Dragó',
);

print(alum1);
print(prof1);

alum1.imprimirDatos();
prof1.imprimirDatos();
}


Librerías. Privado/Público

  • Un fichero `.dart` puede contener múltiples declaraciones: clases, funciones, variables, etc.
 : Normalmente, un fichero Dart constituye una biblioteca (library).
 : También es posible que una biblioteca esté formada por varios ficheros mediante el mecanismo `part`.
Podemos agrupar bibliotecas y otros recursos para formar paquetes.
Más información: Libraries & imports.
  • Para utilizar declaraciones definidas en otra biblioteca debemos importar dicha biblioteca utilizando la palabra reservada import.
  • Este ejemplo será necesario realizarlo en un IDE como Android Studio o VS Code.
Supongamos que tenemos un archivo denominado centro.dart con las siguientes clases:
class Personal {
String nombreCompleto;

Personal(this.nombreCompleto);
}

class Alumno extends Personal {
String numCarnet;

Alumno({
required this.numCarnet,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
String toString() =>
'Alumna/o: $nombreCompleto con número de carnet: $numCarnet';
}

class Profesor extends Personal {
String nrp;

Profesor({
required this.nrp,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
String toString() =>
'Profesor/a: $nombreCompleto con número de registro personal: $nrp';
}
Ahora queremos utilizar estas clases desde otro archivo Dart.
Para ello empleamos `import`:
import 'centro.dart';

void main() {
final alum1 = Alumno(
numCarnet: 'C1A',
nombreCompleto: 'Juan Luis Rodriguez',
);

final prof1 = Profesor(
nrp: '1234534',
nombreCompleto: 'Luisa Sanchez Dragó',
);

print(alum1);
print(prof1);
}


  • Dart incluye numerosas bibliotecas que podemos utilizar mediante el `import` correspondiente.
 : Por ejemplo, la biblioteca dart:math proporciona funciones y clases relacionadas con operaciones matemáticas y generación de números pseudoaleatorios.
import 'dart:math';

void main() {
final semilla = Random();

for (var cont = 0; cont < 10; cont++) {
print('Aleatorio generado: ${semilla.nextInt(50)}');
}
}
  • Podemos asignar un alias o prefijo a una biblioteca mediante la palabra reservada as:
import 'dart:math' as math;

void main() {
final semilla = math.Random();

for (var cont = 0; cont < 10; cont++) {
print('Aleatorio generado: ${semilla.nextInt(50)}');
}
}



  • Si una biblioteca contiene muchas declaraciones podemos importar únicamente aquellas que queremos utilizar mediante la palabra reservada `show`:
import 'package02/centro/centro.dart' show Alumno, Profesor;
`show` no se limita a clases. Puede utilizarse con cualquier nombre exportado por la biblioteca, como funciones, variables, enumeraciones, etc.
  • También podemos hacer lo contrario mediante `hide`, importando todas las declaraciones públicas excepto las indicadas:
import 'package02/centro/centro.dart' hide Personal;




  • En Dart no existen modificadores de acceso `public`, `private` y `protected` equivalentes a los de lenguajes como Java.
Los identificadores son públicos de forma predeterminada.
Para declarar un miembro o una clase como privado debemos anteponer un guion bajo (`_`) a su nombre.
Importante: La privacidad en Dart se aplica a nivel de biblioteca, no estrictamente a nivel de clase ni de fichero.
Por tanto, un identificador que comienza por `_` puede ser utilizado desde cualquier código perteneciente a la misma biblioteca, pero no desde una biblioteca externa.
  • Siguiendo el ejemplo anterior, si no queremos exponer directamente la clase `Personal` fuera de su biblioteca podemos declararla como `_Personal`:
class _Personal {
String nombreCompleto;

_Personal(this.nombreCompleto);
}

class Alumno extends _Personal {
String numCarnet;

Alumno({
required this.numCarnet,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
String toString() =>
'Alumna/o: $nombreCompleto con número de carnet: $numCarnet';
}

class Profesor extends _Personal {
String nrp;

Profesor({
required this.nrp,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
String toString() =>
'Profesor/a: $nombreCompleto con número de registro personal: $nrp';
}
Desde otra biblioteca que importe este fichero no podremos referenciar directamente la clase `_Personal`:
Flutter dart private 1.JPG
  • El mismo mecanismo puede aplicarse a métodos, propiedades, funciones, variables y otras declaraciones.
Nota: El carácter `_` controla la visibilidad de un identificador, pero no impide que la propia biblioteca cree objetos de esa clase.
Si queremos indicar que una clase no debe poder instanciarse directamente, podemos declararla como `abstract`, como veremos a continuación.


Getters y Setters

  • Los getters y setters son miembros especiales que permiten controlar el acceso y la modificación de determinadas propiedades manteniendo una sintaxis similar a la lectura o asignación directa de una variable.
  • Un getter (`get`) permite obtener un valor.
  • Un setter (`set`) permite asignar o modificar un valor.
La llamada a un getter o setter se realiza sin utilizar paréntesis.
void main() {
final prof1 = Profesor(
nrp: '1234534',
nombreCompleto: 'Luisa Sanchez Dragó',
);

print(prof1);

print('Sueldo: ${prof1.sueldo}');

prof1.sueldo = 100.34; // Llama al setter.
print('Nuevo sueldo: ${prof1.sueldo}');

prof1.sueldo = -50; // El setter ajustará el valor.
print('Nuevo sueldo tras intento negativo: ${prof1.sueldo}');
}

class _Personal {
String nombreCompleto;

_Personal(this.nombreCompleto);
}

class Profesor extends _Personal {
String nrp;
double _sueldo = 0;

double get sueldo => _sueldo;

set sueldo(double nuevoSueldo) {
_sueldo = nuevoSueldo <= 0 ? 0 : nuevoSueldo;
}

Profesor({
required this.nrp,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
String toString() =>
'Profesor/a: $nombreCompleto con número de registro personal: $nrp';
}


  • También podemos utilizar getters y setters para validar los valores almacenados en las propiedades de una clase:
void main() {
final alum1 = Alumno(
num: 'C1A',
nombreCompleto: 'Juan Luis Rodriguez',
);

print(alum1);
}

class _Personal {
String nombreCompleto;

_Personal(this.nombreCompleto);
}

class Alumno extends _Personal {
String _numCarnet;

String get numCarnet => _numCarnet;

set numCarnet(String numero) {
_numCarnet = numero.isNotEmpty ? numero : '1';
}

Alumno({
required String num,
required String nombreCompleto,
})  : _numCarnet = num.isNotEmpty ? num : '1',
super(nombreCompleto);

@override
String toString() =>
'Alumna/o: $nombreCompleto con número de carnet: $numCarnet';
}



Clases abstractas / Interfaces

  • Más información:
 :* Class modifiers
 :* Abstract
 :* Interface
  • Una clase abstracta se declara utilizando el modificador `abstract`.
 : Una clase abstracta no puede instanciarse directamente.
 : Puede contener miembros con implementación y miembros abstractos, es decir, miembros cuya implementación tendrán que proporcionar las subclases.
  • Además, en Dart toda clase define implícitamente una interfaz formada por los miembros de instancia que otras clases pueden implementar mediante `implements`.
  • Desde Dart 3 existen también modificadores de clase específicos, entre ellos `interface`.
Por ejemplo:
interface class Ejemplo {
}
El modificador `interface` expresa que la clase puede utilizarse como interfaz desde otras bibliotecas.
Cuando queremos definir una clase destinada únicamente a expresar un contrato y que, además, no pueda ser instanciada directamente, podemos combinar ambos modificadores:
abstract interface class Ejemplo {
}

Clases abstractas

Veamos primero un ejemplo utilizando una clase abstracta como superclase:
void main() {
final alum1 = Alumno(
numCarnet: 'C1A',
nombreCompleto: 'Juan Luis Rodriguez',
);

final prof1 = Profesor(
nrp: '1234534',
nombreCompleto: 'Luisa Sanchez Dragó',
);

print(alum1);
alum1.pagarCafeteria();

print(prof1);
prof1.pagarCafeteria();

// final per1 = Personal('Nombre');
// ERROR: no se pueden instanciar directamente las clases abstractas.
}

abstract class Personal {
String nombreCompleto;

Personal(this.nombreCompleto);

void pagarCafeteria();
}

class Alumno extends Personal {
String numCarnet;

Alumno({
required this.numCarnet,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
void pagarCafeteria() {
print('El alumno $numCarnet paga en efectivo');
}

@override
String toString() =>
'Alumna/o: $nombreCompleto con número de carnet: $numCarnet';
}

class Profesor extends Personal {
String nrp;

Profesor({
required this.nrp,
required String nombreCompleto,
}) : super(nombreCompleto);

@override
void pagarCafeteria() {
print('El profesor $nrp paga con vale descuento');
}

@override
String toString() =>
'Profesor/a: $nombreCompleto con número de registro personal: $nrp';
}
Flutter dart abstract 1.JPG


Interfaces

  • Cuando queremos definir principalmente un contrato que otras clases tengan que implementar podemos utilizar una `abstract interface class`.
En este caso utilizamos `implements`.
Cuando una clase implementa una interfaz debe proporcionar una implementación para todos sus miembros de instancia requeridos.
void main() {
  final cuadrado1 = Cuadrado(lado: 10);
  final circulo1 = Circulo(5);

  print('Área del cuadrado: ${cuadrado1.getArea()}');
  print('Área del círculo: ${circulo1.getArea()}');
}

abstract interface class Figura {
  static const pi = 3.1416;

  double getArea();
}

class Cuadrado implements Figura {
  final double _lado;

  Cuadrado({
    required double lado,
  }) : _lado = lado;

  @override
  double getArea() => _lado * _lado;
}

class Circulo implements Figura {
  final double _radio;

  Circulo(this._radio);

  @override
  double getArea() => Figura.pi * _radio * _radio;
}
Fijarse en la diferencia entre los ejemplos anteriores:
  • `Alumno extends Personal` indica una relación de herencia.
  • `Cuadrado implements Figura` indica que `Cuadrado` se compromete a cumplir el contrato definido por `Figura`.

Polimorfismo

  • Podemos utilizar una referencia cuyo tipo sea una clase abstracta o una interfaz para trabajar con objetos de diferentes clases que cumplan ese contrato.
Por ejemplo:
void main() {
  final cuadrado1 = Cuadrado(lado: 10);
  final circulo1 = Circulo(5);

  imprimirArea(cuadrado1);
  imprimirArea(circulo1);
}

void imprimirArea(Figura figura) {
  print(
    'El área del ${figura.runtimeType} es ${figura.getArea()}',
  );
}
Tanto `Cuadrado` como `Circulo` pueden enviarse como argumento porque ambos implementan `Figura`.
En tiempo de ejecución se ejecutará el método `getArea()` correspondiente al tipo real del objeto.


Mixins

  • Más información:
 :* Mixins
 :* Dart: What are mixins?
  • Los mixins permiten reutilizar implementaciones entre diferentes clases sin establecer necesariamente una relación de herencia entre ellas.
 : Son especialmente útiles cuando queremos compartir comportamiento entre clases pertenecientes a diferentes jerarquías.
Los mixins se incorporan a una clase mediante la palabra reservada with.
mixin OperacionesListas {
  void imprimirLista(List<int> lista) {
    for (final elem in lista) {
      print('Elemento de la lista: $elem');
    }
  }
}

mixin OperacionesMapas {
  void imprimirMapa(Map<String, String> mapa) {
    for (final entry in mapa.entries) {
      print(
        'La clave ${entry.key} tiene el valor: ${entry.value}',
      );
    }
  }
}

class ClaseListas with OperacionesListas {
  List<int> lista;

  ClaseListas(this.lista);
}

class ClaseListasYMapas with OperacionesListas, OperacionesMapas {
  List<int> lista;
  Map<String, String> mapa;

  ClaseListasYMapas(this.lista, this.mapa);
}

void main() {
  final claseListas = ClaseListas(
    [1, 2, 3, 4],
  );

  final claseListasMapas = ClaseListasYMapas(
    [10, 11, 12, 13],
    {
      'nombre': 'Jose',
      'edad': '45',
    },
  );

  claseListas.imprimirLista(
    claseListas.lista,
  );

  claseListasMapas.imprimirLista(
    claseListasMapas.lista,
  );

  claseListasMapas.imprimirMapa(
    claseListasMapas.mapa,
  );
}


Ejercicios


Ejercicio 1

Crea un conjunto de clases para representar diferentes miembros de una escuela, tales como profesores y estudiantes.

Utilizarás herencia para compartir propiedades y comportamientos comunes, mientras que una interfaz permitirá definir el contrato que deberán cumplir los miembros del sistema.

Requisitos del sistema:

  • Interfaz Persona:
Define Persona mediante una abstract interface class.
Debe disponer de propiedades para el nombre y la edad, incluyendo los getters y setters correspondientes.
  • Clase abstracta MiembroEscolar:
Crea una clase abstracta MiembroEscolar que implemente Persona.
Añade una propiedad id para identificar unívocamente al miembro.
Implementa en esta clase los getters y setters definidos en Persona.
El setter de la edad no debe permitir valores negativos.
Define un método abstracto mostrarDatos().
  • Clase Alumno:
Debe heredar de MiembroEscolar.
Añade una propiedad numExpediente.
Sobrescribe el método mostrarDatos() para mostrar toda la información del alumno.
  • Clase Profesor:
Debe heredar de MiembroEscolar.
Añade una propiedad nrp.
Sobrescribe el método mostrarDatos() para mostrar toda la información del profesor.
  • Programa principal:
Crea un objeto de tipo Alumno y otro de tipo Profesor.
Muestra sus datos utilizando el método mostrarDatos().
Modifica la edad de alguno de ellos utilizando el setter y vuelve a mostrar sus datos.


Ejercicio 2

Crea un programa que utilice mixins para añadir funcionalidades comunes a distintas clases sin utilizar herencia múltiple. Requisitos del sistema:

  • Mixin Registrable:
Define un mixin denominado Registrable.
Debe contener un método registrarEntrada() que muestre por pantalla un mensaje indicando que se ha registrado una entrada en el sistema.
  • Mixin Notificable:
Define un mixin denominado Notificable.
Debe contener un método enviarNotificacion() que muestre un mensaje indicando que se ha enviado una notificación.
  • Clase Empleado:
Debe disponer de las propiedades nombre y departamento.
Debe utilizar los mixins Registrable y Notificable.
  • Clase Visitante:
Debe disponer de las propiedades nombre y empresa.
Debe utilizar únicamente el mixin Registrable.
  • Programa principal:
Crea un objeto de tipo Empleado y otro de tipo Visitante.
Llama a los métodos proporcionados por sus respectivos mixins.
Comprueba que Empleado dispone de las dos funcionalidades mientras que Visitante únicamente puede registrar su entrada.


Ejercicio 3

Crea una clase que permita transformar los datos recibidos en un Map<String, dynamic> en un objeto Dart. Requisitos del sistema:

  • Clase Producto:
Debe disponer de las propiedades id, nombre y precio.
id será de tipo int.
nombre será de tipo String.
precio será de tipo double.
  • Constructor principal:
Define un constructor utilizando parámetros con nombre y obligatorios mediante required.
  • Constructor factory:
Define un constructor factory denominado Producto.fromJson.
Recibirá un Map<String, dynamic>.
Utiliza int.tryParse() para convertir el identificador.
Utiliza double.tryParse() para convertir el precio.
Si alguno de los dos valores no puede convertirse correctamente, lanza una FormatException.
  • Método toString:
Sobrescribe el método toString() para devolver la información del producto.
  • Programa principal:
Crea un mapa que represente un producto.
Crea un objeto Producto utilizando Producto.fromJson().
Muestra el objeto por pantalla.




Soluciones


Solución Ejercicio 1

abstract interface class Persona {
  String get nombre;
  set nombre(String valor);

  int get edad;
  set edad(int valor);
}

abstract class MiembroEscolar implements Persona {
  final int id;
  String _nombre;
  int _edad;

  MiembroEscolar({required this.id, required String nombre, required int edad})
    : _nombre = nombre,
      _edad = edad >= 0 ? edad : 0;

  @override
  String get nombre => _nombre;

  @override
  set nombre(String valor) {
    _nombre = valor;
  }

  @override
  int get edad => _edad;

  @override
  set edad(int valor) {
    _edad = valor >= 0 ? valor : 0;
  }

  void mostrarDatos();
}

class Alumno extends MiembroEscolar {
  String numExpediente;

  Alumno({
    required super.id,
    required super.nombre,
    required super.edad,
    required this.numExpediente,
  });

  @override
  void mostrarDatos() {
    print('Alumno: $nombre');
    print('Edad: $edad');
    print('Id: $id');
    print('Número de expediente: $numExpediente');
  }
}

class Profesor extends MiembroEscolar {
  String nrp;

  Profesor({
    required super.id,
    required super.nombre,
    required super.edad,
    required this.nrp,
  });

  @override
  void mostrarDatos() {
    print('Profesor: $nombre');
    print('Edad: $edad');
    print('Id: $id');
    print('NRP: $nrp');
  }
}

void main() {
  final alumno = Alumno(
    id: 1,
    nombre: 'Ana López',
    edad: 20,
    numExpediente: 'EXP001',
  );

  final profesor = Profesor(
    id: 2,
    nombre: 'Luis García',
    edad: 45,
    nrp: 'NRP12345',
  );

  alumno.mostrarDatos();

  print('');

  profesor.mostrarDatos();

  print('');

  alumno.edad = 21;
  alumno.mostrarDatos();
}


Solución Ejercicio 2

mixin Registrable {
  void registrarEntrada() {
    print('Entrada registrada en el sistema');
  }
}

mixin Notificable {
  void enviarNotificacion() {
    print('Notificación enviada');
  }
}

class Empleado with Registrable, Notificable {
  String nombre;
  String departamento;

  Empleado({required this.nombre, required this.departamento});
}

class Visitante with Registrable {
  String nombre;
  String empresa;

  Visitante({required this.nombre, required this.empresa});
}

void main() {
  final empleado = Empleado(nombre: 'Laura', departamento: 'Informática');

  final visitante = Visitante(nombre: 'Carlos', empresa: 'Empresa S.A.');

  print('Empleado: ${empleado.nombre}');
  empleado.registrarEntrada();
  empleado.enviarNotificacion();

  print('');

  print('Visitante: ${visitante.nombre}');
  visitante.registrarEntrada();
}
En este ejemplo la clase `Empleado` incorpora las funcionalidades de los dos mixins:
class Empleado with Registrable, Notificable
Por el contrario, `Visitante` solamente incorpora `Registrable`:
class Visitante with Registrable
Por tanto, el siguiente código produciría un error:
visitante.enviarNotificacion();


Solución Ejercicio 3

class Producto {
  int id;
  String nombre;
  double precio;

  Producto({required this.id, required this.nombre, required this.precio});

  factory Producto.fromJson(Map<String, dynamic> dato) {
    
    final id = int.tryParse(dato['id'].toString());
    final precio = double.tryParse(dato['precio'].toString());

    if (id == null) {
      throw FormatException('El identificador no es válido');
    }

    if (precio == null) {
      throw FormatException('El precio no es válido');
    }

    return Producto(id: id, nombre: dato['nombre'] as String, precio: precio);
  }

  @override
  String toString() {
    return 'Producto: $nombre - Id: $id - Precio: $precio €';
  }
}

void main() {
  
  final productoJson = <String, dynamic>{
    'id': '25',
    'nombre': 'Teclado',
    'precio': '29.95',
  };

  final producto = Producto.fromJson(productoJson);

  print(producto);
}
En este ejemplo utilizamos `tryParse()` en lugar de `parse()`:
    final id = int.tryParse(dato['id'].toString());
    final precio = double.tryParse(dato['precio'].toString());
La principal diferencia es que `tryParse()` devuelve `null` cuando el valor no puede convertirse.
Esto nos permite comprobar explícitamente si la conversión ha sido correcta:
if (id == null) {
  throw FormatException('El identificador no es válido');
}
De esta forma podemos controlar el error antes de crear el objeto, en lugar de depender directamente de la excepción producida por `parse()`.




Enlace a la página principal de la UD2

Enlace a la página principal del curso