Skip to main content

Kotlin Coding Conventions

Introduction

Coding conventions are a set of guidelines for writing code in a specific programming language. They help maintain consistency across projects and teams, making code more readable, maintainable, and easier to collaborate on. For Kotlin, following established conventions not only improves code quality but also helps you leverage the language's features effectively.

In this guide, we'll explore the official Kotlin coding conventions as well as commonly accepted best practices that will help you write clean, idiomatic Kotlin code.

Why Coding Conventions Matter

Before diving into specific conventions, let's understand why they're important:

  • Readability: Consistent formatting makes code easier to read and understand
  • Maintainability: Standardized code is easier to modify and debug
  • Collaboration: Team members can understand each other's code more quickly
  • Onboarding: New developers can get up to speed faster
  • Tool Integration: Many tools follow conventions for features like auto-formatting

Naming Conventions

Package Names

Package names are written in lowercase, without underscores, and use the reverse domain notation:

kotlin
package com.example.myapplication
package org.kotlinlang.demo

Class and Interface Names

Use PascalCase (also known as UpperCamelCase) for class and interface names:

kotlin
class MainActivity : AppCompatActivity()
interface OnClickListener
class UserRepository

Function and Property Names

Use camelCase for function and property names:

kotlin
fun calculateTotalPrice(): Double
val userName: String
var isEnabled: Boolean

Constants

Constants (immutable values known at compile time) use uppercase with underscores:

kotlin
const val MAX_COUNT = 10
const val API_BASE_URL = "https://api.example.com"

Backing Properties

When you need a backing property, name the private property with an underscore prefix:

kotlin
class User {
private val _messages: MutableList<String> = mutableListOf()
val messages: List<String> get() = _messages
}

Source File Organization

File Structure

A Kotlin source file should be organized as follows:

  1. Package statement
  2. Import statements (alphabetically sorted, no wildcards)
  3. Top-level declarations (classes, functions, properties, etc.)

Example:

kotlin
package com.example.project

import android.content.Context
import android.os.Bundle
import android.widget.TextView
import java.util.Date

class MainActivity {
// Class implementation
}

fun utilityFunction() {
// Function implementation
}

One Class Per File

Generally, place each class or interface in a separate file. Name the file after the class, using PascalCase:

  • User.kt for a class named User
  • NetworkService.kt for a class named NetworkService

Multiple related classes may be placed in a single file if they're small and closely related.

Formatting

Indentation

Use 4 spaces for indentation, not tabs:

kotlin
fun exampleFunction() {
if (condition) {
doSomething()
}
}

Line Length

Limit line length to 100 characters. For longer lines, break them according to these principles:

kotlin
// Break after commas
fun longFunctionName(
argument1: String,
argument2: String,
argument3: String
): ReturnType {
// Function body
}

// Break after operators
val result = veryLongVariableName +
anotherLongVariableName +
yetAnotherLongVariableName

Braces

Place opening braces at the end of the line and closing braces on a new line:

kotlin
if (elements != null) {
for (element in elements) {
// Do something
}
}

For empty blocks, you can use concise syntax:

kotlin
class EmptyClass {}  // OK

For single-line functions, you can omit braces:

kotlin
fun sum(a: Int, b: Int) = a + b

Whitespace

Use a single blank line to separate:

  • Logical sections within a file
  • Method definitions
  • Properties and their accessors
  • Different logical groups of import statements
kotlin
package com.example

import android.content.Context
import android.view.View

import java.util.Date
import java.util.UUID

class UserProfile(private val context: Context) {
private val userId: String = UUID.randomUUID().toString()
private var lastUpdated: Date? = null

fun updateProfile(name: String, email: String) {
// Update profile logic
lastUpdated = Date()
}

fun displayProfile(): View {
// Display profile logic
return TextView(context)
}
}

Language Features Usage

Using val vs var

Prefer val (immutable) over var (mutable) when possible:

kotlin
// Prefer this
val user = User("John")

// Rather than
var user = User("John")

Type Inference

Use type inference where the type is obvious, but declare types when it improves readability:

kotlin
// Type inference (good when type is clear)
val users = listOf(user1, user2)

// Explicit types (good for clarity)
val userMap: Map<Int, User> = getUsersById()

String Templates

Use string templates instead of string concatenation:

kotlin
// Preferred
val message = "User $username has $itemCount items"

// Avoid
val message = "User " + username + " has " + itemCount + " items"

Expression Bodies

Use expression bodies for simple functions:

kotlin
// Use expression body for simple functions
fun double(x: Int) = x * 2

// Use block body for complex functions
fun processData(data: List<String>): Results {
// Multiple lines of processing
return Results(processedData)
}

Using when

Prefer when over long if-else chains:

kotlin
// Preferred
when (response.code) {
200 -> handleSuccess(response.body)
401 -> promptLogin()
404 -> showNotFound()
else -> showGenericError()
}

// Avoid
if (response.code == 200) {
handleSuccess(response.body)
} else if (response.code == 401) {
promptLogin()
} else if (response.code == 404) {
showNotFound()
} else {
showGenericError()
}

Named Arguments

Use named arguments to improve readability, especially when multiple parameters of the same type are involved:

kotlin
// Without named arguments - not clear
createUser("John", "Doe", 30, true)

// With named arguments - more readable
createUser(
firstName = "John",
lastName = "Doe",
age = 30,
isPremium = true
)

Comments

Documentation Comments

Use KDoc format for documentation comments:

kotlin
/**
* Calculates the sum of two numbers.
*
* @param a The first number
* @param b The second number
* @return The sum of a and b
*/
fun sum(a: Int, b: Int): Int {
return a + b
}

Regular Comments

Use regular comments to explain complex logic, bug fixes, or workarounds:

kotlin
// Workaround for API bug #1234
// The server sometimes returns duplicated IDs, so we need to filter them
val uniqueIds = response.ids.distinct()

Real-World Example

Let's put these conventions together in a real-world example. Here's a simple user management class following Kotlin conventions:

kotlin
package com.example.usermanagement

import java.time.LocalDateTime
import java.util.UUID

/**
* Manages user data and operations.
*/
class UserManager(private val dataSource: UserDataSource) {

private val _users = mutableListOf<User>()
val users: List<User> get() = _users.toList()

companion object {
private const val MAX_USERS = 100
const val DEFAULT_USER_STATUS = "ACTIVE"
}

/**
* Creates a new user and adds them to the system.
*
* @param name User's full name
* @param email User's email address
* @param isPremium Whether this user has premium membership
* @return The created User or null if the user limit is reached
*/
fun createUser(
name: String,
email: String,
isPremium: Boolean = false
): User? {
if (_users.size >= MAX_USERS) {
return null
}

val newUser = User(
id = UUID.randomUUID().toString(),
name = name,
email = email,
status = DEFAULT_USER_STATUS,
isPremium = isPremium,
createdAt = LocalDateTime.now()
)

_users.add(newUser)
dataSource.saveUser(newUser)

return newUser
}

fun getUserById(id: String): User? {
return _users.find { it.id == id }
?: dataSource.fetchUserById(id)?.also {
_users.add(it)
}
}

fun updateUserStatus(id: String, newStatus: String): Boolean {
val user = getUserById(id) ?: return false

val updatedUser = user.copy(
status = newStatus,
updatedAt = LocalDateTime.now()
)

val index = _users.indexOfFirst { it.id == id }
if (index >= 0) {
_users[index] = updatedUser
} else {
_users.add(updatedUser)
}

return dataSource.updateUser(updatedUser)
}
}

data class User(
val id: String,
val name: String,
val email: String,
val status: String,
val isPremium: Boolean,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime? = null
)

interface UserDataSource {
fun saveUser(user: User): Boolean
fun fetchUserById(id: String): User?
fun updateUser(user: User): Boolean
}

Tools for Enforcing Conventions

To help maintain coding conventions, consider using these tools:

  1. IntelliJ IDEA/Android Studio: Built-in formatting tools that follow Kotlin conventions
  2. ktlint: A static code analysis tool that checks your code against the official Kotlin style guide
  3. detekt: A static code analysis tool for Kotlin that can also enforce style conventions

Example of setting up ktlint in your Gradle build:

kotlin
plugins {
id("org.jlleitschuh.gradle.ktlint") version "10.3.0"
}

ktlint {
verbose.set(true)
android.set(true)
}

Summary

Adhering to coding conventions is essential for writing clean, maintainable Kotlin code. In this guide, we've covered:

  • Naming conventions for packages, classes, functions, and variables
  • Source file organization and structure
  • Formatting guidelines including indentation, line length, and braces
  • Best practices for using Kotlin language features
  • Documentation and commenting styles

Following these conventions will help you write more professional and idiomatic Kotlin code, making your projects easier to maintain and collaborate on.

Additional Resources

Exercises

  1. Take an existing Java class and convert it to Kotlin following the conventions outlined in this guide.
  2. Set up ktlint in a project and fix any style violations it reports.
  3. Review a piece of Kotlin code and identify any violations of the conventions discussed here.
  4. Write a small Kotlin application from scratch, strictly adhering to the conventions.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)