Sitemap

Hidden KOTLIN Functions That Will Make Your Android Code Elegant

9 min readJun 20, 2026

--

By a Senior Android Developer who’s been writing KOTLIN since day one

Press enter or click to view image in full size

After years of writing Android code first in Java, then KOTLIN. I’ve come to appreciate one thing above all else: readable code is maintainable code. KOTLIN ships with a surprisingly rich standard library, and most developers only scratch the surface. Today, I’m going to walk you through the functions I use daily that make my codebase cleaner, more expressive, and a joy to read.

1. also — The "peek and continue" operator

Most developers know let, run, apply, and with. But also the underdog.

also is perfect when you want to perform a side effect without breaking your chain.

// ❌ Before
val user = createUser()
Log.d("TAG", "Created user: $user")
saveToDatabase(user)

// ✅ After
val user = createUser()
.also { Log.d("TAG", "Created user: $it") }
.also { saveToDatabase(it) }

Where it really shines builder patterns and debugging:

fun buildRequest(): Request {
return Request.Builder()
.url(endpoint)
.addHeader("Authorization", token)
.build()
.also { Log.d("Network", "Request built: $it") } // peek without breaking chain
}

2. takeIf and takeUnless — Conditional returns without if-else noise

These two are criminally underused. They return the receiver if a predicate is met, or null otherwise.

// ❌ Before
fun getValidUsername(input: String): String? {
return if (input.length >= 3) input else null
}

// ✅ After
fun getValidUsername(input: String): String? {
return input.takeIf { it.length >= 3 }
}

takeUnless is the inverse:

// Return user only if they're NOT banned
val activeUser = user.takeUnless { it.isBanned }

// Perfect for view visibility logic
binding.errorView.takeUnless { viewModel.isLoading }?.isVisible = true

Real-world Android example:

override fun onTextChanged(text: String) {
val validInput = text.trim()
.takeIf { it.isNotEmpty() }
?.takeIf { it.length <= MAX_CHAR_LIMIT }

submitButton.isEnabled = validInput != null
}

3. run as an expression block — Replace temp variables with scoped blocks

run without a receiver lets you create an expression block, great for initializing complex objects inline.

// ❌ Before — pollutes outer scope with temp variables
val headerColor: Int
val textSize: Float
if (isPremiumUser) {
headerColor = Color.GOLD
textSize = 18f
} else {
headerColor = Color.GRAY
textSize = 14f
}

// ✅ After — scoped, clean, readable
val (headerColor, textSize) = run {
if (isPremiumUser) Color.GOLD to 18f
else Color.GRAY to 14f
}

Another great use case lazy initialization logic that’s too complex for a single expression:

val analyticsClient: AnalyticsClient = run {
val config = Config.load()
val token = SecureStorage.getToken()
AnalyticsClient(config, token).apply { initialize() }
}

4. with — Stop repeating yourself on the same object

When you’re performing multiple operations on the same object, with removes the noise.

// ❌ Before
binding.userProfileCard.isVisible = true
binding.userProfileCard.setBackgroundColor(Color.WHITE)
binding.userProfileCard.elevation = 8f
binding.userProfileCard.setOnClickListener { openProfile() }

// ✅ After
with(binding.userProfileCard) {
isVisible = true
setBackgroundColor(Color.WHITE)
elevation = 8f
setOnClickListener { openProfile() }
}

5. measureTimeMillis — Built-in performance profiling

Hidden in kotlin.system, this is a cleaner alternative to manual System.currentTimeMillis() calls.

// ❌ Before
val start = System.currentTimeMillis()
processLargeDataset(data)
Log.d("Perf", "Took: ${System.currentTimeMillis() - start}ms")

// ✅ After
val duration = measureTimeMillis {
processLargeDataset(data)
}
Log.d("Perf", "Took: ${duration}ms")

6. repeat — Kill the for (i in 0 until n) pattern

// ❌ Before
for (i in 0 until 5) {
adapter.addPlaceholderItem()
}

// ✅ After
repeat(5) { adapter.addPlaceholderItem() }

// With index if needed
repeat(5) { index ->
adapter.addSkeletonItem(position = index)
}

7. groupBy — Powerful collection transformation

Replaces verbose loop-and-map patterns:

// ❌ Before
val groupedTransactions = mutableMapOf<String, MutableList<Transaction>>()
for (transaction in transactions) {
val category = transaction.category
if (groupedTransactions[category] == null) {
groupedTransactions[category] = mutableListOf()
}
groupedTransactions[category]!!.add(transaction)
}

// ✅ After
val groupedTransactions: Map<String, List<Transaction>> =
transactions.groupBy { it.category }

Combine it with mapValues for even more power:

val totalPerCategory: Map<String, Double> = transactions
.groupBy { it.category }
.mapValues { (_, txns) -> txns.sumOf { it.amount } }

8. associateBy — Turn lists into maps instantly

A pattern I see repeated in every codebase converting a list to a lookup map:

// ❌ Before
val userMap = mutableMapOf<String, User>()
users.forEach { userMap[it.id] = it }

// ✅ After
val userMap: Map<String, User> = users.associateBy { it.id }

// Then use it
val targetUser = userMap[userId]

Need key AND value transforms? Use associate:

val idToName: Map<String, String> = users.associate { it.id to it.displayName }

9. orEmpty() — Null-safe collection handling

// ❌ Before
val tags = user.tags ?: emptyList()
val bio = user.bio ?: ""

// ✅ After
val tags = user.tags.orEmpty()
val bio = user.bio.orEmpty()

// In a chain
user.permissions
.orEmpty()
.filter { it.isActive }
.map { it.name }

10. lazy with LazyThreadSafetyMode — Smarter lazy initialization

Everyone uses by lazy, but few know you can tune its thread-safety mode:

// Default — synchronized (thread-safe but has overhead)
val heavyObject by lazy { HeavyObject() }

// Single-threaded — no synchronization overhead (perfect for ViewModels, Fragments)
val analyticsHelper by lazy(LazyThreadSafetyMode.NONE) {
AnalyticsHelper(context)
}

// Published — allows multiple initializations but only one value wins (rare use case)
val config by lazy(LazyThreadSafetyMode.PUBLICATION) {
RemoteConfig.fetch()
}

In Android, NONE is almost always what you want inside Fragment or ViewModel since they're accessed on the main thread.

11. Destructuring declarations on data classes — Stop indexing into objects

data class LatLng(val lat: Double, val lng: Double)

// ❌ Before
val location = getLocation()
val lat = location.lat
val lng = location.lng
drawPin(lat, lng)

// ✅ After
val (lat, lng) = getLocation()
drawPin(lat, lng)

// In lambdas too
locations.forEach { (lat, lng) ->
mapView.addMarker(lat, lng)
}

12. buildString / buildList / buildMap — Declarative builders for standard types

KOTLIN 1.6+ added these and they’re fantastic:

// ❌ Before
val sb = StringBuilder()
sb.append("Hello, ")
sb.append(userName)
if (isPremium) sb.append(" ⭐")
val greeting = sb.toString()

// ✅ After
val greeting = buildString {
append("Hello, ")
append(userName)
if (isPremium) append(" ⭐")
}

// buildList — replaces mutableListOf + add pattern
val menuItems = buildList {
add(MenuItem.Home)
add(MenuItem.Search)
if (isLoggedIn) add(MenuItem.Profile)
if (isAdmin) add(MenuItem.Settings)
}

Bonus: Operator overloading on contains — The in keyword

data class DateRange(val start: LocalDate, val end: LocalDate) {
operator fun contains(date: LocalDate) = date in start..end
}

val range = DateRange(startDate, endDate)

// Now reads like plain English
if (selectedDate in range) {
showAvailability()
}

Putting it all together — A real-world example

Here’s a fragment of real ViewModel code, before and after applying these patterns:

// ❌ Before — 30 lines of noise
fun loadDashboard(userId: String) {
val rawUsers = repository.getUsers()
val userMap = mutableMapOf<String, User>()
rawUsers.forEach { userMap[it.id] = it }

val currentUser = userMap[userId]
if (currentUser != null && !currentUser.isBanned) {
val transactions = repository.getTransactions(userId)
val grouped = mutableMapOf<String, MutableList<Transaction>>()
for (tx in transactions) {
grouped.getOrPut(tx.category) { mutableListOf() }.add(tx)
}
val totals = mutableMapOf<String, Double>()
for ((cat, txList) in grouped) {
totals[cat] = txList.sumOf { it.amount }
}
_uiState.value = DashboardState.Success(currentUser, totals)
}
}

// ✅ After — intention is crystal clear
fun loadDashboard(userId: String) {
val user = repository.getUsers()
.associateBy { it.id }[userId]
?.takeUnless { it.isBanned }
?: return

val categoryTotals = repository.getTransactions(userId)
.groupBy { it.category }
.mapValues { (_, txns) -> txns.sumOf { it.amount } }

_uiState.value = DashboardState.Success(user, categoryTotals)
}

13. check, require, error — Precondition functions that document intent

These are arguably the most underused KOTLIN builtins in Android codebases. Most developers write verbose if guards; these replace them with self-documenting, intention-revealing assertions.

require — Validate function arguments

Use when validating inputs to a function. Throws IllegalArgumentException on failure.

// ❌ Before
fun createUser(name: String, age: Int) {
if (name.isBlank()) throw IllegalArgumentException("Name cannot be blank")
if (age < 0) throw IllegalArgumentException("Age must be non-negative")
// ...
}

// ✅ After
fun createUser(name: String, age: Int) {
require(name.isNotBlank()) { "Name cannot be blank" }
require(age >= 0) { "Age must be non-negative, got: $age" }
// ...
}

The lambda message is lazily evaluated no string allocation unless the check fails.

check — Validate object/state consistency

Use when validating object state, not arguments. Throws IllegalStateException.

class MediaPlayer {
private var isPrepared = false

fun prepare() {
isPrepared = true
}

fun play() {
check(isPrepared) { "MediaPlayer must be prepared before calling play()" }
// ...
}
}

Rule of thumb: require what callers pass in. check for what your object currently is.

error — Unconditional state failure

A cleaner replacement for throw IllegalStateException(...). Returns Nothing, so the compiler knows execution stops.

// ❌ Before
val userType = when (role) {
"admin" -> UserType.ADMIN
"guest" -> UserType.GUEST
else -> throw IllegalStateException("Unknown role: $role")
}

// ✅ After
val userType = when (role) {
"admin" -> UserType.ADMIN
"guest" -> UserType.GUEST
else -> error("Unknown role: $role")
}

Because error() returns Nothing, the compiler is satisfied that all branches return a UserType no cast gymnastics needed.

checkNotNull and requireNotNull — Smart null assertion with context

Better than the !! operator because they tell you why it failed:

// ❌ Before — NullPointerException with zero context
val token = authManager.getToken()!!

// ✅ After — clear failure message
val token = checkNotNull(authManager.getToken()) {
"Auth token is null — user may not be logged in"
}

// requireNotNull — same but for argument validation
fun processImage(bitmap: Bitmap?) {
val safeBitmap = requireNotNull(bitmap) { "Bitmap must not be null" }
// safeBitmap is smart-cast to Bitmap here
}

Real-world example — A ViewModel method with proper guards

class CheckoutViewModel(
private val cartRepository: CartRepository,
private val paymentService: PaymentService
) : ViewModel() {

private var sessionId: String? = null

fun initSession(userId: String) {
require(userId.isNotBlank()) { "userId cannot be blank" }
sessionId = cartRepository.createSession(userId)
}

fun processPayment(amount: Double, cardToken: String) {
val activeSession = checkNotNull(sessionId) {
"Session not initialized — call initSession() first"
}
require(amount > 0) { "Payment amount must be positive, got: $amount" }
require(cardToken.isNotBlank()) { "Card token cannot be blank" }

paymentService.charge(activeSession, amount, cardToken)
}
}

This reads like a contract. Anyone calling processPayment immediately understands the preconditions without reading the implementation.

Quick reference

Press enter or click to view image in full size

These five functions replace noisy if/throw blocks with single-line, intention-revealing guards. They make the contract of your functions visible at a glance , which is exactly what readable code should do.

Conclusion

Kotlin’s standard library is a goldmine. The functions covered here also, takeIf, takeUnless, run, with, associateBy, groupBy, buildList, and the rest aren't obscure tricks. They're the language telling you how it wants to be written.

Get Himanshugaur’s stories in your inbox

Join Medium for free to get updates from this writer.

The goal isn’t to be clever. It’s to write code so clear that the next developer (or future you at 2 am) can read it and immediately understand the intent.

Start with one. Pick takeIf or also this week and use it wherever it fits. The rest will follow naturally.

If this resonated with you, follow me for more deep dives into KOTLIN, Jetpack Compose, and Android architecture patterns. Happy coding.

#Kotlin #AndroidDev #MobileDevelopment #CleanCode #Programming

👋 Let’s Connect

If you enjoy content about Android development, Jetpack Compose, Kotlin, software architecture, and engineering best practices, I’d love to stay connected.

📺 YouTube
In-depth Android tutorials, real-world projects, architecture discussions, and practical development tips.

💼 LinkedIn
Professional updates, technical insights, articles, and lessons from my software engineering journey.

✍️ Medium
More deep dives into Android development, clean architecture, testing, performance optimization, and modern engineering practices.

📚 Want to Go Deeper?

If you’re looking for structured, comprehensive learning resources, check out my Android development books, where I cover concepts in much greater depth than a typical article.

☕ Support My Work

Creating free technical content, books, tutorials, and open educational resources takes considerable time and effort. If this article helped you learn something valuable, you can support my work by:

• Purchasing one of my books
• Sharing my articles with others
• Buying me a coffee

Every bit of support helps me continue creating high-quality content for the Android community.

Press enter or click to view image in full size

Click here to support

SOLID Principles for Android Developers — Learn how to write maintainable, scalable, and testable Android applications using SOLID principles with real-world examples.

👉 Explore all my books: Book Link

--

--

Himanshugaur
Himanshugaur

Written by Himanshugaur

Android dev for 6+ years. Survived SDK migrations, wrestled with RecyclerView. Sharing practical tips, deep dives & honest takes on building great Android apps.