FechaVersiónDescripción
07/12/20211.0.0Versión inicial
19/12/20211.0.1Incorporación de Paquetes

Unidad 4 - POO Clases y Objetos (Kotlin & Dart)

1. POO en Dart

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.

1.1 Clases y constructores

La sintaxis básica para crear una clase en Dart es bastante similar a otros lenguajes, como Java:

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.

 

1.1.1 Instanciación de objetos

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:

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:

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:

Ejemplo: https://dartpad.dev/4b6c3275600d8eef5e0ff87233467b70

1.1.2 Simplificación del constructor

El constructor puede simplificarse de la siguiente manera:

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:

1.1.3 Constructores con paso de argumento por nombre

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 {}:

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:

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

1.1.4 Múltiples constructores con nombre (named constructor)

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:

2. POO en Kotlin

2.1 Clases

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:

Comienza con la palabra clave de la clase, seguida de un nombre y un conjunto de llaves de apertura y cierre. Las llaves contienen el cuerpo de la clase que describe su diseño azul.

Estas son las convenciones de nombres recomendadas para una clase:

Una clase consta de tres partes principales:

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:

Define una clase SmartDevice:

  1. En el Playground de Kotlin, reemplaza el contenido por una función main() vacía:

  1. En la línea anterior a la función main(), define una clase SmartDevice con un cuerpo que incluya un comentario // empty body:

2.2 Crea una instancia de una clase

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:

1d25bc4f71c31fc9.png

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:

f58430542f2081a9.png

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:

2.3 Métodos de Clase

En la Unidad 1, aprendiste lo siguiente:

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:

  1. En el cuerpo de la clase SmartDevice, define un método turnOn() con un cuerpo vacío:

  1. En el cuerpo del método turnOn(), agrega una sentencia println() y pásale una string "Smart device is turned on.":

  1. Después del método turnOn(), agrega un método turnOff() que imprima una string "Smart device is turned off.":

Llama a un método en un objeto.

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:

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:

fc609c15952551ce.png

Llama a los métodos turnOn() y turnOff() en el objeto:

  1. En la función main() de la línea después de la variable smartTvDevice, llama al método turnOn():

  1. En la línea después del método turnOn(), llama al método turnOff():

  1. Ejecuta el código.

Este es el resultado:

2.4 Propiedades de clase

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:

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:

  1. En la línea anterior al método turnOn(), define la propiedad name y asígnala a una string "Android TV":

  1. 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":

  1. 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}":

  1. Ejecuta el código.

Este es el resultado:

Funciones get y set en propiedades

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:

f2cf50a63485599f.png

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:

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:

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:

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:

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.

 

2.5 Constructor

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.

Constructor predeterminado

Un constructor predeterminado es aquel que no tiene parámetros. Puedes definir un constructor predeterminado como se muestra en este fragmento de código:

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:

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.

Define un constructor parametrizado

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:

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:

bbe674861ec370b6.png

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:

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:

Existen dos tipos principales de constructores en Kotlin:

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:

aa05214860533041.png

El constructor secundario se encierra en el cuerpo de la clase y su sintaxis incluye tres partes:

Puedes ver la sintaxis en este diagrama:

2dc13ef136009e98.png

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: