João Freitas

The following article describes some mistakes often seen in Kotlin codebases and examples on how to improve them.

https://proandroiddev.com/kotlin-unknotting-from-realizing-anti-patterns-to-becoming-a-better-developer-c1dfa6c3bab6


Kotlin has rapidly become a top choice for modern software development, particularly in the Android ecosystem. Kotlin’s interoperability with Java, its concise syntax, and its emphasis on safety, especially in terms of nullability, makes it a preferred language for many.

However, as with any programming language, Kotlin has its nuances and pitfalls. While it streamlines many programming tasks, certain practices can lead to less efficient, less readable, and less maintainable code. This is particularly true for developers transitioning from Java or other languages, who might bring habits that don’t align well with Kotlin’s philosophy and capabilities.

The Purpose of This Article

This article aims to delve into common bad practices observed in Kotlin development. We will explore real-world examples to illustrate these pitfalls and provide practical advice on how to avoid them. The goal is to empower developers to write cleaner, more efficient, and more idiomatic Kotlin code.

Common Bad Practices in Kotlin

Kotlin, like any language, has its quirks. Sometimes, we Kotlin developers, in our quest for code elegance, end up in a comedy of errors. Let’s revisit those moments with a dash of humor and some real-world, advanced examples.

A) Anti-Patterns — The Kotlin “Oops”

Oops! — Photo by Jelleke Vanooteghem on Unsplash

  1. Nullability Overload: We get it, nulls are scary. But Kotlin’s nullable types are not garlic to ward off the vampire of NullPointerException. Consider this over-cautious code snippet:

    // Instead of  
    val name: String? = person?.name?.let { it } ?: ""  
    
    // Use  
    val name = person?.name ?: "Default Name"
    

It’s like wearing a belt and suspenders and then staying home. Instead, embrace elvis operator or non-null asserted calls when you’re sure about non-nullity.

2. Lateinit Overuse — The Procrastinator’s Dream: lateinit is like promising to clean your room later. It’s tempting but leads to the infamous UninitializedPropertyAccessException at runtime. Initialize upfront or consider lazy initialization.

lateinit var lifeGoals: List<Goal> // Someday, I will initialize it...

3. Scope Functions Misadventures:

myObject.let { it.doThis() }.apply { doThat() }.also { println(it) }

This is the Kotlin equivalent of an over-packed tourist. Why carry everything in one line? Each scope function has its vacation spot. Use let for transformations, apply for object configuration, also for additional side-effects, and run when you need a bit of both let and apply.

B) Performance Overkill — The Need for Speed

Photo by Nick Fewings on Unsplash

  1. Collection Frenzy:

    list.filter { it > 10 }.map { it * 2 }
    

This is the coding equivalent of going to the store twice because you forgot the milk. Use sequences (asSequence) to turn two trips into one.

2. Object Creation Party:

for (i in 1..1000) { val point = Point(i, i) }

It’s like inviting too many guests to your party. Sure, it’s fun, but your house (or memory) won’t be happy. Be selective with object creation, especially in loops.

C) Readability and Maintainability — The Art of Code Poetry

Photo by Markus Winkler on Unsplash

  1. Overcomplicated Expressions:

    fun calculate() = this.first().second.third { it.fourth().fifth() }
    

It’s like explaining a joke — if you need to dissect it that much, it’s not working. Break it down; your future self will thank you.

2. Style Wars: Without a consistent style, reading your code is like switching between a drama and a comedy every other minute. Establish a style guide and stick to it. Kotlin’s official style guide is a good start.

3. Comment Black Holes: Sure, Kotlin is more readable than ancient hieroglyphs, but it’s not self-explanatory. Comment your complex logic, not for archaeologists, but for your fellow coders.

D) Concurrency — The Multi-Threaded Maze

Photo by Benjamin Elliott on Unsplash

  1. Coroutine Chaos: Coroutines are not free passes to Async Land. Misusing them is like trying to cook a gourmet meal in a microwave — fast but unsatisfying. Handle exceptions properly and avoid blocking the main thread.
  2. Thread Safety Assumptions: Assuming thread safety in Kotlin is like assuming it won’t rain in London. Always consider synchronization and be explicit about thread safety to avoid data races and other concurrency issues.

By recognizing these scenarios in our daily coding life, we can stride towards writing more efficient, readable, and robust Kotlin code.

Writing Better Kotlin Code — Real-World Scenarios

Now, let’s apply our newfound wisdom to real-world scenarios. We’ll transform the previously discussed bad practices into best practices with examples that are common in advanced Kotlin development.

Photo by James Lee on Unsplash

A) Enhanced Nullability Handling

Scenario: You’re working on a user profile feature and need to handle potentially null values in a user object.

Bad Practice:

val city = user?.address?.city?.let { it } ?: "Unknown"

Better Approach:

val city = user?.address?.city ?: "Unknown"

Explanation: By removing redundant let and using the Elvis operator (?:), the code becomes more concise and readable. It also efficiently handles the null case.

Let’s look at another scenario:

Developing a function in a financial application that applies a discount only if the order amount exceeds a certain threshold.

Usual Practice: Verbose conditional checks with nullable types:

val discount = if (order != null && order.amount > 100) order.calculateDiscount() else null

Better Approach: Using takeIf to succinctly apply the condition:

val discount = order?.takeIf { it.amount > 100 }?.calculateDiscount()

Explanation: The takeIf function here elegantly handles the condition, making the code more readable. It checks if the order’s amount is greater than 100; if so, calculateDiscount() is called, otherwise, it returns null. This use of takeIf encapsulates the condition within a concise, readable expression, showcasing Kotlin’s prowess in writing clear and efficient conditional logic.

B) Optimizing Collection Operations

Photo by Eran Menashri on Unsplash

Scenario: Filtering and transforming a large list of products.

Bad Practice:

val discountedProducts = products.filter { it.isOnSale }.map { it.applyDiscount() }

Better Approach:

val discountedProducts = products.asSequence()  
                                .filter { it.isOnSale }  
                                .map { it.applyDiscount() }  
                                .toList()

Explanation: Using sequences (asSequence) for chain operations on collections improves performance, especially for large datasets, by creating a single pipeline.

C) Refactoring Overcomplicated Expressions

Bad Practice:

fun calculateMetric() = data.first().transform().aggregate().finalize()

Better Approach:

fun calculateMetric(): Metric {  
    val initial = data.first()  
    val transformed = initial.transform()  
    val aggregated = transformed.aggregate()  
    return aggregated.finalize()  
}

Explanation: Breaking down the complex one-liner into multiple steps enhances readability and maintainability, making each step clear and manageable.

D) Updating Shared Resources

Photo by Nick Fewings on Unsplash

Scenario: Implementing thread-safe access to a shared data structure in a multi-threaded environment.

Bad Practice:

var sharedList = mutableListOf<String>()  

fun addToList(item: String) {  
    sharedList.add(item) // Prone to concurrent modification errors  
}

Better Approach: Using [Mutex](https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html#mutual-exclusion) from Kotlin’s coroutines library to safely control access to the shared resource.

val mutex = Mutex()  
var sharedResource: Int = 0  

suspend fun safeIncrement() {  
    mutex.withLock {  
        sharedResource++  // Safe modification with Mutex  
    }  
}

Edited — Answer updated based on Xoangon’s comment. _CoroutineScope::actor_ has been marked as obsolete and it does not mention anything about synchronization.

E) Over Complicating Type-Safe Builders

Complicated — Photo by Indra Utama on Unsplash

Bad Practice: Creating an overly complex DSL for configuring a simple object.

class Configuration {  
    fun database(block: DatabaseConfig.() -> Unit) { ... }  
    fun network(block: NetworkConfig.() -> Unit) { ... }  
    // More nested configurations  
}  

// Usage  
val config = Configuration().apply {  
    database {  
        username = "user"  
        password = "pass"  
        // More nested settings  
    }  
    network {  
        timeout = 30  
        // More nested settings  
    }  
}

Pitfall: The DSL is unnecessarily verbose for simple configuration tasks, making it harder to read and maintain.

Solution: Simplify the DSL or use a straightforward approach like data classes for configurations.

data class DatabaseConfig(val username: String, val password: String)  
data class NetworkConfig(val timeout: Int)  

val dbConfig = DatabaseConfig(username = "user", password = "pass")  
val netConfig = NetworkConfig(timeout = 30)

Explanation: This approach makes the configuration clear, concise, and maintainable, avoiding the complexity of a deep DSL.

F) Misusing Delegation and Properties

Bad Practice: Incorrect use of by lazy for a property that needs to be recalculated.

val userProfile: Profile by lazy { fetchProfile() }  
fun updateProfile() { /\* userProfile should be recalculated \*/ }

Pitfall: The userProfile remains the same even after updateProfile is called, leading to outdated data being used.

Solution: Implement a custom getter or a different state management strategy.

private var \_userProfile: Profile? = null  
val userProfile: Profile  
    get() = \_userProfile ?: fetchProfile().also { \_userProfile = it }  
fun updateProfile() { \_userProfile = null /\* Invalidate the cache \*/ }

Explanation: This approach allows userProfile to be recalculated when needed, ensuring that the data remains up-to-date.

G) Inefficient Use of Inline Functions and Reified Type Parameters

Bad Practice: Indiscriminate use of inline for a large function.

inline fun <reified T> processLargeData(data: List<Any>, noinline transform: (T) -> Unit) {  
    data.filterIsInstance<T>().forEach(transform)  
}

Pitfall: Inlining a large function can lead to increased bytecode size and can impact performance.

Solution: Use inline selectively, especially for small, performance-critical functions.

inline fun <reified T> filterByType(data: List<Any>, noinline action: (T) -> Unit) {  
    data.filterIsInstance<T>().forEach(action)  
}

Explanation: Restricting inline to smaller functions or critical sections of code prevents code bloat and maintains performance.

H) Overusing Reflection

Photo by Marc-Olivier Jodoin on Unsplash

Bad Practice: Excessive use of Kotlin reflection, impacting performance.

val properties = MyClass::class.memberProperties // Frequent use of reflection

Pitfall: Frequent use of reflection can significantly degrade performance, especially in critical paths of an application.

Solution: Minimize reflection use, leverage Kotlin’s powerful language features.

// Use data class, sealed class, or enum when possible  
val properties = myDataClass.toMap() // Convert to map without reflection

Explanation: Avoiding reflection and using Kotlin’s built-in features like data classes for introspection tasks enhances performance and readability.

Conclusion

Photo by Alex Sheldon on Unsplash

As we’ve journeyed through the quirky alleys and hidden trapdoors of Kotlin, it’s clear that every language, no matter how elegantly designed, has its pitfalls. But, with a bit of insight and a dash of humor, we can turn these “oops” moments into “aha!” realizations.

Remember, the road to Kotlin mastery isn’t just about learning the syntax; it’s about embracing the philosophy. It’s about knowing when to be lazy (but not with lateinit), and when to stop reflecting (literally) and start acting (with well-thought-out code).

So, whether you’re untangling a coroutine conundrum, simplifying a complex DSL, or just trying to make your nulls feel a little less null and void, remember: Kotlin is a journey, not a destination. And every line of code is a step towards becoming a Kotlin wizard, or at least a highly competent Kotlin muggle.

Closing Remarks

If you liked what you read, please feel free to leave your valuable feedback or appreciation. I am always looking to learn, collaborate and grow with fellow developers.

If you have any questions feel free to message me!

Follow me on Medium for more articles — Medium Profile

Connect with me on LinkedIn and Twitter for collaboration.

Happy Kotlin Coding!

#reads #nirbhay pherwani #kotlin #mistakes #anti-pattern #android