Introduction
▶ We will comment on some aspects of Kotlin that facilitate
programming
VJ1229, Mobile Device Applications ▶ We will see:
• Interfaces
Advanced Aspects of Kotlin
• Some uses of the object keyword
• Some aspects of generics
• Scope functions
• The builder pattern
©2024, Juan Miguel Vilar Torres
1/35 2/35
Interfaces Example
We want to manage the NPCs for a game:
data class Point(val x: Int, val y: Int)
interface NPC {
▶ Declare abstract functions and functions implementations var location: Point
fun move()
▶ Can have abstract properties ➩ Cannot store state fun draw()
}
▶ If I is an interface, the variable class ManageNPCs {
val NPCs = ArrayList<NPC>()
var v: I
accepts objects of any class that implements the interface I fun addNPC(npc: NPC) = NPCs.add(npc)
fun updateScreen() {
for (npc in NPCs) {
npc.move()
npc.draw()
}
}
}
3/35 4/35
Example (2) Some Uses
Now we can declare our NPCs like this:
class Vampire(val name: String,
override var location: Point) : NPC { ▶ Group classes with similar characteristics
override fun move() {
// .. ▶ Allow the use of Collections with elements of different types
}
▶ Isolate one class from the implementation of other classes
override fun draw() {
// .. ▶ Ease testing
}
}
// ..
val manager = ManageNPCs()
manager.addNPC(Vampire("Dracula", Point(0, 10)))
5/35 6/35
Object Expressions An Example
interface StringProcessor {
fun transform(s: String): String
}
of an interface
▶ Used when you need an object
of a slightly different class fun main () {
val duplicator = object : StringProcessor {
and you don’t want to write a new class
override fun transform(s: String): String {
▶ They are written as the object keyword followed by a colon, return s + s
the interface or parent class, and the body }
}
val doubleGreeting = duplicator.transform("Hello")
}
7/35 8/35
Object for Singletons Example
object Identifiers {
private var counter = 0
fun getNumber() = counter++
▶ Used when there must be only one instance of a class
fun getId() = "id${getNumber()}"
▶ Eg: a single object to access a database, to access the
}
internet, etc
fun main () {
▶ Implemented with the object keyword and a name
val n = Identifiers.getNumber()
val m = Identifiers.getNumber()
▶ Don’t abuse it ➩ it is similar to a global variable
println("Two numbers: $n, $m") // 0, 1
val st1 = Identifiers.getId()
val st2 = Identifiers.getId()
println("Two strings: $st1, $st2") // id2, id3
}
9/35 10/35
Companion Objects Example
class Vampire(val name: String,
▶ A companion object is an object that can be associated to a override var location: Point) : NPC {
class // ..
▶ It allows the definition of attributes accessed through the class companion object {
name, no need to use an instance const val INITIAL_LIFE = 10
▶ Typical use: defining constants, factory methods, etc const val FIGHT_STRENGTH = 20
}
}
11/35 12/35
Generics Example
▶ Allow the parameterization of a class, function or interface
interface Stack<T> {
with a type val length : Int
▶ The type parameter is marked using <T> (where T is the name val isEmpty : Boolean
of the parameter) fun push(item : T)
fun pop(): T
▶ Very useful for collections }
13/35 14/35
Covariance and Contravariance Covariance and
Contravariance (2)
▶ It is possible to specify whether objects of the type variable
are only produced (covariant class) or consumed ▶ The use of covariance and contravariance annotations allow
(contravariant class) type safe assignments
▶ If T is only produced, it is marked as “out T”: ▶ If we have a class C with a type parameter out T:
interface Producer <out T> { • If S is a subtype of B then a value of type C<S> can be
fun produce(): T assigned to a variable of type C<B>
} val b: B = s
val cb : C<B> = cs
▶ If T is only consumed, it is marked as “in T”:
interface Consumer <in T> { ▶ If we have a class C with a type parameter in T:
operator fun consume(value: T): Int • If S is a subtype of B then a value of type C<B> can be
} assigned to a variable of type C<S>
val b: B = s
val cs : C<S> = cb
15/35 16/35
Example Example (2)
open class Animal
class Cat: Animal()
class Dog: Animal() interface Veterinary<in T> {
fun cure(patient: T)
interface PetShop<out T> { }
fun sell(money: Int): T
} fun testVeterinary(catVet: Veterinary<Cat>,
genericVet: Veterinary<Animal>) {
fun testPetShop(catPetShop: PetShop<Cat>, val dVet: Veterinary<Dog> = catVet // Error
genericPetshop: PetShop<Animal>) { val cVet: Veterinary<Cat> = genericVet // OK
val dPetShop: PetShop<Dog> = catPetShop // Error val gVet: Veterinary<Animal> = catVet // Error
val cPetShop: PetShop<Cat> = genericPetshop // Error }
val gPetShop: PetShop<Animal> = catPetShop // OK
}
17/35 18/35
Generic Functions Scope Functions
▶ They are used to apply some functions to an object
▶ Generic functions are marked with <T> before the class name: ▶ E.g. in the class ManageNPCs we could have written
updateScreen like this:
fun <T> mutableListOf(vararg elements: T): MutableList<T>
▶ When used, the type parameter can be omitted if it is inferred fun updateScreen() { fun updateScreen() {
for (npc in NPCs) { for (npc in NPCs)
from the context:
npc.move() with (npc) {
val list = mutableListOf(1, 2, 3) npc.draw() move()
} draw()
} }
}
19/35 20/35
Scope Functions (2) apply
▶ Applies some functions to an object and returns it
▶ Two axis of classification:
▶ This
• The object is accessed as this or it
return Vampire("Dracula", Point(2, 3)).apply {
• The return is the object or the value of the lambda
move()
▶ Available functions: draw()
}
Returns
Access ▶ is equivalent to
Object Lambda
val vampire = Vampire("Dracula", Point(2, 3))
this apply run, with vampire.move()
it also let vampire.draw()
return vampire
21/35 22/35
run with
▶ Similar to apply but returns the result of the lambda ▶ Similar to run but the object is received as a parameter
▶ This ▶ This
val currentPosition = vampire.run { val currentPosition = with (vampire) {
move() move()
draw() draw()
position position
} }
▶ is equivalent to ▶ is equivalent to
vampire.move() vampire.move()
vampire.draw() vampire.draw()
val currentPosition = vampire.position val currentPosition = vampire.position
23/35 24/35
also let
▶ Similar to apply but the object is accessed through it
▶ Similar to also but the result is the value of the lambda
▶ This
▶ This
return Vampire("Dracula", Point(2,3)).also {
it.move() val currentPosition = vampire.let {
it.draw() it.move()
println("The name of the vampire is ${it.name}") println("The name of the vampire is ${it.name}")
} it.position
}
▶ is equivalent to
▶ is equivalent to
val vampire = Vampire("Dracula", Point(2,3))
vampire.move() vampire.move()
vampire.draw() println("The name of the vampire is ${vampire.name}")
println("The name of the vampire is ${vampire.name}") val currentPosition = vampire.position
return vampire
25/35 26/35
The Builder Pattern The Screen Class
▶ Suppose we have a class for storing the information about a
▶ Used to build complex objects
screen in the game:
▶ Offers functions to add components or change options as class Screen (val dayLight: Double,
needed val lightSources: List<LightSource>,
▶ It usually uses fluid interfaces: val NPCs: List<NPC>,
val buildings: List<Building>
• The functions return the same object ) {
• It can be used as a (limited) DSL // ...
}
▶ It usually has a function that returns the object that has been
built ▶ Creating and filling the parameters of the constructor will be
cumbersome
27/35 28/35
The Builder Class The Builder Class (2)
And we add a function for each of the elements that can be added:
We create a class to incrementally build all the parameters:
fun setDayLight(dayLight: Double): ScreenBuilder {
class ScreenBuilder { this.dayLight = dayLight
private var dayLight: Double = 0.0 return this
private var lightSources = ArrayList<LightSource>() }
private var NPCs = ArrayList<NPC>()
Or, using apply:
private var buildings = ArrayList<Building>()
fun setDayLight(dayLight: Double) =
apply { this.dayLight = dayLight }
29/35 30/35
The Builder Class (3) The Builder Class (4)
The rest of elements that can be added:
fun addLightSource(lightSource: LightSource) = And a final function to actually build the screen
apply { lightSources.add(lightSource) }
fun createScreen() = Screen(dayLight,
fun addNPC(npc: NPC) = lightSources,
apply { NPCs.add(npc)} NPCs,
buildings)
fun addBuilding(building: Building) =
apply { buildings.add(building) }
31/35 32/35
Example of usage Example of usage (2)
Since each function returned the builder, they can be concatenated
like this:
Using scope functions, we can even shorten this code:
fun mainScreen(): Screen {
val builder = ScreenBuilder() fun mainScreen(): Screen = with(ScreenBuilder()) {
setDayLight(12.0)
builder.setDayLight(12.0) addLightSource(LightSource(Point(10, 20), 2.4))
.addLightSource(LightSource(Point(10, 20), 2.4)) addBuilding(Building("Saloon"))
.addBuilding(Building("Saloon")) addBuilding(Building("Drugstore"))
.addBuilding(Building("Drugstore")) addNPC(Vampire("Dracula", Point(100, 100)))
.addNPC(Vampire("Dracula", Point(100, 100))) createScreen()
}
return builder.createScreen()
}
33/35 34/35
Further Information
▶ In the Kotlin book
▶ Chapter “Abstract Classes and Interfaces”
▶ Chapter “The object Keyword”
▶ Chapter “Covariance in Generics”
▶ Chapter “Contravariance in Generics”
▶ Chapter “Scope Functions”
35/35