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

Android KTX: A dash of Kotlin makes all the difference! (KotlinConf 2018)

Dan Kim
October 05, 2018

Android KTX: A dash of Kotlin makes all the difference! (KotlinConf 2018)

When Kotlin hit the Android scene, one of the first things many of us did was wrap difficult-to-use Android Framework APIs with extension functions. That little sprinkle of Kotlin made all the difference, and life was great — we finally we had a way to make our least favorite calls more concise and easier to work with!

Android KTX builds on this same idea and makes them available to everyone. It's a new set of open source Kotlin extensions (maintained by Google) that sits on top of commonly used Framework methods, abstracting much of the complexity and boilerplate away. This vastly improves the conciseness and readability of your code, making it more idiomatic and enjoyable to work with.

In this talk we'll start with a brief intro to KTX — what it is, why it's so great, and what it means for us as Android developers. Next we'll take a tour of what extensions are currently available, highlighting some of my favorites. We'll compare the old, hard ways of doing things against the new, friendlier ways that KTX provides so you can clearly see how life with KTX is better!

By the end you’ll have a better idea of 1) how you can improve your Android code by using KTX in your daily work and 2) what well-designed extension methods look like so you can wrap/extend the Android framework for your own use (or to contribute back into KTX!)

Dan Kim

October 05, 2018
Tweet

More Decks by Dan Kim

Other Decks in Programming

Transcript

  1. Android KTX
    A dash of Kotlin makes

    all the difference!
    @dankim

    View full-size slide

  2. Android KTX
    A dash of Kotlin makes

    all the difference!
    @dankim

    View full-size slide

  3. What is KTX?
    (besides just a cool name)

    View full-size slide

  4. Kotlin extensions designed
    specifically for Android

    View full-size slide

  5. So why should I care?

    View full-size slide

  6. Makes Android development more
    concise, pleasant, and idiomatic®

    View full-size slide

  7. Writing clean, readable code is a
    lot easier and a lot more fun

    View full-size slide

  8. Android has a long,
    colorful history
    Android has a long,
    colorful history

    View full-size slide

  9. view.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
    override fun onLayoutChange(
    view: View?,
    left: Int,
    top: Int,
    right: Int,
    bottom: Int,
    oldLeft: Int,
    oldTop: Int,
    oldRight: Int,
    oldBottom: Int
    ) {
    doSomething()
    }
    })

    View full-size slide

  10. view.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
    override fun onLayoutChange(
    view: View?,
    left: Int,
    top: Int,
    right: Int,
    bottom: Int,
    oldLeft: Int,
    oldTop: Int,
    oldRight: Int,
    oldBottom: Int
    ) {
    doSomething()
    }
    })

    View full-size slide

  11. // Original
    view.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
    override fun onLayoutChange(
    view: View?,
    left: Int,
    top: Int,
    right: Int,
    bottom: Int,
    oldLeft: Int,
    oldTop: Int,
    oldRight: Int,
    oldBottom: Int
    ) {
    ...
    }
    })
    // KTX
    view.doOnNextLayout { doSomething() }

    View full-size slide

  12. You can learn a lot

    View full-size slide

  13. Are we adding any
    new functionality?

    View full-size slide

  14. Are we leveraging
    Kotlin-specific language features?

    View full-size slide

  15. Will the extension be useful
    in the long-term?

    View full-size slide

  16. Does the extension provide a
    meaningful upgrade?

    View full-size slide

  17. Is the intent
    of the extension clear?

    View full-size slide

  18. Enough talk, more code!

    View full-size slide

  19. repositories {
    google()
    }
    dependencies {
    implementation 'androidx.core:core-ktx:1.0.0'
    }

    View full-size slide

  20. androidx.core:core-ktx
    androidx.fragment:fragment-ktx
    androidx.palette:palette-ktx
    androidx.sqlite:sqlite-ktx
    androidx.collection:collection-ktx
    androidx.lifecycle:lifecycle-viewmodel-ktx
    androidx.lifecycle:lifecycle-reactivestreams-ktx
    android.arch.navigation:navigation-common-ktx
    android.arch.navigation:navigation-fragment-ktx
    android.arch.navigation:navigation-runtime-ktx
    android.arch.navigation:navigation-testing-ktx
    android.arch.navigation:navigation-ui-ktx
    android.arch.work:work-runtime-ktx

    View full-size slide

  21. androidx.core:core-ktx

    View full-size slide

  22. androidx.core.animation
    androidx.core.content
    androidx.core.graphics
    androidx.core.graphics.drawable
    androidx.core.net
    androidx.core.os
    androidx.core.preference
    androidx.core.text
    androidx.core.transition
    androidx.core.util
    androidx.core.view
    androidx.core.widget

    View full-size slide

  23. A note about examples

    View full-size slide

  24. Named parameters &
    default values

    View full-size slide

  25. drawable.setBounds(drawable.bounds.left,
    drawable.bounds.top,
    drawable.bounds.right,
    100)

    View full-size slide

  26. // Original
    drawable.setBounds(drawable.bounds.left,
    drawable.bounds.top,
    drawable.bounds.right,
    100)
    // KTX
    drawable.updateBounds(bottom = 100)

    View full-size slide

  27. // Implementation
    fun Drawable.updateBounds(@Px left: Int = bounds.left,
    @Px top: Int = bounds.top,
    @Px right: Int = bounds.right,
    @Px bottom: Int = bounds.bottom) {xx
    setBounds(left, top, right, bottom)
    }xx

    View full-size slide

  28. // Original
    animator.addListener(object : Animator.AnimatorListener {
    override fun onAnimationEnd(a: Animator?) { ... }
    override fun onAnimationStart(a: Animator?) { }
    override fun onAnimationCancel(a: Animator?) { }
    override fun onAnimationRepeat(a: Animator?) { }
    })x

    View full-size slide

  29. // Original
    animator.addListener(object : Animator.AnimatorListener {
    override fun onAnimationEnd(a: Animator?) { ... }
    override fun onAnimationStart(a: Animator?) { }
    override fun onAnimationCancel(a: Animator?) { }
    override fun onAnimationRepeat(a: Animator?) { }
    })x
    // KTX
    animator.addListener(onEnd = { ... })

    View full-size slide

  30. // Original
    animator.addListener(object : Animator.AnimatorListener {
    override fun onAnimationEnd(a: Animator?) { ... }
    override fun onAnimationStart(a: Animator?) { }
    override fun onAnimationCancel(a: Animator?) { }
    override fun onAnimationRepeat(a: Animator?) { }
    })
    // KTX
    animator.addListener(onEnd = { ... })
    animator.doOnEnd { ... }

    View full-size slide

  31. // Implementation
    fun Animator.doOnEnd(crossinline action: (a: Animator) -> Unit) =
    addListener(onEnd = action)

    View full-size slide

  32. // Implementation
    fun Animator.doOnEnd(crossinline action: (a: Animator) -> Unit) =
    addListener(onEnd = action)

    View full-size slide

  33. // Implementation
    inline fun Animator.addListener(
    crossinline onEnd: (a: Animator) -> Unit = {},
    crossinline onStart: (a: Animator) -> Unit = {},
    crossinline onCancel: (a: Animator) -> Unit = {},
    crossinline onRepeat: (a: Animator) -> Unit = {}
    ): Animator.AnimatorListener {x
    ...
    }x

    View full-size slide

  34. animator.doOnEnd { }
    animator.doOnCancel { }
    animator.doOnPause { }
    animator.doOnRepeat { }
    animator.doOnResume { }
    animator.doOnStart { }

    View full-size slide

  35. transition.addListener(object : Transition.TransitionListener {
    override fun onTransitionEnd(t: Transition?) { ... }
    override fun onTransitionStart(t: Transition?) { }
    override fun onTransitionCancel(t: Transition) { }
    override fun onTransitionResume(t: Transition?) { }
    override fun onTransitionPause(t: Transition?) { }
    })x

    View full-size slide

  36. // Original
    transition.addListener(object : Transition.TransitionListener {
    override fun onTransitionEnd(t: Transition?) { ... }
    override fun onTransitionStart(t: Transition?) { }
    override fun onTransitionCancel(t: Transition) { }
    override fun onTransitionResume(t: Transition?) { }
    override fun onTransitionPause(t: Transition?) { }
    })x
    // KTX
    transition.addListener(onEnd = { ... })

    View full-size slide

  37. // Original
    transition.addListener(object : Transition.TransitionListener {
    override fun onTransitionEnd(t: Transition?) { ... }
    override fun onTransitionStart(t: Transition?) { }
    override fun onTransitionCancel(t: Transition) { }
    override fun onTransitionResume(t: Transition?) { }
    override fun onTransitionPause(t: Transition?) { }
    })
    // KTX
    transition.addListener(onEnd = { ... })
    transition.doOnEnd { ... }

    View full-size slide

  38. Operator Overloading

    View full-size slide

  39. a++
    a--
    a + b
    a += b
    a * b
    a / b
    a % b
    a..b
    a in b
    a[i]
    a[i] = b
    ...

    View full-size slide

  40. bitmap.getPixel(100, 100)

    View full-size slide

  41. // Original
    bitmap.getPixel(100, 100)
    // KTX
    bitmap[100, 100]

    View full-size slide

  42. // Implementation
    inline operator fun Bitmap.get(x: Int, y: Int) = getPixel(x, y)

    View full-size slide

  43. // Implementation
    inline operator fun Bitmap.get(x: Int, y: Int) = ...

    View full-size slide

  44. bitmap.setPixel(100, 100, colorInt)

    View full-size slide

  45. // Original
    bitmap.setPixel(100, 100, colorInt)
    // KTX
    bitmap[100, 100] = colorInt

    View full-size slide

  46. // Implementation
    inline operator fun Bitmap.set(
    x: Int,
    y: Int,
    @ColorInt color: Int) = setPixel(x, y, color)

    View full-size slide

  47. // Implementation
    inline operator fun Bitmap.set(
    x: Int,
    y: Int,
    @ColorInt color: Int) = setPixel(x, y, color)

    View full-size slide

  48. rect.union(rect2)

    View full-size slide

  49. // Original
    rect.union(rect2)
    // KTX
    rect + rect2

    View full-size slide

  50. // Implementation
    inline operator fun Rect.plus(r: Rect): Rect {
    return Rect(this).apply {
    union(r)
    }xx
    }xx

    View full-size slide

  51. // Implementation
    inline operator fun Rect.plus(r: Rect): Rect {
    return Rect(this).apply {
    union(r)
    }x
    }x

    View full-size slide

  52. menu.removeItem(menuItem.itemId)

    View full-size slide

  53. // Original
    menu.removeItem(menuItem.itemId)
    // KTX
    menu -= menuItem

    View full-size slide

  54. // Implementation
    inline operator fun Menu.minusAssign(item: MenuItem) =
    removeItem(item.itemId)

    View full-size slide

  55. // Implementation
    inline
    operator fun Menu.
    minusAssign(item: MenuItem) =
    removeItem(item.itemId)

    View full-size slide

  56. Destructuring

    View full-size slide

  57. val lat = location.latitude
    val long = location.longitude

    View full-size slide

  58. // Original
    val lat = location.latitude
    val long = location.longitude
    // KTX
    val (lat, long) = location

    View full-size slide

  59. // Implementation
    inline operator fun Location.component1() = this.latitude
    inline operator fun Location.component2() = this.longitude

    View full-size slide

  60. // Implementation
    inline operator fun Location.component1() = this.latitude
    inline operator fun Location.component2() = this.longitude

    View full-size slide

  61. val left = rect.left
    val top = rect.top
    val right = rect.right
    val bottom = rect.bottom

    View full-size slide

  62. // Original
    val left = rect.left
    val top = rect.top
    val right = rect.right
    val bottom = rect.bottom
    // KTX
    val (left, top, right, bottom) = rect

    View full-size slide

  63. inline operator fun Rect.component1() = this.left
    inline operator fun Rect.component2() = this.top
    inline operator fun Rect.component3() = this.right
    inline operator fun Rect.component4() = this.bottom

    View full-size slide

  64. inline operator fun Rect.component1() = this.left
    inline operator fun Rect.component2() = this.top
    inline operator fun Rect.component3() = this.right
    inline operator fun Rect.component4() = this.bottom

    View full-size slide

  65. val red = colorInt.red
    val green = colorInt.green
    val blue = colorInt.blue
    val alpha = colorInt.alpha

    View full-size slide

  66. // Original
    val red = colorInt.red
    val green = colorInt.green
    val blue = colorInt.blue
    val alpha = colorInt.alpha
    // KTX
    val (red, green, blue, alpha) = colorInt

    View full-size slide

  67. // Implementation
    fun @receiver:ColorInt Int.component1() = (this shr 24) and 0xff
    fun @receiver:ColorInt Int.component2() = (this shr 16) and 0xff
    fun @receiver:ColorInt Int.component3() = (this shr 8) and 0xff
    fun @receiver:ColorInt Int.component4() = this and 0xffx

    View full-size slide

  68. // Implementation
    fun @receiver:ColorInt Int.component1() = (this shr 24) and 0xff
    fun @receiver:ColorInt Int.component2() = (this shr 16) and 0xff
    fun @receiver:ColorInt Int.component3() = (this shr 8) and 0xff
    fun @receiver:ColorInt Int.component4() = this and 0xffx

    View full-size slide

  69. Pseudo Collections

    View full-size slide

  70. viewGroup.childCount != 0

    View full-size slide

  71. // Original
    viewGroup.childCount != 0
    // KTX
    viewGroup.isEmpty()
    viewGroup.isNotEmpty()

    View full-size slide

  72. // Implementation
    inline fun ViewGroup.isEmpty() = childCount == 0
    inline fun ViewGroup.isNotEmpty() = childCount != 0

    View full-size slide

  73. for (index in 0 until viewGroup.childCount) {
    doSomething(viewGroup.getChildAt(index))
    }
    x

    View full-size slide

  74. // Original
    for (index in 0 until viewGroup.childCount) {
    doSomething(viewGroup.getChildAt(index))
    }
    x
    // KTX
    viewGroup.forEach { doSomething() }

    View full-size slide

  75. // Implementation
    inline fun ViewGroup.forEach(action: (view: View) -> Unit) {
    for (index in 0 until childCount) {
    action(getChildAt(index))
    }
    x
    }
    x

    View full-size slide

  76. // Implementation
    inline fun ViewGroup.forEach(action: (view: View) -> Unit) {
    for (index in 0 until childCount) {
    action(getChildAt(index))
    }
    x
    }
    x

    View full-size slide

  77. viewGroup.indexOfChild(view) != -1

    View full-size slide

  78. // Original
    viewGroup.indexOfChild(view) != -1
    // KTX
    viewGroup.contains(view)

    View full-size slide

  79. // Implementation
    inline operator fun ViewGroup.contains(view: View) =
    indexOfChild(view) != -1

    View full-size slide

  80. // Implementation
    inline operator fun ViewGroup.contains(view: View) =
    indexOfChild(view) != -1

    View full-size slide

  81. viewGroup.contains(view.textView)
    viewGroup.forEach { ... }
    viewGroup.forEachIndexed { index, view -> ... }
    viewGroup.size
    viewGroup.isEmpty()
    viewGroup.isNotEmpty()
    val view = viewGroup[0]

    View full-size slide

  82. for (index in 0 until menu.size()) {
    if (menu.getItem(index) == menuItem) {
    return true
    }
    x
    }
    x
    return false

    View full-size slide

  83. // Original
    for (index in 0 until menu.size()) {
    if (menu.getItem(index) == menuItem) {
    return true
    }
    x
    }
    x
    return false
    // KTX
    menu.contains(menuItem)

    View full-size slide

  84. // Implementation
    operator fun Menu.contains(item: MenuItem): Boolean {
    for (index in 0 until size()) {
    if (getItem(index) == item) {
    return true
    }
    x
    }
    x
    return false
    }
    x

    View full-size slide

  85. // Implementation
    operator fun Menu.contains(item: MenuItem): Boolean {
    for (index in 0 until size()) {
    if (getItem(index) == item) {
    return true
    }
    x
    }
    x
    return false
    }
    x

    View full-size slide

  86. menu.contains(menuItem)
    menu.size
    menu.isEmpty()
    menu.isNotEmpty()
    menu.forEach { ... }
    menu.forEachIndexed { i, item -> ... }

    View full-size slide

  87. Making Life Easier /
    Discoverability

    View full-size slide

  88. Bitmap.createBitmap(100,
    100,
    Bitmap.Config.ARGB_8888,
    true,
    ColorSpace.get(ColorSpace.Named.SRGB))

    View full-size slide

  89. // Original
    Bitmap.createBitmap(100,
    100,
    Bitmap.Config.ARGB_8888,
    true,
    ColorSpace.get(ColorSpace.Named.SRGB))
    // KTX
    createBitmap(100, 100)

    View full-size slide

  90. // Implementation
    inline fun createBitmap(
    width: Int,
    height: Int,
    config: Bitmap.Config = Bitmap.Config.ARGB_8888,
    hasAlpha: Boolean = true,
    colorSpace: ColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
    ): Bitmap {
    return Bitmap.createBitmap(width,
    height,
    config,
    hasAlpha,
    colorSpace)
    }x

    View full-size slide

  91. // Implementation
    inline fun createBitmap(
    width: Int,
    height: Int,
    config: Bitmap.Config = Bitmap.Config.ARGB_8888,
    hasAlpha: Boolean = true,
    colorSpace: ColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
    ): Bitmap {
    return Bitmap.createBitmap(width,
    height,
    config,
    hasAlpha,
    colorSpace)
    }x

    View full-size slide

  92. val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    canvas.translate(-view.scrollX.toFloat(), -view.scrollY.toFloat())
    view.draw(canvas)

    View full-size slide

  93. // Original
    val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    canvas.translate(-view.scrollX.toFloat(), -view.scrollY.toFloat())
    view.draw(canvas)
    // KTX
    view.drawToBitmap()

    View full-size slide

  94. sharedPrefs.edit()
    .putBoolean(key, value)
    .apply()

    View full-size slide

  95. // Original
    sharedPrefs.edit()
    .putBoolean( key, value
    x
    )
    .apply()
    // KTX
    sharedPrefs.edit { putBoolean(key, value) }

    View full-size slide

  96. val builder = SpannableStringBuilder()
    val start = builder.length
    builder.append("Hello")
    builder.setSpan(
    StyleSpan(Typeface.BOLD),
    start,
    builder.length,
    Spanned.SPAN_INCLUSIVE_EXCLUSIVE
    )x

    View full-size slide

  97. // Original
    val builder = SpannableStringBuilder()
    val start = builder.length
    builder.append("Hello")
    builder.setSpan(
    StyleSpan(Typeface.BOLD),
    start,
    builder.length,
    Spanned.SPAN_INCLUSIVE_EXCLUSIVE
    )x
    // KTX
    builder.bold { append("Hello") }

    View full-size slide

  98. // Original
    val builder = SpannableStringBuilder()
    val start = builder.length
    builder.append("Hello")
    builder.setSpan(
    StyleSpan(Typeface.BOLD),
    start,
    builder.length,
    Spanned.SPAN_INCLUSIVE_EXCLUSIVE
    )x
    // KTX
    builder.bold { append("Hello") }
    builder.bold { italic { underline { append("Hello") } } }

    View full-size slide

  99. bundle.putString("Hello", “KotlinConf")
    bundle.putInt((“Kotlin"), 100)
    bundle.putBoolean("Java", false)x

    View full-size slide

  100. // Original
    bundle.putString("Hello", "KotlinConf")
    bundle.putInt(("Kotlin"), 100)
    bundle.putBoolean("Java", false)x
    // KTX
    bundleOf("Hello" to "KotlinConf", "Kotlin" to 100, "Java" to true)

    View full-size slide

  101. // Implementation
    fun bundleOf(vararg pairs: Pair) = Bundle(pairs.size).apply {
    for ((key, value) in pairs) {
    when (value) {
    null -> putString(key, null) // Any nullable type will suffice.
    // Scalars
    is Boolean -> putBoolean(key, value)
    is Byte -> putByte(key, value)
    is Char -> putChar(key, value)
    is Double -> putDouble(key, value)
    is Float -> putFloat(key, value)
    is Int -> putInt(key, value)
    is Long -> putLong(key, value)
    is Short -> putShort(key, value)
    // References
    is Bundle -> putBundle(key, value)
    is CharSequence -> putCharSequence(key, value)
    is Parcelable -> putParcelable(key, value)
    // Scalar arrays
    is BooleanArray -> putBooleanArray(key, value)
    is ByteArray -> putByteArray(key, value)
    is CharArray -> putCharArray(key, value)
    is DoubleArray -> putDoubleArray(key, value)
    is FloatArray -> putFloatArray(key, value)
    is IntArray -> putIntArray(key, value)
    is LongArray -> putLongArray(key, value)
    is ShortArray -> putShortArray(key, value)
    // Reference arrays
    is Array<*> -> {
    val componentType = value::class.java.componentType
    @Suppress("UNCHECKED_CAST") // Checked by reflection.
    when {
    Parcelable::class.java.isAssignableFrom(componentType) -> {
    putParcelableArray(key, value as Array)
    }
    String::class.java.isAssignableFrom(componentType) -> {
    putStringArray(key, value as Array)
    }
    CharSequence::class.java.isAssignableFrom(componentType) -> {
    putCharSequenceArray(key, value as Array)

    View full-size slide

  102. db.beginTransaction()
    try {
    ...x
    db.setTransactionSuccessful()
    } finally {
    db.endTransaction()
    }x

    View full-size slide

  103. // Original
    db.beginTransaction()
    try {
    ...x
    db.setTransactionSuccessful()
    } finally {
    db.endTransaction()
    }x
    // KTX
    db.transaction { ... }

    View full-size slide

  104. ContextCompat.getSystemService(this, AlarmManager::class.java)

    View full-size slide

  105. // Original
    ContextCompat.getSystemService(this, AlarmManager::class.java)
    // KTX
    getSystemService()

    View full-size slide

  106. view.postDelayed({ ... }, 200)

    View full-size slide

  107. // Original
    view.postDelayed({ ... }, 200)
    // KTX
    view.postDelayed(200) { ... }

    View full-size slide

  108. Things to keep in mind

    View full-size slide

  109. Conflicts with your own
    extensions?

    View full-size slide

  110. What’s good for core-ktx is not a
    value judgement on all extensions

    View full-size slide

  111. Should I still use Anko &
    Kotlin Android Extensions?

    View full-size slide

  112. KEEP-110
    https://github.com/Kotlin/KEEP/issues/110

    View full-size slide

  113. Contributing to KTX

    View full-size slide

  114. They're "just" extensions

    View full-size slide

  115. Low barrier to entry

    View full-size slide

  116. Don't be discouraged by "no"

    View full-size slide

  117. Start with discussion

    View full-size slide

  118. AOSP:
    https://android.googlesource.com/platform/frameworks/support/
    Issue tracker:
    https://issuetracker.google.com/issues?q=componentid:
    396204%20status:open

    View full-size slide

  119. Thank you!
    @dankim

    View full-size slide