Flutter DART Clases
Ir a la navegación
Ir a la búsqueda
Sumario
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.
- Podéis consultar cómo se implementan estos conceptos en Java en Fundamentos de POO en Java y en la ampliación de la POO.
- 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
- Más información: Factory constructors.
- 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
- Más información: Extending a class.
- 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`:
- 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
- Más información: Getters and 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'; }
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()`.