This article is based off my talk at Droidcon SF this year.
Kotlin is well-known for null-safety and extension functions features among android developers. Extension functions provide the ability to make new functionality of an extending class without making changes to it. In addition, the new functionality can be declared outside of the extending class. They’re also very beneficial when you want to tweak an api that can’t be modified.
Writing a good extension function comes with practice, and there are no universal rules on how or when to make them. Every developer declares the rules for themselves based on the business need of an app. But regardless of what the needs are, there are some rules I’ve set for myself.
Let’s go over some sad examples of making extensions:
Extending android and java common types to meet business logic
This reasoning comes up as a quick solution for many of us. But down the road, it’ll get dirty and unmanageable.
fun CharSequence.toAnimal(): Animal {
return Animal(
id = this.toString(),
name = this.toString(),
logo = "",
isDomestic = false,
inRedList = false,
description = this.toString()
)
}
Instead I’d recommend to separate business and common types. Extension functions are great for mapping business to business logic:
fun WildAnimal.toAnimal(): Animal {
return Animal(
id = id,
name = name,
logo = logo,
isDomestic = false,
description = shortDescription,
inRedList = inRedList
)
}
or common to common:
@kotlin.internal.InlineOnly
public inline fun String.toPattern(flags: Int = 0): java.util.regex.Pattern {
return java.util.regex.Pattern.compile(this, flags)
}
Modifying members of an extending class
Extension functions are meant to extend a class with new functionality, not to modify or insert members into it.
fun MainActivity.showWhiteLoadingIndicator() {
mNumActiveLoadingIndicators++
runOnUiThread {
. . .
}
}
showWhiteLoadingIndicator()
is incrementing a member of MainActivity
mNumActiveLoadingIndicators
.class MainActivity : AppCompatActivity() {
var mNumActiveLoadingIndicators: Int = 0
}
Although this might work and some developers do practice it, making an extension function out of it seems as an overhead to me. No one cancelled out regular member functions, which can help you achieve the same functionality in the end.
class MainActivity : AppCompatActivity() {
var mNumActiveLoadingIndicators: Int = 0
fun showWhiteLoadingIndicator() {
mNumActiveLoadingIndicators++
runOnUiThread {
. . .
}
}
Using the same name as a member function
This is not a sad example, more of a caution to developers, who are switching to Kotlin and trying out extension functions. When making an extension function, be careful with naming. If it matches a member function, and is applicable to given arguments, compiler will choose the member function over an extension function.
@RemotableViewMethod
public void setBackgroundColor(@ColorInt int color) {
if (mBackground instanceof ColorDrawable) {
((ColorDrawable) mBackground.mutate()).setColor(color);
computeOpaqueFlags();
mBackgroundResource = 0;
} else {
setBackground(new ColorDrawable(color));
}
}
setBackgroundColor(@ColorInt int color)
is a member function of View
class in android, and it accepts argument of type int
.
Now if we look at the below extension function, the name and argument type match the member function.fun View.setBackgroundColor(colorResId: Int) {
setBackgroundColor(
ContextCompat.getColor(
context, colorResId
)
)
}
Instead, the easiest solution will be to rename the extension function:
fun View.setBackgroundColorResId(colorId: Int) {
setBackgroundColor(
ContextCompat.getColor(
context, colorId
)
)
}
Extending context on every occasion
We have all heard that the best way to start converting your app to kotlin is by converting util or helper methods into extension functions. Most likely, those methods at some point require the context to be passed:
public static void showGif(String url, Context context, ImageView imageView) {
Glide.with(context).asGif()
.listener(new RequestListener<GifDrawable>() {
@Override public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<GifDrawable> target, boolean isFirstResource) {
return false;
}
@Override public boolean onResourceReady(GifDrawable resource, Object model,
Target<GifDrawable> target, DataSource dataSource, boolean isFirstResource) {
return false;
}
})
.load(url)
.into(imageView);
}
And naturally, the first thought is to extend the context where the rest of parameters can be passed to it:
fun Context.showGif(imageView: ImageView, url: String) {
Glide.with(this)
.asGif()
.listener(object : RequestListener<GifDrawable> {
override fun onLoadFailed(e: GlideException?, model: Any?,
target: Target<GifDrawable>?,isFirstResource: Boolean
) = false
override fun onResourceReady(resource: GifDrawable?, model: Any?,
target: Target<GifDrawable>?, dataSource: DataSource?,
isFirstResource: Boolean
) = false
})
.load(url)
.into(imageView)
}
At first glance, compiler doesn’t give any errors, the code runs, and the same functionality is preserved. But if we look at the docs, we can see that the ImageView
class already has a member function, that returns a type Context
. Therefore we can retrieve context directly from it by extending Imageview
:
fun ImageView.asGif(url: String) {
Glide.with(this.context)
.asGif()
.listener(object : RequestListener<GifDrawable> {
override fun onLoadFailed(e: GlideException?, model: Any?,
target: Target<GifDrawable>?, isFirstResource: Boolean
) = false
override fun onResourceReady(resource: GifDrawable?, model: Any?,
target: Target<GifDrawable>?, dataSource: DataSource?,
isFirstResource: Boolean
) = false
})
.load(url)
.into(this)
}
Another case I commonly see is to extend context to display some other views:
fun Context.showDialog(message: String, activity: Activity?) {
if (activity != null && !activity.isFinishing) {
MaterialDialog.Builder(this)
.content(message)
.positiveText("Ok")
.show()
}
}
Be aware, that by extending Context
, you’re letting every other context subclass that doesn’t have access to Window
class have the ability to call the extension function. For instance, you can access context inside service, therefore you can call context.showDialog()
, but dialog needs to be drawn on window, that needs an access to content which lives on activity, therefore calling showDialog()
in service will crash.
Now about some good practices:
Making an extension function out of a nullable receiver
public actual fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean {
if (this === null)
return other === null
return if (!ignoreCase)
(this as java.lang.String).equals(other)
else
(this as java.lang.String).equalsIgnoreCase(other)
}
Adding iterators via extension functions
This is a perfect example of extending a limited api:
inline fun Menu.forEach(action: (item: MenuItem) -> Unit) {
for (index in 0 until size()) {
action(getItem(index))
}
}
Converting from one interface to another
public fun <K, V> Map<out K, V>.asSequence(): Sequence<Map.Entry<K, V>> {
return entries.asSequence()
}
It’s important to structure extension functions properly. For example, I usually create a file that contains all extension functions of a receiver type. It’s easy down the road for new developers in the team to not make duplicated functions, and also easy to copy a file from one project to another 😄 .
Code samples can be found here
Huge thanks to Eugenio Lopez for being so patient and reviewing the article many times 🙏