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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Extension Functions and Properties in Kotlin
20 mins
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.
Navigate to the starter project directory you downloaded and click Open.
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:
Go through the Registration flow once to register your hand for this awesome platform:
Now, log out of the app and go through the login flow once:
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.
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:
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:
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:
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:
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:
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:
- Takes the input from
EditText
and converts it into a string. - Declares a
regex
pattern that accept strings ending with some number, like bond007, coder1, etc. It then matches the input with theregex
and stores whether it’s a valid string or not. - Sets the error hint if the username is invalid.
- 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.
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:
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.
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 outsideMainActivity
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 ofHand
. - To refer to an instance of a dispatch receiver like
MainActivity
, the code use-qualified this syntax asthis@MainActivity
. - You can declare the extension as open instead of
private
so that subclasses ofMainActivity
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:
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.
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more