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

Mastering API Visibility in Kotlin (Rheinwerk Konferenz für Kotlin 2021)

4047c64e3a1e2f81addd4ba675ddc451?s=47 Marton Braun
September 14, 2021

Mastering API Visibility in Kotlin (Rheinwerk Konferenz für Kotlin 2021)

When designing a library, minimizing your API surface - the types, methods, properties, and functions you expose to the outside world - is a great idea. This doesn't apply to just libraries: it's a consideration you should make for every module in a multi-module project. In this talk, we'll look at all the ways that Kotlin lets you get your visibility just right.

Resources and more: https://zsmb.co/talks/mastering-api-visibility/

4047c64e3a1e2f81addd4ba675ddc451?s=128

Marton Braun

September 14, 2021
Tweet

Transcript

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

  2. API surface

  3. API surface

  4. API surface

  5. API surface

  6. API surface

  7. API surface

  8. API surface

  9. API surface

  10. API surface

  11. API surface

  12. API surface

  13. API surface

  14. API surface

  15. API surface

  16. API surface

  17. API surface

  18. API surface

  19. Minimal APIs

  20. Minimal APIs Item 15: Minimize the accessibility of classes and

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

    members Chapter 4: Classes and Interfaces
  22. Minimal APIs › Easier to maintain and change

  23. Minimal APIs › Easier to maintain and change

  24. Minimal APIs › Easier to maintain and change

  25. Minimal APIs › Easier to maintain and change

  26. Minimal APIs › Easier to maintain and change

  27. Minimal APIs › Easier to maintain and change

  28. Minimal APIs › Easier to maintain and change

  29. Minimal APIs › Easier to maintain and change public fun

    Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )
  30. 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, )
  31. 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, )
  32. 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, )
  33. 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, )
  34. 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, )
  35. 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, )
  36. 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, )
  37. 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, )
  38. Minimal APIs › Easier to maintain and change

  39. Minimal APIs › Easier to maintain and change › Easier

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

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

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

    to learn › Harder to misuse
  43. Planning

  44. Planning

  45. Planning

  46. Planning

  47. Visibility in Kotlin public

  48. Visibility in Kotlin public private

  49. Visibility in Kotlin public private internal

  50. Visibility in Kotlin public private internal

  51. Internal visibility public interface Service { fun createUser(): User }

    internal class NetworkService : Service { override fun createUser(): User { ... } }
  52. Internal visibility public class NetworkClient { public var state: State

    = Disconnected internal set public fun connect() { state = Connected } }
  53. Internal visibility internal fun String.asMention(): String = "@$this"

  54. Internal visibility

  55. Testing

  56. Testing private var state: State

  57. @Test fun verifyState() { assertEquals(expectedState, state) } Testing private var

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

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

    state: State
  60. @Test fun verifyState() { assertEquals(expectedState, state) } Testing @VisibleForTesting(otherwise =

    PRIVATE) internal var state: State
  61. Java interop

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

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

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

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

    { internal fun createEntity() { ... } }
  66. class Repository { internal fun createEntity() { ... } }

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

    { internal fun createEntity() { ... } } Cannot resolve method 'createEntity' in 'Repository' @JvmSynthetic (); createEntity
  68. Explicit API mode

  69. Explicit API mode › Explicit visibility modifiers for all declarations

  70. Explicit API mode › Explicit visibility modifiers for all declarations

    › Explicit types for public declarations
  71. Explicit API mode kotlin { explicitApi() }

  72. Explicit API mode kotlin { explicitApi() } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions

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

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

    { freeCompilerArgs += [ '-Xexplicit-api=warning', '-progressive', ] } }
  75. Explicit API mode class User(var name: String, var age: Int)

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

    { fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } }
  77. 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 } }
  78. 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
  79. 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 } }
  80. 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 } }
  81. Explicit API mode private val DEFAULT_CLIENT = Client() public interface

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

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

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

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

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

    ClientFactory { public fun client(): Client = DEFAULT_CLIENT }
  87. Explicit API mode › Explicit visibility modifiers for all declarations

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

    › Explicit types for public declarations › Configured per-module
  89. Explicit API mode › Explicit visibility modifiers for all declarations

    › Explicit types for public declarations › Configured per-module › Strict or warning level
  90. Published API

  91. Published API public inline fun song() { secretFunction() } internal

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

    fun secretFunction() { println("through the mountains") } fun clientCode() { song() }
  93. 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()
  94. Published API public inline fun song() { secretFunction() } internal

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

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

    fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }
  97. 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
  98. Published API fun clientCode() { } song() @PublishedApi public inline

    fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }
  99. None
  100. Opt-in APIs core

  101. Opt-in APIs core addon

  102. Opt-in APIs core addon

  103. core addon Opt-in APIs

  104. core addon Opt-in APIs

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

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

  107. 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
  108. 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
  109. 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
  110. 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', ] } }
  111. 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', ] } }
  112. 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', ] } }
  113. 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()
  114. 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
  115. 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
  116. core addon @InternalMyLibraryApi public fun coreApi() Opting in public fun

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

    { coreApi() }
  118. 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
  119. Opting in @InternalMyLibraryApi public fun coreApi() public fun addonFunction() {

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

    @OptIn(InternalMyLibraryApi::class) coreApi() }
  121. 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', ] } }
  122. 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', ] } }
  123. 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', ] } }
  124. 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', ] } }
  125. 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', ] } }
  126. Opt-in conventions InternalCoroutinesApi DelicateCoroutinesApi ExperimentalCoroutinesApi ObsoleteCoroutinesApi

  127. Android time

  128. Android time

  129. Android time implementation 'androidx.annotation:annotation:1.1.0'

  130. Android time implementation 'androidx.annotation:annotation-experimental:1.1.0'

  131. core Validating API

  132. core Validating API buildscript { dependencies { classpath 'org.jetbrains.kotlinx:binary-compatibility-validator:0.5.0' }

    } apply plugin: 'binary-compatibility-validator'
  133. core apiValidation { ignoredPackages += [ 'com/getstream/sdk/chat/databinding', 'io/getstream/chat/android/ui/databinding', ] ignoredProjects

    += [ 'stream-chat-android-docs', 'stream-chat-android-sample', 'stream-chat-android-ui-components-sample', 'stream-chat-android-test', ] nonPublicMarkers += [ 'io.getstream.chat.android.core.internal.InternalStreamChatApi', ] } Validating API
  134. core apiValidation { ignoredPackages += [ 'com/getstream/sdk/chat/databinding', 'io/getstream/chat/android/ui/databinding', ] ignoredProjects

    += [ 'stream-chat-android-docs', 'stream-chat-android-sample', 'stream-chat-android-ui-components-sample', 'stream-chat-android-test', ] nonPublicMarkers += [ 'io.getstream.chat.android.core.internal.InternalStreamChatApi', ] } Validating API
  135. core apiValidation { ignoredPackages += [ 'com/getstream/sdk/chat/databinding', 'io/getstream/chat/android/ui/databinding', ] ignoredProjects

    += [ 'stream-chat-android-docs', 'stream-chat-android-sample', 'stream-chat-android-ui-components-sample', 'stream-chat-android-test', ] nonPublicMarkers += [ 'io.getstream.chat.android.core.internal.InternalStreamChatApi', ] } Validating API
  136. core apiValidation { ignoredPackages += [ 'com/getstream/sdk/chat/databinding', 'io/getstream/chat/android/ui/databinding', ] ignoredProjects

    += [ 'stream-chat-android-docs', 'stream-chat-android-sample', 'stream-chat-android-ui-components-sample', 'stream-chat-android-test', ] nonPublicMarkers += [ 'io.getstream.chat.android.core.internal.InternalStreamChatApi', ] } Validating API
  137. core Validating API gradlew apiDump

  138. core Validating API public abstract interface class io/getstream/chat/android/client/ChatEventListener { public

    abstract fun onEvent (Lio/getstream/chat/android/client/events/ChatEvent;)V } public final class io/getstream/chat/android/client/api/models/AutocompleteFilterObject : io/getstream/chat/android/client/api/models/FilterObject { public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/Autocompl eteFilterObject; public fun equals (Ljava/lang/Object;)Z public final fun getFieldName ()Ljava/lang/String; public final fun getValue ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } gradlew apiDump
  139. core Validating API gradlew apiCheck

  140. core Validating API Execution failed for task ':stream-chat-android-client:apiCheck'. > API

    check failed for project stream-chat-android-client. @@ -218,9 +218,8 @@ public final class io/getstream/chat/android/client/api/models/AutocompleteFilterObject : io/getstream/chat/android/client/api/models/FilterObject { public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/Autocompl eteFilterObject; + public final fun copy (Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/AutocompleteFilterObject; public fun equals (Ljava/lang/Object;)Z public final fun getFieldName ()Ljava/lang/String; public final fun getValue ()Ljava/lang/String; You can run :stream-chat-android-client:apiDump task to overwrite API declarations gradlew apiCheck
  141. core Validating API Execution failed for task ':stream-chat-android-client:apiCheck'. > API

    check failed for project stream-chat-android-client. @@ -218,9 +218,8 @@ public final class io/getstream/chat/android/client/api/models/AutocompleteFilterObject : io/getstream/chat/android/client/api/models/FilterObject { public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/Autocompl eteFilterObject; + public final fun copy (Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/AutocompleteFilterObject; public fun equals (Ljava/lang/Object;)Z public final fun getFieldName ()Ljava/lang/String; public final fun getValue ()Ljava/lang/String; You can run :stream-chat-android-client:apiDump task to overwrite API declarations gradlew apiCheck
  142. core Validating API Execution failed for task ':stream-chat-android-client:apiCheck'. > API

    check failed for project stream-chat-android-client. @@ -218,9 +218,8 @@ public final class io/getstream/chat/android/client/api/models/AutocompleteFilterObject : io/getstream/chat/android/client/api/models/FilterObject { public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/Autocompl eteFilterObject; + public final fun copy (Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/AutocompleteFilterObject; public fun equals (Ljava/lang/Object;)Z public final fun getFieldName ()Ljava/lang/String; public final fun getValue ()Ljava/lang/String; You can run :stream-chat-android-client:apiDump task to overwrite API declarations gradlew apiCheck
  143. core Validating API

  144. • 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/ • How to Build Awesome Android Libraries  https://zsmb.co/talks/how-to-build-awesome-android-libraries/ • Effective Java, 3rd Edition  https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997 • Effective Kotlin  https://leanpub.com/effectivekotlin/ Resources
  145. zsmb13 zsmb.co/talks

  146. 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