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

1b4a6c2e7bafdb5c945865dac35a9eb3?s=47 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!)

1b4a6c2e7bafdb5c945865dac35a9eb3?s=128

Dan Kim

October 05, 2018
Tweet

Transcript

  1. Android KTX A dash of Kotlin makes all the difference!

    @dankim
  2. Android KTX A dash of Kotlin makes all the difference!

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

  4. Kotlin extensions designed specifically for Android

  5. So why should I care?

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

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

    lot more fun
  8. Android has a long, colorful history Android has a long,

    colorful history
  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() } })
  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() } })
  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() }
  12. You can learn a lot

  13. Are we adding any new functionality?

  14. Are we leveraging Kotlin-specific language features?

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

  16. Does the extension provide a meaningful upgrade?

  17. Is the intent of the extension clear?

  18. Enough talk, more code!

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

  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
  21. androidx.core:core-ktx

  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
  23. A note about examples

  24. Named parameters & default values

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

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

    100)
  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
  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
  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 = { ... })
  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 { ... }
  31. // Implementation fun Animator.doOnEnd(crossinline action: (a: Animator) -> Unit) =

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

    addListener(onEnd = action)
  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
  34. animator.doOnEnd { } animator.doOnCancel { } animator.doOnPause { } animator.doOnRepeat

    { } animator.doOnResume { } animator.doOnStart { }
  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
  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 = { ... })
  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 { ... }
  38. Operator Overloading

  39. a++ a-- a + b a += b a *

    b a / b a % b a..b a in b a[i] a[i] = b ...
  40. bitmap.getPixel(100, 100)

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

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

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

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

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

    colorInt
  46. // Implementation inline operator fun Bitmap.set( x: Int, y: Int,

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

    @ColorInt color: Int) = setPixel(x, y, color)
  48. rect.union(rect2)

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

  50. // Implementation inline operator fun Rect.plus(r: Rect): Rect { return

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

    Rect(this).apply { union(r) }x }x
  52. menu.removeItem(menuItem.itemId)

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

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

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

  56. Destructuring

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

  58. // Original val lat = location.latitude val long = location.longitude

    // KTX val (lat, long) = location
  59. // Implementation inline operator fun Location.component1() = this.latitude inline operator

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

    fun Location.component2() = this.longitude
  61. val left = rect.left val top = rect.top val right

    = rect.right val bottom = rect.bottom
  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
  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
  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
  65. val red = colorInt.red val green = colorInt.green val blue

    = colorInt.blue val alpha = colorInt.alpha
  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
  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
  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
  69. Pseudo Collections

  70. viewGroup.childCount != 0

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

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

    fun ViewGroup.isNotEmpty() = childCount != 0
  73. for (index in 0 until viewGroup.childCount) { doSomething(viewGroup.getChildAt(index)) } x

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

    } x // KTX viewGroup.forEach { doSomething() }
  75. // Implementation inline fun ViewGroup.forEach(action: (view: View) -> Unit) {

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

    for (index in 0 until childCount) { action(getChildAt(index)) } x } x
  77. viewGroup.indexOfChild(view) != -1

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

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

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

    -1
  81. viewGroup.contains(view.textView) viewGroup.forEach { ... } viewGroup.forEachIndexed { index, view ->

    ... } viewGroup.size viewGroup.isEmpty() viewGroup.isNotEmpty() val view = viewGroup[0]
  82. for (index in 0 until menu.size()) { if (menu.getItem(index) ==

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

    (menu.getItem(index) == menuItem) { return true } x } x return false // KTX menu.contains(menuItem)
  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
  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
  86. menu.contains(menuItem) menu.size menu.isEmpty() menu.isNotEmpty() menu.forEach { ... } menu.forEachIndexed {

    i, item -> ... }
  87. Making Life Easier / Discoverability

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

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

    100)
  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
  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
  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)
  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()
  94. sharedPrefs.edit() .putBoolean(key, value) .apply()

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

    KTX sharedPrefs.edit { putBoolean(key, value) }
  96. val builder = SpannableStringBuilder() val start = builder.length builder.append("Hello") builder.setSpan(

    StyleSpan(Typeface.BOLD), start, builder.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE )x
  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") }
  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") } } }
  99. bundle.putString("Hello", “KotlinConf") bundle.putInt((“Kotlin"), 100) bundle.putBoolean("Java", false)x

  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)
  101. // Implementation fun bundleOf(vararg pairs: Pair<String, Any?>) = 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<Parcelable>) } String::class.java.isAssignableFrom(componentType) -> { putStringArray(key, value as Array<String>) } CharSequence::class.java.isAssignableFrom(componentType) -> { putCharSequenceArray(key, value as Array<CharSequence>)
  102. db.beginTransaction() try { ...x db.setTransactionSuccessful() } finally { db.endTransaction() }x

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

    db.endTransaction() }x // KTX db.transaction { ... }
  104. ContextCompat.getSystemService(this, AlarmManager::class.java)

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

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

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

    ... }
  108. Things to keep in mind

  109. Conflicts with your own extensions?

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

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

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

  113. Contributing to KTX

  114. AOSP

  115. They're "just" extensions

  116. Low barrier to entry

  117. Don't be discouraged by "no"

  118. Start with discussion

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

  120. Thank you! @dankim