Slide 1

Slide 1 text

Márton Braun zsmb.co zsmb13 Mastering API Visibility in Kotlin

Slide 2

Slide 2 text

API surface

Slide 3

Slide 3 text

API surface

Slide 4

Slide 4 text

API surface

Slide 5

Slide 5 text

API surface

Slide 6

Slide 6 text

API surface

Slide 7

Slide 7 text

API surface

Slide 8

Slide 8 text

API surface

Slide 9

Slide 9 text

API surface

Slide 10

Slide 10 text

API surface

Slide 11

Slide 11 text

API surface

Slide 12

Slide 12 text

API surface

Slide 13

Slide 13 text

API surface

Slide 14

Slide 14 text

API surface

Slide 15

Slide 15 text

API surface

Slide 16

Slide 16 text

API surface

Slide 17

Slide 17 text

API surface

Slide 18

Slide 18 text

API surface

Slide 19

Slide 19 text

Minimal APIs

Slide 20

Slide 20 text

Minimal APIs Item 15: Minimize the accessibility of classes and members

Slide 21

Slide 21 text

Minimal APIs Item 15: Minimize the accessibility of classes and members Chapter 4: Classes and Interfaces

Slide 22

Slide 22 text

Minimal APIs Item 15: Minimize the accessibility of classes and members Chapter 4: Classes and Interfaces

Slide 23

Slide 23 text

Minimal APIs › Easier to maintain and change

Slide 24

Slide 24 text

Minimal APIs › Easier to maintain and change

Slide 25

Slide 25 text

Minimal APIs › Easier to maintain and change

Slide 26

Slide 26 text

Minimal APIs › Easier to maintain and change

Slide 27

Slide 27 text

Minimal APIs › Easier to maintain and change

Slide 28

Slide 28 text

Minimal APIs › Easier to maintain and change

Slide 29

Slide 29 text

Minimal APIs › Easier to maintain and change

Slide 30

Slide 30 text

Minimal APIs › Easier to maintain and change public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 31

Slide 31 text

replaceWith = ReplaceWith( "this.stringPref(key).validate(isValid)", ), Minimal APIs › Easier to maintain and change @Deprecated( message = "Use a chained .validate {} call instead", level = DeprecationLevel.WARNING, ) public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 32

Slide 32 text

replaceWith = ReplaceWith( "this.stringPref(key).validate(isValid)", ), Minimal APIs › Easier to maintain and change @Deprecated( message = "Use a chained .validate {} call instead", level = DeprecationLevel.WARNING, ) public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 33

Slide 33 text

replaceWith = ReplaceWith( "this.stringPref(key).validate(isValid)", ), Minimal APIs › Easier to maintain and change @Deprecated( message = "Use a chained .validate {} call instead", level = DeprecationLevel.WARNING, ) public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 34

Slide 34 text

Minimal APIs › Easier to maintain and change @Deprecated( message = "Use a chained .validate {} call instead", level = DeprecationLevel.WARNING, replaceWith = ReplaceWith( "this.stringPref(key).validate(isValid)", imports = arrayOf("com.example.validate"), ), ) public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 35

Slide 35 text

Minimal APIs › Easier to maintain and change @Deprecated( message = "Use a chained .validate {} call instead", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith( "this.stringPref(key).validate(isValid)", imports = arrayOf("com.example.validate"), ), ) public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 36

Slide 36 text

Minimal APIs › Easier to maintain and change @Deprecated( message = "Use a chained .validate {} call instead", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith( "this.stringPref(key).validate(isValid)", imports = arrayOf("com.example.validate"), ), ) public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 37

Slide 37 text

Minimal APIs › Easier to maintain and change @Deprecated( message = "Use a chained .validate {} call instead", level = DeprecationLevel.HIDDEN, replaceWith = ReplaceWith( "this.stringPref(key).validate(isValid)", imports = arrayOf("com.example.validate"), ), ) public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 38

Slide 38 text

Minimal APIs › Easier to maintain and change @Deprecated( message = "Use a chained .validate {} call instead", level = DeprecationLevel.HIDDEN, replaceWith = ReplaceWith( "this.stringPref(key).validate(isValid)", imports = arrayOf("com.example.validate"), ), ) public fun Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )

Slide 39

Slide 39 text

Minimal APIs › Easier to maintain and change

Slide 40

Slide 40 text

Minimal APIs › Easier to maintain and change › Easier to learn

Slide 41

Slide 41 text

Minimal APIs › Easier to maintain and change › Easier to learn

Slide 42

Slide 42 text

Minimal APIs › Easier to maintain and change › Easier to learn

Slide 43

Slide 43 text

Minimal APIs › Easier to maintain and change › Easier to learn › Harder to misuse

Slide 44

Slide 44 text

Planning

Slide 45

Slide 45 text

Planning

Slide 46

Slide 46 text

Planning

Slide 47

Slide 47 text

Planning

Slide 48

Slide 48 text

Visibility in Kotlin public

Slide 49

Slide 49 text

Visibility in Kotlin public private

Slide 50

Slide 50 text

Visibility in Kotlin public private internal

Slide 51

Slide 51 text

Visibility in Kotlin public private internal

Slide 52

Slide 52 text

Internal visibility public interface Service { fun createUser(): User } internal class NetworkService : Service { override fun createUser(): User { ... } }

Slide 53

Slide 53 text

Internal visibility public class NetworkClient { public var state: State = Disconnected internal set public fun connect() { state = Connected } }

Slide 54

Slide 54 text

Internal visibility internal fun String.asMention(): String = "@$this"

Slide 55

Slide 55 text

Internal visibility

Slide 56

Slide 56 text

Testing

Slide 57

Slide 57 text

Testing private var state: State

Slide 58

Slide 58 text

@Test fun verifyState() { assertEquals(expectedState, state) } Testing private var state: State

Slide 59

Slide 59 text

@Test fun verifyState() { assertEquals(expectedState, state) } Testing private var state: State

Slide 60

Slide 60 text

@Test fun verifyState() { assertEquals(expectedState, state) } Testing internal var state: State

Slide 61

Slide 61 text

@Test fun verifyState() { assertEquals(expectedState, state) } Testing @VisibleForTesting(otherwise = PRIVATE) internal var state: State

Slide 62

Slide 62 text

Java interop

Slide 63

Slide 63 text

Java interop class Repository { internal fun createEntity() { ... } }

Slide 64

Slide 64 text

Java interop void repositoryExample() { } class Repository { internal fun createEntity() { ... } }

Slide 65

Slide 65 text

Java interop void repositoryExample() { new Repository().createEntity$examplelibrary(); } class Repository { internal fun createEntity() { ... } }

Slide 66

Slide 66 text

Java interop void repositoryExample() { new Repository().createEntity$examplelibrary(); } class Repository { internal fun createEntity() { ... } }

Slide 67

Slide 67 text

class Repository { internal fun createEntity() { ... } } Java interop @JvmName("pleaseDoNotCallThisMethod") (); void repositoryExample() { new Repository(). } pleaseDoNotCallThisMethod

Slide 68

Slide 68 text

Java interop void repositoryExample() { new Repository(). } class Repository { internal fun createEntity() { ... } } Cannot resolve method 'createEntity' in 'Repository' @JvmSynthetic (); createEntity

Slide 69

Slide 69 text

Explicit API mode

Slide 70

Slide 70 text

Explicit API mode › Explicit visibility modifiers for all declarations

Slide 71

Slide 71 text

Explicit API mode › Explicit visibility modifiers for all declarations › Explicit types for public declarations

Slide 72

Slide 72 text

Explicit API mode kotlin { explicitApi() }

Slide 73

Slide 73 text

Explicit API mode kotlin { explicitApi() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xexplicit-api=strict', ] } }

Slide 74

Slide 74 text

Explicit API mode kotlin { explicitApi() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xexplicit-api=strict', '-progressive', ] } }

Slide 75

Slide 75 text

Explicit API mode kotlin { explicitApiWarning() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xexplicit-api=warning', '-progressive', ] } }

Slide 76

Slide 76 text

Explicit API mode class User(var name: String, var age: Int) { fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } }

Slide 77

Slide 77 text

Explicit API mode class User(var name: String, var age: Int) { fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } }

Slide 78

Slide 78 text

public Explicit API mode public class User(var name: String, var age: Int) { fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } }

Slide 79

Slide 79 text

Explicit API mode public class User(public var name: String, var age: Int) { fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } } public

Slide 80

Slide 80 text

Explicit API mode public class User(public var name: String, public var age: Int) { public fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } }

Slide 81

Slide 81 text

Explicit API mode public class User(public var name: String, public var age: Int) { public fun sayHi() { println("Hi, I'm $name") } internal fun reset() { name = "" age = 0 } }

Slide 82

Slide 82 text

Explicit API mode private val DEFAULT_CLIENT = Client() public interface ClientFactory { public fun client() = DEFAULT_CLIENT }

Slide 83

Slide 83 text

Explicit API mode private val DEFAULT_CLIENT = Client() public interface ClientFactory { public fun client() = DEFAULT_CLIENT }

Slide 84

Slide 84 text

Explicit API mode private val DEFAULT_CLIENT = OfflineClient() public interface ClientFactory { public fun client() = DEFAULT_CLIENT }

Slide 85

Slide 85 text

Explicit API mode private val DEFAULT_CLIENT = OfflineClient() public interface ClientFactory { public fun client(): Client = DEFAULT_CLIENT }

Slide 86

Slide 86 text

Published API

Slide 87

Slide 87 text

Published API public inline fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }

Slide 88

Slide 88 text

Published API public inline fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") } fun clientCode() { song() }

Slide 89

Slide 89 text

fun clientCode() { } secretFunction() Published API public inline fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") } e: Public-API inline function cannot access non-public-API song()

Slide 90

Slide 90 text

Published API public inline fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") } fun clientCode() { } secretFunction()

Slide 91

Slide 91 text

Published API fun clientCode() { } secretFunction() public inline fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }

Slide 92

Slide 92 text

Published API fun clientCode() { } song() @PublishedApi public inline fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }

Slide 93

Slide 93 text

Published API fun clientCode() { song() secretFunction() } public inline fun song() { secretFunction() } @PublishedApi internal fun secretFunction() { println("through the mountains") } e: Cannot access 'secretFunction': it is internal

Slide 94

Slide 94 text

Published API fun clientCode() { } song() @PublishedApi public inline fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }

Slide 95

Slide 95 text

No content

Slide 96

Slide 96 text

Opt-in APIs core

Slide 97

Slide 97 text

Opt-in APIs core addon

Slide 98

Slide 98 text

Opt-in APIs core addon

Slide 99

Slide 99 text

core addon Opt-in APIs

Slide 100

Slide 100 text

core addon Opt-in APIs

Slide 101

Slide 101 text

package com.example.lib.core public annotation class InternalMyLibraryApi core addon Opt-in APIs

Slide 102

Slide 102 text

Opt-in APIs package com.example.lib.core public annotation class InternalMyLibraryApi

Slide 103

Slide 103 text

Opt-in APIs package com.example.lib.core @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "This is internal API for my library, " + "please don't rely on it." ) public annotation class InternalMyLibraryApi

Slide 104

Slide 104 text

Opt-in APIs package com.example.lib.core @RequiresOptIn( level = RequiresOptIn.Level.WARNING, message = "This is internal API for my library, " + "please don't rely on it." ) public annotation class InternalMyLibraryApi

Slide 105

Slide 105 text

Opt-in APIs package com.example.lib.core @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "This is internal API for my library, " + "please don't rely on it." ) public annotation class InternalMyLibraryApi

Slide 106

Slide 106 text

Opt-in APIs package com.example.lib.core @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "This is internal API for my library, " + "please don't rely on it." ) public annotation class InternalMyLibraryApi tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xopt-in=kotlin.RequiresOptIn', ] } }

Slide 107

Slide 107 text

Opt-in APIs package com.example.lib.core @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "This is internal API for my library, " + "please don't rely on it." ) public annotation class InternalMyLibraryApi @InternalMyLibraryApi public fun coreApi()

Slide 108

Slide 108 text

Opt-in APIs package com.example.lib.core @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "This is internal API for my library, " + "please don't rely on it." ) public annotation class InternalMyLibraryApi @InternalMyLibraryApi public fun coreApi()

Slide 109

Slide 109 text

package com.example.lib.core @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "This is internal API for my library, " + "please don't rely on it." ) public annotation class InternalMyLibraryApi @InternalMyLibraryApi public fun coreApi() core addon Opt-in APIs

Slide 110

Slide 110 text

public fun addonFunction() { coreApi() } package com.example.lib.core @RequiresOptIn( level = RequiresOptIn.Level.ERROR, message = "This is internal API for my library, " + "please don't rely on it." ) public annotation class InternalMyLibraryApi @InternalMyLibraryApi public fun coreApi() core addon Opt-in APIs

Slide 111

Slide 111 text

core addon @InternalMyLibraryApi public fun coreApi() Opting in public fun addonFunction() { coreApi() }

Slide 112

Slide 112 text

Opting in @InternalMyLibraryApi public fun coreApi() @InternalMyLibraryApi public fun addonFunction() { coreApi() }

Slide 113

Slide 113 text

Opting in tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xopt-in=com.example.lib.core.InternalMyLibraryApi', ] } } @InternalMyLibraryApi public fun coreApi() public fun addonFunction() { coreApi() } @InternalMyLibraryApi

Slide 114

Slide 114 text

Opting in @InternalMyLibraryApi public fun coreApi() public fun addonFunction() { coreApi() }

Slide 115

Slide 115 text

Opting in @InternalMyLibraryApi public fun coreApi() public fun addonFunction() { @OptIn(InternalMyLibraryApi::class) coreApi() }

Slide 116

Slide 116 text

Opting in @InternalMyLibraryApi public fun coreApi() public fun addonFunction() { @OptIn(InternalMyLibraryApi::class) coreApi() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xopt-in=kotlin.RequiresOptIn', ] } }

Slide 117

Slide 117 text

Opting in @InternalMyLibraryApi public fun coreApi() @OptIn(InternalMyLibraryApi::class) public fun addonFunction() { coreApi() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xopt-in=kotlin.RequiresOptIn', ] } }

Slide 118

Slide 118 text

Opting in @InternalMyLibraryApi @JvmSynthetic public fun coreApi() @OptIn(InternalMyLibraryApi::class) public fun addonFunction() { coreApi() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xopt-in=kotlin.RequiresOptIn', ] } }

Slide 119

Slide 119 text

Resources • Mastering API Visibility in Kotlin  https://zsmb.co/mastering-api-visibility-in-kotlin/ • Maintaining Compatibility in Kotlin Libraries  https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/ • Effective Java, 3rd Edition  https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997 • Effective Kotlin  https://leanpub.com/effectivekotlin/

Slide 120

Slide 120 text

zsmb13 zsmb.co/talks

Slide 121

Slide 121 text

Mastering API Visibility in Kotlin zsmb.co/talks zsmb13 Márton Braun › Minimal API is nice › Internal visibility › Explicit API mode › Published API › Opt-in APIs