Unidad 4 - POO Clases y Objetos (Kotlin & Dart)1. POO en Dart1.1 Clases y constructores1.1.1 Instanciación de objetos1.1.2 Simplificación del constructor1.1.3 Constructores con paso de argumento por nombre1.1.4 Múltiples constructores con nombre (named constructor)2. POO en Kotlin2.1 Clases2.2 Crea una instancia de una clase2.3 Métodos de ClaseLlama a un método en un objeto.2.4 Propiedades de claseFunciones get y set en propiedades2.5 ConstructorConstructor predeterminadoDefine un constructor parametrizado
Fecha | Versión | Descripción |
---|---|---|
07/12/2021 | 1.0.0 | Versión inicial |
19/12/2021 | 1.0.1 | Incorporación de Paquetes |
La orientación a objetos es de gran importancia en Dart, y sobre todo en Flutter, ya que en estos conceptos se basará todo el diseño de interfaces mediante widgets.
La sintaxis básica para crear una clase en Dart es bastante similar a otros lenguajes, como Java:
xxxxxxxxxx
class NombreClase {
Tipo1? propiedad1;
Tipo2? propiedad2;
...
// Constructor (opcional)
NombreClase(Tipo1 arg1, Tipo2 arg2,...){
propiedad1=arg1; // Podemos utilizar this.propiedad1, pero no se recomienda
propiedad2=arg2; // De igual modo con this.propiedad2
...
}
}
Vemos algunos detalles. Por un lado, el constructor de clase es opcional. En caso de que éste no se declare, Dart utiliza un constructor predeterminado sin argumentos. Si incorporamos un constructor a la clase, éste se trata de un método con el mismo nombre que la clase y sin tipos, tal y como se hace en Java. Fijémonos en que en el ejemplo hemos definido las propiedades como nullables. En caso de no hacerlo así, el compilador nos daría el error Non-nullable instance field 'nombre_propiedad' must be initialized indicando que es necesario inicializar esta propiedad. Estos valores iniciales se pueden dar en la misma definición de las propiedades.
xxxxxxxxxx
class NomClasse {
Tipo1 propiedad1=valor_inicial_1;
Tipo2 propiedad2=valor_inicial_2;
...
}
Para crear un objeto de la clase anterior, podríamos hacerlo con:
Aunque podemos utilizar la palabra clave new (new Clase(param1, param2)), ésta es opcional, y cuando trabajamos con Flutter, se recomienda no utilizarla. Por otra parte, si intentamos imprimir el objeto (print(objeto);) nos dirá que es una instancia de NomClasse. Si lo que queremos es que nos muestre el contenido, deberíamos sobreescribir el método toString de la siguiente manera:
xxxxxxxxxx
String toString() {
return 'Propiedad1: $propiedad1, Propiedad2: $propiedad2';
}
Hay que decir que Dart, la anotación @override también es opcional, y podríamos omitirla. Por otra parte, en expresiones como la anterior debemos tener especial cuidado si optamos por utilizar el this. Aunque no es recomendable, si utilizamos éste habría que hacer uso de las claves para delimitar el alcance del $, de la siguiente manera:
xxxxxxxxxx
return 'Propiedad1: ${this.propiedad1}, Propiedad2: ${this.propiedad2}';
Si sólo utilizamos $this.propiedad1 o $this.propetat2 estaríamos intentando imprimir la misma clase ($this), por lo que se invocaría a este método de forma recursiva, entrando pues en un bucle infinito. Un ejemplo más completo de lo explicado podría ser el siguiente:
xxxxxxxxxx
class Persona { String? nombre; String? apellidos;
Persona(String arg1, String arg2){
nombre=arg1;
apellidos=arg2;
}
String toString() {
return 'Nombre: $nombre, Apellidos: $apellidos';
}
}
void main (){
Persona objeto=Persona("Luke", "Skywalker");
print(objeto.toString());
}
Ejemplo: https://dartpad.dev/4b6c3275600d8eef5e0ff87233467b70
El constructor puede simplificarse de la siguiente manera:
xxxxxxxxxx
NombreClase(this.propiedad1, this.propiedad2);
Con lo que, además, conseguimos que las propiedades se inicialicen en la misma definición, de forma que no sea necesario declarar éstas como nullables. Para el ejemplo de la clase Persona tendríamos:
xxxxxxxxxx
Persona(this.nombre, this.apellidos);
También es bastante habitual pasar los parámetros de inicialización del constructor por nombre en lugar de hacerlo de forma posicional. Esto lo conseguimos con las claves {}:
xxxxxxxxxx
NombreClase({
required this.propiedad1,
required this.propiedad2
});
El uso de la palabra reservada required indica la obligatoriedad de incluir el argumento, evitando valores nulos. Si no utilizamos el required, habría que indicar bien que es una propiedad nullable o indicarle un valor predeterminado, bien sea en su definición o bien en los parámetros del constructor:
xxxxxxxxxx
// En la definición de la propiedad
class NombreClasse{
...
Tipo? propiedad1; // Propiedad Nullable
Tipo propiedad2=valor; // Con valor predeterminado
Tipo propiedad3;
...
NombreClase({
this.propiedad1,
this.propiedad2,
this.propiedad3=valor // Valor predeterminado en el constructor
});
}
Y no importa el orden en el que ponemos los argumentos, puesto que lo que cuenta ahora es el nombre.
Ejemplo en DartPad: https://dartpad.dev/438a3989dd9643df3c5eaa97e259ae29
Dart no soporta sobrecarga de constructores. Para posibilitar la construcción de objetos mediante distintos métodos se utilizan los constructores con nombre, que también aportan mayor claridad a las declaraciones. Para definir un constructor con nombre hacemos uso del punto para separar al constructor del nombre:
xxxxxxxxxx
NombreClase.constructor_con_nombre1(lista_argumentos_1){...} NombreClase.constructor_con_nombre2(lista_argumentos_2){...}
Cuando defines una clase, especificas las propiedades y los métodos que deben tener todos los objetos de esa clase.
La definición de una clase comienza con la palabra clave class
, seguida de un nombre y un conjunto de llaves. La parte de la sintaxis anterior a la llave de apertura también se conoce como encabezado de clase. Entre llaves, puedes especificar las propiedades y funciones de la clase. Pronto aprenderás sobre las propiedades y funciones. Puedes ver la sintaxis de una definición de clase en este diagrama:
Estas son las convenciones de nombres recomendadas para una clase:
Puedes elegir el nombre de clase que desees, pero no uses las palabras clave de Kotlin como nombre de clase (por ejemplo, fun
).
El nombre de la clase está escrito en PascalCase, por lo que cada palabra comienza con mayúscula y no hay espacios entre ellas. Por ejemplo, en SmartDevice, la primera letra de cada palabra aparece en mayúscula y no hay un espacio entre ellas.
Una clase consta de tres partes principales:
Propiedades. Son variables que especifican los atributos de los objetos de la clase.
Métodos. Son funciones que contienen los comportamientos y las acciones de la clase.
Constructores. Una función de miembro especial que crea instancias de la clase a lo largo del programa en el que se define.
Esta no es la primera vez que trabajas con clases. En codelabs anteriores, aprendiste sobre tipos de datos, como Int
, Float
, String
y Double
. Estos tipos de datos se definen como clases en Kotlin. Cuando definas una variable como se muestra en este fragmento de código, crea un objeto de la clase Int
, en el que se creará una instancia con un valor 1
:
xxxxxxxxxx
val number: Int = 1
Define una clase SmartDevice
:
En el Playground de Kotlin, reemplaza el contenido por una función main()
vacía:
xxxxxxxxxx
fun main() {
}
En la línea anterior a la función main()
, define una clase SmartDevice
con un cuerpo que incluya un comentario //
empty
body
:
xxxxxxxxxx
class SmartDevice {
// empty body
}
fun main() {
}
Como aprendiste, una clase es un plano para un objeto. El tiempo de ejecución de Kotlin usa la clase, o plano, para crear un objeto de ese tipo en particular. Con la clase SmartDevice
, tienes un plano de un dispositivo inteligente. Para tener un dispositivo inteligente real en tu programa, debes crear una instancia de objeto SmartDevice
. La sintaxis de la creación de una instancia comienza con el nombre de la clase seguido de un conjunto de paréntesis, como se puede ver en este diagrama:
Para usar un objeto, debes crear ese objeto y asignarlo a una variable, de manera similar a como se define una variable. Usa la palabra clave val
a fin de crear una variable inmutable y la palabra clave var
para una variable mutable. A las palabras clave val
o var
les siguen el nombre de la variable, un operador de asignación =
y la creación de instancias del objeto de clase. Puedes ver la sintaxis en este diagrama:
Nota: Cuando defines la variable con la palabra clave val
para hacer referencia al objeto, la variable en sí es de solo lectura, pero el objeto de la clase permanece mutable. Esto significa que no puedes reasignar otro objeto a la variable, pero puedes cambiar el estado del objeto cuando actualices los valores de sus propiedades.
Crea una instancia de la clase SmartDevice
como objeto:
En la función main()
, usa la palabra clave val
para crear una variable llamada smartTvDevice
e inicializarla como una instancia de la clase SmartDevice
:
xxxxxxxxxx
fun main() {
val smartTvDevice = SmartDevice()
}
En la Unidad 1, aprendiste lo siguiente:
La definición de una función usa la palabra clave fun
seguida de un conjunto de paréntesis y un conjunto de llaves. Las llaves contienen código, que son las instrucciones necesarias para ejecutar una tarea.
La llamada a una función hace que se ejecute el código contenido en ella.
Las acciones que la clase puede realizar se definen como funciones en ella. Por ejemplo, imagina que tienes un dispositivo inteligente, una smart TV o una lámpara inteligente que puedes encender y apagar con tu teléfono móvil. El dispositivo inteligente se traduce a la clase SmartDevice
en programación, y la acción para encenderlo y apagarlo se representa con las funciones turnOn()
y turnOff()
, que permiten activar y desactivar el comportamiento.
La sintaxis para definir una función en una clase es idéntica a la que aprendiste anteriormente. La única diferencia es que la función se coloca en el cuerpo de la clase. Cuando defines una función en el cuerpo de la clase, se la conoce como una función de miembro o método, y representa el comportamiento de la clase. En el resto de este codelab, a las funciones se las denominará métodos siempre que aparezcan en el cuerpo de una clase.
Define un método turnOn()
y turnOff()
en la clase SmartDevice
:
En el cuerpo de la clase SmartDevice
, define un método turnOn()
con un cuerpo vacío:
xxxxxxxxxx
class SmartDevice {
fun turnOn() {
}
}
En el cuerpo del método turnOn()
, agrega una sentencia println()
y pásale una string "Smart
device
is
turned
on."
:
xxxxxxxxxx
class SmartDevice {
fun turnOn() {
println("Smart device is turned on.")
}
}
Después del método turnOn()
, agrega un método turnOff()
que imprima una string "Smart
device
is
turned
off."
:
xxxxxxxxxx
class SmartDevice {
fun turnOn() {
println("Smart device is turned on.")
}
fun turnOff() {
println("Smart device is turned off.")
}
}
Hasta ahora, definiste una clase que funciona como plano para un dispositivo inteligente, creaste una instancia de la clase y asignaste esa instancia a una variable. Ahora, usarás los métodos de la clase SmartDevice
para encender y apagar el dispositivo.
La llamada a un método en una clase es similar a la llamada a otras funciones de la función main()
del codelab anterior. Por ejemplo, si necesitas llamar al método turnOff()
desde el método turnOn()
, puedes escribir algo similar a este fragmento de código:
xxxxxxxxxx
class SmartDevice {
fun turnOn() {
// A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
turnOff()
...
}
...
}
Para llamar a un método de clase fuera de la clase, comienza con el objeto de clase, seguido del operador .
, el nombre de la función y un conjunto de paréntesis. Si corresponde, los paréntesis contendrán los argumentos que requiere el método. Puedes ver la sintaxis en este diagrama:
Llama a los métodos turnOn()
y turnOff()
en el objeto:
En la función main()
de la línea después de la variable smartTvDevice
, llama al método turnOn()
:
xxxxxxxxxx
fun main() {
val smartTvDevice = SmartDevice()
smartTvDevice.turnOn()
}
En la línea después del método turnOn()
, llama al método turnOff()
:
xxxxxxxxxx
fun main() {
val smartTvDevice = SmartDevice()
smartTvDevice.turnOn()
smartTvDevice.turnOff()
}
Ejecuta el código.
Este es el resultado:
xxxxxxxxxx
Smart device is turned on.
Smart device is turned off.
En la Unidad 1, aprendiste sobre las variables, que son contenedores de datos individuales. Aprendiste a crear una variable de solo lectura con la palabra clave val
y una variable mutable con var
.
Mientras que los métodos definen las acciones que puede realizar una clase, las propiedades definen las características o los atributos de los datos de la clase. Por ejemplo, un dispositivo inteligente tiene las siguientes propiedades:
Nombre. Indica el nombre del dispositivo.
Categoría. Indica un tipo de dispositivo inteligente, como entretenimiento, utilidad o cocina.
Estado del dispositivo. Indica si el dispositivo está encendido, apagado, en línea o sin conexión. El dispositivo se considera en línea cuando está conectado a Internet. De lo contrario, se considera como sin conexión.
Básicamente, las propiedades son variables que se definen en el cuerpo de la clase, y no el cuerpo de la función. Esto significa que la sintaxis para definir las propiedades y las variables es idéntica. Debes definir una propiedad inmutable con la palabra clave val
y una propiedad mutable con la palabra clave var
.
Implementa las características mencionadas anteriormente como propiedades de la clase SmartDevice
:
En la línea anterior al método turnOn()
, define la propiedad name
y asígnala a una string "Android
TV"
:
xxxxxxxxxx
class SmartDevice {
val name = "Android TV"
fun turnOn() {
println("Smart device is turned on.")
}
fun turnOff() {
println("Smart device is turned off.")
}
}
En la línea que sigue a la propiedad name
, define la propiedad category
y asígnala a una cadena "Entertainment"
. Luego, define una propiedad deviceStatus
y asígnala a una cadena "online"
:
xxxxxxxxxx
class SmartDevice {
val name = "Android TV"
val category = "Entertainment"
var deviceStatus = "online"
fun turnOn() {
println("Smart device is turned on.")
}
fun turnOff() {
println("Smart device is turned off.")
}
}
En la línea después de la variable smartTvDevice
, llama a la función println()
y pásale una string "Device
name
is:
${smartTvDevice.name}"
:
xxxxxxxxxx
fun main() {
val smartTvDevice = SmartDevice()
println("Device name is: ${smartTvDevice.name}")
smartTvDevice.turnOn()
smartTvDevice.turnOff()
}
Ejecuta el código.
Este es el resultado:
xxxxxxxxxx
Device name is: Android TV
Smart device is turned on.
Smart device is turned off.
Las propiedades pueden hacer más de lo que hace una variable. Por ejemplo, imagina que creas una estructura de clase para representar una smart TV. Una de las acciones comunes que realizarás será aumentar y disminuir el volumen. Para representar esta acción en la programación, puedes crear una propiedad llamada speakerVolume
, que contenga el nivel de volumen actual establecido en la bocina de la TV, pero ese valor de volumen pertenece a un rango. El volumen mínimo que puedes establecer es 0, mientras que el máximo es 100. Para asegurarte de que la propiedad speakerVolume
nunca supere los 100 ni caiga debajo 0, puedes escribir una función set. Cuando actualices el valor de la propiedad, debes verificar si está en el rango de 0 a 100. Como otro ejemplo, imagina que uno de los requisitos es garantizar que el nombre esté siempre en mayúsculas. Puedes implementar una función get para convertir la propiedad name
en mayúsculas.
Antes de profundizar en cómo implementar estas propiedades, debes comprender la sintaxis completa para declararlas. La sintaxis completa para definir una propiedad mutable comienza con la definición de la variable seguida de las funciones get()
y set()
opcionales. Puedes ver la sintaxis en este diagrama:
Cuando no defines la función de método get y set para una propiedad, el compilador de Kotlin crea las funciones a nivel interno. Por ejemplo, si usas la palabra clave var
para definir una propiedad speakerVolume
y asignarle un valor 2
, el compilador genera automáticamente las funciones de método get y set, como puedes ver en este fragmento de código:
xxxxxxxxxx
var speakerVolume = 2
get() = field
set(value) {
field = value
}
No verás estas líneas en tu código porque el compilador las agrega en segundo plano.
La sintaxis completa de una propiedad inmutable tiene dos diferencias:
Comienza con la palabra clave val
.
Las variables de tipo val
son variables de solo lectura, por lo que no tienen funciones set()
.
Las propiedades de Kotlin usan un campo de copia de seguridad para conservar un valor en la memoria. Un campo de copia de seguridad es básicamente una variable de clase definida internamente en las propiedades. Un campo de copia de seguridad tiene alcance en una propiedad, lo que significa que solo puedes acceder a él a través de las funciones de propiedad get()
o set()
.
Para leer el valor de la propiedad en la función get()
o actualizarlo en la función set()
, debes usar el campo de copia de seguridad de la propiedad. El compilador de Kotlin lo genera automáticamente y se hace referencia a él con un identificador field
.
Por ejemplo, cuando deseas actualizar el valor de la propiedad en la función set()
, debes usar el parámetro de la función set()
, que se denomina value
, y asignarlo a la variable field
como se ve en este fragmento de código:
xxxxxxxxxx
var speakerVolume = 2
set(value) {
field = value
}
Advertencia: No uses el nombre de la propiedad para obtener o establecer un valor. Por ejemplo, en la función set()
, si intentas asignar el parámetro value
a la propiedad speakerVolume
, el código ingresa en un bucle infinito porque el entorno de ejecución de Kotlin intenta actualizar el valor de la propiedad speakerVolume
, que activa una llamada a la función set varias veces.
Por ejemplo, para asegurarte de que el valor asignado a la propiedad speakerVolume
esté en el rango de 0 a 100, puedes implementar la función set, como se muestra en este fragmento de código:
xxxxxxxxxx
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
Las funciones set()
verifican si el valor Int
está en un rango de 0 a 100 usando la palabra clave in
seguida del rango de valor. Si el valor está en el rango esperado, se actualiza el valor de field
. De lo contrario, el valor de la propiedad no se modifica.
Debes incluir esta propiedad en una clase de la sección Implementa una relación entre clases de este codelab, por lo que no necesitas agregar la función set al código ahora.
El objetivo principal del constructor es especificar cómo se crean los objetos de la clase. En otras palabras, los constructores inicializan un objeto y lo preparan para su uso. Tú lo hiciste cuando creaste una instancia del objeto. El código dentro del constructor se ejecuta cuando se crea una instancia del objeto de la clase. Puedes definir un constructor con o sin parámetros.
Un constructor predeterminado es aquel que no tiene parámetros. Puedes definir un constructor predeterminado como se muestra en este fragmento de código:
xxxxxxxxxx
class SmartDevice constructor() {
...
}
Kotlin tiene como objetivo ser conciso, por lo que puedes quitar la palabra clave constructor
si no hay anotaciones ni modificadores de visibilidad, sobre los que aprenderás pronto. También puedes quitar los paréntesis si el constructor no tiene parámetros, como se muestra en este fragmento de código:
xxxxxxxxxx
class SmartDevice {
...
}
El compilador de Kotlin genera automáticamente el constructor predeterminado. No verás el constructor predeterminado generado automáticamente en tu código porque lo agrega el compilador en segundo plano.
En la clase SmartDevice
, las propiedades name
y category
son inmutables. Debes asegurarte de que todas las instancias de la clase SmartDevice
inicialicen las propiedades name
y category
. Con la implementación actual, los valores de las propiedades name
y category
están codificados. Esto significa que todos los dispositivos inteligentes se nombran con la string "Android
TV"
y se categorizan con la string "Entertainment"
.
A fin de mantener la inmutabilidad y evitar los valores codificados, usa un constructor parametrizado para inicializarlos:
En la clase SmartDevice
, mueve las propiedades name
y category
al constructor sin asignar valores predeterminados:
xxxxxxxxxx
class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
fun turnOn() {
println("Smart device is turned on.")
}
fun turnOff() {
println("Smart device is turned off.")
}
}
Ahora, el constructor acepta parámetros para configurar sus propiedades, por lo que también cambia la forma de crear una instancia de un objeto para esa clase. En este diagrama, se puede ver la sintaxis completa para crear una instancia de un objeto:
Nota: Si la clase no tiene un constructor predeterminado y tratas de crear una instancia del objeto sin argumentos, el compilador informa un error.
Esta es la representación del código:
xxxxxxxxxx
SmartDevice("Android TV", "Entertainment")
Ambos argumentos del constructor son strings. No se sabe con exactitud a qué parámetro se debe asignar el valor. Para solucionarlo, similar a como pasaste los argumentos de las funciones, puedes crear un constructor con argumentos nombrados, como se muestra en este fragmento de código:
xxxxxxxxxx
SmartDevice(name = "Android TV", category = "Entertainment")
Existen dos tipos principales de constructores en Kotlin:
Constructor principal. Una clase solo puede tener un constructor principal, que se define como parte del encabezado de la clase. Un constructor principal puede ser un constructor predeterminado o parametrizado. El constructor principal no tiene un cuerpo, lo que significa que no puede contener código.
Constructor secundario. Una clase puede tener varios constructores secundarios. Puedes definir el constructor secundario con o sin parámetros. El constructor secundario puede inicializar la clase y tiene un cuerpo, que puede contener lógica de inicialización. Si la clase tiene un constructor principal, cada constructor secundario debe inicializarlo.
Puedes usar el constructor principal para inicializar propiedades en el encabezado de la clase. Los argumentos que se pasan al constructor se asignan a las propiedades. La sintaxis para definir un constructor principal comienza con el nombre de la clase seguido de la palabra clave constructor
y un conjunto de paréntesis. Los paréntesis contienen los parámetros del constructor principal. Si hay más de un parámetro, las comas separan las definiciones de cada uno. En este diagrama, puedes ver la sintaxis completa para definir un constructor principal:
El constructor secundario se encierra en el cuerpo de la clase y su sintaxis incluye tres partes:
Declaración del constructor secundario. La definición del constructor secundario comienza con la palabra clave constructor
, seguida de paréntesis. Si corresponde, los paréntesis contienen los parámetros que requiere el constructor secundario.
Inicialización del constructor principal. La inicialización comienza con dos puntos, seguidos de la palabra clave this
y un conjunto de paréntesis. Si corresponde, los paréntesis contienen los parámetros que requiere el constructor principal.
Cuerpo del constructor secundario. A la inicialización del constructor principal le sigue un conjunto de llaves, que contienen el cuerpo del constructor secundario.
Puedes ver la sintaxis en este diagrama:
Por ejemplo, imagina que deseas integrar una API desarrollada por un proveedor de dispositivos inteligentes. Sin embargo, esta muestra el código de estado de tipo Int
para indicar el estado inicial del dispositivo. La API muestra un valor 0
si el dispositivo no tiene conexión y un valor 1
si el dispositivo está en línea. Para cualquier otro valor de número entero, el estado se considera desconocido. Puedes crear un constructor secundario en la clase SmartDevice
a fin de convertir este parámetro statusCode
en una representación de string, como se puede ver en este fragmento de código:
class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
constructor(name: String, category: String, statusCode: Int) : this(name, category) {
deviceStatus = when (statusCode) {
0 -> "offline"
1 -> "online"
else -> "unknown"
}
}
...
}