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/

Márton Braun

April 27, 2021
Tweet

More Decks by Márton Braun

Other Decks in Programming

Transcript

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

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

    members Chapter 4: Classes and Interfaces
  3. Minimal APIs › Easier to maintain and change 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. 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, )
  7. 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, )
  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.ERROR, 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. 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, )
  12. Internal visibility public interface Service { fun createUser(): User }

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

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

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

    { internal fun createEntity() { ... } } Cannot resolve method 'createEntity' in 'Repository' @JvmSynthetic (); createEntity
  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. Explicit API mode class User(var name: String, var age: Int)

    { fun sayHi() { println("Hi, I'm $name") } fun reset() { name = "" age = 0 } }
  18. 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 } }
  19. 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
  20. 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 } }
  21. 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 } }
  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 = OfflineClient() public interface

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

    ClientFactory { public fun client(): Client = DEFAULT_CLIENT }
  26. Published API public inline fun song() { secretFunction() } internal

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

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

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

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

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

    fun song() { secretFunction() } internal fun secretFunction() { println("through the mountains") }
  34. 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
  35. 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
  36. 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
  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 tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs += [ '-Xopt-in=kotlin.RequiresOptIn', ] } }
  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 @InternalMyLibraryApi public fun coreApi()
  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 @InternalMyLibraryApi public fun coreApi()
  40. 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
  41. 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
  42. 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
  43. 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', ] } }
  44. 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', ] } }
  45. 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', ] } }
  46. 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/
  47. 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