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:
package com.example.myapplication
package org.kotlinlang.demo
Class and Interface Names
Use PascalCase (also known as UpperCamelCase) for class and interface names:
class MainActivity : AppCompatActivity()
interface OnClickListener
class UserRepository
Function and Property Names
Use camelCase for function and property names:
fun calculateTotalPrice(): Double
val userName: String
var isEnabled: Boolean
Constants
Constants (immutable values known at compile time) use uppercase with underscores:
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:
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:
- Package statement
- Import statements (alphabetically sorted, no wildcards)
- Top-level declarations (classes, functions, properties, etc.)
Example:
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 namedUser
NetworkService.kt
for a class namedNetworkService
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:
fun exampleFunction() {
if (condition) {
doSomething()
}
}
Line Length
Limit line length to 100 characters. For longer lines, break them according to these principles:
// 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:
if (elements != null) {
for (element in elements) {
// Do something
}
}
For empty blocks, you can use concise syntax:
class EmptyClass {} // OK
For single-line functions, you can omit braces:
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
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:
// 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:
// 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:
// Preferred
val message = "User $username has $itemCount items"
// Avoid
val message = "User " + username + " has " + itemCount + " items"
Expression Bodies
Use expression bodies for simple functions:
// 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:
// 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:
// 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:
/**
* 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:
// 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:
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:
- IntelliJ IDEA/Android Studio: Built-in formatting tools that follow Kotlin conventions
- ktlint: A static code analysis tool that checks your code against the official Kotlin style guide
- detekt: A static code analysis tool for Kotlin that can also enforce style conventions
Example of setting up ktlint in your Gradle build:
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
- Official Kotlin Coding Conventions
- Android Kotlin Style Guide
- ktlint - Kotlin linter
- detekt - Kotlin static code analyzer
Exercises
- Take an existing Java class and convert it to Kotlin following the conventions outlined in this guide.
- Set up ktlint in a project and fix any style violations it reports.
- Review a piece of Kotlin code and identify any violations of the conventions discussed here.
- 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! :)