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

Idiomatic Kotlin

Avatar for Anton Arhipov Anton Arhipov
May 06, 2021
940

Idiomatic Kotlin

Avatar for Anton Arhipov

Anton Arhipov

May 06, 2021
Tweet

More Decks by Anton Arhipov

Transcript

  1. Idiomatic - using, containing, or denoting expressions that are natural

    to a native speaker In case of a programming language: •Conforms to a commonly accepted style •E ff ectively uses features of the programming language
  2. fun adjustSpeed(weather: Weather): Drive { var result: Drive if (weather

    is Rainy) { result = Safe() } else { result = Calm() } return result }
  3. fun adjustSpeed(weather: Weather): Drive { var result: Drive if (weather

    is Rainy) { result = Safe() } else { result = Calm() } return result }
  4. fun adjustSpeed(weather: Weather): Drive { val result: Drive if (weather

    is Rainy) { result = Safe() } else { result = Calm() } return result }
  5. fun adjustSpeed(weather: Weather): Drive { val result: Drive if (weather

    is Rainy) { result = Safe() } else { result = Calm() } return result }
  6. fun adjustSpeed(weather: Weather): Drive { val result: Drive result =

    if (weather is Rainy) { Safe() } else { Calm() } return result }
  7. fun adjustSpeed(weather: Weather): Drive { val result: Drive result =

    if (weather is Rainy) { Safe() } else { Calm() } return result }
  8. fun adjustSpeed(weather: Weather): Drive { val result: Drive = if

    (weather is Rainy) { Safe() } else { Calm() } return result }
  9. fun adjustSpeed(weather: Weather): Drive { val result: Drive = if

    (weather is Rainy) { Safe() } else { Calm() } return result }
  10. fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else

    Calm() fun adjustSpeed(weather: Weather): Drive { var result: Drive if (weather is Rainy) { result = Safe() } else { result = Calm() } return result }
  11. abstract class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() else -> Calm() }
  12. sealed class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() / / else -> Calm() }
  13. sealed class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() / / else -> Calm() }
  14. sealed class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() is Sunny -> TODO() }
  15. sealed class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() is Sunny -> TODO() } Use expressions! Use when as expression body Use sealed classes with when
  16. Use try as expression body fun tryParse(number: String) : Int?

    { try { return Integer.parseInt(number) } catch (e: NumberFormatException) { return null } }
  17. Use try as expression body fun tryParse(number: String) = try

    { Integer.parseInt(number) } catch (e: NumberFormatException) { null }
  18. Use try as expression fun tryParse(number: String) : Int? {

    val n = try { Integer.parseInt(number) } catch (e: NumberFormatException) { null } println(n) return n }
  19. Use elvis operator as return and throw class Person(val name:

    String?, val age: Int?) fun processPerson(person: Person) { val name = person.name if (name = = null) throw IllegalArgumentException("Named required") val age = person.age if (age == null) return println("$name: $age") }
  20. Use elvis operator as return and throw class Person(val name:

    String?, val age: Int?) fun processPerson(person: Person) { val name = person.name if (name = = null) throw IllegalArgumentException("Named required") val age = person.age if (age == null) return println("$name: $age") }
  21. Use elvis operator as return and throw class Person(val name:

    String?, val age: Int?) fun processPerson(person: Person) { val name = person.name if (name = = null) throw IllegalArgumentException("Named required") val age = person.age if (age == null) return println("$name: $age") }
  22. Use elvis operator as return and throw class Person(val name:

    String?, val age: Int?) fun processPerson(person: Person) { val name = person.name ? : throw IllegalArgumentException("Named required") val age = person.age ?: return println("$name: $age") }
  23. Consider using null-safe call val order = retrieveOrder() if (order

    == null || order.customer = = null || order.customer.address == null){ throw IllegalArgumentException("Invalid Order") } val city = order.customer.address.city
  24. Consider using null-safe call val order = retrieveOrder() val city

    = order ?. customer ? . address ?. city ?: throw IllegalArgumentException("Invalid Order")
  25. Avoid not-null assertions !! val order = retrieveOrder() val city

    = order !! .customer !! .address !! .city “You may notice that the double exclamation mark looks a bit rude: it’s almost like you’re yelling at the compiler. This is intentional.” - Kotlin in Action
  26. Avoid not-null assertions !! class MyTest { class State(val data:

    String) private var state: State? = null @BeforeEach fun setup() { state = State("abc") } @Test fun foo() { assertEquals("abc", state !! .data) } }
  27. Avoid not-null assertions !! class MyTest { class State(val data:

    String) private var state: State? = null @BeforeEach fun setup() { state = State("abc") } @Test fun foo() { assertEquals("abc", state !! .data) } } class MyTest { class State(val data: String) private lateinit var state: State @BeforeEach fun setup() { state = State("abc") } @Test fun foo() { assertEquals("abc", state.data) } } - use lateinit
  28. Consider using ?.let for null-checks val order = retrieveOrder() if

    (order != null){ processCustomer(order.customer) }
  29. Consider using ?.let for null-checks val order = retrieveOrder() if

    (order != null){ processCustomer(order.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let { :: processCustomer } or
  30. Consider using ?.let for null-checks val order = retrieveOrder() if

    (order != null){ processCustomer(order.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } No need for an extra variable retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let { :: processCustomer } or
  31. Consider using ?.let for null-checks val order = retrieveOrder() if

    (order != null){ processCustomer(order.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let { :: processCustomer } or
  32. Consider using safe cast for type checking override fun equals(other:

    Any?) : Boolean { val command = other as Command return command.id == id }
  33. Consider using safe cast for type checking override fun equals(other:

    Any?) : Boolean { val command = other as Command return command.id == id } override fun equals(other: Any?) : Boolean { return (other as? Command) ?. id == id }
  34. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } }
  35. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } }
  36. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } } for (i in 0 until args.size) { println("$i: ${args[i]}") }
  37. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } } for (i in 0 until args.size) { println("$i: ${args[i]}") } for (i in args.indices) { println("$i: ${args[i]}") }
  38. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } } for (i in 0 until args.size) { println("$i: ${args[i]}") } for (i in args.indices) { println("$i: ${args[i]}") } for ((i, arg) in args.withIndex()) { println("$i: $arg") }
  39. Don’t create classes just to hold functions class StringUtils {

    companion object { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } }
  40. Don’t create classes just to hold functions class StringUtils {

    companion object { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } } object StringUtils { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } }
  41. Don’t create classes just to hold functions class StringUtils {

    companion object { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } } object StringUtils { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() }
  42. Use extension functions class StringUtils { companion object { fun

    isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } } object StringUtils { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } fun String.isPhoneNumber() = length == 7 && all { it.isDigit() }
  43. Extension or a member? https://kotlinlang.org/docs/coding-conventions.html#extension-functions •Use extension functions liberally. •If

    a function works primarily on an object, consider making it an extension with that object as a receiver. •Minimize API pollution, restrict the visibility. •As necessary, use local extension functions, member extension functions, or top-level extension functions with private visibility.
  44. Use default values instead of overloading class Phonebook { fun

    print() { print(",") } fun print(columnSeparator: String) {} } fun main(args: Array<String>) { Phonebook().print("|") }
  45. Use default values instead of overloading class Phonebook { fun

    print() { print(",") } fun print(columnSeparator: String) {} } fun main(args: Array<String>) { Phonebook().print("|") } class Phonebook { fun print(separator: String = ",") {} fun someFun(x: Int) {} } fun main(args: Array<String>) { Phonebook().print(separator = "|") }
  46. Return multiple values using data classes fun namedNum(): Pair<Int, String>

    = 1 to "one" // same but shorter fun namedNum2() = 1 to "one" fun main(args: Array<String>) { val pair = namedNum() val number = pair.first val name = pair.second }
  47. Return multiple values using data classes fun namedNum(): Pair<Int, String>

    = 1 to "one" // same but shorter fun namedNum2() = 1 to "one" fun main(args: Array<String>) { val pair = namedNum() val number = pair.first val name = pair.second } data class GameResult( val rank: Int, val name: String ) fun namedNum() = GameResult(1, "Player 1") fun main(args: Array<String>) { val (rank, name) = namedNum() println("$name, rank $rank") }
  48. Return multiple values using data classes data class GameResult( val

    rank: Int, val name: String ) fun namedNum() = GameResult(1, "Player 1") fun main(args: Array<String>) { val (rank, name) = namedNum() println("$name, rank $rank") } GameResult var1 = namedNum(); int var2 = var1.component1(); String var3 = var1.component2();
  49. Destructuring in loops fun printMap(map: Map<String, String>) { for (item

    in map.entries) { println("${item.key} -> ${item.value}") } }
  50. Destructuring in loops fun printMap(map: Map<String, String>) { for (item

    in map.entries) { println("${item.key} -> ${item.value}") } } fun printMap(map: Map<String, String>) { for ((key, value) in map) { println("$key -> $value") } }
  51. Destructuring in lists data class NameExt( val name: String, val

    ext: String? ) fun splitNameExt(filename: String): NameExt { if ('.' in filename) { val parts = filename.split('.', limit = 2) return NameExt(parts[0], parts[1]) } return NameExt(filename, null) } fun splitNameAndExtension(filename: String): NameExt { if ('.' in filename) { val (name, ext) = filename.split('.', limit = 2) return NameExt(name, ext) } return NameExt(filename, null) }
  52. Use type aliases for functional types class Event class EventDispatcher

    { fun addClickHandler(handler: (Event) -> Unit) {} fun removeClickHandler(handler: (Event) -> Unit) {} }
  53. Use type aliases for functional types class Event class EventDispatcher

    { fun addClickHandler(handler: (Event) -> Unit) {} fun removeClickHandler(handler: (Event) -> Unit) {} } typealias ClickHandler = (Event) -> Unit class EventDispatcher { fun addClickHandler(handler: ClickHandler) { } fun removeClickHandler(handler: ClickHandler) { } }
  54. Verify parameters using require() class Person( val name: String?, val

    age: Int ) fun processPerson(person: Person) { if (person.age < 18) { throw IllegalArgumentException("Adult required") } }
  55. Verify parameters using require() class Person( val name: String?, val

    age: Int ) fun processPerson(person: Person) { if (person.age < 18) { throw IllegalArgumentException("Adult required") } } fun processPerson(person: Person) { require(person.age > = 18) { "Adult required" } }
  56. Select objects by type with filterIsInstance fun findAllStrings(objects: List<Any>) =

    objects.filter { it is String } fun findAllStrings(objects: List<Any>) = objects.filterIsInstance<String>()
  57. Select objects by type with filterIsInstance fun findAllStrings(objects: List<Any>) :

    List<Any> = objects.filter { it is String } fun findAllStrings(objects: List<Any>) : List<String> = objects.filterIsInstance<String>()
  58. Apply operation to non-null elements mapNotNull data class Result( val

    data: Any?, val error: String? ) fun listErrors(results: List<Result>): List<String> = results.map { it.error }.filterNotNull() fun listErrors(results: List<Result>): List<String> = results.mapNotNull { it.errorMessage }
  59. compareBy compares by multiple keys class Person( val name: String,

    val age: Int ) fun sortPersons(persons: List<Person>) = persons.sortedWith(Comparator<Person> { person1, person2 -> val rc = person1.name.compareTo(person2.name) if (rc != 0) rc else person1.age - person2.age })
  60. compareBy compares by multiple keys class Person( val name: String,

    val age: Int ) fun sortPersons(persons: List<Person>) = persons.sortedWith(Comparator<Person> { person1, person2 -> val rc = person1.name.compareTo(person2.name) if (rc != 0) rc else person1.age - person2.age }) fun sortPersons(persons: List<Person>) = persons.sortedWith(compareBy(Person :: name, Person : : age))
  61. groupBy to group elements class Request( val url: String, val

    remoteIP: String, val timestamp: Long ) fun analyzeLog(log: List<Request>) { val map = mutableMapOf<String, MutableList<Request >> () for (request in log) { map.getOrPut(request.url) { mutableListOf() } .add(request) } }
  62. groupBy to group elements class Request( val url: String, val

    remoteIP: String, val timestamp: Long ) fun analyzeLog(log: List<Request>) { val map = mutableMapOf<String, MutableList<Request >> () for (request in log) { map.getOrPut(request.url) { mutableListOf() } .add(request) } } fun analyzeLog(log: List<Request>) { val map = log.groupBy(Request :: url) }
  63. Use coerceIn to ensure numbers in range fun updateProgress(value: Int)

    { val actualValue = when { value < 0 - > 0 value > 100 -> 100 else - > value } } fun updateProgress(value: Int) { val actualValue = value.coerceIn(0, 100) }
  64. Initializing objects with apply val dataSource = BasicDataSource( ) dataSource.driverClassName

    = "com.mysql.jdbc.Driver" dataSource.url = "jdbc:mysql://domain:3309/db" dataSource.username = "username" dataSource.password = "password" dataSource.maxTotal = 40 dataSource.maxIdle = 40 dataSource.minIdle = 4 val dataSource = BasicDataSource().apply { driverClassName = "com.mysql.jdbc.Driver" url = "jdbc:mysql://domain:3309/db" username = "username" password = "password" maxTotal = 40 maxIdle = 40 minIdle = 4 }
  65. Initializing objects with apply final ClientBuilder builder = new ClientBuilder();

    builder.setFirstName("Anton"); builder.setLastName("Arhipov"); final TwitterBuilder twitterBuilder = new TwitterBuilder(); twitterBuilder.setHandle("@antonarhipov"); builder.setTwitter(twitterBuilder.build()); final CompanyBuilder companyBuilder = new CompanyBuilder(); companyBuilder.setName("JetBrains"); companyBuilder.setCity("Tallinn"); builder.setCompany(companyBuilder.build()); final Client client = builder.build(); System.out.println("Created client is: " + client);
  66. Initializing objects with apply val builder = ClientBuilder() builder.firstName =

    "Anton" builder.lastName = "Arhipov" val twitterBuilder = TwitterBuilder() twitterBuilder.handle = "@antonarhipov" builder.twitter = twitterBuilder.build() val companyBuilder = CompanyBuilder() companyBuilder.name = "JetBrains" companyBuilder.city = "Tallinn" builder.company = companyBuilder.build() val client = builder.build() println("Created client is: $client")
  67. Initializing objects with apply val builder = ClientBuilder() builder.firstName =

    "Anton" builder.lastName = "Arhipov" val twitterBuilder = TwitterBuilder() twitterBuilder.handle = "@antonarhipov" builder.twitter = twitterBuilder.build() val companyBuilder = CompanyBuilder() companyBuilder.name = "JetBrains" companyBuilder.city = "Tallinn" builder.company = companyBuilder.build() val client = builder.build() println("Created client is: $client") val client = ClientBuilder().apply { firstName = "Anton" lastName = "Arhipov" twitter = TwitterBuilder().apply { handle = "@antonarhipov" }.build() company = CompanyBuilder().apply { name = "JetBrains" city = "Tallinn" }.build() }.build() println("Created client is: $client")
  68. “Domain Speci fi c”, i.e. tailored for a speci fi

    c task Examples: •Compose strings - stringBuilder •Create HTML documents - kotlinx.html •Con fi gure routing logic for a web app - ktor •Generally, build any object graphs. See “type-safe builders”
  69. buildString //Java String name = "Joe"; StringBuilder sb = new

    StringBuilder(); for (int i = 0; i < 5; i++) { sb.append("Hello, "); sb.append(name); sb.append("!\n"); } System.out.println(sb); //Kotlin val name = "Joe" val s = buildString { repeat(5) { append("Hello, ") append(name) appendLine("!") } } println(s)
  70. Ktor fun main() { embeddedServer(Netty, port = 8080, host =

    "0.0.0.0") { routing { get("/html-dsl") { call.respondHtml { body { h1 { +"HTML" } ul { for (n in 1 .. 10) { li { +"$n" } } } } } } } }.start(wait = true) }
  71. Ktor fun main() { embeddedServer(Netty, port = 8080, host =

    "0.0.0.0") { routing { get("/html-dsl") { call.respondHtml { body { h1 { +"HTML" } ul { for (n in 1 .. 10) { li { +"$n" } } } } } } } }.start(wait = true) } Ktor’s routing
  72. Ktor fun main() { embeddedServer(Netty, port = 8080, host =

    "0.0.0.0") { routing { get("/html-dsl") { call.respondHtml { body { h1 { +"HTML" } ul { for (n in 1 .. 10) { li { +"$n" } } } } } } } }.start(wait = true) } kotlinx.html Ktor’s routing
  73. Build your vocabulary to abstract from scope functions val client

    = ClientBuilder().apply { firstName = "Anton" lastName = "Arhipov" twitter = TwitterBuilder().apply { handle = "@antonarhipov" }.build() company = CompanyBuilder().apply { name = "JetBrains" city = "Tallinn" }.build() }.build() println("Created client is: $client")
  74. Build your vocabulary to abstract from scope functions fun client(c:

    ClientBuilder.() - > Unit): Client { val builder = ClientBuilder() c(builder) return builder.build() } fun ClientBuilder.company(block: CompanyBuilder.() - > Unit) { company = CompanyBuilder().apply(block).build() } fun ClientBuilder.twitter(block: TwitterBuilder.() - > Unit) { twitter = TwitterBuilder().apply(block).build() } val client = ClientBuilder().apply { firstName = "Anton" lastName = "Arhipov" twitter = TwitterBuilder().apply { handle = "@antonarhipov" }.build() company = CompanyBuilder().apply { name = "JetBrains" city = "Tallinn" }.build() }.build() println("Created client is: $client")
  75. val client = client { firstName = "Anton" lastName =

    "Arhipov" twitter { handle = "@antonarhipov" } company { name = "JetBrains" city = "Tallinn" } } println("Created client is: $client") Build your vocabulary to abstract from scope functions val client = ClientBuilder().apply { firstName = "Anton" lastName = "Arhipov" twitter = TwitterBuilder().apply { handle = "@antonarhipov" }.build() company = CompanyBuilder().apply { name = "JetBrains" city = "Tallinn" }.build() }.build() println("Created client is: $client")