Share
Embed
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
Segun Famisa
July 20, 2018
Transcript
134 slides
-
Imperative vs Functional Programming Functions in Kotlin - lambdas, stdlib
functions, etc Java & Kotlin Interop -
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” -
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” -
// Java - print even numbers for (int i =
start; i < end; i++) { if (i % 2 == 0) { System.out.println(i); } } -
// 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) } -
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 -
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 -
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 -
Functions are first class citizens They can: • be stored
in a variable - just like other types -
Functions are first class citizens They can: • be stored
in a variable - just like other types • take another function as parameter -
Functions are first class citizens They can: • be stored
in a variable - just like other types • take another function as parameter • return a function -
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 -
Lambdas & higher order functions fun atLeastAndroidP(action: () -> Unit)
{ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { action() } } -
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() } } -
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 } -
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 } -
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() } } -
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 -
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() } } -
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() } } -
() -> 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 -
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 } -
Improving lambdas’ performance - the inline keyword // Kotlin fun
atLeastAndroidP(action: () -> Unit) {...} -
Improving lambdas’ performance - the inline keyword // Kotlin inline
fun atLeastAndroidP(action: () -> Unit) {...} -
Improving lambdas’ performance - the inline keyword // Kotlin inline
fun atLeastAndroidP(action: () -> Unit) {...} // Usage: fun doSomethingOnP() { atLeastAndroidP { println("I'm on Android P") } } -
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); } } -
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 -
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: -
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() } } -
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 -
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: -
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: -
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: -
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 } } -
crossinline keyword - used when the lambdas are not used
directly in the inline function Other good-to-know concepts when working with lambdas: -
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() { [email protected](this) action() } }) } -
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() { [email protected](this) action() } }) } Non-local returns are not allowed within the lambda -
Standard library functions Some of these functions are also referred
to as Scoping functions -
let - Standard.kt public inline fun <T, R> T.let(block: (T)
-> R): R = block(this) -
let - Standard.kt public inline fun <T, R> T.let(block: (T)
-> R): R = block(this) Lambda function -
let - Standard.kt public inline fun <T, R> T.let(block: (T)
-> R): R = block(this) Receiver -
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) } -
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) -
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) -
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 -
run - Standard.kt public inline fun <T, R> T.run(block: T.()
-> R): R = block() Receiver -
run - Standard.kt public inline fun <T, R> T.run(block: T.()
-> R): R = block() Function literal with receiver -
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 } -
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 } -
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 } -
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 -
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 -
also - Standard.kt inline fun <T> T.also(block: (T) -> Unit):
T { block(this); return this } -
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" } -
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" } -
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" } -
apply - Standard.kt inline fun <T> T.apply(block: T.() -> Unit):
T { block(); return this } -
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" } -
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” -
Things to consider • Avoid overusing these scoping functions -
readability can degrade quickly -
Things to consider • Avoid overusing these scoping functions -
readability can degrade quickly • Consider renaming it to something better when possible -
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 -
// 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 -
// 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 -
// 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 -
// 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 -
// 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 -
Java & Kotlin Interoperability Important because: • Android APIs are
written in Java • Kotlin & Java may be mixed in the same codebase -
String! String? What do these mean when you think about
nullability & types? String Pop -
String! String? String Non-nullable type Nullable type Platform type Kotlin
compiler has no idea whether it’s nullable or not -
// Calling Java from Kotlin // Compiles fine. Safe in
runtime val nameLength = myObject.name.length // Java @NotNull public String getName() { return "Segun"; } ... -
// Calling Java from Kotlin // Compiler error. Need safe
access val nameLength = myObject.name.length // Java @Nullable public String getName() { return null; } ... -
// 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; } ... -
// Calling Java from Kotlin // Compiles fine. NPE in
runtime val nameLength = myObject.name.length // Java public String getName() { return null; } ... -
// 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 -
@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 -
Kotlin package-level functions Functions can be defined in a file
directly without being in any class or object. // InteropDemo.kt fun getName() = "Segun" -
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(); -
Kotlin package-level functions @JvmName annotation helps to tell Kotlin compiler
what the generated Java class name should be when consumed from Java -
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 -
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 -
@JvmName // Kotlin code @JvmName("getInteropName") fun getName() = "Segun" The
annotation can also be applied to functions -
@JvmName // Kotlin code @JvmName("getInteropName") fun getName() = "Segun" //
Consumed from Java demo.getInteropName(); The annotation can also be applied to functions -
@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() -
@JvmStatic Kotlin does not have static methods. Instead, it has
objects, companion objects -
@JvmStatic // Kotlin Object object StringUtils { fun upperCase(text: String)
= text.toUpperCase() } -
@JvmStatic // Kotlin Object object StringUtils { fun upperCase(text: String)
= text.toUpperCase() } // Calling from Java String upperCaseName = StringUtils.INSTANCE.upperCase("Segun"); -
@JvmStatic // Kotlin Object object StringUtils { + @JvmStatic fun
upperCase(text: String) = text.toUpperCase() } // Calling from Java String upperCaseName = StringUtils.INSTANCE.upperCase("Segun"); -
@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"); -
@JvmStatic - Static providers in Dagger Specially useful when working
with Dagger static Providers on Android -
@Module abstract class RepositoryModule { @Binds abstract fun bindRepo(repository: Repository):
IRepository companion object { // static provider @Provides fun provideApiService: ApiService = ... } } @JvmStatic - Static providers in Dagger -
@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 -
@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() {...} } } -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@JvmOverloads // Kotlin class with default values class Point(val x:
Int = 0, val y: Int = 0) -
@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); -
@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 -
@JvmOverloads // Kotlin class with default values class Point @JvmOverloads
constructor(val x: Int = 0, val y: Int = 0) -
@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) {...} } -
@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) {...} }