Extension Functions and Properties in Kotlin

In this tutorial for Android, you’ll learn to use Kotlin extension functions and properties to extend the functionality of existing classes. By Rajdeep Singh.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Creating an Extension Manually

In this section, you’ll see what the extension functions decompile down to. You’ll also write a function to manually implement something similar, which will help you understand how it works.

Open Extensions.kt and select Tools ▸ Kotlin ▸ Show Kotlin Bytecode in the top menu:

Android Studio with the Kotlin Bytecode tool item selected

Click Decompile and you’ll see the decompiled Java version for the Kotlin code you’ve written. Look at loadImage:

public static final void loadImage(@NotNull ImageView $this$loadImage, @NotNull String imageUrl) {
  Intrinsics.checkParameterIsNotNull($this$loadImage, "$this$loadImage");
  Intrinsics.checkParameterIsNotNull(imageUrl, "imageUrl");
  Glide.with((View)$this$loadImage).load(imageUrl).into($this$loadImage);
}

The Kotlin code for the loadImage extension function you wrote looks like this:

fun ImageView.loadImage(imageUrl: String) {
  Glide.with(this)
      .load(imageUrl)
      .into(this)
}

Look at the decompiled code and you’ll notice that it’s a static function that takes the receiver class of extension function as its first parameter. The remaining parameters are whatever you define.

Also, notice Intrinsics.checkParameterIsNotNull, which throws an IllegalArgumentException if the receiver is null.

Open LoginActivity and RegisterActivity and you’ll see that in the success case in both the login and registration flow, an Intent opens the MainActivity with Intent.FLAG_ACTIVITY_NEW_TASK and Intent.FLAG_ACTIVITY_CLEAR_TASK flags to start the new activity.

You will first extract this functionality into a function which takes a Context as parameter. Later you’ll replace this function with an extension function to make the code concise.

Open Extensions.kt and add the following snippet below TODO: 8. Then click OK to add the required import statements:

fun startActivityAndClearStack(context: Context, clazz: Class<*>,
    extras: Bundle?) {
  val intent = Intent(context, clazz)
  intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
  if (extras != null) {
    intent.putExtras(extras)
  }
  context.startActivity(intent)
}

Open LoginActivity and replace the code below TODO: 9 with the following snippet, then add the necessary imports:

startActivityAndClearStack(this, MainActivity::class.java, null)

Open RegisterActivity and replace the code below TODO: 10 with the following snippet and add the necessary imports:

startActivityAndClearStack(this, MainActivity::class.java, null)

Build and run; the app should work the same as before:

App launching

Now you will convert startActivityAndClearStack to an extension function.

Open Extensions.kt and replace startActivityAndClearStack with the following code snippet:

fun Context.startActivityAndClearStack(clazz: Class<*>, extras: Bundle?) {
  val intent = Intent(this, clazz)
  intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
  if (extras != null) {
    intent.putExtras(extras)
  }
  startActivity(intent)
}

Decompile Extensions.kt by selecting Tools ▸ Kotlin ▸ Show Kotlin Bytecode in the top menu and clicking Decompile button.

You’ll find the following decompiled snippet for the method you just added:

public static final void startActivityAndClearStack(
  @NotNull Context $this$startActivityAndClearStack, 
  @NotNull Class clazz, 
  @Nullable Bundle extras) {
  Intrinsics.checkParameterIsNotNull($this$startActivityAndClearStack, "$this$startActivityAndClearStack");
  Intrinsics.checkParameterIsNotNull(clazz, "clazz");

  Intent intent = new Intent($this$startActivityAndClearStack, clazz);
  intent.setFlags(268468224);

  if (extras != null) {
    intent.putExtras(extras);
  }

  $this$startActivityAndClearStack.startActivity(intent);
}

This is very similar to the one you created earlier but with additional null checks and simplified flag value.

So now you know how to create the underlying code for extensions yourself.

Open LoginActivity and replace startActivityAndClearStack(this, MainActivity::class.java, null) with the following snippet:

startActivityAndClearStack(MainActivity::class.java, null)

Open RegisterActivity and replace startActivityAndClearStack(this, MainActivity::class.java, null) with the following snippet:

startActivityAndClearStack(MainActivity::class.java, null)

Build and run; the app should work the same as before:

App launching

Adding Username Suggester

In this section, you’ll add an extension function in EditText to validate the username.

Usually, this is an API call to the server to determine whether or not the selected username is available and valid. But in this case, you’ll write a simple offline validator to ensure that each username ends with a number.

Open Extensions.kt and add the following import statements:

import android.widget.EditText
import java.util.regex.Pattern

Below TODO: 11, add the following snippet:

fun EditText.validateUsername(): Boolean {
  //1
  val username = text.toString()

  //2
  val pattern = Pattern.compile("^[a-zA-Z]+[0-9]+$")
  val matcher = pattern.matcher(username)
  val isValid = matcher.matches()

  //3
  if (!isValid) {
    error = context.getString(R.string.username_validation_error, username)
  }

  //4
  return isValid
}

This code adds an extension function in EditText. Going through the code, it:

  1. Takes the input from EditText and converts it into a string.
  2. Declares a regex pattern that accept strings ending with some number, like bond007, coder1, etc. It then matches the input with the regex and stores whether it’s a valid string or not.
  3. Sets the error hint if the username is invalid.
  4. Returns whether the input entered is a valid username or not.

Open RegisterActivity and add the following snippet below TODO: 12:

val isUsernameValid = binding.usernameInput.validateUsername()
if (!isUsernameValid) {
  return
}

This code calls the validateUsername() extension function and shows an error with a list of suggested usernames.

Build and run and register a new user.

Handbook registration username suggestor

Registration process showing suggested usernames

Understanding Extension Properties

Similar to extension functions, Kotlin also supports extension properties. Extension properties are similar to extension functions, in that you can’t insert an actual member in a class.

To define an extension property, use this syntax:

val <T> List<T>.lastIndex: Int
    get() = size - 1

This example code declares an extension property with the name lastIndex that uses size to calculate the last index of the list.

Since Kotlin isn’t inserting a member, there’s no way for the property to have a backing field. This means you can’t initialize the property explicitly or have a setter.

Extension properties can use only public members of the class to calculate values on the fly. This means the following isn’t allowed:

val <T> List<T>.lastIndex: Int = 1 //error

To read more, visit Kotlin’s official Backing Fields documentation.

Right now, the app shows thumb count and finger count separately from the logged-in hands. You’ll change this to a single total finger count, which combines both fingers and thumbs by using an extension property.

Open Extensions.kt and add the following extension property under TODO: 13:

val Hand.totalFingers: String
  get() {
    return (fingersCount + thumbsCount).toString()
  }

Now that you’ve created the extension property, open MainActivity and add this snippet below TODO: 14, then add the required imports:

binding.userDescriptionTv.text = getString(R.string.user_description_total_fingers,
      hand.bio, hand.totalFingers)

Also, remove the code that adds text to binding.userDescriptionTv above the TODO: 14, which looks like this:

binding.userDescriptionTv.text = getString(R.string.user_description,
  hand.bio, hand.fingersCount, hand.thumbsCount)

In the above steps, you’ve changed the value of binding.userDescriptionTv‘s text to use the extension property fingersCount. This displays the combined thumb and fingers count.

Build and run and log in to see the updated UI:

Extension property for fingers count

Logged-in UI for the Handbook Android app