Slide 1

Slide 1 text

Crafting Idiomatic APIs with Kotlin and Compose Droidcon Berlin - September '25 ashdavies.dev 1

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Protocols SOAP / Rest / GraphQL / gRPC ashdavies.dev 3

Slide 4

Slide 4 text

SDKs ashdavies.dev 4

Slide 5

Slide 5 text

"Ordinary" Code ashdavies.dev 5

Slide 6

Slide 6 text

Everything is an API Droidcon Berlin - Oct '21 ashdavies.dev/talks/everything-is-an-api-berlin-droidcon/ 6

Slide 7

Slide 7 text

Unit Tests ashdavies.dev 7

Slide 8

Slide 8 text

Temporary Code ashdavies.dev 8

Slide 9

Slide 9 text

There is nothing more permanent than a temporary solution ashdavies.dev 9

Slide 10

Slide 10 text

Who is your API for? ashdavies.dev 10

Slide 11

Slide 11 text

Consumers ashdavies.dev 11

Slide 12

Slide 12 text

Colleagues ashdavies.dev 12

Slide 13

Slide 13 text

Future Self ashdavies.dev 13

Slide 14

Slide 14 text

Consequences ashdavies.dev 14

Slide 15

Slide 15 text

Consequences Job security ashdavies.dev 15

Slide 16

Slide 16 text

Consequences Increased learning curve ashdavies.dev 16

Slide 17

Slide 17 text

Consequences Longer peer review ashdavies.dev 17

Slide 18

Slide 18 text

Consequences Slowed feature delivery ashdavies.dev 18

Slide 19

Slide 19 text

Every existing thing is born without reason, prolongs itself out of weakness, and dies by chance — Jean-Paul Sartre ashdavies.dev 19

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Code Maintenance Photo by jessica rigollot 21

Slide 22

Slide 22 text

Idioms kotlinlang.org/docs/idioms.html 22

Slide 23

Slide 23 text

Idioms Default Parameters fun foo(a: Int = 0, b: String = "") { /* ... */ } ashdavies.dev 23

Slide 24

Slide 24 text

Idioms Lazy Properties val property: String by lazy { /* ... */ } ashdavies.dev 24

Slide 25

Slide 25 text

Idioms Null Coalescing val fileSize = files?.size ?: run { val someSize = getSomeSize() someSize * 2 } ashdavies.dev 25

Slide 26

Slide 26 text

Idioms Expression return statements fun getDisplayNameOrDefault(userId: String?): String = getDisplayName(userId ?: return "default") kotlinlang.org/docs/whatsnew2220.html 26

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Kotlin Coding Conventions kotlinlang.org/docs/coding-conventions.html 28

Slide 29

Slide 29 text

ashdavies.dev 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Coroutines ashdavies.dev 31

Slide 32

Slide 32 text

Reactive / Imperative ashdavies.dev 32

Slide 33

Slide 33 text

jakewharton.com/the-state-of-managing-state-with-rxjava/ 33

Slide 34

Slide 34 text

@Suppress("DEPRECATION") class CallbackLoginPresenter(val service: SessionService, val goTo: (Screen) -> Unit) { var onModel: (LoginUiModel) -> Unit = {} var task: AsyncTask? = 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() { 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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Coroutines Structured Concurrency ashdavies.dev 36

Slide 37

Slide 37 text

Declarative Architecture ashdavies.dev 37

Slide 38

Slide 38 text

Declarative Architecture Immutability ashdavies.dev 38

Slide 39

Slide 39 text

Declarative Architecture Idempotency / Determinism ashdavies.dev 39

Slide 40

Slide 40 text

Declarative Architecture Unidirectional Data Flow ashdavies.dev 40

Slide 41

Slide 41 text

Jetpack, Compose, & Multiplatform ashdavies.dev 41

Slide 42

Slide 42 text

ashdavies.dev 42

Slide 43

Slide 43 text

fun Counter($composer: Composer) { $composer.startRestartGroup(-1913267612) /* ... */ $composer.endRestartGroup() } ashdavies.dev 43

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Compose Scopes @Composable fun TopAppBar( actions: @Composable (RowScope.() -> Unit)? = null, // ... ) TopAppBar( actions = { ImageButton(onClick = { /* ... */ }) { Image(/* ... */) } ImageButton(onClick = { /* ... */ }) { Image(/* ... */) } }, ) ashdavies.dev 46

Slide 47

Slide 47 text

Compose Property Delegates inline operator fun State.getValue(thisObj: Any?, property: KProperty<*>): T = value val conference by remember { mutableStateOf("Droidcon Berlin") } ashdavies.dev 47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

RootNode ChildNode ChildNode ChildNode ChildNode ChildNode ChildNode ChildNode ChildNode ChildNode ChildNode ChildNode ashdavies.dev 49

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

fun CoroutineScope.launchCounter(): StateFlow { return launchMolecule(mode = ContextClock) { var count by remember { mutableStateOf(0) } LaunchedEffect(Unit) { while (true) { delay(1_000) count++ } } count } } ashdavies.dev 51

Slide 52

Slide 52 text

Beyond the UI mDevCamp Prague - Jun '25 ashdavies.dev/talks/beyond-the-ui-prague/ 52

Slide 53

Slide 53 text

Language ashdavies.dev 53

Slide 54

Slide 54 text

! ashdavies.dev 54

Slide 55

Slide 55 text

colour ashdavies.dev 55

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

fun showCancelBookingAndBackToVerbindungsuebersichtDialog() fun handleLoadBuchungskundenDatenOrOpenRechnungFailure() fun getAngebotsKonditionIconAndContentDescription() ashdavies.dev 57

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Business Specific Terminology ashdavies.dev 59

Slide 60

Slide 60 text

Stornierung, Ersatzverkehr, Signalstörung... Business Specific Terminology ashdavies.dev 60

Slide 61

Slide 61 text

Business Specific Terminology ashdavies.dev 61

Slide 62

Slide 62 text

What makes a "good" API? ashdavies.dev 62

Slide 63

Slide 63 text

There is no such thing as perfect code ashdavies.dev 63

Slide 64

Slide 64 text

Perfect Solution Fallacy ashdavies.dev 64

Slide 65

Slide 65 text

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(val name: String) { /** * Adds a [member] to this group. * @return the new size of the group. */ fun add(member: T): Int { ... } } ashdavies.dev 65

Slide 66

Slide 66 text

Good code is easy to delete programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to 66

Slide 67

Slide 67 text

High Cohesion - Loose Coupling ashdavies.dev 67

Slide 68

Slide 68 text

Dependency Control ashdavies.dev 68

Slide 69

Slide 69 text

Tooling ashdavies.dev 69

Slide 70

Slide 70 text

Tooling explicitApiMode() kotlinlang.org/docs/whatsnew14.html#explicit-api-mode-for-library-authors 70

Slide 71

Slide 71 text

Tooling com.slack.lint.compose:compose-lint-checks slackhq.github.io/compose-lints/ 71

Slide 72

Slide 72 text

Tooling io.nlopez.compose.rules:ktlint mrmans0n.github.io/compose-rules/ktlint/ 72

Slide 73

Slide 73 text

Tooling @Suppress ashdavies.dev 73

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

What makes a bad API? ashdavies.dev 75

Slide 76

Slide 76 text

Thank You! ashdavies.dev 76