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

Mastering API Visibility in Kotlin (Rheinwerk K...

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Márton Braun Márton 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/

Avatar for Márton Braun

Márton Braun

September 14, 2021
Tweet

More Decks by Márton Braun

Other Decks in Technology

Transcript

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

    members Chapter 4: Classes and Interfaces
  2. Minimal APIs › Easier to maintain and change public fun

    Krate.stringPref( key: String, isValid: (newValue: String) -> Boolean, )
  3. 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, )
  4. 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, )
  5. 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, )
  6. 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, )
  7. 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, )
  8. 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, )
  9. 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, )
  10. 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, )
  11. Internal visibility public interface Service { fun createUser(): User }

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

    = Disconnected internal set public fun connect() { state = Connected } }
  13. class Repository { internal fun createEntity() { ... } }

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

    { internal fun createEntity() { ... } } Cannot resolve method 'createEntity' in 'Repository' @JvmSynthetic (); createEntity
  15. Explicit API mode class User(var name: String, var age: Int)

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

    { fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } }
  17. 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 } }
  18. 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
  19. 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 } }
  20. 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 } }
  21. Explicit API mode private val DEFAULT_CLIENT = Client() public interface

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

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

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

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

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

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

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

    › Explicit types for public declarations › Configured per-module › Strict or warning level
  29. Published API public inline fun song() { secretFunction() } internal

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

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

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

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

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

    fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }
  37. 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
  38. 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
  39. 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
  40. 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', ] } }
  41. 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', ] } }
  42. 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', ] } }
  43. 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()
  44. 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
  45. 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
  46. 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
  47. 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', ] } }
  48. 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', ] } }
  49. 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', ] } }
  50. 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', ] } }
  51. 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', ] } }
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. • 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
  61. 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