The following article describes some mistakes often seen in Kotlin codebases and examples on how to improve them.
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
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
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
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
- 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.
- 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