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
- Refactor a poorly formatted Java class to follow Kotlin conventions
- Set up ktlint and detekt in a project and fix all style violations
- Write comprehensive KDoc documentation for a complex service class
- Create a style guide document for your team based on these conventions
Quiz
- What's the recommended maximum line length in Kotlin?
- How should boolean variables and functions be named?
- What's the proper order of class members in Kotlin?
Show Answers
- The recommended maximum line length is 120 characters, though some teams prefer 100 characters.
- Boolean variables should use "is", "has", "can", or "should" prefixes (isActive, hasPermission), and boolean functions should be questions (canAccess(), shouldUpdate()).
- Class member order: companion object, properties, init blocks, public methods, private methods, nested classes.