Top mistakes of beginner Kotlin developers: how to avoid them and write clean code
Kotlin is a modern, concise, and safe language that has quickly gained popularity in the world of Android development and backend. However, its flexibility and syntactic sugar often mislead beginners. In this article, we will look at 4 typical mistakes that beginner Kotlin developers make and show how to fix them using code examples.
1. Ignoring null safety and incorrect use of the !! operator
One of the main features of Kotlin is null safety. But many beginners, accustomed to Java, continue to use !! (not-null assertion operator) mindlessly. This is a direct path to NullPointerException.
Bad example:
fun getUserName(user: User?): String { return user!!.name // If user == null, there will be NPE}Good example (safe work with nullable):
fun getUserNameSafe(user: User?): String { return user?.name ?: "Unknown user"}Use ?.let{}, ?: (Elvis operator) and ?.run{} for safe access. The !! operator is justified only in rare cases when you are 100% sure that the value is not null (for example, in tests).
2. Incorrect use of sealed classes and when
Sealed classes are a powerful tool for modeling restricted hierarchies. But beginners often forget that when must be exhaustive when working with sealed classes. Or, conversely, they use else where it is not needed.
Bad example (missing branch):
sealed class Result { data class Success(val data: String) : Result() data class Error(val message: String) : Result()}
fun handleResult(result: Result) { when (result) { is Result.Success -> println(result.data) // Branch for Error is missing — there will be a warning }}Good example (exhaustive when):
fun handleResultExhaustive(result: Result) { when (result) { is Result.Success -> println(result.data) is Result.Error -> println(result.message) } // The compiler does not require else, since all options are covered}If you add a new branch to a sealed class, the compiler will tell you where to update the when. This protects the code from errors during refactoring.
3. Overusing coroutines: launching without context and ignoring structured concurrency
Coroutines are a great way to work with asynchrony, but beginners often launch them without the correct context or forget about structured concurrency. This leads to memory leaks and uncontrolled background tasks.
Bad example (global scope and loss of context):
fun loadData() { GlobalScope.launch { // Bad! Lives as long as the application lives val data = fetchData() // May execute after Activity is destroyed updateUI(data) // Error: UI already destroyed }}Good example (structured concurrency with viewModelScope):
class MyViewModel : ViewModel() { fun loadData() { viewModelScope.launch { val data = fetchData() updateUI(data) // Automatically cancelled when ViewModel is cleared } }}Always use coroutineScope, viewModelScope or your own CoroutineScope with a clear lifecycle. Avoid GlobalScope in production code.
4. Operator overloading without understanding semantics
Kotlin allows overloading operators (+, -, in