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

Droidcon Berlin: Crafting Idiomatic APIs with K...

Avatar for Ash Davies Ash Davies
September 24, 2025

Droidcon Berlin: Crafting Idiomatic APIs with Kotlin and Compose

What’s in a language? What does it mean to be idiomatic? Writing clean, intuitive APIs isn’t just about functionality — it’s about designing interfaces that feel natural and intuitive. With Kotlin and Compose, we have a powerful set of tools and syntax that, when used effectively, can reduce cognitive load and make your APIs both elegant and expressive.

In this talk, we’ll dive into the principles of idiomatic Kotlin and explore how they apply to designing Compose APIs. You’ll learn how to leverage Kotlin’s language features, from DSLs and inline functions to advanced type safety and composable conventions, to craft APIs that are a joy to use. By mastering these techniques, you can deliver APIs that not only “work” but truly feel like Kotlin.

Avatar for Ash Davies

Ash Davies

September 24, 2025
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. What is an API? curl --request GET \ --url "https://api.github.com/octocat"

    \ --header "Authorization: Bearer YOUR-TOKEN" \ --header "X-GitHub-Api-Version: 2022-11-28" ashdavies.dev 2
  2. Every existing thing is born without reason, prolongs itself out

    of weakness, and dies by chance — Jean-Paul Sartre ashdavies.dev 19
  3. Every line of code is written without reason, maintained out

    of weakness, and deleted by chance — Jean-Paul Sartre’s Programming in ANSI C. programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to 20
  4. Idioms Default Parameters fun foo(a: Int = 0, b: String

    = "") { /* ... */ } ashdavies.dev 23
  5. Idioms Null Coalescing val fileSize = files?.size ?: run {

    val someSize = getSomeSize() someSize * 2 } ashdavies.dev 25
  6. Idioms Guard Conditions fun feedAnimal(animal: Animal) { when (animal) {

    is Animal.Dog -> feedDog() is Animal.Cat if !animal.mouseHunter -> feedCat() else -> println("Unknown animal") } } ashdavies.dev 27
  7. Feb 2016 Kotlin 1.1 Type Aliases, Bound References, Lambda destructuring

    Nov 2017 Kotlin 1.2 Array Literals, lateinit properties Oct 2018 Kotlin 1.3 Coroutines, Multiplatform projects, Contracts, when subject Aug 2020 Kotlin 1.4 Sam conversions, explicit API mode, trailing comma May 2021 Kotlin 1.5.0 Sealed interfaces, improved inline classes Nov 2021 Kotlin 1.6.0 Exhaustive when, suspending functions as supertypes Jun 2022 Kotlin 1.7.0 K2 compiler alpha, underscore operator Apr 2023 Kotlin 1.8.20 Wasm target, data objects, secondary constructor bodies May 2024 Kotlin 2.0.0 Compose compiler Gradle plugin Jun 2025 Kotlin 2.2.0 Context parameters, guard conditions, non-local break and continue, multi-dollar interpolation Kotlin ashdavies.dev 30
  8. @Suppress("DEPRECATION") class CallbackLoginPresenter(val service: SessionService, val goTo: (Screen) -> Unit)

    { var onModel: (LoginUiModel) -> Unit = {} var task: AsyncTask<Submit,Void,LoginResult>? = null fun start() = onModel(Content) fun stop() = task?.cancel(true) fun onEvent(event: LoginUiEvent) = when (event) { is Submit -> task = LoginAsyncTask().also { it.execute(event) } } inner class LoginAsyncTask : AsyncTask<Submit, Void, LoginResult>() { private var username: String = "" override fun doInBackground(vararg events: Submit?): LoginResult { val event = events[0]!! username = event.username return runBlocking { service.login(event.username, event.password) } } override fun onPostExecute(result: LoginResult?) = when (result) { is Success -> goTo(LoggedInScreen(username)) is Failure -> goTo(ErrorScreen(result.throwable?.message ?: "")) else -> Unit } } } ashdavies.dev 34
  9. Observable.just("Hey") .subscribeOn(Schedulers.io()) .map(String::length) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { doAction() } .flatMap

    { doAction() Observable.timer(1, TimeUnit.SECONDS) .subscribeOn(Schedulers.single()) .doOnSubscribe { doAction() } } .subscribe { doAction() } proandroiddev.com/how-rxjava-chain-actually-works-2800692f7e13 35
  10. Compose Higher-Order Functions @Composable fun TopAppBar( navigationIcon: @Composable (() ->

    Unit)? = null, title: @Composable () -> Unit, actions: @Composable (RowScope.() -> Unit)? = null, // ... ) TopAppBar( navigationIcon = { Image(/* ... */) }, // ... ) chrisbanes.me/posts/slotting-in-with-compose-ui 44
  11. Compose Scopes @Stable interface WeightScope { fun Modifier.weight(weight: Float): Modifier

    } @Composable fun WeightedRow( modifier: Modifier = Modifier, content: @Composable WeightScope.() -> Unit ) { // ... // Usage: WeightedRow { Text("Hello", Modifier.weight(1f)) Text("World", Modifier.weight(2f)) } android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#layout_scoped-modifiers 45
  12. Compose Scopes @Composable fun TopAppBar( actions: @Composable (RowScope.() -> Unit)?

    = null, // ... ) TopAppBar( actions = { ImageButton(onClick = { /* ... */ }) { Image(/* ... */) } ImageButton(onClick = { /* ... */ }) { Image(/* ... */) } }, ) ashdavies.dev 46
  13. Compose Property Delegates inline operator fun <T> State<T>.getValue(thisObj: Any?, property:

    KProperty<*>): T = value val conference by remember { mutableStateOf("Droidcon Berlin") } ashdavies.dev 47
  14. Compose Architecture downloadManager .downloadFile("https://.../") .flatMapLatest { state -> when (state)

    { is State.Loaded -> stateFileManager.saveFile( name = "storage/file", value = state.value, ) else -> state } } .onEach { state -> when (state) { is State.Saved -> println("Downloaded file successfully") is State.Loading -> /* ... */ } } .launchIn(coroutineScope) ashdavies.dev 48
  15. val downloadState = downloadManager .downloadFile("https://.../") .collectAsState(State.Downloading) val fileState = when(downloadState)

    { is State.Loaded -> stateFileManager .saveFile("storage/file", downloadState.value) else -> downloadState } when (fileState) { is State.Loading -> /* ... */ is State.Saved -> LaunchedEffect(fileState) { println("Downloaded file successfully") } } ashdavies.dev 50
  16. fun CoroutineScope.launchCounter(): StateFlow<Int> { return launchMolecule(mode = ContextClock) { var

    count by remember { mutableStateOf(0) } LaunchedEffect(Unit) { while (true) { delay(1_000) count++ } } count } } ashdavies.dev 51
  17. Luxemburg Netherlands Denmark Belgium Austria Germany Poland UK 0 0.5

    1 1.5 2 2.5 3 3.5 4 Average number of languages spoken per person in Europe ashdavies.dev 56
  18. Teuton: Eine Deutsche Programmiersprache # -*- coding: iso-8859-1 -*- schön

    = Wahr häßlich = Falsch für bäh in [schön, häßlich]: drucke bäh def sovielwiemöglich(): "gib" zurück "was mir gehört" fiber-space.de/EasyExtend/doc/teuton/teuton.htm 58
  19. Documentation /** * A group of *members*. * * This

    class has no useful logic; it's just a documentation example. * * @param T the type of a member in this group. * @property name the name of this group. * @constructor Creates an empty group. */ class Group<T>(val name: String) { /** * Adds a [member] to this group. * @return the new size of the group. */ fun add(member: T): Int { ... } } ashdavies.dev 65
  20. Multiline expression wrapping standard:multiline-expression-wrapping val foo = foo( parameterName =

    "The quick brown fox " .plus("jumps ") .plus("over the lazy dog"), ) ashdavies.dev 74