Genéricos, objetos y extensiones

1. Introducción

Con el paso de las décadas, los programadores idearon varias funciones del lenguaje de programación para ayudarte a escribir mejor código, de modo que puedas expresar la misma idea con menos código, usar abstracciones para expresar ideas complejas y escribir código que evite que otros desarrolladores cometan errores por accidente. El lenguaje Kotlin no es la excepción, y hay una serie de funciones diseñadas para ayudar a los desarrolladores a escribir código más expresivo.

Lamentablemente, estas funciones pueden complicar un poco las cosas si es la primera vez que programas. Si bien pueden parecer útiles, el alcance de su utilidad y los problemas que resuelven no siempre resultan evidentes. Es probable que ya hayas visto algunas funciones que se usan en Compose y otras bibliotecas.

Si bien no hay nada que pueda reemplazar la experiencia, en este codelab se muestran varios conceptos de Kotlin que te ayudarán a estructurar apps de mayor tamaño:

  • Genéricos
  • Diferentes tipos de clases (clases enum y clases de datos)
  • Objetos singleton y complementarios
  • Propiedades y funciones de extensión
  • Funciones de alcance

Al final de este codelab, tendrás un conocimiento más profundo del código que ya viste en este curso y aprenderás algunos ejemplos de cuándo encontrarás o usarás estos conceptos en tus propias apps.

Requisitos previos

  • Tener conocimientos de conceptos de programación orientada a objetos, incluida la herencia
  • Saber cómo definir e implementar las interfaces

Qué aprenderás

  • Cómo definir un parámetro de tipo genérico para una clase
  • Cómo crear una instancia de una clase genérica
  • Cuándo usar clases enum o de datos
  • Cómo definir un parámetro de tipo genérico que debe implementar una interfaz
  • Cómo usar las funciones de alcance para acceder a las propiedades y los métodos de la clase
  • Cómo definir objetos singleton y complementarios para una clase
  • Cómo extender las clases existentes con propiedades y métodos nuevos

Requisitos

  • Un navegador web con acceso a Playground de Kotlin

2. Cómo crear una clase reutilizable con genéricos

Supongamos que estás escribiendo una app para un cuestionario en línea, similar a los que viste en este curso. Por lo general, hay varios tipos de preguntas de cuestionario, como aquellas en las que se debe completar un espacio en blanco o indicar si algo es verdadero o falso. Una pregunta individual puede representarse mediante una clase con varias propiedades.

El texto de las preguntas puede representarse con una cadena. Las preguntas también deben representar la respuesta. Sin embargo, es posible que los diferentes tipos de pregunta, como las que piden indicar verdadero o falso, deban representar la respuesta con un tipo de datos diferente. Definamos tres tipos diferentes de preguntas.

  • Pregunta para completar: La respuesta es una palabra representada por una String.
  • Pregunta de verdadero o falso: La respuesta se representa con un elemento Boolean.
  • Problemas matemáticos: La respuesta es un valor numérico. La respuesta para un problema aritmético simple se representa con un elemento Int.

Además, las preguntas de cuestionario de nuestro ejemplo, independientemente del tipo de pregunta, también tendrán una calificación de dificultad. La calificación de dificultad se representa con una cadena con tres valores posibles: "easy", "medium" o "hard".

Define clases para representar cada tipo de pregunta de cuestionario:

  1. Ve al Playground de Kotlin.
  2. Arriba de la función main(), define una clase llamada FillInTheBlankQuestion para las preguntas en las que se completará un espacio en blanco, la cual tendrá una propiedad de tipo String para el questionText, una de tipo String para la answer y una de tipo String para la difficulty.
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)
  1. Debajo de la clase FillInTheBlankQuestion, define otra clase llamada TrueOrFalseQuestion para preguntas en las que se indicará verdadero o falso, la cual tendrá una propiedad de tipo String para questionText, una de tipo Boolean para answer y una de tipo String para difficulty.
class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
  1. Por último, debajo de las otras dos clases, define una clase NumericQuestion, la cual tendrá una propiedad de tipo String para questionText, una de tipo Int para answer y una de tipo String para difficulty.
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)
  1. Observa el código que escribiste. ¿Notas la repetición?
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)

class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)

Las tres clases tienen exactamente las mismas propiedades: questionText, answer y difficulty. La única diferencia es el tipo de datos de la propiedad de answer. Podrías pensar que la solución obvia es crear una clase superior con questionText y difficulty, y que cada subclase defina la propiedad de answer.

Sin embargo, usar la herencia tiene el mismo problema que antes. Cada vez que agregas un nuevo tipo de pregunta, debes agregar una propiedad de answer. La única diferencia es el tipo de datos. También parece extraño tener una clase superior Question que no tenga una propiedad de respuesta.

Cuando quieres que una propiedad acepte diferentes tipos de datos, utilizar subclases no es la respuesta. En su lugar, Kotlin ofrece algo llamado tipos genéricos, que te permite tener una sola propiedad que puede tener diferentes tipos de datos, según el caso de uso específico.

¿Qué es un tipo de datos genérico?

Los tipos de datos genéricos, también llamados simplemente genéricos, permiten que un tipo de datos, como una clase, especifique un tipo de datos de marcador de posición desconocido que se puede usar con sus propiedades y métodos. ¿Qué significa exactamente?

En el ejemplo anterior, en lugar de definir una propiedad de respuesta para cada tipo posible de datos, puedes crear una sola clase para que represente cualquier pregunta y usar un nombre de marcador de posición en el tipo de datos de la propiedad de answer. El tipo de datos real (String, Int, Boolean, etc.) se especifica cuando se crea una instancia de esa clase. Siempre que se use el nombre del marcador de posición, se utilizará el tipo de datos que se pasó a la clase. A continuación, se muestra la sintaxis para definir un tipo genérico de una clase:

67367d9308c171da.png

Cuando se crea una instancia de una clase, se proporciona un tipo de datos genérico, por lo que debe definirse como parte de la firma de la clase. Después del nombre de la clase, se muestra un corchete angular de apertura (<), seguido de un nombre de marcador de posición para el tipo de datos y, luego, un corchete angular de cierre (>).

El nombre del marcador de posición se puede usar cuando uses un tipo real de datos dentro de la clase, como haces para una propiedad.

81170899b2ca0dc9.png

Es idéntico a cualquier otra declaración de propiedad, excepto que se usa el nombre del marcador de posición en lugar del tipo de datos.

¿Cómo puede saber tu clase qué tipo de datos usar? El tipo de datos que utiliza el tipo genérico se pasa como parámetro entre corchetes angulares cuando creas una instancia de la clase.

9b8fce54cac8d1ea.png

Después del nombre de la clase, se muestra un corchete angular de apertura (<), seguido del tipo de datos real (String, Boolean, Int, etc.) y, a continuación, un corchete de cierre (>). El tipo de datos del valor que pasas para la propiedad genérica debe coincidir con aquel entre corchetes angulares. Esto hará que la propiedad de respuesta sea una genérica, de modo que puedas usar una clase para representar cualquier tipo de pregunta de cuestionario, independientemente de que la respuesta sea de tipo String, Boolean, Int o cualquier tipo de dato arbitrario.

Cómo refactorizar tu código para usar genéricos

Refactoriza tu código para usar una sola clase llamada Question con una propiedad de respuesta genérica.

  1. Quita las definiciones de clase de FillInTheBlankQuestion, TrueOrFalseQuestion y NumericQuestion.
  2. Crea una clase nueva llamada Question.
class Question()
  1. Después del nombre de la clase, pero antes de los paréntesis, agrega un parámetro de tipo genérico con corchetes angulares de apertura y cierre. Llama al tipo genérico T.
class Question<T>()
  1. Agrega las propiedades questionText, answer y difficulty. questionText debe ser del tipo String. answer debe ser del tipo T porque se especifica su tipo de datos cuando se crea una instancia de la clase Question. La propiedad difficulty debe ser del tipo String.
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: String
)
  1. Si deseas ver cómo funciona con varios tipos de preguntas (como las de completar el espacio en blanco, las de indicar verdadero o falso, etc.), crea tres instancias de la clase Question en main(), como se muestra a continuación.
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
    val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
    val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}
  1. Ejecuta tu código para verificar que todo funcione correctamente. Ahora deberías tener tres instancias de la clase Question, cada una con diferentes tipos de datos para la respuesta, en lugar de tener tres clases diferentes o de usar la herencia. Si deseas admitir preguntas con un tipo de respuesta diferente, puedes volver a usar la misma clase de Question.

3. Cómo usar una clase enum

En la sección anterior, definiste una propiedad de dificultad con tres valores posibles: "easy", "medium" y "hard". Si bien esto funciona, hay algunos problemas.

  1. Si escribes mal por accidente una de las tres cadenas posibles, podrías introducir errores.
  2. Si los valores cambian, por ejemplo, si se cambia el nombre de "medium" a "average", deberás actualizar todos los usos de la cadena.
  3. No hay nada que te impida a ti ni a otro desarrollador usar accidentalmente una cadena diferente que no sea uno de los tres valores válidos.
  4. Si agregas más niveles de dificultad, el código será más difícil de mantener.

Kotlin te ayuda a resolver estos problemas con un tipo especial de clase llamada clase enum. Una clase enum se usa para crear tipos con un conjunto limitado de valores posibles. En el mundo real, por ejemplo, los cuatro puntos cardinales (norte, sur, este y oeste) pueden estar representadas por una clase enum. No es necesario usar ningún punto adicional, y el código no debería permitirlo. A continuación, se muestra la sintaxis de una clase enum.

f4bddb215eb52392.png

A cada valor posible de una enum se lo conoce como una constante enum. Las constantes de enumeración se colocan dentro de las llaves separadas por comas. Se usa mayúscula en todas las letras del nombre de la constante.

Se debe hacer referencia a las constantes enum usando el operador de punto.

f3cfa84c3f34392b.png

Cómo usar una constante enum

Modifica tu código para usar una constante enum, en lugar de una String, para representar la dificultad.

  1. Debajo de la clase Question, define una clase enum llamada Difficulty.
enum class Difficulty {
    EASY, MEDIUM, HARD
}
  1. En la clase Question, cambia el tipo de datos de la propiedad difficulty de String a Difficulty.
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. Cuando inicialices las tres preguntas, pasa la constante enum para la dificultad.
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

4. Cómo usar una clase de datos

Muchas de las clases con las que trabajaste hasta ahora, como las subclases de Activity, tienen varios métodos para realizar diferentes acciones. Estas clases no solo representan datos, sino que también contienen muchas funcionalidades.

Por el contrario, las clases como Question solo contienen datos. No tienen ningún método que realice una acción. Estas se pueden definir como una clase de datos. Definir una clase como una clase de datos permite que el compilador de Kotlin formule ciertas hipótesis e implemente automáticamente algunos métodos. Por ejemplo, la función println() llama a toString() en segundo plano. Cuando usas una clase de datos, toString() y otros métodos se implementan automáticamente según las propiedades de esa clase.

Para definir una clase de datos, simplemente agrega la palabra clave data antes de la palabra clave class.

e7cd946b4ad216f4.png

Cómo convertir Question en una clase de datos

Primero, verás lo que sucede cuando intentas llamar a un método como toString() en una clase que no es de datos. Luego, convertirás Question en una clase de datos de modo que se implementen este y otros métodos de forma predeterminada.

  1. En main(), imprime el resultado de la llamada a toString() en question1.
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
    println(question1.toString())
}
  1. Ejecuta tu código. El resultado solo muestra el nombre de la clase y un identificador único para el objeto.
Question@37f8bb67
  1. Convierte Question en una clase de datos con la palabra clave data.
data class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. Vuelve a ejecutar tu código. Si marcas esto como una clase de datos, Kotlin podrá determinar cómo mostrar las propiedades de la clase cuando se llame a toString().
Question(questionText=Quoth the raven ___, answer=nevermore, difficulty=MEDIUM)

Cuando una clase se define como una clase de datos, se implementan los siguientes métodos:

  • equals()
  • hashCode() (verás este método cuando trabajes con ciertos tipos de colecciones)
  • toString()
  • componentN(): component1(), component2(), etc.
  • copy()

5. Cómo usar un objeto singleton

Hay muchas situaciones en las que querrás que una clase tenga solo una instancia. Por ejemplo:

  1. Estadísticas del jugador actual en un juego para dispositivos móviles
  2. Interacciones con un solo dispositivo de hardware, como enviar audio a través de una bocina
  3. Un objeto para acceder a una fuente de datos remota (como una base de datos de Firebase)
  4. Autenticación, en función de la cual solo debe acceder un usuario a la vez

En las situaciones anteriores, es probable que necesites usar una clase. Sin embargo, solo necesitarás una instancia de esa clase. Si solo hay un dispositivo de hardware o si accedió solo un usuario a la vez, no habrá motivo para crear más de una instancia. Tener dos objetos que acceden al mismo dispositivo de hardware de forma simultánea podría causar un comportamiento muy extraño y con errores.

Puedes comunicar claramente en tu código que un objeto debe tener una sola instancia definiéndolo como un singleton. Un singleton es una clase que solo puede tener una única instancia. Kotlin proporciona una construcción especial, llamada objeto, que se puede usar para crear una clase singleton.

Cómo definir un objeto singleton

645e8e8bbffbb5f9.png

La sintaxis de un objeto es similar a la de una clase. Simplemente utiliza la palabra clave object en lugar de la palabra clave class. Un objeto singleton no puede tener un constructor, ya que no puedes crear instancias directamente. En cambio, todas las propiedades se definen entre llaves y reciben un valor inicial.

Es posible que algunos de los ejemplos mencionados no parezcan evidentes, sobre todo si todavía no trabajaste con dispositivos de hardware específicos ni abordaste la autenticación en tus apps. Sin embargo, notarás que se menciona a los objetos singleton a medida que sigues aprendiendo sobre el desarrollo de Android. Veamos esto en acción con un ejemplo sencillo, en el que se usa un objeto para el estado del usuario y solo se necesita una instancia.

Para un cuestionario, sería muy útil tener una manera de llevar un registro de la cantidad total de preguntas y de la cantidad de preguntas que el alumno respondió hasta el momento. Solo necesitarás que exista una instancia de esta clase, por lo que, en lugar de declararla como una clase, declárala como un objeto singleton.

  1. Crea un objeto llamado StudentProgress.
object StudentProgress {
}
  1. Para este ejemplo, supondremos que hay un total de diez preguntas, y que tres de ellas se respondieron hasta el momento. Agrega dos propiedades de tipo Int: total con un valor de 10 y answered con un valor de 3.
object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}

Cómo acceder a un objeto singleton

¿Recuerdas que no puedes crear una instancia de un objeto singleton directamente? Entonces, ¿cómo puedes acceder a sus propiedades?

Como solo existe una instancia de StudentProgress a la vez, puedes acceder a sus propiedades haciendo referencia al nombre del objeto en sí, seguido del operador de punto (.) y, a continuación, el nombre de la propiedad.

1b610fd87e99fe25.png

Actualiza la función main() para acceder a las propiedades del objeto singleton.

  1. En main(), agrega una llamada a println(), que muestra las preguntas answered y total del objeto StudentProgress.
fun main() {
    ...
    println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
  1. Ejecuta tu código para verificar que todo funcione correctamente.
...
3 of 10 answered.

Cómo declarar objetos como complementarios

Las clases y los objetos de Kotlin se pueden definir dentro de otros tipos y pueden ser una excelente manera de organizar tu código. Puedes definir un objeto singleton dentro de otra clase por medio de un objeto complementario. Un objeto complementario te permite acceder a sus propiedades y métodos desde adentro de la clase, si las propiedades y los métodos del objeto pertenecen a esa clase, lo que permite una sintaxis más concisa.

Para declarar un objeto complementario, simplemente agrega la palabra clave companion antes de la palabra clave object.

68b263904ec55f29.png

Crearás una nueva clase llamada Quiz para almacenar las preguntas del cuestionario y harás que StudentProgress sea un objeto complementario de la clase Quiz.

  1. Debajo de la enum Difficulty, define una nueva clase llamada Quiz.
class Quiz {
}
  1. Traslada question1, question2 y question3 de main() a la clase Quiz. Si aún no lo hiciste, quita println(question1.toString()).
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

}
  1. Mueve el objeto StudentProgress a la clase Quiz.
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

    object StudentProgress {
        var total: Int = 10
        var answered: Int = 3
    }
}
  1. Marca el objeto StudentProgress con la palabra clave companion.
companion object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}
  1. Actualiza la llamada a println() para hacer referencia a las propiedades con Quiz.answered y Quiz.total. Aunque estas propiedades se declaran en el objeto StudentProgress, se puede acceder a ellas con notación de puntos usando solo el nombre de la clase Quiz.
fun main() {
    println("${Quiz.answered} of ${Quiz.total} answered.")
}
  1. Ejecuta tu código para verificar el resultado.
3 of 10 answered.

6. Cómo extender clases con propiedades y métodos nuevos

A la hora de trabajar con Compose, quizás hayas notado una sintaxis interesante si especificaste el tamaño de los elementos de la IU. Los tipos numéricos, como Double, parecen tener propiedades como dp y sp que especifican las dimensiones.

a25c5a0d7bb92b60.png

¿Por qué los diseñadores del lenguaje Kotlin incluirían propiedades y funciones en tipos de datos integrados, específicamente para compilar la IU de Android? ¿Pudieron predecir el futuro? ¿Se diseñó Kotlin para usarse con Compose incluso antes de que existiera Compose?

¡Por supuesto que no! Cuando escribes una clase, por lo general, no sabes exactamente cómo otro desarrollador la usará o intentará usarla en su app. No es posible predecir todos los casos de uso futuros, ni tampoco te recomendamos que agregues contenido innecesario a tu código para cubrir algunos casos de uso imprevistos.

Lo que hace el lenguaje Kotlin es permitir que otros desarrolladores extiendan los tipos de datos existentes agregando propiedades y métodos a los que se pueda acceder con sintaxis de punto, como si fueran parte de ese tipo de datos. Un desarrollador que no trabajó en los tipos de punto flotante en Kotlin, por ejemplo, como alguien que compila la biblioteca de Compose, podría optar por agregar propiedades y métodos específicos para las dimensiones de la IU.

Como viste esta sintaxis cuando aprendiste Compose en las primeras dos unidades, es hora de que aprendas cómo funciona este proceso en niveles más profundos. Agregarás algunas propiedades y métodos para extender los tipos existentes.

Cómo agregar una propiedad de extensión

Para definir una propiedad de extensión, agrega el nombre de tipo y un operador de punto (.) antes del nombre de la variable.

1e8a52e327fe3f45.png

Refactorizarás el código de la función main() para imprimir el progreso del cuestionario con una propiedad de extensión.

  1. Debajo de la clase Quiz, define una propiedad de extensión de Quiz.StudentProgress llamada progressText de tipo String.
val Quiz.StudentProgress.progressText: String
  1. Define un método get para la propiedad de extensión que muestra la misma cadena que se usó antes en main().
val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. Reemplaza el código en la función main() por el código que imprime progressText. Como esta es una propiedad de extensión del objeto complementario, puedes acceder a ella con notación de puntos usando el nombre de la clase, Quiz.
fun main() {
    println(Quiz.progressText)
}
  1. Ejecuta tu código para verificar que funcione.
3 of 10 answered.

Cómo agregar una función de extensión

Para definir una función de extensión, agrega el nombre de tipo y un operador de punto (.) antes del nombre de la función.

879ff2761e04edd9.png

Agregarás una función de extensión para mostrar el progreso del cuestionario como una barra de progreso. Como no puedes crear una barra de progreso en el Playground de Kotlin, imprimirás una barra de progreso de estilo retro con texto.

  1. Agrega una función de extensión al objeto StudentProgress llamada printProgressBar(). La función no debe tomar parámetros ni mostrar un valor.
fun Quiz.StudentProgress.printProgressBar() {
}
  1. Imprime el carácter , la cantidad de veces answered, con repeat(). Esta parte de la barra de progreso con sombreado oscuro representa la cantidad de preguntas respondidas. Usa print(), ya que no quieres una línea nueva después de cada carácter.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
}
  1. Imprime el carácter , la cantidad equivalente a la diferencia entre total y answered, mediante repeat(). Esta parte con sombreado claro representa las preguntas restantes en la barra de proceso.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
}
  1. Imprime una línea nueva con println() sin argumentos y, luego, imprime progressText.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. Actualiza el código en main() para llamar a printProgressBar().
fun main() {
    Quiz.printProgressBar()
}
  1. Ejecuta tu código para verificar el resultado.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

¿Es obligatorio hacer esto? Por supuesto que no. Sin embargo, contar con las propiedades y los métodos de extensión te brinda más opciones para exponer tu código a otros desarrolladores. El uso de sintaxis de punto en otros tipos puede facilitar la lectura de tu código, tanto para ti como para otros desarrolladores.

7. Cómo reescribir las funciones de extensión con interfaces

En la página anterior, viste cómo agregar propiedades y métodos al objeto StudentProgress sin agregarle código directamente, usando las propiedades y funciones de extensión. Si bien esta es una excelente manera de agregar funcionalidad a una clase ya definida, no siempre es necesario extender una clase si tienes acceso al código fuente. También hay situaciones en las que no sabes cuál debería ser la implementación, solo que debe existir un método o una propiedad determinados. Si necesitas que varias clases tengan las mismas propiedades y métodos adicionales, quizás con comportamientos diferentes, puedes definirlos con una interfaz.

Por ejemplo, además de responder cuestionarios, supongamos que también tienes clases para encuestas, pasos en una receta o cualquier otro dato ordenado que pueda usar una barra de progreso. Puedes definir algo llamado interfaz que especifique los métodos o las propiedades que debe incluir cada una de estas clases.

eeed58ed687897be.png

Una interfaz se define con la palabra clave interface, seguida de un nombre en mayúsculas y minúsculas, y llaves de apertura y cierre. Dentro de las llaves, puedes definir las firmas de métodos o las propiedades de solo acceso que debe implementar cualquier clase que cumpla con la interfaz.

6b04a8f50b11f2eb.png

Una interfaz es un contrato. Se dice que una clase que se ajusta a una interfaz extiende la interfaz. Una clase puede declarar que le gustaría extender una interfaz con dos puntos (:), seguido de un espacio y de su nombre.

78af59840c74fa08.png

A cambio, la clase debe implementar todas las propiedades y los métodos especificados en la interfaz. Esto te permite asegurarte de que cualquier clase que necesite extender la interfaz implemente exactamente los mismos métodos con la misma firma. Si modificas la interfaz de alguna manera (por ejemplo, si agregas o quitas propiedades o métodos, o cambias una firma de método), el compilador requerirá que actualices cualquier clase que extienda la interfaz, lo que mantendrá la coherencia de tu código y facilitará su mantenimiento.

Las interfaces pueden variar el comportamiento de las clases que las extienden. Proporcionar la implementación depende de cada clase.

Veamos cómo puedes reescribir la barra de progreso para usar una interfaz y hacer que la clase Quiz extienda esa interfaz.

  1. Arriba de la clase Quiz, define una interfaz llamada ProgressPrintable. Elegimos el nombre ProgressPrintable porque permite que cualquier clase que lo extienda pueda imprimir una barra de progreso.
interface ProgressPrintable {
}
  1. En la interfaz de ProgressPrintable, define una propiedad llamada progressText.
interface ProgressPrintable {
    val progressText: String
}
  1. Modifica la declaración de la clase Quiz para extender la interfaz ProgressPrintable.
class Quiz : ProgressPrintable {
    ...
}
  1. En la clase Quiz, agrega una propiedad llamada progressText de tipo String, como se especifica en la interfaz ProgressPrintable. Como la propiedad proviene de ProgressPrintable, se debe anteponer val a la palabra clave "override".
override val progressText: String
  1. En la propiedad de extensión progressText anterior, copia el método get de la propiedad.
override val progressText: String
        get() = "${answered} of ${total} answered"
  1. Quita la propiedad de extensión progressText anterior.

Código que debes borrar:

val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. En la interfaz de ProgressPrintable, agrega un método llamado printProgressBar que no tome parámetros y no tenga un valor para mostrar.
interface ProgressPrintable {
    val progressText: String
    fun printProgressBar()
}
  1. En la clase Quiz, agrega el método printProgressBar() con la palabra clave override.
override fun printProgressBar() {
}
  1. Mueve el código de la antigua función de extensión printProgressBar() a la nueva printProgressBar() desde la interfaz. Modifica la última línea para hacer referencia a la nueva variable progressText desde la interfaz quitando la referencia a Quiz.
override fun printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(progressText)
}
  1. Quita la función de extensión printProgressBar(). Esta funcionalidad ahora pertenece a la clase Quiz que extiende ProgressPrintable.

Código que debes borrar:

fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. Actualiza el código en main(). Como la función printProgressBar() ahora es un método de la clase Quiz, primero debes crear una instancia de un objeto Quiz y, luego, llamar a printProgressBar().
fun main() {
    Quiz().printProgressBar()
}
  1. Ejecuta tu código. El resultado no se modificó, pero el código ahora es más modular. A medida que tus bases de código crecen, puedes agregar fácilmente clases que se ajusten a la misma interfaz para volver a usar el código sin heredar de una superclase.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

Existen numerosos casos de uso para las interfaces que pueden ayudarte a estructurar tu código, y comenzarás a ver con frecuencia su uso en las unidades comunes. A continuación, se incluyen algunos ejemplos de interfaces que puedes encontrar mientras continúas trabajando con Kotlin.

  • Inserción manual de dependencias: Crea una interfaz que defina todas las propiedades y métodos de la dependencia. Solicita la interfaz como el tipo de datos de la dependencia (actividad, caso de prueba, etc.), de modo que se pueda usar una instancia de cualquier clase que implemente la interfaz. Esto te permite intercambiar las implementaciones subyacentes.
  • Simulación para pruebas automatizadas: Tanto la clase ficticia como la real cumplen con la misma interfaz.
  • Acceso a las mismas dependencias en una app multiplataforma de Compose: Por ejemplo, crea una interfaz que proporcione un conjunto común de propiedades y métodos para Android y computadoras de escritorio, incluso si la implementación subyacente difiere para cada plataforma.
  • Varios tipos de datos en Compose, como Modifier, son interfaces. Esto te permite agregar modificadores nuevos sin necesidad de acceder o modificar el código fuente subyacente.

8. Cómo usar funciones de alcance para acceder a las propiedades y los métodos de la clase

Como ya viste, Kotlin incluye muchas funciones que hacen que tu código resulte más conciso.

Una de esas funciones que encontrarás a medida que continúes aprendiendo sobre el desarrollo de Android son las funciones de alcance. Las funciones de alcance te permiten acceder de forma concisa a propiedades y métodos de una clase sin tener que acceder varias veces al nombre de la variable. ¿Qué significa exactamente? Veamos un ejemplo.

Cómo eliminar las referencias de objetos repetitivos con funciones de alcance

Las funciones de alcance son funciones de orden superior que te permiten acceder a las propiedades y los métodos de un objeto sin hacer referencia al nombre del objeto. Se llaman funciones de alcance porque el cuerpo de la función pasada toma el alcance del objeto con el que se llama a la función de alcance. Por ejemplo, algunas funciones de alcance te permiten acceder a las propiedades y los métodos en una clase, como si las funciones se definieran como un método de esa clase. Esto permite que tu código sea más legible, ya que te permite omitir el nombre del objeto cuando incluirlo resulte redundante.

Para ilustrar mejor este concepto, veamos algunas funciones de alcance diferentes que encontrarás más adelante en el curso.

Cómo reemplazar los nombres de objetos largos mediante let()

La función let() te permite hacer referencia a un objeto en una expresión lambda mediante el identificador it, en lugar del nombre real del objeto. Esto puede ayudarte a evitar el uso de un nombre de objeto largo y descriptivo varias veces cuando accedas a más de una propiedad. La función let() es una función de extensión a la que se puede llamar en cualquier objeto de Kotlin con la notación de puntos.

Intenta acceder a las propiedades de question1, question2 y question3 con let():

  1. Agrega una función a la clase Quiz llamada printQuiz().
fun printQuiz() {

}
  1. Agrega el siguiente código que imprime los elementos questionText, answer y difficulty de la pregunta. Si bien se accede a varias propiedades de question1, question2 y question3, siempre se usa el nombre completo de la variable. Si el nombre de la variable cambia, deberás actualizar cada uso.
fun printQuiz() {
    println(question1.questionText)
    println(question1.answer)
    println(question1.difficulty)
    println()
    println(question2.questionText)
    println(question2.answer)
    println(question2.difficulty)
    println()
    println(question3.questionText)
    println(question3.answer)
    println(question3.difficulty)
    println()
}
  1. Encierra el código que accede a las propiedades questionText, answer y difficulty con una llamada a la función let() en question1, question2 y question3. Reemplaza el nombre de variable en cada expresión lambda por "it".
fun printQuiz() {
    question1.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question2.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question3.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
}
  1. Actualiza el código en main() para crear una instancia de la clase Quiz llamada quiz.
fun main() {
    val quiz = Quiz()
}
  1. Llama a printQuiz().
fun main() {
    val quiz = Quiz()
    quiz.printQuiz()
}
  1. Ejecuta tu código para verificar que todo funcione correctamente.
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

Cómo llamar a los métodos de un objeto sin una variable mediante apply()

Uno de los aspectos geniales de las funciones de alcance es que puedes llamarlas en un objeto incluso antes de que este se asigne a una variable. Por ejemplo, la función apply() es una función de extensión a la que se puede llamar en un objeto con la notación de puntos. La función apply() también muestra una referencia a ese objeto de modo que se pueda almacenar en una variable.

Actualiza el código en main() para llamar a la función apply().

  1. Llama a apply() después del paréntesis de cierre cuando crees una instancia de la clase Quiz. Puedes omitir los paréntesis cuando llames a apply() y usar la sintaxis lambda final.
val quiz = Quiz().apply {
}
  1. Mueve la llamada a printQuiz() dentro de la expresión lambda. Ya no necesitarás hacer referencia a la variable quiz ni usar la notación de puntos.
val quiz = Quiz().apply {
    printQuiz()
}
  1. La función apply() muestra la instancia de la clase Quiz, pero, como ya no la usas en ningún lugar, quita la variable quiz. Con la función apply(), ni siquiera necesitas una variable para llamar a los métodos en la instancia de Quiz.
Quiz().apply {
    printQuiz()
}
  1. Ejecuta tu código. Observa que pudiste llamar a este método sin hacer referencia a la instancia de Quiz. La función apply() mostró los objetos almacenados en quiz.
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

Si bien no es obligatorio usar las funciones de alcance para obtener el resultado deseado, los ejemplos anteriores ilustran cómo pueden hacer que tu código sea más conciso y evitar repetir el mismo nombre de variable.

El código anterior muestra solo dos ejemplos, pero te recomendamos que agregues a favoritos la documentación de funciones de alcance, y que la consultes más adelante en este curso.

9. Resumen

Acabas de ver varias funciones nuevas de Kotlin en acción. Los genéricos permiten que se pasen tipos de datos como parámetros a las clases, las clases enum definen un conjunto limitado de valores posibles, y las clases de datos ayudan a generar automáticamente algunos métodos útiles para las clases.

También viste cómo crear un objeto singleton, que está restringido a una instancia, cómo convertirlo en un objeto complementario de otra clase y cómo extender las clases existentes con nuevos métodos y propiedades de solo acceso. Por último, viste algunos ejemplos de cómo las funciones de alcance pueden proporcionar una sintaxis más simple a la hora de acceder a propiedades y métodos.

Verás estos conceptos en las próximas unidades, a medida que obtengas más información sobre Kotlin, el desarrollo de Android y Compose. Ahora comprendes mejor cómo funcionan y cómo pueden mejorar la reutilización y la legibilidad de tu código.

10. Más información