Kotlin - Coding Conventions

Overview

Kotlin coding conventions provide guidelines for writing clean, consistent, and readable code. This tutorial covers the official Kotlin style guide, naming conventions, formatting rules, and best practices followed by the Kotlin community and major projects.

๐ŸŽฏ Learning Objectives:
  • Understand official Kotlin coding conventions
  • Learn proper naming conventions for all language elements
  • Master code formatting and style guidelines
  • Apply best practices for code organization
  • Use automated tools for code formatting and style checking

Official Kotlin Style Guide

Basic Formatting Rules

// โœ… Correct indentation (4 spaces)
class User {
    val name: String = ""
    
    fun displayInfo() {
        if (name.isNotEmpty()) {
            println("User: $name")
        }
    }
}

// โœ… Proper bracing style
fun processData(data: List): List {
    return data
        .filter { it.isNotBlank() }
        .map { it.trim() }
}

// โœ… Line length (120 characters max recommended)
val longVariableName = "This is a reasonably long string that demonstrates proper line length handling"

// โœ… Breaking long parameter lists
fun createUser(
    name: String,
    email: String,
    age: Int,
    address: String,
    phoneNumber: String
): User {
    return User(name, email, age, address, phoneNumber)
}

Whitespace and Spacing

// โœ… Proper spacing around operators
val result = a + b * c
val isValid = name.isNotEmpty() && age > 0
val range = 1..10

// โœ… Spacing in function calls
fun calculate(x: Int, y: Int) = x + y
val sum = calculate(10, 20)

// โœ… Spacing in collections
val numbers = listOf(1, 2, 3, 4, 5)
val map = mapOf("key1" to "value1", "key2" to "value2")

// โœ… Spacing around colons
val name: String = "John"
fun getValue(): Int = 42

// โŒ Avoid excessive spacing
val result = a+b*c  // No spaces around operators
val sum = calculate(10,20)  // No spaces after commas
val numbers = listOf( 1 , 2 , 3 )  // Excessive spaces

Naming Conventions

Classes and Interfaces

// โœ… PascalCase for classes
class UserRepository
class DatabaseConnection
class ApiResponse

// โœ… Interface names
interface UserService
interface Drawable
interface ClickListener

// โœ… Abstract classes
abstract class BaseEntity
abstract class AbstractProcessor

// โœ… Data classes
data class User(val name: String, val email: String)
data class ApiResponse(val data: T, val success: Boolean)

// โœ… Sealed classes
sealed class Result
data class Success(val data: T) : Result()
data class Error(val message: String) : Result()

// โœ… Enum classes
enum class Status { PENDING, APPROVED, REJECTED }
enum class HttpMethod { GET, POST, PUT, DELETE }

Functions and Variables

// โœ… camelCase for functions
fun calculateTotal(items: List): Double { }
fun isValidEmail(email: String): Boolean { }
fun getUserById(id: Long): User? { }

// โœ… camelCase for variables
val userName: String = "john_doe"
val totalAmount: Double = 999.99
val isLoggedIn: Boolean = true

// โœ… Boolean variables and functions
val isActive: Boolean = true
val hasPermission: Boolean = false
fun canAccess(): Boolean = true
fun shouldUpdate(): Boolean = false

// โœ… Collection variables
val userList: List = emptyList()
val userMap: Map = emptyMap()
val activeUsers: Set = emptySet()

// โŒ Avoid Hungarian notation
val strUserName: String = "john"  // Avoid type prefixes
val intAge: Int = 25  // Type is already clear

Constants and Properties

// โœ… UPPER_SNAKE_CASE for constants
class ApiConstants {
    companion object {
        const val BASE_URL = "https://api.example.com"
        const val MAX_RETRY_ATTEMPTS = 3
        const val DEFAULT_TIMEOUT = 30000L
    }
}

// โœ… Object constants
object DatabaseConfig {
    const val DRIVER_CLASS_NAME = "org.postgresql.Driver"
    const val CONNECTION_POOL_SIZE = 10
}

// โœ… Top-level constants
const val API_VERSION = "v1"
const val DEFAULT_PAGE_SIZE = 20

// โœ… Enum constants
enum class Status {
    PENDING,
    IN_PROGRESS,
    COMPLETED,
    CANCELLED
}

// โœ… Property naming
class User {
    val firstName: String = ""
    val lastName: String = ""
    val emailAddress: String = ""
    var isActive: Boolean = true
    
    // โœ… Computed properties
    val fullName: String
        get() = "$firstName $lastName"
    
    val displayName: String
        get() = if (fullName.isNotBlank()) fullName else emailAddress
}

Packages and Imports

// โœ… Package names (lowercase, dots as separators)
package com.example.myapp.data.repository
package com.example.myapp.ui.components  
package com.example.myapp.utils.extensions

// โœ… Import organization
import com.example.myapp.data.User
import com.example.myapp.data.UserRepository
import com.example.myapp.utils.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.time.LocalDateTime
import java.util.*

// โœ… Wildcard imports for large groups (optional)
import com.example.myapp.data.*  // Only when importing many classes
import java.time.*  // Common for date/time operations

// โŒ Avoid unnecessary imports
import java.lang.String  // String is imported by default
import kotlin.collections.List  // List is imported by default

Code Organization

Class Structure

// โœ… Recommended class member ordering
class UserService(
    private val userRepository: UserRepository,
    private val emailService: EmailService
) {
    // 1. Companion object
    companion object {
        const val MAX_LOGIN_ATTEMPTS = 5
        private val logger = LoggerFactory.getLogger(UserService::class.java)
    }
    
    // 2. Properties
    private val activeUsers = mutableSetOf()
    
    // 3. Init blocks
    init {
        logger.info("UserService initialized")
    }
    
    // 4. Public methods
    fun createUser(userData: CreateUserRequest): User {
        return userRepository.save(User(userData.name, userData.email))
    }
    
    fun getUserById(id: Long): User? {
        return userRepository.findById(id)
    }
    
    // 5. Private methods
    private fun validateUserData(userData: CreateUserRequest): Boolean {
        return userData.name.isNotBlank() && userData.email.contains("@")
    }
    
    private fun sendWelcomeEmail(user: User) {
        emailService.sendEmail(user.email, "Welcome!", "Welcome to our platform!")
    }
    
    // 6. Nested classes
    data class CreateUserRequest(val name: String, val email: String)
}

File Organization

// โœ… File header (optional copyright notice)
/*
 * Copyright 2024 Example Company
 * Licensed under the Apache License, Version 2.0
 */

// โœ… Package declaration
package com.example.myapp.service

// โœ… Imports (grouped and sorted)
import com.example.myapp.data.User
import com.example.myapp.repository.UserRepository
import kotlinx.coroutines.flow.Flow
import java.time.LocalDateTime

// โœ… Top-level declarations
const val DEFAULT_CACHE_SIZE = 100

// โœ… Main class
class UserService {
    // Class implementation
}

// โœ… Extension functions (related to main class)
fun User.isActive(): Boolean = this.status == UserStatus.ACTIVE

// โœ… Utility functions (if closely related)
private fun generateUserId(): String = UUID.randomUUID().toString()

Function and Method Conventions

Function Declaration Style

// โœ… Single-expression functions
fun double(x: Int) = x * 2
fun isEven(number: Int) = number % 2 == 0
fun formatName(first: String, last: String) = "$first $last"

// โœ… Block body for complex functions
fun processOrder(order: Order): OrderResult {
    validateOrder(order)
    val result = calculateTotal(order)
    updateInventory(order.items)
    return OrderResult(result, Status.PROCESSED)
}

// โœ… Parameter alignment for long parameter lists
fun createComplexObject(
    name: String,
    description: String,
    category: Category,
    price: BigDecimal,
    availability: Boolean,
    metadata: Map
): ComplexObject {
    // Implementation
}

// โœ… Return type placement
fun calculateTax(amount: Double, rate: Double): Double {
    return amount * rate
}

// โœ… Generic function formatting
fun  List.second(): T? = if (size >= 2) this[1] else null

fun  Iterable.mapNotNull(transform: (T) -> R?): List {
    return mapNotNullTo(ArrayList(), transform)
}

Lambda Expressions

// โœ… Lambda formatting
val numbers = listOf(1, 2, 3, 4, 5)

// Short lambdas on single line
val doubled = numbers.map { it * 2 }
val evens = numbers.filter { it % 2 == 0 }

// Longer lambdas with multiple lines
val processed = numbers.map { number ->
    val doubled = number * 2
    val formatted = "Number: $doubled"
    formatted.uppercase()
}

// โœ… Lambda with multiple parameters
val pairs = listOf("a" to 1, "b" to 2, "c" to 3)
pairs.forEach { (key, value) ->
    println("$key = $value")
}

// โœ… Trailing lambda syntax
fun withTransaction(block: () -> Unit) {
    // Implementation
}

withTransaction {
    // Multiple lines of code
    performDatabaseOperation()
    updateCache()
    logTransaction()
}

// โœ… Lambda parameter naming
users.filter { user -> user.isActive }  // Clear parameter name
users.sortedBy { user -> user.name }   // Better than 'it' when complex

Documentation and Comments

KDoc Documentation

/**
 * Service for managing user operations including creation, updates, and authentication.
 * 
 * This service provides comprehensive user management functionality with proper
 * validation, security checks, and audit logging.
 *
 * @property userRepository Repository for user data persistence
 * @property emailService Service for sending email notifications
 * @since 1.0.0
 * @author Development Team
 */
class UserService(
    private val userRepository: UserRepository,
    private val emailService: EmailService
) {
    
    /**
     * Creates a new user with the provided information.
     *
     * The method validates the input data, checks for duplicate emails,
     * and sends a welcome email upon successful creation.
     *
     * @param userData The user creation request containing name and email
     * @return The created user object with generated ID and timestamps
     * @throws IllegalArgumentException if the user data is invalid
     * @throws DuplicateEmailException if the email already exists
     * @see validateUserData
     * @sample sampleCreateUser
     */
    fun createUser(userData: CreateUserRequest): User {
        require(validateUserData(userData)) { "Invalid user data provided" }
        
        if (userRepository.existsByEmail(userData.email)) {
            throw DuplicateEmailException("Email already exists: ${userData.email}")
        }
        
        val user = User(
            name = userData.name,
            email = userData.email,
            createdAt = LocalDateTime.now()
        )
        
        val savedUser = userRepository.save(user)
        sendWelcomeEmail(savedUser)
        
        return savedUser
    }
}

/**
 * Sample usage of createUser method.
 */
fun sampleCreateUser() {
    val service = UserService(userRepository, emailService)
    val request = CreateUserRequest("John Doe", "[email protected]")
    val user = service.createUser(request)
    println("Created user: ${user.name}")
}

Code Comments

class PaymentProcessor {
    
    fun processPayment(amount: BigDecimal, paymentMethod: PaymentMethod): PaymentResult {
        // Validate minimum payment amount
        require(amount >= BigDecimal("0.01")) { "Amount must be positive" }
        
        // TODO: Implement fraud detection
        // Check against known fraud patterns and risk scores
        
        when (paymentMethod.type) {
            PaymentType.CREDIT_CARD -> {
                // Credit card processing requires additional security checks
                validateCreditCard(paymentMethod as CreditCardPayment)
                return processCreditCardPayment(amount, paymentMethod)
            }
            PaymentType.BANK_TRANSFER -> {
                // Bank transfers typically take 1-3 business days
                return processBankTransfer(amount, paymentMethod as BankTransferPayment)
            }
            else -> {
                // FIXME: Add support for digital wallets (PayPal, Apple Pay, etc.)
                throw UnsupportedOperationException("Payment method not supported: ${paymentMethod.type}")
            }
        }
    }
    
    private fun validateCreditCard(payment: CreditCardPayment) {
        // Luhn algorithm for credit card validation
        val cardNumber = payment.cardNumber.replace("\\s".toRegex(), "")
        
        // Implementation note: This is a simplified validation
        // Production code should use a proper payment validation library
        require(cardNumber.length in 13..19) { "Invalid card number length" }
    }
}

Error Handling Conventions

Exception Handling Style

// โœ… Specific exception handling
fun parseUserData(json: String): User {
    return try {
        Json.decodeFromString(json)
    } catch (e: SerializationException) {
        throw IllegalArgumentException("Invalid user JSON format", e)
    } catch (e: IllegalArgumentException) {
        throw IllegalArgumentException("Invalid user data", e)
    }
}

// โœ… Resource management with use
fun readUserFile(filename: String): String {
    return File(filename).bufferedReader().use { reader ->
        reader.readText()
    }
}

// โœ… Custom exception classes
class UserNotFoundException(userId: Long) : Exception("User not found with ID: $userId")

class DuplicateEmailException(email: String) : Exception("User with email '$email' already exists")

class InvalidPasswordException(message: String) : Exception(message)

// โœ… Result-based error handling
sealed class UserResult {
    data class Success(val data: T) : UserResult()
    data class Error(val message: String, val cause: Throwable? = null) : UserResult()
}

fun getUserSafely(id: Long): UserResult {
    return try {
        val user = userRepository.findById(id)
        if (user != null) {
            UserResult.Success(user)
        } else {
            UserResult.Error("User not found")
        }
    } catch (e: Exception) {
        UserResult.Error("Database error", e)
    }
}

Testing Conventions

Test Naming and Structure

class UserServiceTest {
    
    // โœ… Descriptive test method names
    @Test
    fun `should create user with valid data`() {
        // Given
        val userData = CreateUserRequest("John Doe", "[email protected]")
        
        // When
        val result = userService.createUser(userData)
        
        // Then
        assertEquals("John Doe", result.name)
        assertEquals("[email protected]", result.email)
        assertNotNull(result.id)
    }
    
    @Test
    fun `should throw exception when email already exists`() {
        // Given
        val existingUser = CreateUserRequest("Jane Doe", "[email protected]")
        userService.createUser(existingUser)
        val duplicateUser = CreateUserRequest("John Doe", "[email protected]")
        
        // When & Then
        assertThrows {
            userService.createUser(duplicateUser)
        }
    }
    
    @Test
    fun `should return null when user not found`() {
        // Given
        val nonExistentId = 999L
        
        // When
        val result = userService.getUserById(nonExistentId)
        
        // Then
        assertNull(result)
    }
    
    // โœ… Parameterized tests
    @ParameterizedTest
    @ValueSource(strings = ["", " ", "invalid-email", "@example.com", "user@"])
    fun `should reject invalid email addresses`(invalidEmail: String) {
        // Given
        val userData = CreateUserRequest("John Doe", invalidEmail)
        
        // When & Then
        assertThrows {
            userService.createUser(userData)
        }
    }
    
    // โœ… Test data builders
    private fun createValidUser() = CreateUserRequest(
        name = "Test User",
        email = "[email protected]"
    )
    
    private fun createUserWithEmail(email: String) = CreateUserRequest(
        name = "Test User",
        email = email
    )
}

IDE Configuration and Automation

IntelliJ IDEA Code Style

// .editorconfig file
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.{kt,kts}]
indent_size = 4
max_line_length = 120

[*.{yaml,yml}]
indent_size = 2

[*.json]
indent_size = 2

Ktlint Configuration

// build.gradle.kts
plugins {
    id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
}

ktlint {
    version.set("0.50.0")
    debug.set(true)
    verbose.set(true)
    android.set(false)
    outputToConsole.set(true)
    outputColorName.set("RED")
    ignoreFailures.set(false)
    
    filter {
        exclude("**/generated/**")
        include("**/kotlin/**")
    }
}

Detekt Configuration

# detekt.yml
style:
  MaxLineLength:
    maxLineLength: 120
    excludePackageStatements: true
    excludeImportStatements: true
    
  FunctionNaming:
    functionPattern: '^([a-z][a-zA-Z0-9]*)|(`.*`)$'
    excludeClassOnConstruct: true
    
  ClassNaming:
    classPattern: '[A-Z][a-zA-Z0-9]*'
    
  VariableNaming:
    variablePattern: '[a-z][a-zA-Z0-9]*'
    privateVariablePattern: '(_)?[a-z][a-zA-Z0-9]*'

Project Structure Conventions

Recommended Directory Structure

src/
โ”œโ”€โ”€ main/
โ”‚   โ”œโ”€โ”€ kotlin/
โ”‚   โ”‚   โ””โ”€โ”€ com/
โ”‚   โ”‚       โ””โ”€โ”€ example/
โ”‚   โ”‚           โ””โ”€โ”€ myapp/
โ”‚   โ”‚               โ”œโ”€โ”€ config/          # Configuration classes
โ”‚   โ”‚               โ”œโ”€โ”€ controller/      # REST controllers
โ”‚   โ”‚               โ”œโ”€โ”€ data/           
โ”‚   โ”‚               โ”‚   โ”œโ”€โ”€ entity/      # JPA entities
โ”‚   โ”‚               โ”‚   โ”œโ”€โ”€ repository/  # Data repositories
โ”‚   โ”‚               โ”‚   โ””โ”€โ”€ dto/         # Data transfer objects
โ”‚   โ”‚               โ”œโ”€โ”€ service/         # Business logic
โ”‚   โ”‚               โ”œโ”€โ”€ utils/           # Utility classes
โ”‚   โ”‚               โ”œโ”€โ”€ exception/       # Custom exceptions
โ”‚   โ”‚               โ””โ”€โ”€ Application.kt   # Main application class
โ”‚   โ””โ”€โ”€ resources/
โ”‚       โ”œโ”€โ”€ application.yml
โ”‚       โ”œโ”€โ”€ logback.xml
โ”‚       โ””โ”€โ”€ static/
โ””โ”€โ”€ test/
    โ””โ”€โ”€ kotlin/
        โ””โ”€โ”€ com/
            โ””โ”€โ”€ example/
                โ””โ”€โ”€ myapp/
                    โ”œโ”€โ”€ controller/      # Controller tests
                    โ”œโ”€โ”€ service/         # Service tests
                    โ”œโ”€โ”€ repository/      # Repository tests
                    โ””โ”€โ”€ integration/     # Integration tests

Best Practices Summary

Do's and Don'ts

// โœ… DO: Use meaningful names
class UserAuthenticationService
fun calculateMonthlyPayment(principal: Double, rate: Double, months: Int): Double

// โŒ DON'T: Use abbreviations or unclear names
class UAS  // Unclear abbreviation
fun calc(p: Double, r: Double, m: Int): Double  // Unclear parameters

// โœ… DO: Prefer immutability
val users = listOf(user1, user2, user3)
data class User(val name: String, val email: String)

// โŒ DON'T: Use mutable state unnecessarily
var users = mutableListOf(user1, user2, user3)  // When immutable list would work

// โœ… DO: Use extension functions appropriately
fun String.isValidEmail(): Boolean = contains("@") && contains(".")

// โŒ DON'T: Create unnecessary extension functions
fun String.length(): Int = this.length  // Built-in property exists

// โœ… DO: Handle nullability explicitly
fun processUser(user: User?) {
    user?.let { u ->
        println("Processing ${u.name}")
    }
}

// โŒ DON'T: Use !! unless absolutely necessary
fun processUser(user: User?) {
    println("Processing ${user!!.name}")  // Dangerous!
}

Code Review Guidelines

Review Checklist

  • Naming: Are names descriptive and follow conventions?
  • Functions: Are functions focused and single-purpose?
  • Classes: Do classes have clear responsibilities?
  • Comments: Are complex algorithms documented?
  • Error Handling: Are exceptions handled appropriately?
  • Tests: Are there adequate unit tests?
  • Performance: Are there obvious performance issues?
  • Security: Are there security concerns?

Key Takeaways

  • Follow official Kotlin coding conventions for consistency
  • Use descriptive names that clearly communicate intent
  • Maintain proper code organization and structure
  • Write comprehensive documentation using KDoc
  • Use automated tools like ktlint and detekt for style enforcement
  • Prefer immutability and functional programming patterns
  • Handle nullability explicitly and safely
  • Write clear, descriptive test names and methods

Practice Exercises

  1. Refactor a poorly formatted Java class to follow Kotlin conventions
  2. Set up ktlint and detekt in a project and fix all style violations
  3. Write comprehensive KDoc documentation for a complex service class
  4. Create a style guide document for your team based on these conventions

Quiz

  1. What's the recommended maximum line length in Kotlin?
  2. How should boolean variables and functions be named?
  3. What's the proper order of class members in Kotlin?
Show Answers
  1. The recommended maximum line length is 120 characters, though some teams prefer 100 characters.
  2. Boolean variables should use "is", "has", "can", or "should" prefixes (isActive, hasPermission), and boolean functions should be questions (canAccess(), shouldUpdate()).
  3. Class member order: companion object, properties, init blocks, public methods, private methods, nested classes.