Exploring KTX for Android

Yesterday Google announced android-ktx, which is a set of Kotlin extensions for Android app development. It looks like the aim of the library is to continue with the advantages that kotlin brings to our project — less code, more fun and a simpler to understand project.

Now, the library is only in preview so it is likely to change — it will probably up offering more capabilities when it is released. I’ve written this article whilst looking through the documentation for KTX and thought I’d write some notes on it as an overview. It’s likely you will just use the documentation when it comes to it, but hopefully this post will give you a look at what it currently has to offer. And if you’re not already using kotlin, you’ll see what you’re missing out on 🙂

The documentation for KTX can be found here:

https://android.github.io/android-ktx/core-ktx/index.html

and the library itself, here:

https://android.github.io/android-ktx/core-ktx/index.html

Animator functions

There’s a collection of extensions related to animations within the library, let’s take a quick look at what we have available in the current release!

Animation listener

To begin with, in KTX we can set an animation listener on an animator instance like so:

animator.addListener { handleAnimation(it) }

This allows us to receive the callbacks for an animation event. We can even add function for specific callbacks of the listener, you’ll only need to pass functions for the callbacks that you want to receive data for:

animator.addListener(
        onEnd = {}, 
        onStart = {}, 
        onCancel = {}, 
        onRepeat = {}
)

This itself is a massive reduction in the code for listener callbacks that we may not need and use.

Individual animation event listeners

We have the ability to listen for individual events in android, with KTX adding a pause listener can be done in the same way that the addListener() function:

animator.addPauseListener { handleAnimation(it) }
// or
animator.addPauseListener(
        onPause = {},
        onResume = {}
)

Again, only requiring to pass in the functions that we require to be used for the callbacks.

We can also listen for individual animation events in a single line of code:

animator.doOnPause { handleAnimation(it) }
animator.doOnCancel { handleAnimation(it) }
animator.doOnEnd { handleAnimation(it) }
animator.doOnRepeat { handleAnimation(it) }
animator.doOnStart { handleAnimation(it) }
animator.doOnResume { handleAnimation(it) }

If you’re currently using java code for animations in your Android projects, you’ll notice how much less code (and how much easier to read) this is. In each of these calls above, it represents the Animator instance that is in use.

Content

There are a bunch of extension functions that have been added within the Content package. If we need to retrieve system service then there is an extension available for us to do so:

val alarmManager = systemService<AlarmManager>()

Styled attributes can also be worked with using the provided extensions:

context.withStyledAttributes(set = someAttributeSet, attrs = attributes, defStyleAttr = ..., defStyleRes = ...) {
    // block to execute
}
context.withStyledAttributes(set = someAttributeSet, attrs =
attributes) {
    // block to execute
}

Performing write operations to shared preferences is now super nice with the use the edit function:

sharedPreferences.edit { 
    putBoolean(key, value) 
}

We can also create a new ContentValues instance using the contentValuesOf function, passing in varargs of Pair instances:

val contentValues = contentValuesOf(somePairs...)

Time operations

KTX also offers a collection of operations related to Time. Let’s take a look at what there is currently on offer.

We can now access the DayOfWeek, Month and Year instances as an Int value with a simple call:

DayOfWeek.FRIDAY.asInt()
Month.APRIL.asInt()
Year.now().asInt()

The Duration class also has a bunch of functions available:

// Retrieve values from destructuring
val (seconds, nanoseconds) = Duration.ofSeconds(1)
// Perform multiplication
val result = Duration.ofSeconds(1) * 2
// Perform division
val result = Duration.ofSeconds(2) / 2
// Perform negation
val result = -Duration.ofSeconds(2)

The Instant, LocalData, LocalDateTime, LocalTime properties can also be accessed by these functions:

// Retrieve values from destructuring
val (seconds, nanoseconds) = Instant.now()
// Retrieve values from destructuring
val (year, month, day) = LocalDate.now()
// Retrieve values from destructuring
val (localDate, localTime) = LocalDateTime.now()
// Retrieve values from destructuring
val (hour, minute, second, nanosecond) = LocalTime.now()

As well as these, accessing properties of the MonthDay, OffsetDateTime and OffsetTime classes can easily be done via these component calls:

// Retrieve values from destructuring
val (month, day) = MonthDay.now()
// Retrieve values from destructuring
val (localDataTime, ZoneOffset) = OffsetDateTime.now()
// Retrieve values from destructuring
val (localTime, ZoneOffset) = OffsetTime.now()

If you’re using the Period class, the KTX library contains some functions to access properties as well as perform some operations:

// Retrieve values from destructuring
val (years, month, days) = Period.ofDays(2)
// Perform multiplication
val result = Period.ofDays(2) * 2
// Perform negation
val result = -Period.ofDays(2)

There are also some other component functions which can be used to retrieve desired values:

// Retrieve values from destructuring
val (year, month) = YearMonth.now()
// Retrieve values from destructuring
val (localDateTime, ZoneId) = ZonedDateTime.now()

These following functions are really cool additions, these allow us to easily take an Int value and retrieve the corresponding representation for the given function call:

someInt.asDayOfWeek() // return DayOfWeek instance
someInt.asMonth() // returns Month instance
someInt.asYear() // returns Year instance
someInt.days() // returns Period instance
someInt.hours() // returns Duration instance
someInt.millis() // returns Duration instance
someInt.minutes() // returns Duration instance
someInt.months() // returns Period instance
someInt.nanos() // returns Duration instance
someInt.seconds() // returns Duration instance
someInt.years() // returns Period instance

The same goes for a Long instance also, using these functions the following representations can be retrieved:

someLong.asEpochMillis() // returns Instant instance
someLong.asEpochSeconds() // returns Instant instance
someLong.hours() // returns Duration instance
someLong.millis() // returns Duration instance
someLong.minutes() // returns Duration instance
someLong.nanos() // returns Duration instance
someLong.seconds() // returns Duration instance

OS

There are a collection of functions provided for the android OS package. This includes some extensions to working with the handler class:

handler.postAtTime(uptimeMillis = 200L) {
    // some action
}
handler.postDelayed(delayInMillis = 200L) {
    // some action
}

Creating new instance of the Bundle class is now also a lot nicer:

val bundle = bundleOf("some_key" to 12, "another_key" to 15)
val bundle = persistableBundleOf("some_key" to 12, "another_key" to 15)

And if you’re writing trace events for the Systrace tool, writing trace messages can be done nice and easily:

trace("section_name") { }

Utils

Within the Util package there are a collection of functions related to files, arrays and other general data types.

To begin with, if you’re working with AtomicFiles you’ll be able to make use of the following functions:

val fileBytes = atomicFile.readBytes()
val text = atomicFile.readText(charset = Charset.defaultCharset())
atomicFile.tryWrite {
// some write operations
}
atomicFile.writeBytes(byteArrayOf())
atomicFile.writeText("some string", charset = Charset.defaultCharset())

For the LongSparseArray, SparseArray, SparseBooleanArray, SparseIntArray, SparseLongArray types we have all of the following functions available:

array.contains(someKey)
array.containsKey(someKey)
array.containsValue(someValue)
array.forEach { key, value -> doSomething(key, value) }
array.getOrDefault(key = keyValue, defaultValue = defaultValue)
array.getOrElse(key = keyValue, defaultValue = defaultValue)
array.isEmpty()
array.isNotEmpty()
val keyIterator = array.keyIterator()
val valueIterator = array.valueIterator()
array.plus(anotherArray)
array.putAll(anotherArray)
array.remove(key = keyValue, value = value)
array.set(key = keyValue, value = value)
array.size

Working with the Pair class now becomes a little easier:

val pair = android.util.Pair("dsfn", "sdihfg")
// Retrieve values from destructuring
val (key, value) = pair
// Convert an Android framework pair to the kotlin Pair
val kotlinPair = pair.toKotlinPair()

We can also convert a Kotlin Pair directly to an Android Pair:

val pair = Pair("dsfn", "sdihfg")
val androidPair = pair.toAndroidPair()

If you’re working with the Half class, it’s now simpler to convert other data types to this:

short.toHalf()
string.toHalf()
float.toHalf()
double.toHalf()

An instance of the ClosedRange class can now be converted to a Range using a provided function:

val range = closedRange.toRange()

We can also perform a bunch of operations on a Range instance with provided extension functions:

val range = closedRange.toClosedRange()
// returns the intersection of the two ranges
val result = closedRange and someOtherRange
// returns the smallest range that includes the two ranges
val result = closedRange += someOtherCloseRange
// returns the intersection of the range and the given value
val result = closedRange += someValue

Both the Size and SizeF classes can be worked with using the provided functions:

val size = Size(5, 5)
// Retrieve values from destructuring
val (width, height) = size

Database Cursor

There are a collection of functions available for the Cursor class. For each of the function groupsstate below:

  • The first get function returns a non-null type
  • The second function returns a data type (or null) using the given column name
  • The third function returns a data type (or null) using the given index
cursor.getBlob(columnName = "some_column")
cursor.getBlobOrNull(columnName = "some_column")
cursor.getBlobOrNull(index = 0)

cursor.getDouble(columnName = "some_column")
cursor.getDoubleOrNull(columnName = "some_column")
cursor.getDoubleOrNull(index = 0)

cursor.getFloat(columnName = "some_column")
cursor.getFloatOrNull(index = 0)
cursor.getFloatOrNull(columnName = "some_column")

cursor.getInt(columnName = "some_column")
cursor.getIntOrNull(index = 0)
cursor.getIntOrNull(columnName = "some_column")

cursor.getLong(columnName = "some_column")
cursor.getLongOrNull(index = 0)
cursor.getLongOrNull(columnName = "some_column")

cursor.getShort(columnName = "some_column")
cursor.getShortOrNull(index = 0)
cursor.getShortOrNull(columnName = "some_column")

cursor.getString(columnName = "some_column")
cursor.getStringOrNull(index = 0)
cursor.getStringOrNull(columnName = "some_column")

SQLite

For SQLite there is a single extension function available at this time. Even so, it’s a pretty handy addition that allows us to perform a transaction using the given SQL statement.

sqLiteDatabase.transaction { "some SQL statement" }

Resources

When it comes to resources in our android applications, there have been a collection of functions added for easing the process of working with the TypedArray class. Here is what is currently on offer to us:

val boolean = typedArray.getBooleanOrThrow(0)
val int = typedArray.getColorOrThrow(0)
val colorStateList = typedArray.getColorStateListOrThrow(0)
val float = typedArray.getDimensionOrThrow(0)
val int = typedArray.getDimensionPixelOffsetOrThrow(0)
val int = typedArray.getDimensionPixelSizeOrThrow(0)
val drawable = typedArray.getDrawableOrThrow(0)
val float = typedArray.getFloatOrThrow(0)
val typeface = typedArray.getFontOrThrow(0)
val int = typedArray.getIntOrThrow(0)
val int = typedArray.getIntegerOrThrow(0)
val string = typedArray.getStringOrThrow(0)
val charSequenceArray = typedArray.getTextArrayOrThrow(0)
val charSequence = typedArray.getTextOrThrow(0)

Note: All of these throw a IllegalArgumentException if a value at the given index doesn’t exist

Text

Most of the applications we work in are going to use text somewhere throughout the project and thankfully, KTX provides some extension functions when it comes to these parts. For text we essentially have some functions available for the SpannableStringBuilder class.

For example, after instantiating a Builder instance we can use the build methods to append some bold text:

val builder = SpannableStringBuilder(urlString)        
    .bold { append("hi there") }
// or even some bold / italic / underlined text if you want!
val builder = SpannableStringBuilder(urlString)        
    .bold { italic { underline { append("hi there") } } }

There are also build functions to set the background color or wrap the text that you’re appending in spans:

.backgroundColor(color = R.color.black) { 
    // builder action
}
.inSpans(spans = someSpans) { 
    // builder action
}

There is also a buildSpannedString extension function that allows us to build a string and use the provided builder actions to provide our styling:

textView.text = buildSpannedString { bold { append("hitherejoe") } }

Net

Within the .net package of KTX we have a single extension function that allows us to easily convert a string into a URI representation, nice!

val uri = urlString.toUri()

Graphics

The Graphics related packages within KTX are pretty beefy, which is a great move to make all of the visual niceties in our apps more enjoyable to code.

To begin with we now have some handy functions to easily perform operations that convert bitmaps to the following types:

val adaptiveIcon = bitmap.toAdaptiveIcon()
val drawable = bitmap.toDrawable(resources)
val icon = bitmap.toIcon()
val drawable = someInt.toDrawable()
val icon = someByteArray.toIcon()
val icon = someUri.toIcon()
val colorDrawable = someColor.toDrawable()
val bitmap = drawable.toBitmap(width = someWidth, height = someHeight, config = bitMapConfig)

When it comes to Bitmaps we can now easily perform some key operations using extension functions:

val bitmap = someBitmap.applyCanvas(block = { })
val colorInt = someBitmap.get(x, y)
val bitmap = someBitmap.scale(width, height, filter = true)
someBitmap.set(x, y, color)

And in relation to that, working with the Canvas class has been made simpler too:

canvas.withRotation(degrees, pivotX, pivotY) { // block }
canvas.withSave { // block }
canvas.withScale(x, y, pivotX, pivotY)  { // block }
canvas.withSkew(x, y)  { // block }
canvas.withTranslation(x, y) { // block }

We also have some additions for when it comes to operations performed on Color instances:

// retrieve values from destructuring
val (r, g, b, a) = color
// Compose two color instances togehter
val color += someColor

The plus() function is really cool and allows us to add two colors together, resulting in a Color instance where the two instances have been blended!

Some functions on Matrices now make it easier for us to perform multiplication operations and also retrieve the values contained within the Matrix:

// perform multiplication
val result = matrix * someOtherMatrix
val values = matrix.values()

We can now perform operations on a Picture instance via the use of the record extension function, using the block parameter to execute commands for the instance:

val result = picture.record(width = someWidth, height = someHeight) { 
// do stuff with canvas
}

If we have a drawable instance which we wish to update the bounds for then we can do so with the updateBounds extension function — passing in the desired bounds we wish to be updated:

drawable.updateBounds(left = 16, top = 16, right = 16, bottom = 16)

Need to perform a transform operation on a Shader?

shader.transform { // block }

There are also some operations available for working with the PorterDuff class:

val porterDuffColorFilter = mode.toColorFilter(someColor)
val porterDuffXfermode = mode.toXfermode()

When working with the Region class we can now make use of these functions:

// Return union of the someRegion with a Rect
val region = someRegion and someRect
// Return union of the someRegion with a Region
val region = someRegion and someRegion
// Return difference of the someRegion and a Rect
val region = someRegion - someRect
// Return difference of the someRegion and another Region
val region = someRegion - someRegion
// Return intersection of the someRegion and a Rect
val region = someRegion or someRect
// Return intersection of the someRegion and another Region
val region = someRegion or someRegion
// Return union of the someRegion with a Rect
val region = someRegion + someRect
// Return union of the someRegion with a Region
val region = someRegion + someRegion
// Return union minus intersection of someRegion and a Rect
val region = someRegion xor someRect
// Return union minus intersection of someRegion and another Region
val region = someRegion xor someRegion
val boolean = someRegion.contains(somePoint)
someRegion.forEach { doSomethingWithEachRect(it) }
val iterator = someRegion.iterator()
// Return negation of someRegion as a new Region
val region = -someRegion

The PointF class has also been given some functions to ease the process when working with them:

val (x, y) = somePointF
val pointF = somePointF - someOtherPointF
val pointF = somePointF - someFloat
val pointF = somePointF + somePointF
val pointF = somePointF + someFloat
val point = somePointF.toPoint()
val pointF = -somePointF

Similar functions are also available for the Point class:

val (x, y) = somePoint
val point = somePoint - somePoint
val point = somePoint - someFloat
val point = somePoint +somePoint
val point = somePoint + someFloat
val point = somePoint.toPointF()
val point = -somePoint

The same goes for the Rect class with some similar functions available:

val rect = someRect and anotherRect
val (left, top, right, bottom) = someRect
someRect.contains(somePoint)
val region = someRect - anotherRect
val rect = someRect - someInt
val rect = someRect - somePoint
val rect = someRect or someRect
val rect = someRect + someRect
val rect = someRect + someInt
val rect = someRect + somePoint
val rectF = someRect.toRectF()
val region = someRect.toRegion()
val region = someRect xor someRect

The RectF class offers pretty much the same capabilities:

val rectF = someRectF and anotherRectF
val (left, top, right, bottom) = somerectF
someRectF.contains(somePoint)
val region = someRectF - anotherRectF
val rectF = someRectF - someInt
val rectF = someRectF - somePoint
val rectF = someRectF or someRect
val rectF = someRectF + someRect
val rectF = someRectF + someInt
val rectF = someRectF + somePoint
val rect = someRectF.toRect()
val region = someRectF.toRegion()
val reactF = someRectF.transform(someMatrix)
val region = someRectF xor someRect

When working with the Path class, we can make use of the below functions:

val path = somePath and anotherPath
val path = somePath.flatten(error = 0.5f)
val path = somePath - anotherPath
val path = somePath or anotherPath
val path = somePath + anotherPath
val path = somePath xor anotherPath

When working with graphics, chances are we will be working with the Int and Long data types. The Int type offers us a collection of functions in KTX:

val alpha = int.alpha
val blue = int.blue
val green = int.green
val red = int.red
val luminance = int.luminance
val (alphaComp, redComp, greenComp, blueComp) = someInt
val color = someInt.toColor()
val color = someInt.toColorLong()

The Long type on the other hand offers us a few differences:

val alpha = long.alpha
val blue = long.blue
val green = long.green
val red = long.red
val luminance = long.luminance
val (alphaComp, redComp, greenComp, blueComp) = someLong
val color = someLong.toColor()
val color = someLong.toColorInt()
long.isSrgb
long.isWideGamut
long.colorSpace

Transitions

when it comes to the Transition class we have a bunch of extension functions available for use. Just like the animation listeners, we can now easily listen for changes in our Transition by using the addListener() function call.

transition.addListener { doSomethingWithTransition(it) }

transition.addListener(onEnd = {}, onStart = {}, onCancel = {}, 
        onResume = {}, onPause = {})

Or on the other hand, we can use the individual functions for each of the transition callbacks:

transition.doOnCancel {  }
transition.doOnEnd {  }
transition.doOnPause {  }
transition.doOnResume {  }
transition.doOnStart {  }

Views

We have some similar functions available for the View class also. Setting callbacks for layout events is super clean:

view.doOnLayout {  }
view.doOnNextLayout {  }
view.doOnPreDraw {  }

The postDelayed method is now available as a function:

view.postDelayed(delayInMillis = 200) { // some action }

The same goes for the postOnAnimationDelayed method:

view.postOnAnimationDelayed(delayInMillis = 200) { // some action }

Updating the padding for a view is now a lot cleaner and easier to do so, there are several functions available for when it comes to this topic:

view.setPadding(16)
view.updatePadding(left = 16, right = 16, top = 16, bottom = 16)
view.updatePaddingRelative(
        start = 16, end = 16, top = 16, bottom = 16)

And if you need to convert a View instance to a Bitmap, you can do so with this single line of code!

val bitmap = view.toBitmap(config = bitmapConfig)

ViewGroup

There are some handy ViewGroup related functions that you’ll likely be using in your projects! For example, checking a if a viewgroup contains a view:

val doesContain = viewGroup.contains(view)

Looping through the children of a viewgroup (where it represents the child):

viewGroup.forEach { doSomethingWithChild(it) }
viewGroup.forEachIndexed { index, view -> 
doSomethingWithChild(index, view) }

Accessing the child at a desired position in kotlin style:

val view = viewGroup[0]

Retrieving a MutableIterator instance for the desired viewgroup:

val viewGroupIterator = viewGroup.iterator()

And performing other various viewgroup related operations:

viewGroup.isEmpty()
viewGroup.isNotEmpty()
viewGroup.size
// Remove a view from the given viewgroup
viewGroup -= view
// Add a view to the given viewgroup
viewGroup += view

Margins

Similar to setting the padding for View instances, we can now alter the margins for our layout param instances in a similar manner with the following functions:

params.setMargins(16)
params.updateMargins(left = 16, right = 16, top = 16, bottom = 16)
params.updateMarginsRelative(
        start = 16, end = 16, top = 16, bottom = 16)

Conclusion

As we can see, KTX offers us some awesome extensions for when it comes to using kotlin in our Android applications. I’m excited to use these in my projects from now and am looking forward to seeing what else gets added soon 🙂

https://android.github.io/android-ktx/core-ktx/index.html

Leave a Reply

Your email address will not be published. Required fields are marked *