Kotlin, beyond the basics

Kotlin, beyond the basics

Talk given at 360AnDev (https://360andev.com/) 2018 in Denver Colorado, USA.

Video: https://www.youtube.com/watch?v=Ot2DOKztu38

Features 3 important topics for intermediate Kotlin developers - adapting to the functional programming paradigm, learning more about functions and lambdas, and learning about Kotlin-Java interoperability

9ab0b3b080e75e0c03a0c643333f8b93?s=128

Segun Famisa

July 20, 2018
Tweet

Transcript

  1. 3.
  2. 8.

    Functional == actions are composed into functions and usually reads

    as “what is done” Imperative == structured in steps of execution and usually reads as “how it’s done” Imperative == structured in steps of execution and usually reads as “how it’s done”
  3. 9.

    Functional == actions are composed into functions and usually reads

    as “what is done” Imperative == structured in steps of execution and usually reads as “how it’s done” Functional == actions are composed into functions and usually reads as “what is done” Imperative == structured in steps of execution and usually reads as “how it’s done”
  4. 10.

    // Java - print even numbers for (int i =

    start; i < end; i++) { if (i % 2 == 0) { System.out.println(i); } }
  5. 11.

    // Java - print even numbers for (int i =

    start; i < end; i++) { if (i % 2 == 0) { System.out.println(i); } } // Kotlin - print even numbers (start until end) .filter { it % 2 == 0 } .map { println(it) }
  6. 14.

    Imperative Functional Immutability isn’t encouraged at language level Encourages immutability

    Instances of classes, structures, objects are first class citizens Functions are first class citizens
  7. 15.

    Imperative Functional Immutability isn’t encouraged at language level Encourages immutability

    Instances of classes, structures, objects are first class citizens Functions are first class citizens Loops, method calls, conditionals Function calls
  8. 16.

    Imperative Functional Immutability isn’t encouraged at language level Encourages immutability

    Instances of classes, structures, objects are first class citizens Functions are first class citizens Loops, method calls, conditionals Function calls Side-effects allowed in functions Pure functions
  9. 19.

    Functions are first class citizens They can: • be stored

    in a variable - just like other types
  10. 20.

    Functions are first class citizens They can: • be stored

    in a variable - just like other types • take another function as parameter
  11. 21.

    Functions are first class citizens They can: • be stored

    in a variable - just like other types • take another function as parameter • return a function
  12. 22.

    Functions are first class citizens They can: • be stored

    in a variable - just like other types • take another function as parameter • return a function a.k.a higher order functions
  13. 25.

    Lambdas & higher order functions fun atLeastAndroidP(action: () -> Unit)

    { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } }
  14. 26.

    Lambdas & higher order functions // Usage: atLeastAndroidP { //

    do something on Android P } fun atLeastAndroidP(action: () -> Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } }
  15. 27.

    Lambdas & higher order functions fun atLeastAndroidP(action: () -> Unit)

    { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } } Lambda // Usage: atLeastAndroidP { // do something on Android P }
  16. 28.

    Lambdas & higher order functions fun atLeastAndroidP(action: () -> Unit)

    { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } } Higher order function // Usage: atLeastAndroidP { // do something on Android P }
  17. 29.

    Lambdas & higher order functions // Usage: atLeastAndroidP { //

    do something on Android P } fun atLeastAndroidP(action: () -> Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } }
  18. 30.

    Lambdas & higher order functions // Usage: atLeastAndroidP { //

    do something on Android P } fun atLeastAndroidP(action: () -> Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } } Tip: Take a look at the generated code to understand what’s happening under the hood
  19. 31.

    Lambdas & higher order functions // Generated Java code public

    final void atLeastAndroidP(@NotNull Function0 action) { if (VERSION.SDK_INT > 28) { action.invoke(); } } fun atLeastAndroidP(action: () -> Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } }
  20. 32.

    Lambdas & higher order functions // Generated Java code public

    final void atLeastAndroidP(@NotNull Function0 action) { if (VERSION.SDK_INT > 28) { action.invoke(); } } Every lambda is an object! Not great for performance fun atLeastAndroidP(action: () -> Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } }
  21. 33.

    () -> Unit Function0<Unit> (Int) -> Boolean Function1<Int,Boolean> (Int, String)

    -> Boolean Function2<Int, String, Boolean> Every lambda corresponds to a FunctionN interface - where N is the number of params in the lambda
  22. 34.

    Lambdas & higher order functions // FunctionN interfaces used by

    Kotlin interface Function0<out R> : Function<R> interface Function1<in P1, out R> : Function<R> interface Function2<in P1, in P2, out R> : Function<R> ... interface Function22<in P1, in P2...in P22, out R> : Function<R> { /** Invokes the function with the specified arguments. */ public operator fun invoke(p1: P1, p2: P2...p22: P22): R }
  23. 35.
  24. 36.

    Improving lambdas’ performance - the inline keyword // Kotlin inline

    fun atLeastAndroidP(action: () -> Unit) {...}
  25. 37.

    Improving lambdas’ performance - the inline keyword // Kotlin inline

    fun atLeastAndroidP(action: () -> Unit) {...} // Usage: fun doSomethingOnP() { atLeastAndroidP { println("I'm on Android P") } }
  26. 38.

    Improving lambdas’ performance - the inline keyword // Kotlin inline

    fun atLeastAndroidP(action: () -> Unit) {...} // Usage: fun doSomethingOnP() { atLeastAndroidP { println("I'm on Android P") } } // Generated Java code public final void doSomethingOnP() { if (VERSION.SDK_INT > 28) { String var0 = "I'm on Android P"; System.out.println(var0); } }
  27. 39.

    Improving lambdas’ performance - the inline keyword // Kotlin inline

    fun atLeastAndroidP(action: () -> Unit) {...} // Usage: fun doSomethingOnP() { atLeastAndroidP { println("I'm on Android P") } } // Generated Java code public final void doSomethingOnP() { if (VERSION.SDK_INT > 28) { String var0 = "I'm on Android P"; System.out.println(var0); } } Method body is copied to the call site
  28. 41.

    noinline keyword - used to mark lambdas in an inline

    function that we don’t want to inline Other good-to-know concepts when working with lambdas:
  29. 42.

    noinline keyword - used to mark lambdas in an inline

    function that we don’t want to inline Other good-to-know concepts when working with lambdas: inline fun atLeastAndroidP(action: () -> Unit, noinline fallback: () -> Unit) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { action() } else { fallback() } }
  30. 43.

    noinline keyword - used to mark other lambda params in

    an inline function that we don’t want to inline Other good-to-know concepts when working with lambdas: inline fun atLeastAndroidP(action: () -> Unit, noinline fallback: () -> Unit) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { action() } else { fallback() } } the `fallback` lambda is not inlined
  31. 44.

    non-local returns - typically non-local returns are not allowed within

    lambdas unless the function is inlined. Other good-to-know concepts when working with lambdas:
  32. 45.

    non-local returns - typically non-local returns are not allowed within

    lambdas unless the function is inlined. fun useNormalLambda() { normalLambda { ... return@normalLambda // works fine (local return) } } Other good-to-know concepts when working with lambdas:
  33. 46.

    non-local returns - typically non-local returns are not allowed within

    lambdas unless the function is inlined. fun useNormalLambda() { normalLambda { ... return@normalLambda // works fine (local return) return // compile error (non-local return) } } Other good-to-know concepts when working with lambdas:
  34. 47.

    non-local returns - typically non-local returns are not allowed within

    lambdas unless the function is inlined. Other good-to-know concepts when working with lambdas: fun useNormalLambda() { normalLambda { ... return@normalLambda // works fine (local return) return // compile error (non-local return } } fun useInlineLambda() { inlineLambda { return // works fine. Returns from useInlineLambda } }
  35. 48.

    crossinline keyword - used when the lambdas are not used

    directly in the inline function Other good-to-know concepts when working with lambdas:
  36. 49.

    crossinline keyword - used when the lambdas are not used

    directly in the inline function Other good-to-know concepts when working with lambdas: inline fun View.waitForLayout(crossinline action: () -> Unit) = with(viewTreeObserver) { addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { this@waitForLayout.viewTreeObserver.removeOnGlobalLayoutListener(this) action() } }) }
  37. 50.

    crossinline keyword - used when the lambdas are not used

    directly in the inline function Other good-to-know concepts when working with lambdas: inline fun View.waitForLayout(crossinline action: () -> Unit) = with(viewTreeObserver) { addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { this@waitForLayout.viewTreeObserver.removeOnGlobalLayoutListener(this) action() } }) } Non-local returns are not allowed within the lambda
  38. 54.

    let - Standard.kt public inline fun <T, R> T.let(block: (T)

    -> R): R = block(this) Lambda function
  39. 56.

    let - Standard.kt public inline fun <T, R> T.let(block: (T)

    -> R): R = block(this) // Sample usage client?.email ?.let { mailer.sendMessage(it, message) }
  40. 57.

    let - Standard.kt // Sample usage client?.email ?.let { mailer.sendMessage(it,

    message) } public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
  41. 58.

    let - Standard.kt // Sample usage client?.email ?.let { mailer.sendMessage(it,

    message) } // Or client?.email ?.let { email -> mailer.sendMessage(email, message) } public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
  42. 59.

    run - Standard.kt public inline fun <T, R> T.run(block: T.()

    -> R): R = block() Runs the block of code and returns the last line in the lambda
  43. 61.

    run - Standard.kt public inline fun <T, R> T.run(block: T.()

    -> R): R = block() Function literal with receiver
  44. 62.

    run - Standard.kt public inline fun <T, R> T.run(block: T.()

    -> R): R = block() // Sample usage fun performTransformation(input: String) : String {...} val transformedName = name.run { println("Transforming name...") val transformedString = performTransformation(this) transformedString }
  45. 63.

    run - Standard.kt public inline fun <T, R> T.run(block: T.()

    -> R): R = block() // Sample usage fun performTransformation(input: String) : String {...} val transformedName = name.run { println("Transforming name...") val transformedString = performTransformation(this) transformedString }
  46. 64.

    run - Standard.kt public inline fun <T, R> T.run(block: T.()

    -> R): R = block() // Sample usage fun performTransformation(input: String) : String {...} val transformedName = name.run { println("Transforming name...") val transformedString = performTransformation(this) transformedString }
  47. 65.

    run - Standard.kt public inline fun <T, R> T.run(block: T.()

    -> R): R = block() // Sample usage fun performTransformation(input: String) : String {...} val transformedName = name.run { println("Transforming name...") val transformedString = performTransformation(this) transformedString } Receiver is accessed as “this”, and exposes the inner members of the receiver class
  48. 66.

    run - Standard.kt public inline fun <T, R> T.run(block: T.()

    -> R): R = block() // Sample usage fun performTransformation(input: String) : String {...} val transformedName = name.run { println("Transforming name...") val transformedString = performTransformation(this) transformedString } transformedString is returned from the lambda
  49. 68.

    also - Standard.kt inline fun <T> T.also(block: (T) -> Unit):

    T { block(this); return this } // Sample usage val dev = Developer().also { it.name = "Segun" it.lovesCatVideos = true it.stack = "Android" }
  50. 69.

    also - Standard.kt inline fun <T> T.also(block: (T) -> Unit):

    T { block(this); return this } The receiver is accessed as “it” // Sample usage val dev = Developer().also { it.name = "Segun" it.lovesCatVideos = true it.stack = "Android" }
  51. 70.

    also - Standard.kt inline fun <T> T.also(block: (T) -> Unit):

    T { block(this); return this } // Sample usage val dev = Developer().also { it.name = "Segun" it.lovesCatVideos = true it.stack = "Android" } // OR val dev = Developer().also { developer -> developer.name = "Segun" developer.lovesCatVideos = true developer.stack = "Android" }
  52. 72.

    apply - Standard.kt inline fun <T> T.apply(block: T.() -> Unit):

    T { block(); return this } // Sample usage val dev = Developer().apply { this.name = "Segun" this.lovesCatVideos = true this.stack = "Android" }
  53. 73.

    apply - Standard.kt inline fun <T> T.apply(block: T.() -> Unit):

    T { block(); return this } // Sample usage val dev = Developer().apply { this.name = "Segun" this.lovesCatVideos = true this.stack = "Android" } Exposes the receiver as “this”
  54. 76.

    Things to consider • Avoid overusing these scoping functions -

    readability can degrade quickly • Consider renaming it to something better when possible
  55. 77.

    Things to consider • Avoid overusing these scoping functions -

    readability can degrade quickly • Consider renaming it to something better when possible • More stdlib functions ◦ takeIf, takeUnless, with, etc
  56. 79.

    // Java code public void sendMessageToClient( @Nullable Client client, @Nullable

    String message, @NotNull Mailer mailer ) { if (client == null || message == null) return; PersonalInfo personalInfo = client.getPersonalInfo(); if (personalInfo == null) return; String email = personalInfo.getEmail(); if (email == null) return; mailer.sendMessage(email, message); } stdlib functions in action
  57. 80.

    // Java code public void sendMessageToClient( @Nullable Client client, @Nullable

    String message, @NotNull Mailer mailer ) { if (client == null || message == null) return; PersonalInfo personalInfo = client.getPersonalInfo(); if (personalInfo == null) return; String email = personalInfo.getEmail(); if (email == null) return; mailer.sendMessage(email, message); } // Kotlin equivalent fun sendMessageToClient( client: Client?, message: String?, mailer: Mailer ) { client?.takeIf { message != null } ?.personalInfo ?.email ?.let { email -> mailer.sendMessage(email, message!!) } } stdlib functions in action
  58. 81.

    // Java code public void sendMessageToClient( @Nullable Client client, @Nullable

    String message, @NotNull Mailer mailer ) { if (client == null || message == null) return; PersonalInfo personalInfo = client.getPersonalInfo(); if (personalInfo == null) return; String email = personalInfo.getEmail(); if (email == null) return; mailer.sendMessage(email, message); } // Kotlin equivalent fun sendMessageToClient( client: Client?, message: String?, mailer: Mailer ) { client?.takeIf { message != null } ?.personalInfo ?.email ?.let { email -> mailer.sendMessage(email, message!!) } } stdlib functions in action
  59. 82.

    // Java code public void sendMessageToClient( @Nullable Client client, @Nullable

    String message, @NotNull Mailer mailer ) { if (client == null || message == null) return; PersonalInfo personalInfo = client.getPersonalInfo(); if (personalInfo == null) return; String email = personalInfo.getEmail(); if (email == null) return; mailer.sendMessage(email, message); } // Kotlin equivalent fun sendMessageToClient( client: Client?, message: String?, mailer: Mailer ) { client?.takeIf { message != null } ?.personalInfo ?.email ?.let { email -> mailer.sendMessage(email, message!!) } } stdlib functions in action
  60. 83.

    // Java code public void sendMessageToClient( @Nullable Client client, @Nullable

    String message, @NotNull Mailer mailer ) { if (client == null || message == null) return; PersonalInfo personalInfo = client.getPersonalInfo(); if (personalInfo == null) return; String email = personalInfo.getEmail(); if (email == null) return; mailer.sendMessage(email, message); } // Kotlin equivalent fun sendMessageToClient( client: Client?, message: String?, mailer: Mailer ) { client?.takeIf { message != null } ?.personalInfo ?.email ?.let { email -> mailer.sendMessage(email, message!!) } } stdlib functions in action Functional, readable, concise
  61. 86.

    Java & Kotlin Interoperability Important because: • Android APIs are

    written in Java • Kotlin & Java may be mixed in the same codebase
  62. 88.
  63. 89.

    String! String? String Non-nullable type Nullable type Platform type Kotlin

    compiler has no idea whether it’s nullable or not
  64. 92.

    // Calling Java from Kotlin // Compiles fine. Safe in

    runtime val nameLength = myObject.name.length // Java @NotNull public String getName() { return "Segun"; } ...
  65. 93.

    // Calling Java from Kotlin // Compiler error. Need safe

    access val nameLength = myObject.name.length // Java @Nullable public String getName() { return null; } ...
  66. 94.

    // Calling Java from Kotlin // Compiler error. Need safe

    access val nameLength = myObject.name.length // Compiles fine. Safe in runtime val nameLength = myObject.name?.length // Java @Nullable public String getName() { return null; } ...
  67. 95.

    // Calling Java from Kotlin // Compiles fine. NPE in

    runtime val nameLength = myObject.name.length // Java public String getName() { return null; } ...
  68. 96.

    // Java public String getName() { return null; } ...

    // Calling Java from Kotlin // Compiles fine. NPE in runtime val nameLength = myObject.name.length // Compiles fine. Safe in runtime val nameLength = myObject.name?.length
  69. 97.

    @Nullable String getName() {..} Kotlin compiler treats as nullable “?”

    @NotNull String getName() {..} Kotlin compiler treats as non-nullable String getName() {..} Kotlin compiler has no clue. Treats as platform type
  70. 100.

    Kotlin package-level functions Functions can be defined in a file

    directly without being in any class or object. // InteropDemo.kt fun getName() = "Segun"
  71. 101.

    Kotlin package-level functions Functions can be defined in a file

    directly without being in any class or object. // InteropDemo.kt fun getName() = "Segun" // Calling from Java InteropDemoKt.getName();
  72. 102.

    Kotlin package-level functions @JvmName annotation helps to tell Kotlin compiler

    what the generated Java class name should be when consumed from Java
  73. 103.

    Kotlin package-level functions // InteropDemo.kt @file:JvmName("InteropDemo") fun getName() = "Segun"

    @JvmName annotation helps to tell Kotlin compiler what the generated Java class name should be when consumed from Java
  74. 104.

    Kotlin package-level functions // InteropDemo.kt @file:JvmName("InteropDemo") fun getName() = "Segun"

    // Calling from Java InteropDemo.getName(); @JvmName annotation helps to tell Kotlin compiler what the generated Java class name should be when consumed from Java
  75. 107.

    @JvmName // Kotlin code @JvmName("getInteropName") fun getName() = "Segun" //

    Consumed from Java demo.getInteropName(); The annotation can also be applied to functions
  76. 108.

    @JvmName The annotation can also be applied to functions //

    Kotlin code @JvmName("getInteropName") fun getName() = "Segun" // Consumed from Java demo.getInteropName(); // Consumed from Kotlin demo.getName()
  77. 111.

    @JvmStatic // Kotlin Object object StringUtils { fun upperCase(text: String)

    = text.toUpperCase() } // Calling from Java String upperCaseName = StringUtils.INSTANCE.upperCase("Segun");
  78. 112.

    @JvmStatic // Kotlin Object object StringUtils { + @JvmStatic fun

    upperCase(text: String) = text.toUpperCase() } // Calling from Java String upperCaseName = StringUtils.INSTANCE.upperCase("Segun");
  79. 113.

    @JvmStatic // Kotlin Object object StringUtils { + @JvmStatic fun

    upperCase(text: String) = text.toUpperCase() } // Calling from Java String upperCaseName = StringUtils.INSTANCE.upperCase("Segun"); String upperCaseName = StringUtils.upperCase("Segun");
  80. 114.
  81. 115.

    @Module abstract class RepositoryModule { @Binds abstract fun bindRepo(repository: Repository):

    IRepository companion object { // static provider @Provides fun provideApiService: ApiService = ... } } @JvmStatic - Static providers in Dagger
  82. 116.

    @Module abstract class RepositoryModule { @Binds abstract fun bindRepo(repository: Repository):

    IRepository companion object { // static provider @Provides fun provideApiService: ApiService = ... } } Won’t compile. Dagger error. Why? @JvmStatic - Static providers in Dagger
  83. 117.

    @JvmStatic - Static providers in Dagger // Generated Java class

    @Module public abstract class RepositoryModule { ... @Binds @NotNull public abstract IRepository bindRepo(@NotNull Repository var1); public static final class Companion { @Provides @NotNull public final ApiService providesApiService() {...} } }
  84. 118.

    @JvmStatic - Static providers in Dagger // Generated Java class

    @Module public abstract class RepositoryModule { ... @Binds @NotNull public abstract IRepository bindRepo(@NotNull Repository var1); public static final class Companion { @Provides @NotNull public final ApiService providesApiService() {...} } } No @Module
  85. 119.

    @JvmStatic - Static providers in Dagger // Generated Java class

    @Module public abstract class RepositoryModule { ... @Binds @NotNull public abstract IRepository bindRepo(@NotNull Repository var1); public static final class Companion { @Provides @NotNull public final ApiService providesApiService() {...} } } No @Module @Provides method is generated as a member of the Companion class
  86. 120.

    @JvmStatic - Static providers in Dagger // Generated Java class

    @Module public abstract class RepositoryModule { ... @Binds @NotNull public abstract IRepository bindRepo(@NotNull Repository var1); public static final class Companion { @Provides @NotNull public final ApiService providesApiService() {...} } } No @Module @Provides method is generated as a member of the Companion class Dagger is unable to provide this dependency
  87. 121.

    @JvmStatic - Static providers in Dagger @Module abstract class RepositoryModule

    { @Binds abstract fun bindRepo(repository: Repository): IRepository + @Module companion object { // static provider @Provides + @JvmStatic fun provideApiService: ApiService = ... } } Fixed by adding @Module and @JvmStatic annotations
  88. 122.

    @JvmStatic - Static providers in Dagger Fixed by adding @Module

    and @JvmStatic annotations // Generated Java class with fixes @Module public abstract class RepositoryModule { @Binds public abstract IRepository bindRepos(@NotNull Repository var1); @Provides @JvmStatic public static final ApiService providesApiService() {...} @Module public static final class Companion { @Provides @JvmStatic public final ApiService providesApiService() {...} ... } } Kotlin compiler generates a static method within the RepositoryModule class
  89. 123.

    @JvmStatic - Static providers in Dagger Fixed by adding @Module

    and @JvmStatic annotations // Generated Java class with fixes @Module public abstract class RepositoryModule { @Binds public abstract IRepository bindRepos(@NotNull Repository var1); @Provides @JvmStatic public static final ApiService providesApiService() {...} @Module public static final class Companion { @Provides @JvmStatic public final ApiService providesApiService() {...} ... } } Dagger will see this module, but generated code will not be used. RepositoryModule.Companion module isn’t registered
  90. 126.

    @JvmOverloads // Kotlin class with default values class Point(val x:

    Int = 0, val y: Int = 0) // Calling from Java Point p1 = new Point(); Point p2 = new Point(1, 3);
  91. 127.

    @JvmOverloads // Kotlin class with default values class Point(val x:

    Int = 0, val y: Int = 0) // Calling from Java Point p1 = new Point(); Point p2 = new Point(1, 3); Specify none or all params
  92. 128.
  93. 129.

    @JvmOverloads // Kotlin class with default values class Point @JvmOverloads

    constructor(val x: Int = 0, val y: Int = 0) // Generated Java code public final class Point { // Overloaded constructors public Point() {...} public Point(int x) {...} public Point(int x, int y) {...} }
  94. 130.

    @JvmOverloads // Calling from Java Point p1 = new Point();

    Point p2 = new Point(1); Point p3 = new Point(1, 3); // Kotlin class with default values class Point @JvmOverloads constructor(val x: Int = 0, val y: Int = 0) // Generated Java code public final class Point { // Overloaded constructors public Point() {...} public Point(int x) {...} public Point(int x, int y) {...} }