Kotlin has many amazing features but one that stands out is extensions. Extensions are used to add functionality to existing classes.
They are extremely useful at helping to reduce boiler plate code. The good thing is that you can add them to any class, including the ones from third-party libraries that you can’t modify.
Most of the time we see Extension functions but Kotlin also supports Extension properties.
Lets start with some extensions that make the code easier to read, the ones that improve the code quality over time by reducing boiler plate code.
The Android API changes over time and something as simple as getting the color for a given ID can become not so simple. The extensions below helps reduce the boiler plate code associated with retrieving resources for a given ID.
fun Context.getCompatColor(@ColorRes colorId: Int) = ResourcesCompat.getColor(resources, colorId, null)
fun Context.getCompatDrawable(@DrawableRes drawableId: Int) = AppCompatResources.getDrawable(this, drawableId)!!
// Activity
getCompatColor(R.color.white)
// Fragment
requireContext().getCompatColor(R.color.white)
We all know how repeating it’s to check whether the user has granted the permissions the app requires. The extension below can help with that. It doesn’t solve the whole problem but helps reduce the code size.
fun Context.hasPermissions(vararg permissions: String) = permissions.all { permission ->
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
// Traditional way (Activity)
(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED)
// Extension (Activity)
hasPermissions(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION)
Copying some text or URL to the clipboard should be something very simple. It only works with plain text but that’s what we use most of the time, if you need to support other formats you can change it.
fun Context.copyToClipboard(content: String) {
val clipboardManager = ContextCompat.getSystemService(this, ClipboardManager::class.java)!!
val clip = ClipData.newPlainText("clipboard", content)
clipboardManager.setPrimaryClip(clip)
}
Is there an URL in your app that the user needs to open the browser to view? Are you handling the case where the browser or equivalent app might be missing? The next extension abstracts most of what’s needed. You just need to give it the Uri and do something such as showing an error if there’s no app to resolve the intent.
fun Context.isResolvable(intent: Intent) = intent.resolveActivity(packageManager) != null
fun Context.view(uri: Uri, onAppMissing: () -> Unit) {
val intent = Intent(Intent.ACTION_VIEW, uri)
if (!isResolvable(intent)) {
onAppMissing()
return
}
startActivity(intent)
}
// Activity
view(uri, onAppMissing = {
// show error message
})
This extension is very useful if you need to log a date object over and over or send it via an API call. Something to notice here is that the ISO format is universal, it doesn’t change from country to country, I’ll show why that’s important later.
val isoFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
fun Date.toISOFormat() : String = isoFormatter.format(this)
Date(1620862687852).toISOFormat() // 2021-05-12 23:38:07.540
Some other extensions that might be useful
fun Date.plusDays(days: Int): Date {
return Date(this.time + (days * ONE_DAY_MS))
}
fun Date.plusMillis(millis: Long): Date {
return Date(this.time + millis)
}
Many things including extensions can be abused. Some of them look good, but are actually disguised code smells.
Let’s start with a useless extension. I was reading an article about extensions the other day and found this one.
fun Any?.isNull() = this == null
if (something.isNull()) { ... }
The extension below may look reasonable to most of us. It formats a Double to its decimal format.
fun Any?.isNull() = this == null
if (something.isNull()) { ... }
The dangerous thing about this code is that money is not formatted equally worldwide. In some countries you’ll see 1,200.00 (comma first, dot after), in others 1.200,00 (dot first, comma after). This is only a problem if the app is/will be used in many countries.
My solution would be to add the formatting pattern to strings.xml
and create an extension on Context
to handle that. That way you can define the monetary format for each language and still use the same function.
fun Context.formatPrice(price: Double) : String {
val formatter = DecimalFormat(getString(R.string.price_formatter))
return formatter.format(price)
}
Kotlin also allows us to add properties as extensions. I’ve not found an use case for them yet but it’s useful to know that it’s possible to use them.
val <T> List<T>.lastIndex: Int
get() = size - 1
Thank you for reading. If there’s anything you disagree with, please leave a comment below so we can discuss it and arrive at a better solution.
Cover photo by Fran Jacquier on Unsplash
Also published here.