Diferencia entre revisiones de «Flutter DART Clases»
Ir a la navegación
Ir a la búsqueda
| (No se muestran 78 ediciones intermedias del mismo usuario) | |||
| Línea 1: | Línea 1: | ||
==Introducción== | ==Introducción== | ||
| − | * Como comenté anteriormente este curso está pensado para alumnos que ya tienen conocimientos previos en algún lenguaje de programación orientado a objetos. | + | * '''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. |
| − | : En esta sección se | + | |
| + | <br /> | ||
| + | |||
| + | * 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 [https://wiki.cifprodolfoucha.es/index.php?title=Programaci%C3%B3n_Java#Fundamentos_Programaci.C3.B3n_Orientada_a_Objetos_.28POO.29 Fundamentos de POO en Java] y [https://wiki.cifprodolfoucha.es/index.php?title=Prog._Ampliaci%C3%B3n_POO 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`: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="1-9"> | ||
| + | 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); | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="7-11"> | ||
| + | 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); | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | : 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 | + | <br /> |
| − | + | * En el ejemplo anterior las propiedades admiten valores nulos porque no se inicializan en el momento de declararlas. | |
| − | : Sin embargo, si vamos a | + | : 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: | ||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="6"> | ||
| + | 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() { | void main() { | ||
| + | final veh1 = Vehiculo(2018, 'Opel', '1234GHZ'); | ||
| + | print(veh1); | ||
| + | |||
| + | final veh2 = Vehiculo(2020, 'Peugeot', '6543HZZ'); | ||
| + | print(veh2); | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | : 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''', [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_DART_Funciones#Par.C3.A1metros_obligatorios.2Fopcionales como ya vimos anteriormente]. | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="7-9,19-21"> | ||
| + | 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'; | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| − | </syntaxhighlight> | + | |
| + | void main() { | ||
| + | final veh1 = Vehiculo( | ||
| + | anoCompra: 2018, | ||
| + | marca: 'Opel', | ||
| + | matricula: '1234GHZ', | ||
| + | ); | ||
| + | print(veh1); | ||
| + | |||
| + | final veh2 = Vehiculo( | ||
| + | anoCompra: 2020, | ||
| + | marca: 'Peugeot', | ||
| + | matricula: '6543HZZ', | ||
| + | ); | ||
| + | print(veh2); | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | '''<u>NOTAS IMPORTANTES:</u>''' | ||
| + | :* 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`. | ||
| + | |||
<br /> | <br /> | ||
| + | ===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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight=""> | ||
| + | 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 | @override | ||
| − | String toString() => 'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra'; | + | String toString() => |
| + | 'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra'; | ||
} | } | ||
| + | |||
void main() { | void main() { | ||
| + | final veh1 = Vehiculo(anoCompra: 2018, marca: 'Opel', matricula: '1234GHZ'); | ||
| + | print(veh1); | ||
| − | final | + | final veh2 = Vehiculo.sinAnho(marca: 'Opel', matricula: '2234GHZ'); |
| − | |||
| − | |||
| − | |||
print(veh2); | print(veh2); | ||
| − | + | ||
| + | final veh3 = Vehiculo.sinAnhoMarca(matricula: '3234GHZ'); | ||
| + | print(veh3); | ||
} | } | ||
| − | </syntaxhighlight> | + | </syntaxhighlight> |
| + | |||
| + | * '''<u>Nota importante:</u>''' 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 (`:`): | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | Vehiculo.sinAnhoMarca({required this.matricula,}) : anoCompra = 2000, marca = 'Opel'; | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | : Las expresiones de la lista de inicialización se ejecutan '''antes del cuerpo del constructor'''. | ||
| + | |||
| + | : Más información: [https://dart.dev/language/constructors Constructores en Dart]. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ===Constructores Factory=== | ||
| + | |||
| + | * Más información: [https://dart.dev/language/constructors#factory-constructors 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: | ||
| − | + | :<syntaxhighlight lang="dart" enclose="div" highlight="12-20"> | |
| − | + | class Vehiculo { | |
| − | :<syntaxhighlight lang=" | + | int anoCompra; |
| − | class Vehiculo{ | ||
| − | |||
| − | int anoCompra; | ||
String matricula; | String matricula; | ||
String marca; | String marca; | ||
| − | + | ||
| − | Vehiculo(this.anoCompra,this.marca, this.matricula); | + | 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 | @override | ||
| − | String toString() => 'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra'; | + | String toString() => |
| − | + | 'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra'; | |
} | } | ||
| + | |||
void main() { | void main() { | ||
| + | final veh2 = Vehiculo.sinAnho(marca: 'Opel', matricula: '2234GHZ'); | ||
| − | |||
| − | |||
| − | |||
| − | |||
print(veh2); | print(veh2); | ||
| − | |||
| − | |||
} | } | ||
| − | </syntaxhighlight> | + | </syntaxhighlight> |
| + | |||
| + | <br /> | ||
| + | |||
| + | ==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 [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_DART_variables_-_tipos_de_datos#Valores_nulos_al_utilizar_mapas valores nulos al emplear mapas]. | ||
| + | :* También utilizaremos [https://wiki.cifprodolfoucha.es/index.php?title=Flutter_DART_Clases#M.C3.BAtliples_constructores._Constructores_con_nombre 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>`. | ||
| + | <center> | ||
| + | {{#ev:youtube|kKczrKiV6Uc}} | ||
| + | </center> | ||
| + | <br /> | ||
| − | + | :<syntaxhighlight lang="dart" enclose="div"> | |
| − | :<syntaxhighlight lang=" | + | class Vehiculo { |
| − | class Vehiculo{ | + | int anoCompra; |
| − | |||
| − | int anoCompra; | ||
String matricula; | String matricula; | ||
String marca; | String marca; | ||
| − | + | ||
Vehiculo({ | 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 | @override | ||
| − | String toString() => 'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra'; | + | String toString() => |
| − | + | 'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra'; | |
} | } | ||
| + | |||
void main() { | void main() { | ||
| + | final vehiculoJson = <String, dynamic>{ | ||
| + | 'anoCompra': '2020', | ||
| + | 'matricula': '1234GHZ', | ||
| + | 'marca': 'Opel', | ||
| + | }; | ||
| + | |||
| + | final veh1 = Vehiculo.fromMap(vehiculoJson); | ||
| − | |||
print(veh1); | print(veh1); | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| − | </syntaxhighlight> | + | |
| + | } </syntaxhighlight> | ||
| + | |||
| + | : El tipo `Map<String, dynamic>` indica que las claves del mapa son cadenas de caracteres, mientras que sus valores pueden ser de diferentes tipos. | ||
<br /> | <br /> | ||
| − | + | * '''<u>Importante:</u>''' 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''' | ||
| − | :<syntaxhighlight lang=" | + | :<syntaxhighlight lang="dart" enclose="div"> |
| − | class Vehiculo{ | + | class Vehiculo { |
| − | + | int anoCompra; | |
| − | int anoCompra; | ||
String matricula; | String matricula; | ||
String marca; | String marca; | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | Vehiculo | + | 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 | @override | ||
| − | String toString() => 'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra'; | + | String toString() => |
| − | + | 'El vehículo de la marca $marca con matrícula $matricula se compró en el año $anoCompra'; | |
} | } | ||
| + | |||
void main() { | void main() { | ||
| + | final vehiculoJson = <String, dynamic>{ | ||
| + | 'anoCompra': '2020', | ||
| + | 'matricula': '1234GHZ', | ||
| + | 'marca': 'Opel', | ||
| + | }; | ||
| + | |||
| + | final veh1 = Vehiculo.fromMap(vehiculoJson); | ||
| − | |||
print(veh1); | print(veh1); | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| − | |||
| − | * '''< | + | } </syntaxhighlight> |
| − | : | + | |
| − | : Más información: https://dart.dev/ | + | |
| + | |||
| + | |||
| + | |||
| + | |||
| + | * Podéis emplear [https://quicktype.io/ 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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="12-16"> | ||
| + | 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); | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==Herencia== | ||
| + | |||
| + | * Más información: [https://dart.dev/language/extend 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(...)`. | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="7,16"> | ||
| + | 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); | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ===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. | ||
| + | |||
| + | * <u>Nota:</u> 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()`. | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="6-8,16-20,42,43"> | ||
| + | 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(); | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==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 [https://dart.dev/tools/pub/packages paquetes]. | ||
| + | |||
| + | : Más información: [https://dart.dev/language/libraries 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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | 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'; | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | : Ahora queremos utilizar estas clases desde otro archivo Dart. | ||
| + | : Para ello empleamos `import`: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="1"> | ||
| + | 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); | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | * Dart incluye numerosas bibliotecas que podemos utilizar mediante el `import` correspondiente. | ||
| + | : Por ejemplo, la biblioteca [https://api.dart.dev/dart-math/ dart:math] proporciona funciones y clases relacionadas con operaciones matemáticas y generación de números pseudoaleatorios. | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="1,4"> | ||
| + | import 'dart:math'; | ||
| + | |||
| + | void main() { | ||
| + | final semilla = Random(); | ||
| + | |||
| + | for (var cont = 0; cont < 10; cont++) { | ||
| + | print('Aleatorio generado: ${semilla.nextInt(50)}'); | ||
| + | } | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | * Podemos asignar un alias o prefijo a una biblioteca mediante la palabra reservada '''as''': | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="1,4"> | ||
| + | import 'dart:math' as math; | ||
| + | |||
| + | void main() { | ||
| + | final semilla = math.Random(); | ||
| + | |||
| + | for (var cont = 0; cont < 10; cont++) { | ||
| + | print('Aleatorio generado: ${semilla.nextInt(50)}'); | ||
| + | } | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | * Si una biblioteca contiene muchas declaraciones podemos importar únicamente aquellas que queremos utilizar mediante la palabra reservada `show`: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | import 'package02/centro/centro.dart' show Alumno, Profesor; </syntaxhighlight> | ||
| + | |||
| + | : `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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | import 'package02/centro/centro.dart' hide Personal; </syntaxhighlight> | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | |||
| + | * 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. | ||
| + | |||
| + | :'''<u>Importante:</u>''' 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`: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="1,4,7,16"> | ||
| + | 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'; | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | : Desde otra biblioteca que importe este fichero no podremos referenciar directamente la clase `_Personal`: | ||
| + | |||
| + | [[Imagen:Flutter_dart_private_1.JPG|500px|center]] | ||
| + | |||
| + | * El mismo mecanismo puede aplicarse a métodos, propiedades, funciones, variables y otras declaraciones. | ||
| + | |||
| + | : '''<u>Nota:</u>''' 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. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==Getters y Setters== | ||
| + | |||
| + | * Más información: [https://dart.dev/language/methods#getters-and-setters 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. | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | 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'; | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | * También podemos utilizar getters y setters para validar los valores almacenados en las propiedades de una clase: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight="16,18,19,21-24"> | ||
| + | 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'; | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | <br /> | ||
| + | |||
| + | ==Clases abstractas / Interfaces== | ||
| + | |||
| + | * Más información: | ||
| + | :* [https://dart.dev/language/class-modifiers Class modifiers] | ||
| + | :* [https://dart.dev/language/class-modifiers#abstract Abstract] | ||
| + | :* [https://dart.dev/language/class-modifiers#interface 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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | interface class Ejemplo { | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | : 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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | abstract interface class Ejemplo { | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | ===Clases abstractas=== | ||
| + | |||
| + | : Veamos primero un ejemplo utilizando una clase abstracta como superclase: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight=""> | ||
| + | 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'; | ||
| + | } </syntaxhighlight> | ||
| + | |||
| + | [[Imagen:Flutter_dart_abstract_1.JPG|500px|center]] | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ===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. | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight=""> | ||
| + | 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; | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | : 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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight=""> | ||
| + | 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()}', | ||
| + | ); | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | : 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. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==Mixins== | ||
| + | |||
| + | * Más información: | ||
| + | :* [https://dart.dev/language/mixins Mixins] | ||
| + | :* [https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3 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'''. | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div" highlight=""> | ||
| + | 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, | ||
| + | ); | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ==Ejercicios== | ||
| + | <br /> | ||
| + | ===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. | ||
| + | <br /> | ||
| + | ===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. | ||
| + | <br /> | ||
| + | ===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. | ||
| + | <br /> <br /> | ||
| + | |||
| + | |||
| + | ==Soluciones== | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | ===Solución Ejercicio 1=== | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | 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(); | ||
| + | } | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | == | + | ===Solución Ejercicio 2=== |
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | 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(); | ||
| + | } | ||
| − | + | </syntaxhighlight> | |
| − | + | : En este ejemplo la clase `Empleado` incorpora las funcionalidades de los dos mixins: | |
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | class Empleado with Registrable, Notificable | ||
| + | </syntaxhighlight> | ||
| + | : Por el contrario, `Visitante` solamente incorpora `Registrable`: | ||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | class Visitante with Registrable | ||
| + | </syntaxhighlight> | ||
| + | : Por tanto, el siguiente código produciría un error: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | visitante.enviarNotificacion(); | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | [ | + | ===Solución Ejercicio 3=== |
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | 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); | ||
| + | } | ||
| + | |||
| + | </syntaxhighlight> | ||
| + | |||
| + | : En este ejemplo utilizamos `tryParse()` en lugar de `parse()`: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | final id = int.tryParse(dato['id'].toString()); | ||
| + | final precio = double.tryParse(dato['precio'].toString()); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | : 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: | ||
| + | |||
| + | :<syntaxhighlight lang="dart" enclose="div"> | ||
| + | if (id == null) { | ||
| + | throw FormatException('El identificador no es válido'); | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| − | + | : De esta forma podemos controlar el error antes de crear el objeto, en lugar de depender directamente de la excepción producida por `parse()`. | |
| − | <br> | + | <br /> |
| + | [https://wiki.cifprodolfoucha.es/index.php?title=Programaci%C3%B3n_de_dispositivos_m%C3%B3viles_con_FLUTTER#Unidad_2:_Lenguaje_DART Enlace a la página principal de la UD2] | ||
| + | |||
| + | [https://wiki.cifprodolfoucha.es/index.php?title=Programaci%C3%B3n_de_dispositivos_m%C3%B3viles_con_FLUTTER Enlace a la página principal del curso] | ||
Revisión actual del 10:53 4 oct 2026
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()`.