Table of Contents

Naming

Class names

Use PascalCase, which starts with an uppercase letter.

class CarModel(val modelName: String)
class UserProfile(val userId: Int, val userName: String)
class PaymentService(val paymentGateway: String)

Interface names

Use the same convention as classes. Avoid the I prefix unless it’s necessary for clarity or common in your codebase.

// Preferred
interface Storage {
    fun store(data: String)
    fun retrieve(): String
}

interface Repository<T> {
    fun getById(id: Int): T
    fun save(item: T)
}

// Only when necessary for clarity
interface IUserService {
    fun getUser(id: Int): User
}

Abstract class names

Follow the same conventions as class names, often using terms like Base or Abstract to indicate abstraction.

abstract class AbstractShape {
    abstract fun calculateArea(): Double
}

abstract class BaseRepository<T> {
    abstract fun findById(id: Int): T?
}

Functions and Methods

Function names: Use camelCase, starting with a lowercase letter.

fun fetchUserData(userId: Int): User {
    // fetch user logic
}

Extension functions: Also use camelCase, and the function name should make sense in the context of the extended class.

fun String.isEmailValid(): Boolean {
    return this.contains('@') && this.contains('.')
}

fun Int.isOdd(): Boolean {
    return this % 2 != 0
}

Variables

Variable names: Use camelCase for variables.

val companyName = "pelagornis"
val totalCount = 100

Constants: Use uppercase with underscores.

const val API_KEY = "qwodciabs1"
const val BASE_URL = "https://api.pelagornis.com"

Parameters

Function parameters: Use descriptive names in camelCase.

fun setUserName(userName: String) {
    println("User name set to: $userName")
}

Boolean parameters: Use “is” or “has” for booleans to indicate state or possession.

fun isActive(user: User): Boolean {
    return user.status == "active"
}

Kotlin Style Rules

No trailing spaces

Remove unnecessary trailing spaces from the end of lines.

Line Length

Limit lines to 120 characters for readability.

Indentation

Use 4 spaces per indentation level. Avoid using tabs.

Curly braces

Always use braces for conditionals and loops, even when the block is a single statement.

if (condition) {
    doSomething()
}

Avoid nested lambdas

If a lambda expression is too nested, consider refactoring the code to improve readability.

// Instead of
list.filter { item ->
    anotherList.any { it == item }
}
// Use a helper function
fun isItemInList(item: Item) = anotherList.contains(item)
someList.filter(::isItemInList)

Visibility Modifiers

Use explicit visibility modifiers. Avoid using default visibility unless it’s necessary.

private val userName: String

Type Inference and Explicit Types

Prefer Kotlin’s type inference whenever possible, but provide explicit types when they improve readability or when dealing with complex types.

val totalAmount = 100.0 // inferred as Double
val user: User = fetchUser() // explicit type when needed

Kotlin Formatting Rules

Function declarations

Always use spaces between function parameters for better readability.

fun calculatePrice(item: Item, discount: Double): Double {
    return item.price - (item.price * discount)
}

No spaces before function parentheses

// Wrong
fun exampleFunction () {
    // ...
}

// Right
fun exampleFunction() {
    // ...
}

Single-line lambdas

If the lambda is a single line, keep it on the same line.

val list = listOf(1, 2, 3)
val doubled = list.map { it * 2 }

Multi-line lambdas

Use braces and align the body properly.

val list = listOf(1, 2, 3)
val result = list.map {
    val newValue = it * 2
    newValue + 1
}

Control Statements

fun isEven(number: Int): Boolean {
    return number % 2 == 0
}

Data classes

Use data keyword for classes that are used as plain data holders and contain no behavior other than storing data.

data class User(val name: String, val age: Int)

Modern Kotlin Features

Sealed Classes

Use sealed classes for representing restricted class hierarchies.

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// Usage
when (val result = fetchData()) {
    is Result.Success -> handleSuccess(result.data)
    is Result.Error -> handleError(result.exception)
    is Result.Loading -> showLoading()
}

Inline Classes

Use inline classes for type-safe wrappers around primitive types.

@JvmInline
value class UserId(val value: Int)

@JvmInline
value class Email(val value: String)

fun sendEmail(userId: UserId, email: Email) {
    // Type-safe parameters
}

Extension Properties

Use extension properties for computed values.

val String.isValidEmail: Boolean
    get() = this.contains("@") && this.contains(".")

val List<Int>.average: Double
    get() = if (isEmpty()) 0.0 else sum().toDouble() / size

Destructuring Declarations

Use destructuring for data classes and collections.

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

val point = Point(10, 20)
val (x, y) = point

// With maps
val map = mapOf("name" to "John", "age" to 30)
for ((key, value) in map) {
    println("$key: $value")
}

Smart Casts

Leverage Kotlin’s smart casting for type safety.

fun processValue(value: Any) {
    when (value) {
        is String -> {
            // value is automatically cast to String here
            println(value.length)
        }
        is Int -> {
            // value is automatically cast to Int here
            println(value * 2)
        }
    }
}

Coroutines

Basic Coroutine Usage

Use coroutines for asynchronous programming.

import kotlinx.coroutines.*

// Suspend functions
suspend fun fetchUserData(userId: Int): User {
    delay(1000) // Simulate network call
    return User(id = userId, name = "User $userId")
}

// Coroutine scope
fun loadUserData() {
    CoroutineScope(Dispatchers.Main).launch {
        try {
            val user = fetchUserData(123)
            updateUI(user)
        } catch (e: Exception) {
            handleError(e)
        }
    }
}

Structured Concurrency

Use structured concurrency with coroutine scopes.

class UserRepository {
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    suspend fun fetchUsers(): List<User> = withContext(Dispatchers.IO) {
        // Network call
        apiService.getUsers()
    }

    fun cleanup() {
        scope.cancel()
    }
}

Flow for Reactive Programming

Use Flow for reactive streams.

fun observeUserUpdates(): Flow<User> = flow {
    while (true) {
        val user = fetchLatestUser()
        emit(user)
        delay(5000)
    }
}.flowOn(Dispatchers.IO)

// Usage
viewModelScope.launch {
    observeUserUpdates()
        .catch { e -> handleError(e) }
        .collect { user -> updateUI(user) }
}

Channel Communication

Use channels for communication between coroutines.

class DataProcessor {
    private val dataChannel = Channel<Data>(Channel.UNLIMITED)

    fun startProcessing() {
        CoroutineScope(Dispatchers.Default).launch {
            for (data in dataChannel) {
                processData(data)
            }
        }
    }

    suspend fun sendData(data: Data) {
        dataChannel.send(data)
    }
}

Functional Programming

Higher-Order Functions

Use higher-order functions for reusable logic.

fun <T> List<T>.filterAndTransform(
    predicate: (T) -> Boolean,
    transform: (T) -> String
): List<String> = filter(predicate).map(transform)

// Usage
val result = users.filterAndTransform(
    predicate = { it.isActive },
    transform = { "${it.name} (${it.email})" }
)

Function Composition

Compose functions for complex operations.

fun <A, B, C> ((A) -> B).andThen(f: (B) -> C): (A) -> C = { a -> f(this(a)) }

val processUser = ::validateUser
    .andThen(::saveUser)
    .andThen(::sendWelcomeEmail)

// Usage
val result = processUser(userData)

Immutable Data Structures

Prefer immutable data structures.

// Use immutable collections
val users: List<User> = listOf(user1, user2, user3)
val userMap: Map<String, User> = mapOf(
    "john" to user1,
    "jane" to user2
)

// Create new instances instead of mutating
val updatedUsers = users + newUser
val updatedMap = userMap + ("bob" to newUser)

Lambda Expressions

Use lambda expressions effectively.

// Simple lambdas
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
val doubled = numbers.map { it * 2 }

// Complex lambdas with multiple statements
val processedData = data.map { item ->
    val processed = processItem(item)
    val validated = validateItem(processed)
    validated
}

Kotlin Multiplatform

Common Code Structure

Organize common code for multiplatform projects.

// commonMain/kotlin/expect.kt
expect class Platform() {
    val name: String
}

expect fun getCurrentTime(): Long

// commonMain/kotlin/actual.kt
expect class Platform() {
    val name: String
}

// androidMain/kotlin/actual.kt
actual class Platform actual constructor() {
    actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

actual fun getCurrentTime(): Long = System.currentTimeMillis()

// iosMain/kotlin/actual.kt
actual class Platform actual constructor() {
    actual val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

actual fun getCurrentTime(): Long = NSDate().timeIntervalSince1970.toLong()

Shared Business Logic

Share business logic across platforms.

// commonMain/kotlin/UserRepository.kt
class UserRepository(private val api: ApiService) {
    suspend fun getUser(id: Int): Result<User> = try {
        val user = api.fetchUser(id)
        Result.success(user)
    } catch (e: Exception) {
        Result.failure(e)
    }

    suspend fun saveUser(user: User): Result<Unit> = try {
        api.saveUser(user)
        Result.success(Unit)
    } catch (e: Exception) {
        Result.failure(e)
    }
}

Testing

Unit Testing

Write comprehensive unit tests.

class UserServiceTest {
    private lateinit var userService: UserService
    private lateinit var mockRepository: UserRepository

    @BeforeEach
    fun setup() {
        mockRepository = mockk()
        userService = UserService(mockRepository)
    }

    @Test
    fun `should return user when valid id provided`() = runTest {
        // Given
        val userId = 123
        val expectedUser = User(id = userId, name = "John")
        coEvery { mockRepository.findById(userId) } returns expectedUser

        // When
        val result = userService.getUser(userId)

        // Then
        assertEquals(expectedUser, result)
        coVerify { mockRepository.findById(userId) }
    }
}

Coroutine Testing

Test coroutine-based code properly.

class DataProcessorTest {
    @Test
    fun `should process data asynchronously`() = runTest {
        // Given
        val processor = DataProcessor()
        val testData = listOf(Data(1), Data(2), Data(3))

        // When
        val result = processor.processDataAsync(testData)

        // Then
        assertEquals(3, result.size)
        assertTrue(result.all { it.isProcessed })
    }
}

Performance

Lazy Initialization

Use lazy initialization for expensive operations.

class ExpensiveService {
    private val heavyComputation by lazy {
        // Expensive computation
        computeHeavyData()
    }

    fun getData() = heavyComputation
}

Collection Operations

Use appropriate collection operations for performance.

// Use sequences for chained operations
val result = largeList
    .asSequence()
    .filter { it.isValid }
    .map { it.transform() }
    .take(100)
    .toList()

// Use appropriate collection types
val userMap = users.associateBy { it.id } // O(1) lookup
val userGroups = users.groupBy { it.department }

Memory Management

Be mindful of memory usage.

// Use weak references when appropriate
class Cache<T> {
    private val cache = mutableMapOf<String, WeakReference<T>>()

    fun get(key: String): T? = cache[key]?.get()

    fun put(key: String, value: T) {
        cache[key] = WeakReference(value)
    }
}