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

Jusqu'où aller trop loin avec Kotlin ?

Jusqu'où aller trop loin avec Kotlin ?

Kotlin nous permet de programmer avec une expressivité incroyable : DSL élégants, extensions omniprésentes, opérateurs surchargés, inline, reified… Mais à force de vouloir écrire du code "beau", certaines équipes finissent par écrire du code que plus personne n’ose lire ni modifier.

Dans ce talk, nous partagerons un retour d’expérience honnête sur des choix Kotlin "trop malins" : DSLs excessives, extensions illisibles, abstractions VS délégation… devenues des freins à la compréhension, à l’onboarding et à l’évolution du produit.

Sans condamner Kotlin — au contraire — nous discuterons du vrai coût de l’élégance, et proposerons des critères concrets pour trouver le bon équilibre entre expressivité, lisibilité et robustesse collective.

Un talk pour les développeurs Android qui regrettent d’avoir fait les malins... et ne savent plus se relire.

(Android Makers DroidCon 2026)

Avatar for Baptiste CARLIER

Baptiste CARLIER

April 10, 2026

More Decks by Baptiste CARLIER

Other Decks in Programming

Transcript

  1. Jusqu’où aller trop loin avec Kotlin Une fable moderne du

    développement mobile Baptiste CARLIER Antoine ROBIEZ
  2. &/ Une fonction java de 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")); } } User.java
  3. &/ Merci le 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")) } } User.kt
  4. &/ Le Kotlin idiomatique qu'on aime tous : fun processUser(dto:

    UserDto?) = runCatching { requireNotNull(dto&.name) { "Invalid User" } User( name = dto.name, isPremium = (dto.age &: 0) > 18 ) } KotlinUser.kt
  5. &/ Quelques mois plus tard, le même besoin, mais écrit

    par une équipe "trop maline" : 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") } SuperKotlinUser.kt
  6. Jusqu’où aller trop loin avec Kotlin Une fable moderne du

    développement mobile Baptiste CARLIER Antoine ROBIEZ
  7. 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 Kotlin 1.0
  8. Le péché originel : Pair<String, String> fun getUserInfo(): Pair<String, String>

    { &&. return "Nicolas" to "FALLON" } &/ Inversion facile : est-ce (Nom, Prénom) ou (Prénom, Nom) ?
  9. Bien consommer une Pair val pair = myFunction(item) pair.first &/

    C'est quoi déjà le premier ? &/ Je dois remonter à la définition de la fonction.
  10. Bien consommer une Pair &/ Mieux : Le Destructuring val

    (title, description) = myFunction(item) &/ On redonne du sens immédiatement. &/ La portée du "mystère" est réduite au minimum
  11. Le piège des Scope Functions avec les Pair &/ 🔴

    Le Red Flag : myFunction(item).let { it.first } &/ ✅ Acceptable (si court) : myFunction(item).let { (title, description) &> … }
  12. data class Address( val street: String, val city: String, val

    zipCode: String ) val address = Address("Rue du Sport", "Lille", "59000") val (street, zipCode, city) = address println("Street: $street / Zip Code: $zipCode / City: $city") &/ Street: Rue du Sport / Zip Code: Lille / City: 59000 ❌
  13. Name-based destructuring Experimental kotlin { explicitApi() compilerOptions { freeCompilerArgs.add("-Xname-based-destructuring=only-syntax") }

    } val address = Address("Rue du Sport", "Lille", "59000") val [x, y, z] = address &/ Déstructuration par position avec [] “only-syntax" “name-mismatch" “complete" Kotlin 2.4
  14. +a operator fun unaryPlus(): R -a operator fun unaryMinus(): R

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

    val isItTheWeekEndSoonUseCase = IsItTheWeekEndSoonUseCase() if (isItTheWeekEndSoonUseCase()) {
  16. data class Point(val x: Int, val y: Int) { operator

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

    Vector(2, 5) newPoint += Vector(2, 8)
  18. data class Version(val major: Int, val minor: Int, val patch:

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

    { &/ Gérer les versions inférieures à 10 } 10.5.0 10.4.6 11.0.2
  20. 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) )
  21. class Company { … operator fun minus(dev: Employee) = this.also

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

  23. &/ UTILE : Construction de graphe ou de contraintes val

    constraint = view1 alignLeftTo view2 val range = 10 step 2 &/ VALIDE : Le "to" de la Standard Library val map = mapOf("key" to "value") &/ VALIDE : DSL custom d’assertion mockedCall assertCalled 3 THE_GOOD.kt THE_BAD.kt THE_BUG.kt
  24. THE_GOOD.kt THE_BAD.kt THE_BUG.kt ?/ Simple order.processWithPaymentMethod("CreditCard") .executeAfterDelay(500) ?/ OK tiers

    order processWithPaymentMethod "CreditCard" executeAfterDelay 500 ?/ Horrible order processWithPaymentMethod "CreditCard" executeAfterDelay 500
  25. ?/ ATTENTION : val result = 1 + 2 multiply

    3 ?/ result = 9 ou 7 ? ?> 9 ❌ THE_GOOD.kt THE_BAD.kt THE_BUG.kt
  26. 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 €" } 💣 Pollution des types universels
  27. dpToPx() Est-ce une transformation pure ? Context.showToast() Est-ce lié à

    un contexte technique Android/Lib ? User.isAdmin() Est-ce de la logique métier ? String.encrypt() Est-ce que ça nécessite une injection ? private La règle de anti-pollution / pro auto completion value class Est-ce que le receiver de l’extension est un type primitif ? Doc & Com Eviter de recréer plusieurs fois la même extention
  28. fun measureTime(action: () &> Unit) { val start = System.currentTimeMillis()

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

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

    System.currentTimeMillis() action() val end = System.currentTimeMillis() println("Execution took ${end - start} ms") }
  31. 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"); measureTime { println("I’m doing something heavy&&.") }
  32. &/ Without inline myFile.use { if (error) return@main it.read() }

    &/ With inline myFile.use { if (error) return it.read() }
  33. Réifier Transformer en chose, réduire à l'état d'objet (un individu,

    une chose abstraite) L'essence de l'encrier n'est pas l'encrier et elle n'est pas l'être, en tant qu'elle est en soi, elle est négation hypostasiée, réifiée, c'est- à-dire précisément qu'elle est un rien, elle appartient au manchon de néant qui entoure et détermine le monde (SARTRE, Être et Néant, 1943, p. 247)
  34. 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) } } }
  35. 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) } } }
  36. val list = listOf("Bonjour", 42, "Kotlin") list.filterAndProcess<String> { text ->

    println("J'ai trouvé une chaîne : $text") } single { CountryInterceptor() }
  37. 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() } }
  38. 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() } }
  39. 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() } }
  40. interface Box<T> { fun get(): T } fun displayFruit(box: Box<Fruit>)

    { val fruit = box.get() println("J’ai eu un fruit : $fruit") }
  41. 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
  42. interface Box<out T> { fun get(): T } fun displayFruit(box:

    Box<Fruit>) { val fruit = box.get() println("J’ai eu un fruit.") } displayFruit(appleBox)
  43. Covariance Si tu sais que tu vas me donner une

    pomme et que je te demande un fruit, c'est OK. <out T>
  44. Covariance Si tu sais que tu vas me donner une

    pomme et que je te demande un fruit, c'est OK. Contravariance Si tu sais quoi faire d’un fruit et que je te donne une pomme, c’est OK. <out T> <in T>
  45. inline infix fun <reified T : Fruit, reified R> Consumer<in

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

    T>.process( crossinline transform: (T) &> R ): Producer<out R>
  47. 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() }
  48. TrackEvent( type = "User_Click", category = "UI", metadata = mapOf(

    id to "123", source to "Home", ) ) trackEvent("User_Click") { category { "UI" } metadata { id("123") source { "Home" } } } Natif, explicite, suggéré par l'IDE Pas d'ambiguïté de scope Facile à instancier Plus difficile à découvrir it et this ambigus Plus compliqué à tester Data class DSL
  49. navigate { target = "Home" onExit { saveToDb() &/ ⚠

    Faire du traitement (réseau, DB, navigation) pendant &/ la construction du DSL. } }
  50. val orderWorkflow = workflow { step("validate-order") { timeout(1_000) retry(2) onlyIf

    { amount > 0 } execute { this.onAction() println("Validation de base pour $amount €") } branch("fraud-check") { whenCondition { amount &= 1000 &| country &= "FR" } step("score-risk") { timeout(500) execute { println("Calcul du score de fraude pour $country") } } step("manual-review") { onlyIf { !customerVip && payment &= PaymentMethod.TRANSFER } execute { println("Passage en revue manuelle") } } Le signal d’alerte : Les niveaux d’imbrications
  51. Ne faites pas de DSL pour une configuration qui ne

    change jamais Ne faites pas de DSL si une data class avec des arguments nommés fait le job Faire un DSL si vous voulez masquer une complexité technique horrible derrière une interface simple et sécurisée RESPECT YOURSELF!
  52. S’outiller (linters, …) Documenter (le code, les choix, les outils,

    …) Établir des guidelines ensemble Challenger les décisions