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

Kotlin: MAX PAYNE (Voxxed Days Minsk, May 2018)

Kotlin: MAX PAYNE (Voxxed Days Minsk, May 2018)

Kotlin gives us a lot of neat features and just general developer satisfaction, but little is known about its hidden costs and pain it causes. This talk is a almost a 3 year combination of small (and no so small) issues we encountered when developing a full-Kotlin application started from the scratch. Bugs, features, performance, testing — it’s all there with our answers, conclusions and advises.

Presented at Voxxed Days Minsk conference on May 26, 2018.

Artur Dryomov

May 26, 2018
Tweet

More Decks by Artur Dryomov

Other Decks in Programming

Transcript

  1. Kotlin: MAX PAYNE
    Artur Dryomov, Juno Lab
    @arturdryomov

    View full-size slide

  2. Juno Kotlin
    Kotlin Juno

    View full-size slide

  3. Juno Rider
    3 years
    100 % Kotlin
    Kotlin EAP in production
    1.1-EAP
    — 6 months
    1.2-EAP
    — 3 months
    99.9 % crash‑free
    2 week sprints
    4 developers, 1 QA

    View full-size slide

  4. Juno Rider
    cloc main/kotlin
    — 56 K
    cloc test/kotlin
    — 49 K
    cloc androidTest/kotlin
    — 17 K

    View full-size slide

  5. Java Tools
    Nope

    View full-size slide

  6. Specifications
    Java AST (Abstract Syntax Tree)
    PSI (Program Structure Interface)
    UAST (Universal AST)

    View full-size slide

  7. Checkstyle
    Checkstyle works only with AST as we depend on ANTLR and Java
    grammar.
    https://github.com/checkstyle/checkstyle/issues/4369

    View full-size slide

  8. Kotlin Tools
    IntelliJ IDEA
    detekt
    + ktlint
    android/lint
    kotlinc --force
    tasks.withType {
    kotlinOptions {
    allWarningsAsErrors = true
    }
    }

    View full-size slide

  9. Infinity War
    val gameBuilder = StringBuilder()
    gameBuilder.append("tic")
    gameBuilder.append("tac")
    gameBuilder.append("toe")
    val game = gameBuilder.toString()

    View full-size slide

  10. Infinity War
    val game = StringBuilder().also {
    it.append("tic")
    it.append("tac")
    it.append("toe")
    }
    .toString()

    View full-size slide

  11. Infinity War
    val game = StringBuilder().let {
    it.append("tic")
    it.append("tac")
    it.append("toe")
    it.toString()
    }

    View full-size slide

  12. Infinity War
    val game = StringBuilder().apply {
    append("tic")
    append("tac")
    append("toe")
    }
    .toString()

    View full-size slide

  13. Infinity War
    val game = StringBuilder().run {
    append("tic")
    append("tac")
    append("toe")
    toString()
    }

    View full-size slide

  14. Infinity War
    val game = with(StringBuilder()) {
    append("tic")
    append("tac")
    append("toe")
    toString()
    }

    View full-size slide

  15. Infinity War
    ?
    CODE_STYLE.md

    View full-size slide

  16. Destructuring
    val (first, second) = Pair(1, 2)
    val (first, second, third) = Triple(1, 2, 3)
    listOf(1 to "one", 2 to "two").map { (number, text) ->
    println("number is $number, text is $text")
    }

    View full-size slide

  17. Destructuring
    data class Person(val firstName: String, val lastName: String)
    val (firstName, lastName) = Person("f", "l")
    data class Person(val lastName: String, val firstName: String)
    val (firstName, lastName) = Person("f", "l") // It works!

    View full-size slide

  18. Destructuring
    data class Person(val firstName: String, val lastName: String)
    {
    operator fun component1() = firstName
    operator fun component2() = lastName
    }

    View full-size slide

  19. Destructuring
    Pair
    , Triple
    data class

    View full-size slide

  20. null
    safety?
    Observable.just(null)
    public static Observable just(T item) {
    ObjectHelper.requireNonNull(item, "The item is null");
    ...

    View full-size slide

  21. null
    safety!!
    @Nullable
    + @NonNull

    View full-size slide

  22. null
    safety!!
    Docs.
    Annotations.
    Abstractions.

    View full-size slide

  23. RxJava 2
    val o: Observable = Observable.just(null)
    // Runtime: NullPointerException
    public static Observable just(T item) {
    ObjectHelper.requireNonNull(item, "The item is null");
    ...

    View full-size slide

  24. Optional
    Swift: ?
    , nil enum Optional
    Kotlin: ?
    , null null

    View full-size slide

  25. Koptional
    https://github.com/gojuno/koptional
    val some = Some(value)
    val none = None
    val optional = nullableValue.toOptional()
    val nullable = o.toNullable()

    View full-size slide

  26. Mockito
    interface Api {
    fun call(id: String)
    }
    class Service(private val api: Api) {
    fun call() = api.call(generateId())
    }
    @Test fun test() {
    val api = Mockito.mock()
    Service(api).call()
    verify(api).call(Mockito.any())
    }
    // Runtime: NullPointerException

    View full-size slide

  27. Mockito
    class Mockito {
    public static T any() {
    return null;
    }
    }

    View full-size slide

  28. Mockito
    fun anything(): T = Mockito.any()
    @Test fun test() {
    val api = Mockito.mock()
    Service(api).call()
    verify(api).call(anything())
    }

    View full-size slide

  29. Mockito
    fun eq(value: T): T = Mockito.eq(obj)
    fun anything(): T = Mockito.any()
    fun whenever(call: T) = Mockito.`when`(call)

    View full-size slide

  30. Gson
    data class Model {
    @SerializedName("model_field")
    val field: String
    }
    val model = Gson().fromJson("{}", Model::javaClass)
    // Runtime: OK
    model.field.toString()
    // Runtime: NullPointerException

    View full-size slide

  31. Gson
    Java Reflection

    View full-size slide

  32. Gson
    data class Model {
    @SerializedName("model_field")
    val field: String
    } : Validatable {
    override fun validate() {
    if (field == null) throw ParseException()
    }
    }

    View full-size slide

  33. Gson
    Moshi vs. Jackson
    Kotlin Reflection (2.5 MiB)

    View full-size slide

  34. null
    safety!!
    Kotlin: The Problem with null
    https://arturdryomov.online/posts/kotlin‑the‑problem‑with‑null/

    View full-size slide

  35. Optimizations

    View full-size slide

  36. inline
    private fun createId(): Int = 42
    vs.
    @Suppress("nothing_to_inline")
    private inline fun createId(): Int = 42

    View full-size slide

  37. inline
    https://youtrack.jetbrains.com/issue/KT‑16769
    Revisit compiler and IDE warning about inline functions

    View full-size slide

  38. const
    companion object {
    private val NUMBER = 42
    }
    public static final class Companion {
    private final int NUMBER = 42;
    private final int getNUMBER() {
    return NUMBER;
    }
    }

    View full-size slide

  39. const
    companion object {
    private const val NUMBER = 42
    }
    public static final class Companion {
    private static final int NUMBER = 42;
    }

    View full-size slide

  40. const
    Primitives + String
    kotlinc
    ?

    View full-size slide

  41. Tools Kotlin Show Kotlin Bytecode

    View full-size slide

  42. Bugs
    fun multiply(double: Double) = double * double
    1.1.3
    org.jetbrains.kotlin.kapt3.diagnostic.KaptError
    Error while annotation processing

    View full-size slide

  43. Bugs
    https://youtrack.jetbrains.com/issue/KT‑18377
    Syntax error while generating kapt stubs

    View full-size slide

  44. Bugs
    Are a thing!

    View full-size slide

  45. Assemble
    javac
    kotlinc
    kapt

    View full-size slide

  46. kapt
    Kotlin Annotation Processing Tool
    Breaks incremental compilation.
    Throws errors.
    *_MemberInjector.java: error: package does not exist

    View full-size slide

  47. kapt
    A Dagger to Remember
    https://arturdryomov.online/posts/a‑dagger‑to‑remember/

    View full-size slide

  48. Assemble
    — i5-5257U @ 2.70GHz
    — i7-6700K @ 4.00GHz

    View full-size slide

  49. Mainframer
    https://github.com/gojuno/mainframer
    $ rsync laptop mainframe
    $ make
    $ rsync mainframe laptop

    View full-size slide

  50. Mainframer
    $ ./gradlew assembleDebug
    4 minutes 10 seconds
    $ mainframer ./gradlew assembleDebug
    1 minute 10 seconds

    View full-size slide

  51. Assemble
    Remove everything possible.
    Gradle build cache.
    Mainframer.

    View full-size slide

  52. Gradle Kotlin DSL

    View full-size slide

  53. Kotlin versions
    Gradle 4.2 — 1.1.4‑3
    Project — 1.2.0‑beta‑31
    Runtime files in the classpath should have the same version.
    These files were found in the classpath:
    .../dists/.../kotlin-stdlib-1.1.4-3.jar (version 1.1)
    .../caches/.../kotlin-stdlib-1.2.0-beta-31.jar (version 1.2)

    View full-size slide

  54. Kotlin versions
    https://github.com/gradle/kotlin‑dsl/issues/519

    View full-size slide

  55. Groovy
    unitTests.all {
    maxParallelForks = 2
    }
    unitTests.all(KotlinClosure1({
    apply {
    maxParallelForks = 2
    }
    }, this))

    View full-size slide

  56. Groovy
    import groovy.lang.Closure;
    public void unitTests(Closure closure)
    https://issuetracker.google.com/issues/78547461

    View full-size slide

  57. Groovy
    2012: groovy.lang.Closure
    2017: org.gradle.api.Action

    View full-size slide

  58. Groovy
    Promote DSL‑agnostic API

    View full-size slide

  59. Gradle Kotlin DSL
    IDE
    Dynamic Groovy
    Workflow
    Alpha!

    View full-size slide

  60. JUnit
    class SystemTest {
    lateinit var system: System
    @BeforeEach fun create() {
    system = System()
    }
    @BeforeEach fun start() {
    system.boot()
    }
    @Test fun `has boot status`() {
    assertThat(system.status()).isEqualTo(BOOTED)
    }
    @Test fun `has positive uptime`() {
    assertThat(system.uptime()).isNotZero()
    }
    }

    View full-size slide

  61. Spek
    class SystemSpec : Spek({
    val system by memoized { System() }
    context("boot") {
    beforeEachTest {
    system.boot()
    }
    it("has boot status") {
    assertThat(system.status()).isEqualTo(BOOTED)
    }
    it("has positive uptime") {
    assertThat(system.uptime()).isNotZero()
    }
    }
    })

    View full-size slide

  62. Versions
    1.0.89
    : DescribeBody Dsl
    1.1.0-beta3
    : Dsl SpecBody
    1.0
    , 1.1.19
    : do not exist.
    JUnit 4 5

    View full-size slide

  63. memoized
    before and after
    class JustSpec : Spek({
    val system by memoized { System() }
    beforeEachTest {
    println("before $system") // system_before
    }
    afterEachTest {
    println("after $system") // system_after
    }
    })
    // system_before != system_after

    View full-size slide

  64. memoized
    before and after
    https://github.com/JetBrains/spek/issues/213
    Memoized value is not the same before and after test

    View full-size slide

  65. JUnit
    JUnit 5!
    Android Studio
    JUnit 4?
    @RunWith(JUnitPlatform::class)
    fon
    , fit
    Spek IJ plugin.
    JUnit 4
    Fun

    View full-size slide

  66. Spek
    jetbrains/spek spekframework/spek
    Compatibility
    Docs
    Bugs
    100 K tests
    2.0

    View full-size slide

  67. Spek
    Spectrum, KotlinTest

    View full-size slide

  68. https://youtrack.jetbrains.com/issues/KT

    View full-size slide

  69. ...
    Kotlin is a thin layer on top of Java, and so it perpetuates through a lot of
    the Javaisms in its model.
    ...
    Chris Lattner (Swift, LLVM, Clang)

    View full-size slide

  70. Java — Executive Committee
    Oracle, Red Hat, Intel, IBM, Eclipse, Twitter, JetBrains...
    24 members
    Kotlin — Language Committee
    JetBrains, Google
    3 members

    View full-size slide

  71. Conclusion
    JVM
    Tooling
    Language

    View full-size slide