Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Best Practices

Dan Rusu
February 13, 2019

Kotlin Best Practices

A collection best practices for using Kotlin effectively

Dan Rusu

February 13, 2019
Tweet

More Decks by Dan Rusu

Other Decks in Programming

Transcript

  1. FR Goals • Code should be obvious to those who

    know Kotlin • Don’t be clever or over-use capabilities (i.e. infix functions everywhere) • But… don’t limit usage due to a lack of training • Look for new ways to improve readability • Kotlin enables new patterns which aren’t possible with Java • These also reduce defect rates and improve productivity • Embrace enhanced thought process • Kotlin is a superset of Java (minus dozens of defect types) • Java-style thinking process will severely limit clean coding & robustness potential 2
  2. FR Kotlin Patterns can be Java Anti-Patterns • Don’t use

    the equals method • E.g. Using dog.equals(cat) compiles but dog == cat doesn’t because it can never be true (for Animal closed classes Cat & Dog ) • Never use Optional • Optional can encounter runtime exceptions whereas nullable types are verified at compile time • Optionals are also less efficient and we lose all the nice Kotlin null operators • NULL is the best way of representing the absence of a value (in Kotlin) • Surprisingly, opposite to Java, using NULL actually reduces mistakes when representing an absent value due to Kotlin’s stronger type system with compile-time verification • Key take-away: Keep an open mind and re-evaluate your Java ideals 3
  3. FR Type Declarations • Don’t declare variable type when it’s

    redundant or obvious • Don’t declare generic types when it’s redundant • Kotlin has a stronger type system than Java and can infer more 4 // Good, type is obvious val name = "Dan" // Bad, redundant type val friend: Person = Person("Dan") // Good, type is obvious List<String> val names = listOf("Dan", "Bob") // Bad, redundant generic type <Person> val friends = listOf<Person>(Person("Dan"), Person("Bob"))
  4. FR Top-level Declarations • Prefer top-level declarations for private non-member

    (static) constants & functions • Don’t use for non-privates to avoid polluting the namespace • Except extension functions as those are already scoped by their type 5 private val LOGGER = getLogger<BankAccount>() class BankAccount { fun deposit(amountCents: Long) { ensurePositiveAmount(amountCents) LOGGER.log("Depositing") } fun withdraw(amountCents: Long) { ensurePositiveAmount(amountCents) LOGGER.log("Withdrawing") } } private fun ensurePositiveAmount(amount: Long) { // This generic utility can be improved (more later) if (amount <= 0) throw IllegalArgumentException(“Amount must be positive") }
  5. FR Nullable Types & Safe Call Operator • Prefer nullable

    types when the value might be missing • Prefer non-null type if a value should always be present • Look for opportunities to use the safe-call operator • Try to avoid non-null assertion (!!) as you lose null-safety 6 // Return type nullable in case no user is found with the specified name fun findUser(name: String): Person? { … } val user = findUser("Dan") // Java style if (user != null) { user.login() } // Kotlin style using safe call operator user?.login()
  6. FR Elvis Operator for handling Null • Elvis operator is

    much better than manually checking for null • Properties / function invocations only happen once • Enables Kotlin compiler to smart cast value to non-null type 7 // Throw exception when null val user = findUser(name) ?: throw IllegalArgumentException("User $name doesn't exist") // Early return when null fun hasPermission(username: String): Boolean { val user = findUser(username) ?: return false … } // Default Value val pageSize = request.pageSize ?: 100 // First non-null value if present val emergencyContact = user.spouse ?: user.father ?: user.mother
  7. FR Elvis & Safe Call operators are friends 8 //

    Java style val nameLength: Int if (name != null) { nameLength = name.length } else { nameLength = 0 } // Kotlin style using safe call & elvis operators for default value val nameLength2 = name?.length ?: 0
  8. FR Read-only Collections • Kotlin distinguishes between read-only and mutable

    collections • E.g. Set vs. MutableSet • Always declare the type as the read-only version if mutation isn’t needed • Unlike Java, Kotlin supports declaration-site variance • Read-only collections are defined as covariant in Kotlin 9 interface Animal class Dog(val name: String) : Animal val dogs = mutableListOf( Dog("Fluffy"), Dog("Scratchy") ) poke(dogs) // Covariant: List<Dog> treated as subtype of List<Animal> fun poke(animals: List<Animal>) { animals.forEach { poke(it) } }
  9. FR Values • Prefer val. Only use var if you

    must re-assign the variable • IntelliJ underlines vars to make it clear that they may have changed • Vals don’t need to be assigned on the spot 10 val friend1 = loadPerson("Bob") val friend2 = loadPerson("Joe") val maxAge: Int val youngestFriend: Person if (friend1.age > friend2.age) { maxAge = friend1.age youngestFriend = friend2 } else { maxAge = friend2.age youngestFriend = friend1 }
  10. FR String templates • Clearer, more concise, reduces mistakes •

    Much more efficient than string format 11 // Java style val message = "Hello" + name + ", your name has " + name.length + " characters" // Kotlin style, missing space is obvious val message = "Hello$name, your name has ${name.length} characters" // Java style val sql = "SELECT * FROM users\n" + "WHERE age > 18\n" + "ORDER BY age" // Kotlin style val sql = """ SELECT * FROM users WHERE age > 18 ORDER BY age """.trimIndent()
  11. FR Inlined Lambdas • Declare functions that accept lambdas as

    inline when possible (but keep inline functions short) • Unlike Java, Kotlin lambdas are closures and also allow checked exceptions • Use inline lambdas liberally to capture patterns in 1 place 12 class BankAcount(private var balanceCents: Long) { fun deposit(amountCents: Long) { performTransaction("Deposit") { balanceCents += amountCents } } fun withdraw(amountCents: Long) { performTransaction("Withdraw") { balanceCents -= amountCents } } private inline fun performTransaction(operation: String, execute: () -> Unit) { LOGGER.log("Performing $operation") try { begin() execute() commit() } catch (exception: Exception) { rollBack() } } }
  12. FR Validation: Require & Check 13 // Java style fun

    generateInRange(lowerBound: Int, upperBound: Int): Int { if (lowerBound > upperBound) { throw IllegalArgumentException(“$upperBound cannot be less than $lowerBound") } … if (result < lowerBound || result > upperBound) { throw IllegalStateException("$result should be in [$lowerBound, $upperBound]") } return result } // Kotlin style using require, check, & range fun generateInRange(lowerBound: Int, upperBound: Int): Int { require(lowerBound <= upperBound) { “$upperBound cannot be less than $lowerBound" } … check(result in lowerBound..upperBound) { "$result should be in [$lowerBound, $upperBound]" } return result }
  13. FR Default Values & Named Parameters • Much better than

    builders (but builders are still used in DSLs behind the scenes) • Required values are guaranteed to be provided and verified at compile time • Much cleaner at declaration site and also cleaner at the call site • Also more efficient and scalable 14 class Person( val name: String, val age: Int = 0, var spouse: Person? = null, val isManager: Boolean = false ) { init { require(age >= 0) { "Age: $age cannot be negative"} } } val bob = Person(name = "Bob") val jane = Person(name = "Jane", isManager = true) val joe = Person( isManager = false, name = "Joe", age = 3 )
  14. FR Expressions • Prefer expressions • Kotlin enforces exhaustive handling

    (when is especially nice) • Even If / Else or try / catch can be used as expressions! • Prefer expression bodies for short functions 15 fun getFullName(): String = "$firstName $lastName" fun isSpecial(): Boolean = age % 7 == 3 fun max(a: Int, b: Int): Int = if (a > b) a else b fun computePriorityColor(priority: Priority): Color = when (priority) { Priority.CRITICAL -> Color.RED Priority.MEDIUM -> Color.YELLOW Priority.LOW -> Color.GREEN }
  15. FR Reified Generics • Kotlin supports reified generics avoiding type

    erasure! • Prefer reified types instead of accepting Class references (e.g. getLogger) 16 interface Animal class Dog(val name: String): Animal class Cat(val name: String): Animal val values = listOf( Dog("Fluffy"), Cat("Scratchy") ) findValueOfType<Cat>(values) inline fun <reified T> findValueOfType(values: Collection<Any>): T? { values.forEach { if (it is T) return it } return null }
  16. FR Extension Functions • Are amazing!!! Use them liberally •

    Can be defined on generic types to add capabilities to multiple classes! • But… make them private / internal if they’re not applicable everywhere 17 // Faire specific string extensions fun <T : DbEntity> String.toToken() = Token.of<T>(this) val retailerToken = "r_xyz".toToken<DbRetailer>() // Using generics fun <T: Comparable<T>> T.coerceAtMost(maximum: T): T { return if ( this > maximum) maximum else this } val char = 'x'.coerceAtMost(maximum = 'c') val number = 17.coerceAtMost(maximum = 12)
  17. FR Kotlin Standard Library • Kotlin enhances common Java classes

    with extension functions to trivialize repetitive actions • Many of these are inlined • Look for opportunities to use these (especially when reviewing PRs) 18 // Java style val names = mutableListOf<String>() for (person in people) { names.add(person.name) } // Kotlin style val names2 = people.map { it.name } // Java style for (person in people) { if (person.name == "Dan") return true } return false // Kotlin style return people.any { it.name == "Dan" }