Skip to main content

Best Practices

Trushi Jasani
EditReport

Kotlin Best Practices

1. Prefer val Over varโ€‹

Always use val (immutable) unless mutation is truly needed.

// Bad
var name = "Alice"
var count = 0

// Good
val name = "Alice"
var count = 0 // Only var when it needs to change

2. Leverage Null Safetyโ€‹

Never use !! unless you are absolutely certain the value is non-null.

// Bad โ€” can crash
val length = name!!.length

// Good โ€” safe handling
val length = name?.length ?: 0

// Better โ€” use let for null-safe blocks
name?.let {
println("Name is $it, length is ${it.length}")
}

3. Use Data Classes for Simple Data Holdersโ€‹

// Bad โ€” too much boilerplate
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?) = ...
override fun hashCode() = ...
override fun toString() = ...
}

// Good
data class Point(val x: Int, val y: Int)

4. Use when Instead of Long if-else Chainsโ€‹

// Bad
fun getDay(num: Int): String {
if (num == 1) return "Monday"
else if (num == 2) return "Tuesday"
else if (num == 3) return "Wednesday"
else return "Unknown"
}

// Good
fun getDay(num: Int) = when (num) {
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
else -> "Unknown"
}

5. Use String Templates Instead of Concatenationโ€‹

// Bad
val greeting = "Hello, " + name + "! You are " + age + " years old."

// Good
val greeting = "Hello, $name! You are $age years old."

6. Use Extension Functionsโ€‹

Add behavior to existing classes cleanly:

// Bad โ€” utility class
object StringUtils {
fun isPalindrome(str: String) = str == str.reversed()
}

// Good โ€” extension function
fun String.isPalindrome() = this == this.reversed()

// Usage
println("racecar".isPalindrome()) // true

7. Use apply, also, let, run, with Appropriatelyโ€‹

// apply โ€” configure an object (returns the object)
val person = Person().apply {
name = "Alice"
age = 30
}

// also โ€” side effects (returns the object)
val numbers = mutableListOf(1, 2, 3)
.also { println("Original: $it") }

// let โ€” transform or null-check (returns lambda result)
val upper = name?.let { it.uppercase() }

// run โ€” execute a block and return result
val length = "Hello".run { this.length }

// with โ€” call multiple methods on an object
val result = with(StringBuilder()) {
append("Hello")
append(", ")
append("World!")
toString()
}

8. Prefer Collection Functions Over Loopsโ€‹

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// Bad โ€” manual loop
val evenSquares = mutableListOf<Int>()
for (n in numbers) {
if (n % 2 == 0) evenSquares.add(n * n)
}

// Good โ€” functional style
val evenSquares = numbers.filter { it % 2 == 0 }.map { it * it }

9. Use Sealed Classes for Representing Statesโ€‹

sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val message: String) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}

fun handle(result: NetworkResult<String>) = when (result) {
is NetworkResult.Success -> println("Data: ${result.data}")
is NetworkResult.Error -> println("Error: ${result.message}")
is NetworkResult.Loading -> println("Loading...")
}

10. Use Default and Named Arguments Instead of Overloadingโ€‹

// Bad โ€” multiple overloads
fun createUser(name: String): User = createUser(name, 18)
fun createUser(name: String, age: Int): User = createUser(name, age, "user")
fun createUser(name: String, age: Int, role: String): User = User(name, age, role)

// Good โ€” one function with defaults
fun createUser(name: String, age: Int = 18, role: String = "user") = User(name, age, role)

// Usage
val user1 = createUser("Alice")
val user2 = createUser("Bob", role = "admin")

11. Use companion object for Factory Methodsโ€‹

class Temperature private constructor(val celsius: Double) {
companion object {
fun fromCelsius(value: Double) = Temperature(value)
fun fromFahrenheit(value: Double) = Temperature((value - 32) * 5 / 9)
fun fromKelvin(value: Double) = Temperature(value - 273.15)
}

val fahrenheit get() = celsius * 9 / 5 + 32
}

fun main() {
val temp = Temperature.fromFahrenheit(98.6)
println("${temp.celsius}ยฐC")
}

12. Avoid Using Any When Generics Can Workโ€‹

// Bad โ€” loses type safety
fun printBox(box: Box<Any>) = println(box.value)

// Good โ€” type-safe with generics
fun <T> printBox(box: Box<T>) = println(box.value)

13. Naming Conventionsโ€‹

ElementConventionExample
ClassesPascalCaseUserProfile
FunctionscamelCasegetUserName()
VariablescamelCasefirstName
ConstantsUPPER_SNAKE_CASEMAX_RETRY_COUNT
Packageslowercasecom.example.app
FilesPascalCaseUserProfile.kt

14. Handle Errors Explicitlyโ€‹

// Bad โ€” crashes on invalid input
val num = readLine()!!.toInt()

// Good โ€” explicit error handling
val num = readLine()?.toIntOrNull() ?: run {
println("Invalid input")
return
}

15. Write Meaningful Testsโ€‹

import kotlin.test.*

class CalculatorTest {
@Test
fun `addition returns correct sum`() {
val calc = Calculator()
assertEquals(5, calc.add(2, 3))
}

@Test
fun `division by zero throws exception`() {
assertFailsWith<ArithmeticException> {
Calculator().divide(10, 0)
}
}
}

Quick Reference Checklistโ€‹

  • Prefer val over var
  • Use ?. and ?: instead of !!
  • Use data class for plain data holders
  • Use when for multi-branch conditionals
  • Use string templates not concatenation
  • Prefer extension functions over utility classes
  • Use scope functions (apply, let, run, etc.)
  • Use collection operations over manual loops
  • Use sealed classes for exhaustive states
  • Follow Kotlin naming conventions
  • Handle nullability and errors explicitly
  • Write clear, testable code
Telemetry Integration

Completed working through this block? Sync progress to workspace.