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

Mastering API Visibility in Kotlin (Android Worldwide 2021 April)

Mastering API Visibility in Kotlin (Android Worldwide 2021 April)

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.

This talk is adapted from the article with the same title: https://zsmb.co/mastering-api-visibility-in-kotlin/

4047c64e3a1e2f81addd4ba675ddc451?s=128

Marton Braun

April 27, 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 Item 15: Minimize the accessibility of classes and

    members Chapter 4: Classes and Interfaces
  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

  30. Minimal APIs › Easier to maintain and change 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. 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, )
  34. 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, )
  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.ERROR, 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 @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, )
  39. Minimal APIs › Easier to maintain and change

  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
  43. Minimal APIs › Easier to maintain and change › Easier

    to learn › Harder to misuse
  44. Planning

  45. Planning

  46. Planning

  47. Planning

  48. Visibility in Kotlin public

  49. Visibility in Kotlin public private

  50. Visibility in Kotlin public private internal

  51. Visibility in Kotlin public private internal

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

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

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

  55. Internal visibility

  56. Testing

  57. 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 private var

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

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

    PRIVATE) internal var state: State
  62. Java interop

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

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

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

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

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

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

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

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

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

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

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

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

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

    { freeCompilerArgs += [ '-Xexplicit-api=warning', '-progressive', ] } }
  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. Explicit API mode class User(var name: String, var age: Int)

    { fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } }
  78. 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 } }
  79. 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
  80. 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 } }
  81. 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 } }
  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 = OfflineClient() public interface

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

    ClientFactory { public fun client(): Client = DEFAULT_CLIENT }
  86. Published API

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

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

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

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

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

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

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

  97. Opt-in APIs core addon

  98. Opt-in APIs core addon

  99. core addon Opt-in APIs

  100. core addon Opt-in APIs

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

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

  103. 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
  104. 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
  105. 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
  106. 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', ] } }
  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 @InternalMyLibraryApi public fun coreApi()
  108. 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()
  109. 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
  110. 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
  111. core addon @InternalMyLibraryApi public fun coreApi() Opting in public fun

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

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

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

    @OptIn(InternalMyLibraryApi::class) coreApi() }
  116. 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', ] } }
  117. 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', ] } }
  118. 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', ] } }
  119. 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/
  120. zsmb13 zsmb.co/talks

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