Extension Functions and Properties in Kotlin


Developers love Kotlin because it provides many features to make the code concise and their lives easy. Extension functions and extension properties are two such features.

Extension functions provide the ability to extend the functionality of existing classes without inheriting them. You can call these new functions in the usual way: as if they were methods of the original class.

You can’t access the private members — because these functions aren’t actually added inside the class — but they provide benefits that you’ll see later in this tutorial.

Throughout this article, you’ll work on a dummy social media app called Handbook — because it helps hands connect with each other. We’re coming for you, Facebook!

You’ll add new functionalities to existing classes. Along the way, you’ll learn about:

  • Using extension functions and extension properties.
  • Resolving them.
  • Their workings under the hood.
  • How to use them with nullable receivers and companion objects.
  • Different type of scopes for extensions.

Getting Started

Click the Download Materials button at the top or bottom of the page to download the starter and final projects.

Open Android Studio and click Open an existing Android Studio project.

Android Studio 3.6 new project wizard

Android Studio 3.6 welcome wizard

Navigate to the starter project directory you downloaded and click Open.

Android Studio selection window box for starter project

Android Studio project selection window

Take some time to familiarize yourself with the code.

Project Structure

You’ll find the following packages and files in the starter project:

  • db: This package contains Constants.kt and HandsDb.kt.
  • Constants.kt: Contains enums for login and registration states.
  • HandsDb.kt: Contains code to save data locally for login, logout and registration features using SharedPreferences.
  • models: A package that contains Hand.kt, which is the data model to denote a user.
  • ui: This package has all the Activities the app uses.
  • utils: A package containing Extensions.kt, which is where you’ll write extension functions.

Now that you have an overview of the files in this project, build and run. You’ll see a screen like this:

Handbook app starting screen

Handbook app starting screen

Go through the Registration flow once to register your hand for this awesome platform:

Handbook app registration flow

Handbook app registration flow

Now, log out of the app and go through the login flow once:

Handbook app login flow

Handbook app login flow

Introducing Extension Functions

Extension functions are a cool Kotlin feature that help you develop Android apps. They provide the ability to add new functionality to classes without having to inherit from them or to use design patterns like Decorator. Read more about Decorator pattern on Wikipedia.

While working with the Android framework, you’ll find yourself writing helper classes — usually named Utils — which contain static methods that take instances of some class and perform operations on public members of that class. An example of such a pattern is:


fun showAlert(context: Context,
              message: String,
              length: Int = Toast.LENGTH_SHORT) {
  Toast.makeText(context, message, length).show()
}

Other parts of the code call this with an instance of Context by writing Utils.showAlert(context, "Uncool way"). This example function is more useful when you’re showing a Toast with a custom UI throughout the app, so you have one place to change it.

Other use cases include modifying third party classes when you want to add functionality that uses public members.

In such cases, extension functions come to the rescue. They provide a nice piece of syntactic sugar that lets you change how you call such methods so they look like regular member functions. For example:


context.showAlert("Cool way")

Extensions are syntactic sugar. They don’t actually modify the class, but they make new functions callable with the dot notation. To declare an extension function, you need to prefix the name of the function with a receiver type that you want to extend.

With that in mind, open Extensions.kt and under TODO: 1, add the following snippet:


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

If Android Studio shows a wizard to add required import statements in the class, click OK. Otherwise, add the following import statements:


import android.widget.ImageView
import com.bumptech.glide.Glide

In the above snippet, you’ve declared your first extension function. You’ve extended ImageView, which acts as a receiver type, and added an extension function named loadImage. The this keyword inside an extension function corresponds to the receiver object. In this case, it refers to an instance of ImageView.

This function uses the Glide library to load the image from a given URL into the ImageView.

Now, open OnBoardingActivity.kt and replace the code below TODO: 2 with the following:


binding.imageIcon.loadImage(getString(R.string.logo_url))

Add the following import statement if Android Studio doesn’t add it automatically:


import com.raywenderlich.android.handbook.utils.loadImage

This project uses view binding to work with XML-based views. You can check out Introduction to View Binding tutorial to learn more about it.

So, in the code above, you called loadImage(string) on the imageIcon ImageView as if it’s a member function of the class.

In the Onboarding screen, Glide loads the Handbook logo just like before. The only difference is the code now looks concise.

Build and run to see the screen below. If you’re logged in, log out to see the onboarding screen.

Handbook app starting screen

Handbook app starting screen using an extension function to load the logo

Resolving Extensions

To understand extension functions, you need to understand how to resolve them.

They’re dispatched statically. In other words, you determine the function that’s called by the type of expression that invokes the function, and not the resulting type at runtime. In a nutshell, they’re not virtual by receiver type.

To understand this better, open Extensions.kt and add the following imports:


import android.widget.Toast
import com.raywenderlich.android.handbook.R
import com.raywenderlich.android.handbook.ui.BaseActivity
import com.raywenderlich.android.handbook.ui.OnBoardingActivity

Now, add the following snippets below TODO: 3 and TODO: 4 respectively:


fun BaseActivity.greet() {
  Toast.makeText(this, getString(R.string.welcome_base_activity), Toast.LENGTH_SHORT).show()
}

fun OnBoardingActivity.greet() {
  Toast.makeText(this, getString(R.string.welcome_onboarding_activity), Toast.LENGTH_SHORT).show()
}

To give some context, OnBoardingActivity extends BaseActivity. You’re defining an extension function greet() with the same signature for both BaseActivity and OnBoardingActivity, but with different messages to show the user.

Next, open OnBoardingActivity and add this method below TODO: 5:


private fun showGreetingMessage(activity: BaseActivity) {
  activity.greet()
}

Also, add this below TODO: 6:


showGreetingMessage(this)

Phew! you’re done now. So the code you added in OnBoardingActivity defines showGreetingMessage, which calls the greet() extension function to greet the user with a toast when they start the app. It takes BaseActivity as a parameter.

Below TODO: 6, you called this method with this as argument, where this refers to the current instance of OnBoardingActivity. So you expect to see the toast with the message defined in R.string.welcome_onboarding_activity.

Build and run and you’ll notice the toast actually shows the message defined in R.string.welcome_base_activity instead:

App opening with a greeting message

App opening with a greeting message

The toast shows the string defined by R.string.welcome_base_activity and not the one you expected. That’s because the extension function depends on the declared type of the parameter, as discussed earlier, which is BaseActivity, and not the type that’s resolved at runtime, which is OnBoardingActivity.

What if there’s already a member function defined with the same name and signature as an extension function?

The Kotlin reference docs say that the member function always wins. If they have different signatures, however, Kotlin calls the extension function.

Open BaseActivity and, below TODO: 7, add the following snippet:


fun greet() {
  Toast.makeText(this, getString(R.string.welcome_base_activity_member),
    Toast.LENGTH_SHORT).show()
}

This code adds a member function with the same name and signature as your extension function in BaseActivity. Build and run:

App launching with toast greeting

Greeting toast from member function

The Toast now shows the message defined in the member function and not the extension function. So the Kotlin docs are correct. :]

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.

Registration process showing suggested usernames

Handbook registration username suggestor

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:

Logged-in UI for the Handbook Android app

Extension property for fingers count

Using Nullable Receivers

Everything you’ve learned about extension properties and extension functions also applies if the receiver is a nullable type. If the value of the variable is null, you can check for this == null before doing any operation in the body.

In the next few steps, you’ll change the currentHand in the MainActivity from type Hand to type Hand?. You’ll also make the necessary changes to the extensions to handle the nullable type.

Start by opening MainActivity. Above TODO: 15, you’ll see the variable definition private lateinit var currentHand: Hand.

Remove it and redeclare it as a nullable variable below TODO: 15:


private var currentHand: Hand? = null

Remove currentHand = handsDb.getLoggedInHand()!! above TODO: 16 and add the following snippet instead:


currentHand = handsDb.getLoggedInHand()

Also, replace showDescription with the following:


private fun showDescription(hand: Hand?) {
  binding.welcomeTv.text = getString(
      R.string.welcome_username, 
      hand?.userName ?: "-"
  )

  binding.userDescriptionTv.text = getString(
      R.string.user_description_total_fingers,
      hand?.bio ?: "-", hand?.totalFingers
  )
}

If hand is null, it won’t show the value of the totalFingers extension property. To fix this and print , open Extensions.kt and replace the totalFingers extension property with this:


val Hand?.totalFingers: String
  get() {
    if (this == null) return "-"
    return (fingersCount + thumbsCount).toString()
  }

You’ve changed the type to nullable and added a null check in the body. Now, your code can handle cases where currentHand is null — for example, when a user who isn’t logged in is exploring the app.

Build and run. The app should run the same as before.

Registration process showing suggested usernames

Defining Companion Object Extensions

Another helpful feature you can use is defining extension properties and functions for companion objects of classes. The syntax is the same as when you define extension properties for the classes themselves. For example, if you have a class Human with a companion object like this:


class Human {
  companion object {}
}

You can define an extension function for the companion object like this:


fun Human.Companion.greetOthers() {
  println("Hello other humans")
}

You call extension functions on companion objects using only the class name, like Human.greetOthers().

Defining the Scope of Extensions

All the extensions you’ve defined are in the top level of Extensions.kt, directly under package. You can use the extensions anywhere in the project by importing them at the call site.

One of the good pratices to follow while working with extensions in a big project is to divide them in different files grouped by the receiver class. For example, all the extensions for ImageView goes in one file and so on.

Most of the time, you’ll define extensions only at the top level, but you can also declare an extension for a class inside another class. In those cases, there are multiple implicit receivers.

The instance of the class inside which you declare the extension is the dispatch receiver and the instance of the class on which you define the extension is the extension receiver.

Open MainActivity and add the following snippet below TODO: 18:


private fun Hand?.showGreeting() {
  if (this == null) {
    Toast.makeText(this@MainActivity, getString(R.string.greeting_anonymous),
        Toast.LENGTH_SHORT).show()
  } else {
    Toast.makeText(this@MainActivity, getString(R.string.greeting_user, userName),
        Toast.LENGTH_SHORT).show()
  }
}

This code adds an extension function showGreeting on Hand within MainActvity. Here are a few things to understand about the code above:

  • The scope of the extension is inside MainActivity only. That means that classes outside MainActivity can’t call the extension.
  • In case of name conflict between members of the dispatch and extension receivers, the extension receiver takes precedence. So in the code above, using the this keyword will refer to the instance of Hand.
  • To refer to an instance of a dispatch receiver like MainActivity, the code use-qualified this syntax as this@MainActivity.
  • You can declare the extension as open instead of private so that subclasses of MainActivity can override it.

To call the extension you defined, add the following snippet under TODO: 19:


currentHand.showGreeting()

Build and run the app. If you’re not logged in, login first and you’ll see a Toast message in MainActivity like this:

Toast message shown on app screen

Scoped extension property greeting

Congratulations, you’ve completed this tutorial!

Where to Go From Here?

You can download the final project using the Download Materials button at the top or bottom of this tutorial.

In this tutorial, you learned to use extension properties and functions.

If you want to learn more about them, check out Kotlin’s official extensions guide.

If you have any questions or comments, please join the discussion below.

Source link

Leave a Reply