MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Extension functions are Kotlin's most addictive feature. Used well, they make APIs more expressive and reduce boilerplate. Used poorly, they scatter code and hide dependencies. Here are the patterns worth adopting and the anti-patterns worth avoiding.
On this page
Extension functions let you add methods to existing classes without inheritance. They're one of Kotlin's most powerful features — and one of the easiest to abuse.
Here's how to use them well.
Converting between types (mappers):
// Domain model ↔ Entity conversions live as extension functions
fun TaskEntity.toDomain(): Task = Task(
id = id,
title = title,
completed = completed,
dueDate = dueDate?.let { LocalDate.parse(it) }
)
fun Task.toEntity(): TaskEntity = TaskEntity(
id = id,
title = title,
completed = completed,
dueDate = dueDate?.toString()
)Clean, readable, doesn't pollute either class, and can be organized in a mapper file.
Utility functions on standard types:
fun String.toSlug(): String = lowercase()
.replace(Regex("[^a-z0-9]+"), "-")
.trim('-')
fun Long.toDateString(): String =
SimpleDateFormat("MMM d, yyyy", Locale.getDefault()).format(Date(this))
fun Int.dp(context: Context): Int =
(this * context.resources.displayMetrics.density).toInt()
// Usage
"My Blog Post Title".toSlug() // → "my-blog-post-title"
task.createdAt.toDateString() // → "Mar 15, 2026"Fluent builder patterns:
fun NotificationCompat.Builder.addDismissAction(context: Context): NotificationCompat.Builder {
val intent = PendingIntent.getBroadcast(
context, 0,
Intent(context, DismissReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE
)
addAction(0, "Dismiss", intent)
return this
}
// Usage — reads like configuration
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.addDismissAction(context) // Extends the builder fluently
.build()Context-specific helpers:
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}
fun Fragment.hideKeyboard() {
val imm = requireContext().getSystemService(InputMethodManager::class.java)
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}Don't scatter extension functions randomly. Organize by:
By the type being extended:
extensions/
StringExtensions.kt // All String extensions
ContextExtensions.kt // All Context extensions
FlowExtensions.kt // All Flow extensions
ViewExtensions.kt // All View extensionsBy domain:
extensions/
TaskExtensions.kt // Task-related extensions across types
DateExtensions.kt // Date/time extensionsThe key: consistency across the project. Pick one convention and stick to it.
Extensions can be defined on nullable receivers:
fun String?.orEmpty(): String = this ?: ""
fun String?.isNotNullOrBlank(): Boolean = !isNullOrBlank()
fun <T> List<T>?.orEmpty(): List<T> = this ?: emptyList()
// Usage — no null checks needed
val title = task.title.orEmpty()
val tags = task.tags.orEmpty()When it hides a dependency that should be explicit:
// Bad — looks like a simple property but is actually a complex operation
fun Task.toFormattedString(): String {
return "${title} (${dateFormatter.format(dueDate)})" // Where does dateFormatter come from?
}
// Better — make the dependency explicit
fun Task.toFormattedString(dateFormatter: DateFormatter): String {
return "${title} (${dateFormatter.format(dueDate)})"
}When the logic belongs in the class itself:
// Bad — business logic masquerading as a utility
fun Task.isHighPriorityAndOverdue(): Boolean =
priority == Priority.HIGH && isOverdue
// Better — belongs in the Task domain model
data class Task(...) {
val isHighPriorityAndOverdue: Boolean get() =
priority == Priority.HIGH && isOverdue
}When it's on a type you control:
If you own the class, add the method directly rather than using an extension. Extensions on your own classes are a code smell — they suggest the method belongs in the class.
When it creates naming confusion:
If
String.format()String.format()Extensions can also be properties:
val Task.displayStatus: String
get() = if (completed) "Done" else "Active"
val Context.screenWidth: Int
get() = resources.displayMetrics.widthPixels
val View.isVisible: Boolean
get() = visibility == View.VISIBLE
set(value) { visibility = if (value) View.VISIBLE else View.GONE }Extension properties are read-only by default. Settable extensions (with
setUseful for factory methods:
data class Task(val id: String, val title: String, val completed: Boolean) {
companion object
}
fun Task.Companion.empty(): Task = Task(
id = UUID.randomUUID().toString(),
title = "",
completed = false
)
// Usage
val newTask = Task.empty()Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read