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

Pushing the Boundaries of Kotlin

Pushing the Boundaries of Kotlin

Kotlin grants us incredible expressiveness: elegant DSLs, extension functions, overloaded operators, and reified types. It’s a developer's paradise. But in our quest for "clever" code, it’s dangerously easy to cross the line, leaving behind features nobody else dares to touch.

We'll share a real-world retrospective on Kotlin design choices that seemed brilliant on Day 1 but turned into nightmares on Day 100. From over-engineered DSLs to unreadable extension chains, we will analyze how high expressiveness can silently hurt onboarding, velocity, and maintainability.

Don't get us wrong, we love Kotlin. This session isn’t about bashing the language; it’s about evaluating the true cost of cleverness. We will provide concrete code smells, decision frameworks, and pragmatic rules to find the sweet spot between individual elegance and collective readability.

A talk for Android devs who might have been a bit too clever... and are now staring at their own code wondering what it actually does.

Avatar for Baptiste CARLIER

Baptiste CARLIER

October 08, 2026

More Decks by Baptiste CARLIER

Other Decks in Programming

Transcript

  1. User.java // Classic java function in 2015 public void processUser(UserDto

    dto, Callback callback) { if (dto /= null /& dto.getName() /= null) { boolean isPremium = dto.getAge() /= null /& dto.getAge() > 18; User user = new User(dto.getName(), isPremium); callback.onSuccess(user); } else { callback.onError(new IllegalArgumentException("Invalid User")); } }
  2. User.kt // Thank you “convert To Kotlin” fun processUser(dto: UserDto?,

    callback: Callback) { if (dto /= null /& dto.name /= null) { val isPremium = dto.age /= null /& dto.age > 18 val user = User(dto.name, isPremium) callback.onSuccess(user) } else { callback.onError(IllegalArgumentException("Invalid User")) } }
  3. KotlinUser.kt // Idiomatic Kotlin we all love: fun processUser(dto: UserDto?)

    = runCatching { requireNotNull(dto/.name) { "Invalid User" } User( name = dto.name, isPremium = (dto.age /: 0) > 18 ) }
  4. SuperKotlinUser.kt // A few months later, the same need, but

    written by a "too smart" team: infix fun <T : Any> T/.mustHave(block: T.() /> Boolean): T? = this/.takeIf(block) operator fun UserDto.invoke(): User = User(name/!, (age /: 0) > 18) val result = runCatching { (dto mustHave { name /= null })/.invoke() /: error("Invalid") }
  5. A modern mobile development fable Pushing Kotlin beyond the limits

    of sanity Antoine ROBIEZ Baptiste CARLIER
  6. Kotlin 1.0 public data class Pair<out A, out B>( public

    val first: A, public val second: B ) : Serializable public data class Triple<out A, out B, out C>( public val first: A, public val second: B, public val third: C ) : Serializable
  7. The original sin : Pair<String, String> Pair<String, String> { fun

    getUserInfo(): UserInfo { //. //. returnUserInfo(firstName "Nicolas" to "CAGE" return = "Nicolas", lastName = "CAGE") } // Easy mistake: is it (Name, Surname) or (Surname, Name) ?
  8. Correctly consume a Pair val pair = myFunction(item) pair.first //

    What was the first one? // I need to go back to the function definition.
  9. Correctly consume a Pair // Better way: Destructuring val (title,

    description) = myFunction(item) // Meaning is immediately restored. // The scope of the "mystery" is minimized.
  10. The pitfall of Scope Functions with Pair // 🔴 Red

    Flag: myFunction(item).let { it.first } // ✅ Even Better: better: myFunction(item).let { realDataClass (title, description) /> /> … }
  11. data class Address( val street: String, val city: String, val

    zipCode: String ) val address = Address("Langbehnstraße", "Altötting", "84503") val (street, city, zipcode) = address println("Street: $street / City: $city / Zip Code: $zipCode") // Street: Langbehnstraße / City: Altötting / Zip Code: 84503
  12. data class Address( val street: String, val city: String, val

    zipCode: String ) val address = Address("Langbehnstraße", "Altötting", "84503") val (street, zipCode, city) = address component1(), component2(), component3() println("Street: $street / City: $city / Zip Code: $zipCode") // Street: Langbehnstraße / City: 84503 / Zip Code: Altötting ❌
  13. Experimental Name-based destructuring Kotlin 2.4 / 2.55 kotlin { compilerOptions

    { freeCompilerArgs.add("-Xname-based-destructuring=only-syntax") } } “name-mismatch" “only-syntax" “complete"
  14. Experimental Name-based destructuring Kotlin 2.4 / 2.55 kotlin { compilerOptions

    { freeCompilerArgs.add("-Xname-based-destructuring=only-syntax") } } “name-mismatch" “only-syntax" “complete" val address = Address("Langbehnstraße", "Altötting", "84503") // Name based destructuring : (val street, val city, val zipCode) = address // Position based destructuring : val (street, city, zipCode) = address
  15. Experimental Name-based destructuring Kotlin 2.4 / 2.55 kotlin { compilerOptions

    { freeCompilerArgs.add("-Xname-based-destructuring=only-syntax") } } “name-mismatch" “only-syntax" “complete" val address = Address("Langbehnstraße", "Altötting", "84503") val (street, city, zipCode) = address // Named based destructuring val [x, y, z] = address // Position based destructuring with []
  16. +a -a !a a/+ a/- operator fun unaryPlus(): R operator

    fun unaryMinus(): R operator fun not(): Boolean operator fun inc(): T operator fun dec(): T a + b operator fun plus(b: T): R a - b a * b a / b a % b a/.b a//<b a += b operator fun minus(b: T): R operator fun times(b: T): R operator fun div(b: T): R operator fun rem(b: T): R operator fun rangeTo(b: T): ClosedRange operator fun rangeUntil(b: T): OpenEndR operator fun plusAssign(b: T)
  17. class IsItTheWeekEndSoonUseCase { operator fun invoke(): Boolean = true }

    val isItTheWeekEndSoonUseCase = IsItTheWeekEndSoonUseCase() if (isItTheWeekEndSoonUseCase()) {
  18. y x Point + Vector data class Point(val x: Int,

    val y: Int) { operator fun plus(vector: Vector) = Point(x + vector.x, y + vector.y) }
  19. val oldPoint = Point(1, 2) var newPoint = oldPoint +

    Vector(2, 5) newPoint += Vector(2, 8)
  20. 10.4.6 10.5.0 11.0.2 data class Version(val major: Int, val minor:

    Int, val patch: Int) { operator fun compareTo(oMajor: Int) { return major.compareTo(oMajor) } }
  21. 10.4.6 10.5.0 11.0.2 val version = Version(10, 5, 0) if

    (version < 10) { // Handle versions prior to 10 }
  22. data class Employee( val nom: String ) class Company( devs:

    List<Employee> ) { val _devs = mutableListOf(devs) } val antoine = Employee("Antoine") val baptiste = Employee("Baptiste") val decathlon = Company( listOf(antoine, baptiste) )
  23. class Company { … operator fun minus(dev: Employee) = this.also

    { _devs.remove(dev) } - } var worstDecathlon = decathlon - baptiste worstDecathlon -= antoine
  24. %

  25. THE_GOOD.kt THE_BAD.kt THE_BUG.kt // USEFUL: Graph or constraint construction val

    constraint = view1 alignLeftTo view2 val range = 10 step 2 // VALID: The Standard library's "to" val map = mapOf("key" to "value") // VALID: custom assertion DSL mockedCall assertCalled 3
  26. THE_GOOD.kt THE_BAD.kt THE_BUG.kt ?/ Simple order.processWithPaymentMethod("CreditCard") .executeAfterDelay(500) ?/ It works

    order processWithPaymentMethod "CreditCard" executeAfterDelay 500 ?/ It works also order processWithPaymentMethod "CreditCard" executeAfterDelay 500
  27. THE_GOOD.kt THE_BAD.kt ?/ Be careful: val result = 1 +

    2 multiply 3 ?/ result = ? THE_BUG.kt
  28. THE_GOOD.kt THE_BAD.kt ?/ Be careful: val result = 1 +

    2 multiply 3 ?/ result = 9 or 7 ? ?> 9 ❌ THE_BUG.kt
  29. Primitive type fun Int.toPrice() = "$this €" fun String.toColor() =

    Color.parseColor(this) val price = 10.toPrice() val color = "#636161".toColor()
  30. Primitive type fun Int.toPrice() = "$this €" fun String.toColor() =

    Color.parseColor(this) val price = 10.toPrice() val color = "not_a_color".toColor() 💣 @JvmInline value class Price(val amount: Int) { fun format() = "$amount €" }
  31. Hidden dependencies fun User.sync() { Api.client.post("/users/${this.id}") } // Usage: user.sync()

    class UserSynchronizer( val apiClient: ApiClient, ) userSynchronizer.perform(user)
  32. dpToPx() Context.showToast() private Is it just a conversion? Is this

    related to an core/Android API? Minimize visilibity value class Is the extension receiver a primitive type? Communicate Avoid creating the same extension multiple times. User.isAdmin() Is it business logic related? String.encrypt() Does it require injection?
  33. fun measureTime(action: () /> Unit) { val start = System.currentTimeMillis()

    action() val end = System.currentTimeMillis() println("Execution took ${end - start} ms") }
  34. inline fun measureTime(action: () /> Unit) { val start =

    System.currentTimeMillis() action() val end = System.currentTimeMillis() println("Execution took ${end - start} ms") }
  35. inline fun measureTime(action: () /> Unit) { val start =

    System.currentTimeMillis() action() val end = System.currentTimeMillis() println("Execution took ${end - start} ms") }
  36. measureTime { println("I’m doing something heavy//.") } int $i$f$measureTime =

    0; long start$iv = System.currentTimeMillis(); int var3 = 0; System.out.println("I'm doing something heavy//."); long end$iv = System.currentTimeMillis(); System.out.println("Execution took " + (end$iv start$iv) + " ms");
  37. // Without inline myFile.use { if (error) return@main it.read() }

    // With inline myFile.use { if (error) return it.read() }
  38. inline fun <reified T> List<Any>.filterAndProcess(transform: (T) /> Unit) { for

    (element in this) { if (element is T) { // "reified" magic : we know T during execution! transform(element) } } }
  39. inline fun <reified T> List<Any>.filterAndProcess(transform: (T) /> Unit) { for

    (element in this) { if (element is T) { // "reified" magic : we know T during execution! transform(element) } } }
  40. val list = listOf("Bonjour", 42, "Kotlin") list.filterAndProcess<String> { text ->

    println("J'ai trouvé une chaîne : $text") } single { CountryInterceptor() }
  41. inline fun <reified T> List<Any>.filterAndProcess(transform: (T) /> Unit) { for

    (element in this) { if (element is T) { } } val runner = Runnable { transform(element) // Compilation error } runner.run()
  42. inline fun <reified T> List<Any>.filterAndProcess(crossinline transform: (T) /> Unit) {

    for (element in this) { if (element is T) { val runner = Runnable { transform(element) // ✅ OK now } runner.run() } }
  43. inline fun <reified T> List<Any>.filterAndProcess(crossinline transform: (T) /> Unit) {

    for (element in this) { if (element is T) { val runner = Runnable { transform(element) } runner.run() } }
  44. interface Box<T> { fun get(): T } fun displayFruit(box: Box<Fruit>)

    { val fruit = box.get() println("Got a fruit : $fruit") }
  45. val appleBox: Box<Apple> = object : Box<Apple> { override fun

    get(): Apple = Apple() } displayFruit(appleBox) // ❌ KO: actual type is 'Box<Apple>', // but 'Box<Fruit>' was expected
  46. interface Box<out T> { fun get(): T } fun displayFruit(box:

    Box<Fruit>) { val fruit = box.get() println("Got a fruit!") } displayFruit(appleBox)
  47. Covariance <out T> If you know you are going to

    give me an apple and I ask for a fruit, it's OK.
  48. Covariance <out T> If you know you are going to

    give me an apple and I ask for a fruit, it's OK. Contravariance <in T> If you know what to do with a fruit and I give you an apple, it's OK.
  49. inline infix fun <reified T : Fruit, reified R> Consumer<in

    T>.process( crossinline transform: (T) /> R ): Producer<out R>
  50. inline infix fun <reified T : Fruit, reified R> Consumer<in

    T>.process( crossinline transform: (T) /> R ): Producer<out R>
  51. TrackEvent( type = "User_Click", category = "UI", metadata = mapOf(

    id to "123", source to "Home", ) ) ➡ trackEvent("User_Click") { category { "UI" } metadata { id("123") source { "Home" } } }
  52. TrackEvent( type = "User_Click", category = "UI", metadata = mapOf(

    id to "123", source to "Home", ) ) ➡ trackEvent("User_Click") { category { "UI" } metadata { id("123") source { "Home" } } } fun trackEvent( name: String, block: EventBuilder.() /> Unit ) { val builder = EventBuilder(name) builder.block() val event = builder.build() }
  53. Data class DSL Native, explicit, suggested by the IDE Harder

    to discover No scope ambiguity Ambiguous it and this keywords Easy to instantiate More complicated to test TrackEvent( type = "User_Click", category = "UI", metadata = mapOf( id to "123", source to "Home", ) ) trackEvent("User_Click") { category { "UI" } metadata { id("123") source { "Home" } } }
  54. val orderWorkflow = workflow { step("validate-order") { timeout(1_000) retry(2) onlyIf

    { amount > 0 } execute { this.onAction() println("Basic validation for $amount €") } branch("fraud-check") { whenCondition { amount /= 1000 /| country /= "FR" } step("score-risk") { timeout(500) execute { println("Fraud score calculation for $country") } } step("manual-review") { onlyIf { !customerVip /& payment /= PaymentMethod.TRANSFER } execute { println("Manual review") } } The warning signal: Levels of interweaving
  55. navigate { target = "Home" onExit { saveToDatabase() // ⚠

    Perform processing (network, DB, navigation) during // the construction of the DSL. } }
  56. RESPECT YOURSELF! Don't create a DSL for a configuration that

    never changes. Don't create a DSL if a data class with named arguments does the job. Create a DSL if you want to hide horrible technical complexity behind a simple, safe interface.