Hidden KOTLIN Functions That Will Make Your Android Code Elegant
By a Senior Android Developer who’s been writing KOTLIN since day one
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 = trueReal-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:
requirewhat callers pass in.checkfor 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
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.
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








