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

Mastering API Visibility in Kotlin (Virtual Kot...

Mastering API Visibility in Kotlin (Virtual Kotlin User Group 2021 June)

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/

Márton Braun

June 24, 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 = OfflineClient() public interface

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

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

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

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

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

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

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

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

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

    fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }
  35. 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
  36. 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
  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.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', ] } }
  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 tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xopt-in=kotlin.RequiresOptIn', ] } }
  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 @InternalMyLibraryApi public fun coreApi()
  42. 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
  43. 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
  44. 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', ] } }
  45. 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', ] } }
  46. 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', ] } }
  47. 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', ] } }
  48. 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', ] } }
  49. 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
  50. 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
  51. 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
  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 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
  54. 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
  55. 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
  56. 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
  57. • 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
  58. 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