← Back to list

15 Kotlin Tricks Senior Android Developers Use Everytime

Kotlin has been the primary language for Android development for years now, but transitioning from writing good Kotlin to idiomatic Kotlin…

Halil Özel · 2026-06-16 15:53 · 1 claps · 3.6 min read paywalled
#kotlin #android #android-development #android-app-development #androiddev
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

15 Kotlin Tricks Senior Android Developers Use Everytime

Kotlin | Android

Kotlin | Android

Kotlin has been the primary language for Android development for years now, but transitioning from writing good Kotlin to idiomatic Kotlin takes time. Senior developers don’t just use Kotlin as “Java with a different syntax” — they leverage its rich standard library, coroutines, and functional programming features to write cleaner, safer, and more maintainable code.

Whether you are building a complex UI with Jetpack Compose or handling background processing, these 15 daily Kotlin tricks will help you write code like a pro.

1. Ditch Nested Ifs for takeIf and takeUnless

Instead of wrapping your logic in multiple if blocks, use takeIf to chain conditions elegantly. It returns the object if it satisfies the condition, or null if it doesn't.

// JR way
if (user.isEligible()) {
    processUser(user)
}

// SR way
user.takeIf { it.isEligible() }?.let { processUser(it) }

2. Sealed Interfaces for Robust UI States

Sealed classes are great, but sealed interfaces are even better for representing UI states, especially when working with Jetpack Compose and ViewModels. They allow a single class to implement multiple state interfaces, giving you extreme flexibility.

sealed interface UiState {
    data object Loading : UiState
    data class Success(val data: String) : UiState
    data class Error(val message: String) : UiState
}

3. Wrapping Legacy Callbacks with suspendCancellableCoroutine

Still dealing with legacy SDKs that use callbacks? Wrap them into Coroutines to keep your asynchronous code sequential and clean.

suspend fun fetchUserData(): User = suspendCancellableCoroutine { continuation ->
    api.getUser(object : Callback {
        override fun onSuccess(user: User) {
            continuation.resume(user)
        }
        override fun onError(error: Exception) {
            continuation.resumeWithException(error)
        }
    })
}

4. Custom Jetpack Compose Modifiers

Senior developers keep their Compose code clean by extracting complex or repetitive UI adjustments into extension functions on Modifier.

fun Modifier.bounceClick() = this.then(
    clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null,
        onClick = { /* Handle click */ }
    )
)

5. require and check for Fail-Fast Execution

Catch errors early by using Kotlin’s built-in precondition functions. require throws an IllegalArgumentException, while check throws an IllegalStateException.

fun processPayment(amount: Double) {
    require(amount > 0) { "Payment amount must be greater than zero" }
    check(isNetworkAvailable) { "Network must be available" }
    // Process payment
}

6. Parallelize Tasks with async and awaitAll

When you need to make multiple independent network calls, don’t run them sequentially. Use Coroutines’ async and awaitAll to execute them in parallel and reduce total loading time.

suspend fun fetchDashboardData() = coroutineScope {
    val usersDeferred = async { api.getUsers() }
    val postsDeferred = async { api.getPosts() }

    val (users, posts) = awaitAll(usersDeferred, postsDeferred)
}

7. Reified Types to Eliminate .class References

Passing Class<T> as a parameter is a Java leftover. Use inline functions with reified type parameters to make your generics type-safe at runtime.

// Creating an Intent elegantly
inline fun <reified T : Activity> Context.startActivity() {
    startActivity(Intent(this, T::class.java))
}

8. buildList and buildMap for Immutable Collections

Instead of creating a mutable collection, populating it, and casting it to an immutable one, use the standard library builder functions for cleaner syntax.

val users = buildList {
    add(User("John"))
    if (includeAdmin) {
        add(User("Admin"))
    }
}

9. Optimize Heavy Operations with Sequence

When chaining multiple collection operations (filter, map, sorted), standard lists create intermediate collections in memory. Using asSequence() optimizes this by evaluating items lazily.

val activeUserNames = users.asSequence()
    .filter { it.isActive }
    .map { it.name }
    .toList()

10. Dagger Hilt @Binds Over @Provides

When injecting interfaces using Dagger Hilt, using @Binds on an abstract class is more performant than using @Provides because it avoids creating extra boilerplate code under the hood.

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    abstract fun bindUserRepository(
        userRepositoryImpl: UserRepositoryImpl
    ): UserRepository
}

11. Destructuring Declarations for Cleaner Data Extraction

Kotlin allows you to unpack objects into multiple variables at once. This is extremely useful for processing Map entries or working with Data Classes.

val (id, name, email) = user

12. Safe Casting with as

Avoid ClassCastException crashes by using the safe cast operator as?. It returns null if the cast fails, allowing you to handle the error gracefully with the Elvis operator (?:).

val fragment = supportFragmentManager.findFragmentById(R.id.container) as? MyCustomFragment
    ?: return

13. Combining StateFlows Flawlessly

When building reactive UIs, you often need to merge multiple data streams. The combine function allows you to react to changes from multiple StateFlow sources and emit a single cohesive UI state.

Kotlin

val uiState = combine(userFlow, settingsFlow) { user, settings ->
    UiState(user.name, settings.theme)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState.Loading)

14. by lazy for Expensive Object Initialization

Don’t instantiate heavy objects until you actually need them. by lazy delays the initialization until the property is accessed for the first time.

val database by lazy { 
    Room.databaseBuilder(context, AppDatabase::class.java, "app-db").build() 
}

15. The Power of apply and also

Scope functions are the bread and butter of Kotlin. Use apply for object configuration (like setting up UI components) and also for side effects (like logging).

val button = Button(context).apply {
    text = "Submit"
    textSize = 16f
}.also {
    Log.d("UI", "Button initialized: ${it.text}")
}

What is your favorite Kotlin trick? Let me know in the comments below!

Writing good code takes time, and writing about it takes coffee. ☕ If you found these tips helpful and want to support my work, you can buy me a coffee below!

[embed]Halil Ozel is developing Mobile apps. 💻 My name is Halil Ozel, I'm an Android 📱 Developer 💻 from Türkiye 🇹🇷buymeacoffee.com

Drop a clap 👏 and follow for more advanced Android Development content!


메타데이터
post_id
c013a731412c
slug
15-kotlin-tricks-senior-android-developers-use-everytime-c013a731412c
url
https://medium.com/@halilozel1903/15-kotlin-tricks-senior-android-developers-use-everytime-c013a731412c
canonical_url
https://medium.com/@halilozel1903/15-kotlin-tricks-senior-android-developers-use-everytime-c013a731412c
author_url
https://medium.com/@halilozel1903
status
ok
fetched_at
2026-06-17 08:20:12