codete Yet Another Kotlin Tutorial for Android Developers Part 2 1 main e844eb2920
Codete Blog

Yet Another Kotlin Tutorial for Android Developers – Part 2

avatar male f667854eaa

01/07/2016 |

7 min read

Witold Wiśniewski

In my previous post, I wrote about properties and null safety, but we have learned how to define class just for understanding the example. It is an appropriate time to know how OOP works in Kotlin. 

Let’s start with some prerequisites.

 

Kotlin file content

First of all, there is no connection between filename and content naming. A single file can consist of any quantity of classes OR top-level declarations (declaration without classes), so in a single file there may be no class at all - as a matter of fact, this can be observed often in the kotlin source.

 

Visibility modifiers

Talking about classes requires a word about visibility modifiers, we have four modifiers to use: 

  • public
  • internal
  • protected
  • private

The default one is public (visible everywhere), there is no package visibility as in Java. Kotlin introduces the `internal` keyword, which tells everything marked with this modifier will be visible inside a current module

But what exactly is this module? It depends on how you compile your code. It is either an IDE module or a project/task defined by your favorite build tool.

Visibility modifiers may have a different meaning for a different context. A private member will be reachable only inside the class - this means an outer class doesn’t see private members of its inner class. Protected visibility works pretty much as a private one with one extension - protected members of the superclass are visible within subclasses, which is what I craved in Java. Top-level declarations cannot be marked as `protected`, and the ‘private’ keyword limits the visibility of the file.

 

Classes 

An example explains more than words, so let’s slightly modify the class familiar from Part 1.  

class Contact public @Inject constructor (numberParam: String) {
   val number: String = numberParam
   var firstName: String? = null
   var lastName: String? = null
}

The first noticeable thing is the `constructor` keyword, not present before. That is because it is necessary only when some customization is needed (injection or visibility modification for instance). Constructors implemented this way are called primary constructors. The body of such constructors consists of the following things:

  • delegation to a superclass constructor
  • initializer block
  • assignment values straight to properties from primary constructor parameters

I omitted the ‘var’ keyword on purpose - `numberParam` is just a parameter, not a property in that case. It can be used for property assignment or inside the initializer block, however, it is not visible for any method.  

class Contact @Inject constructor(numberParam: String) { // public omitted
   val number: String
   var firstName: String? = null
   var lastName: String? = null

 
   init {
       number = numberParam
   }
}

The name primary constructor suggests that there might be a secondary constructor. That’s right, but there is a rule - secondary constructors must delegate primary constructor - via `this` keyword. 

class Contact @Inject constructor(numberParam: String) {
   val number: String
   var firstName: String? = null
   var lastName: String? = null

 
   init {
       number = numberParam
   }

 
   internal constructor(numberParam: String, firstNameParam: String) : this(numberParam)
   {
       firstName = firstNameParam
   }
}

One thing I dislike is the possibility of not declaring any primary constructor. You may have only secondary constructors as well. In that case, there is no need for delegating the primary constructor, simply because we have none.

 

Interfaces 

Interfaces can have properties and methods. Both of them can be abstract or provide default implementation:

interface RegistryContact {
   val number: String //abstract property
   val hasPrefix : Boolean // just a property
       get() = number.startsWith("+")

 
   fun getHistory (type: CallType) : History //abstract method

 
   fun formatNumber() { //method with default implementation
       // some default formatting
   }   
}

Declaring methods require information about parameters/result names and types. Nothing new, just like in Java 8, but wait, where is the `formatNumber` type? In the previous article, a type inference mechanism was mentioned, and that is why the compiler does not protest. Inferred type for `formatNumber` method is Unit - the equivalent of `void’.

The rule for properties is that properties in interfaces cannot have the backing field, because interfaces are not meant to bring the state. Feel free to provide accessors implementation, leave them to be abstract otherwise.

 

Inheritance

Inheritance works pretty much as for Java - class can extend only one supertype, but it can implement multiple interfaces without any limitation. Each has ‘Any’ superclass, which is not `Object`, although `Objects` are translated to `Any` when importing Java source. 

Inheritance can be achieved only using colon character, either for extending classes and implementing interfaces, but it’s possible to put parenthesis after superclass name - which is basically constructor delegation. 

When overriding, you can change the visibility modifier, but only for widening. As an abstract class can inherit from a non-abstract one, members may be overridden as abstract. 

The background is covered, let’s go to some cool things.

 

Data Classes

This mechanism provides some useful features for classes that hold nothing but data.

 

Equality

First of all, `equals` and `hashCode` methods are generated, so they can be used safely with equality comparison. There is one difference from Java - operator `===` checks referential equality, while `==` is meant for structural equality (basically ‘equals’ invocation) - when invoked, all properties from the primary constructor are compared and based on this and the type we have the result. Let’s see an example: 

data class IntWrapper(val value: Int)
data class IntPair(val key: Int) {
   var value: Int? = null
}

 
val a = IntWrapper(1)
val b = IntWrapper(1)
println(a == b) // true, as a structural equality is compared
println(a === b) // false

 
val pair1 = IntPair(1)
pair1.value=2
val pair2 = IntPair(1)
pair2.value=3

 
println(pair1 == pair2) // true, as ‘value’ property is not taken under consideration

 

ToString

Default `toString` method is generated by following template `Classname(property1=value1, property2=value2, …, propertyX=valueX)`

 

Copy

Let’s imagine we want to clone an instance of a data class object with modifying some properties - the `copy` function is something we get for free, but what if we want to modify some properties? Kotlin comes with named arguments as a solution. 

data class IntPair(val key: Int, var value: Int)

 
var pair = IntPair(1, 2)
var anotherPair = pair.copy(value = 3) // choosing which property need to be modified
println(pair) // prints IntPair(key=1, value=2)
println(anotherPair) // prints IntPair(key=1, value=3)

We can change just those values we are interested in. Creating methods with name arguments will be presented in the next part, so be patient.

 

Destructuring declarations

The compiler generates `componentN` functions for data classes providing easy access for each property, a really handy mechanism for mutlideclarations. 

var (number, firstName , lastName) = contact

This declaration is basically equivalent to:

var number = contact.component1()
var firstName = contact.component2()
var lastName = contact.component3()

We can use the same approach with `for` loops:

for ((number, firstName, lastName) in contacts) {
   // ...
}

As data classes are recommended approach for tuple substitution (yes, tuples are not part of kotlin), `Pair` and `Triple` are standard data classes providing all mentioned functionalities. Also, `componentN` functions are combined with the `Map` interface, allowing traversing a map with destructuring declarations.

 

Sealed classes

This is the answer for Algebraic Data Types representation. They are abstract by default and may be subclassed only by nested classes and objects. 

sealed class Result {
   class Success(val message: String) : Result()
   class Failure(val error: Error)
}

For evaluating sealed classes, when the statement comes as a helpful tool. In general, it is just a `switch` known from Java, but more powerful. 

when (something) {
   1 -> println("Special case for 1")
   2, 3 -> println("Special case for 2 and 3")
   in 2..10 -> println("Another special case")
   is Double -> println("Double needs other treatment")
   processNumber(something) -> println("Demonstrating something strange")
}

Matching against constant is not required, as in the last condition check. Returning to sealed class example:

fun onResult(result: Result) {
   when (result) {
       is Result.Success -> printMessage(result.message)
       is Result.Failure -> onError(result.error)
   }
}

You may have noticed one subtle thing - I accessed the `message` property of `result` (and `error` for the second case), but the `Result` sealed class does not have one. The answer is simple. Kotlin supports smart cast, and this is not only for sealed classes but after every type-check with the `is` operator. 

 

Summary

We’ve got another dose of what kotlin offers and how to use it, but it’s not the end, so stay tuned!

Rated: 5.0 / 1 opinions
avatar male f667854eaa

Witold Wiśniewski

Senior Software Developer

Our mission is to accelerate your growth through technology

Contact us

Codete Global
Spółka z ograniczoną odpowiedzialnością

Na Zjeździe 11
30-527 Kraków

NIP (VAT-ID): PL6762460401
REGON: 122745429
KRS: 0000983688

Get in Touch
  • icon facebook
  • icon linkedin
  • icon instagram
  • icon youtube
Offices
  • Kraków

    Na Zjeździe 11
    30-527 Kraków
    Poland

  • Lublin

    Wojciechowska 7E
    20-704 Lublin
    Poland

  • Berlin

    Bouchéstraße 12
    12435 Berlin
    Germany