Spaces have won. Translation of Kotlin Coding Conventions from JetBrains
- Transfer
Hi, Habr! I bring to your attention the author’s translation of the Kotlin Coding Conventions page from JetBrains.
Content:
- Application of Style Guide to Intellij Idea
- Project structure
- Naming rules
- Formatting
- Paperwork
- Avoiding unnecessary constructs
- Idiomatic use of language features
- Immutability
- Default Parameter Value
- Creating aliases [Type alias]
- Parameter naming in lambda expressions
- Return values from lambda expressions
- Calling methods with naming arguments
- Rules for control structures with conditions
if
vswhen
- Use Boolean values? in conditional statements
- Use cycles
- Range loops
- Formatting strings
- Functions vs. properties
- Using Extension Functions
- Using infix functions
- Factory Functions
- Platform types
Использование функций apply
/with
/run
/also
/let
- Rules when creating libraries
Application of Style Guide to Intellij Idea
To apply formatting in Intellij Idea in accordance with the current manual, you need to install Kotlin plugin version 1.2.20 or later, go to Settings | Editor | Code Style | Kotlin, click on the "Set from ..." link in the upper right corner and select the "Predefined style" / Kotlin style guide "from the dropdown menu.
To verify that your code is formatted according to the recommended style, go to the inspection settings and enable the check "Kotlin | Style issues | File is not formatted according to project settings". Other validation rules, such as naming convention checks, are enabled by default.
Project structure
Folder structure
In projects using different languages, files with code on Kotlin should be in the same folder as the code in other languages and use the same file structure that is adopted for the main language. For example, for Java files should be in the folder structure according to the package name.
In projects using only Kotlin, the recommended folder structure is: use folders to organize packages with the root directory skipped, i.e. if all the code in the project is in the package "org.example.kotlin" and its packages, then the source code files belonging to the package "org.example.kotlin" should be in the root directory of the project, and the source code files of the package "org. example.kotlin.foo.bar "should be located in the" foo / bar "subdirectory relative to the project root.
Source File Names
If a Kotlin file contains only one class (possibly related to a high-level declaration [top-level declaration]), then it must be named, as well as a class with an extension .kt
. If the file contains several classes or has only top level declarations, choose a name that describes what the file contains and name the file appropriately. Use camel hump with a capital first letter to name files (for example ProcessDeclarations.kt
).
The name of the files should describe what the code in the file does. That is, you should avoid meaningless words like "Util" for naming files.
Source Code Organization
Placing multiple declarations (classes, functions, or top-level properties) in the same Kotlin source file is welcome if these declarations are closely related to each other semantically and the file size remains reasonable (no more than a few hundred lines).
In particular, when defining extension functions for a class that relate to all aspects of applying this class, put them in the same file where the class itself is defined. When defining extension functions that make sense only for the specific context of use of this class, put them next to the code using this extension function. Do not create files only to store "all Foo extensions".
Class structure
Typically, the class content is sorted in the following order:
- Property declarations and initializer blocks
- Secondary constructors
- Method declarations
- Companion objects
Do not sort the declarations of methods alphabetically or visually and do not separate the usual methods from the extension methods. Instead, put the logically related code together so that someone who reads the class from top to bottom can follow the logic of what is happening. Choose one sort order (high-level code at the beginning [higher-level stuff first], and details later or vice versa) and stick to it.
Place nested classes next to the code that uses these classes. If the classes are intended for external use and are not referenced within the class, put them at the end after the companion object.
Interface implementation structure
When implementing an interface, keep the same structure as the interface being implemented (alternating it if necessary with additional private methods used for implementation)
Override structure
Overrides always put together, one after the other.
Naming rules
Kotlin follows the same naming conventions as Java. In particular:
Package names are in lower case and do not use underscores (org.example.myproject). Using multi-word names is usually not recommended, but if you need to use multiple words, you can either just merge them together, or use camel hump (org.examle.myProject).
The names of classes and objects begin with a capital letter and use camel hump:
openclassDeclarationProcessor{ ... }
object EmptyDeclarationProcessor : DeclarationProcessor() { ... }
Name of functions
The names of functions, properties, and local variables begin with a lowercase letter and do not contain underscores:
funprocessDeclarations() { ... }
var declarationCount = ...
Exception: factory functions used to create instances of classes may have the same name as the class being created:
abstractclassFoo{ ... }
classFooImpl : Foo { ... }funFoo(): Foo { return FooImpl(...) }
The name of the test methods
In tests (and only in tests), it is permissible to use method names with spaces enclosed in backquotes. (Note that these method names are not currently supported by the Android runtime.) Underscores in method names are also allowed in the test code.
classMyTestCase{
@Testfun `ensure everything works`() { ... }
@TestfunensureEverythingWorks_onAndroid() { ... }
}
Property naming
The names of the constants (properties marked const
, or properties of a top level or an object val
without a user function get
that contain immutable data) should be called capital letters separated by underscores:
constval MAX_COUNT = 8val USER_NAME_FIELD = "UserName"
Names of the top level or object properties that contain objects with behavior or variable data should use the usual names in camel hump:
val mutableCollection: MutableSet<String> = HashSet()
Names of properties containing references to Singleton objects can use the same naming style as class declarations:
val PersonComparator: Comparator<Person> = ...
For enumerations, you can use names written in capital letters separated by underscores or camel hump style, starting with a capital letter, depending on use.
enumclassColor{ RED, GREEN }
enumclassColor{ RedColor, GreenColor }
Translator's note: Just do not mix different styles. Choose one style and stick to it in your project.
Naming Hidden Properties
If a class has two properties that are conceptually the same, but one is part of the open API, and the other is part of the implementation, use the underscore as a prefix for the name of the hidden property:
classC{
privateval _elementList = mutableListOf<Element>()
val elementList: List<Element>
get() = _elementList
}
Choosing the right titles
A class name is usually a noun or phrase explaining what a class is:
List, PersonReader
The name of the method is usually a verb or phrase action explaining what the method does:
close, readPersons
The name should also suggest whether the method changes an object or returns a new one. For example, sort
this is a sorting changing the collection, and sorted
returning a new sorted copy of the collection.
Names should clearly indicate the purpose of the entity, so it is best to avoid using meaningless words ( Manager
, Wrapper
etc.) in names.
When using the acronym as part of the name of the ad, use capital letters if it consists of two letters ( IOStream
); or capitalize only the first letter if it is longer ( XmlFormatter
, HttpInputStream
).
Formatting
In most cases, Kotlin follows Java formatting conventions.
Use 4 spaces to indent. Do not use tabs.
For braces, place the opening brace at the end of the line where the structure begins, and the closing brace on a separate line aligned horizontally with the opening structure.
if (elements != null) {
for (element in elements) {
// ...
}
}
(Note: semicolons are optional in Kotlin, so line wrapping is important. The language design assumes Java-style braces, and you may encounter unexpected behavior when executing code if you try to use a different formatting style.)
Horizontal spaces
Put spaces around binary operators (a + b)
. Exception: do not put spaces around the "range to" operator(0..i)
Do not put spaces around unary operators. (a++)
Place the gaps between the key control words ( if
, when
, for
and while
) and the respective opening bracket.
Do not put a space before the opening parenthesis in the primary declaration of a constructor, method, or method.
classA(val x: Int)
funfoo(x: Int) { ... }
funbar() {
foo(1)
}
Never put a space after (
, [
or before ]
, )
.
Never put space around a point .
or operator ?.
:
foo.bar().filter { it > 2 }.joinToString()
foo?.бар()
Put a space after the double slash for the comment //
:
// это комментарий
Do not put spaces around the angle brackets used to specify type parameters:
Class Map<K, V> { ... }
Do not put spaces around a double colon to indicate a reference to a class method ::
:
Foo::class
String::length
Do not put a space before it is ?
used to mark the type that allows the value null
:
String?
As a rule, avoid any type of horizontal alignment. Renaming an identifier to a name of a different length should not affect the formatting of the code.
Colon
Put a space before the colon :
in the following cases:
- when it is used to separate type and super type;
abstractclassFoo<out T : Any>
- when delegating to the constructor of a superclass or another constructor of the same class;
constructor(x: String) : super(x) { ... }
constructor(x: String) : this(x) { ... }
- after the keyword object.
val x = object : IFoo { ... }
Do not put a space in front :
when it separates the ad and its type.
abstractfunfoo(a: Int): T
Always put a space after :
.
abstractclassFoo<out T : Any> : IFoo {abstractfunfoo(a: Int): T
}
classFooImpl : Foo() {
constructor(x: String) : this(x) { ... }
val x = object : IFoo { ... }
}
Formatting Class Declarations
Classes with several basic constructor parameters and short names can be written in one line:
classPerson(id: Int, name: String)
Classes with longer names or a number of parameters must be formatted so that each major constructor parameter is on a separate indented line. Also, the closing parenthesis must be on a new line. If we use inheritance, then the call to the superclass constructor or the list of implemented interfaces should be located on the same line as the bracket:
classPerson(
id: Int,
name: String,
surname: String
) : Human(id, name) { ... }
When specifying the interface and invoking the superclass constructor, first the superclass constructor must be located, then the interface name on the new line will be left-aligned:
classPerson(
id: Int,
name: String,
surname: String
) : Human(id, name),
KotlinMaker { ... }
For classes with a long list of super types, you need to put a line break after the colon and align all the names of the super types horizontally left-handedly:
classMyFavouriteVeryLongClassHolder :
MyLongHolder<MyFavouriteVeryLongClass>(),
SomeOtherInterface,
AndAnotherOne {
funfoo() { ... }
}
To clearly separate the class heading and its body when the class heading is long, either put an empty line after the class heading (as in the example above), or put the opening brace on a separate line:
classMyFavouriteVeryLongClassHolder :
MyLongHolder<MyFavouriteVeryLongClass>(),
SomeOtherInterface,
AndAnotherOne
{
funfoo() { ... }
}
Use regular indent (4 spaces) for constructor parameters.
Rationale: This ensures that properties declared in the main constructor are indented as properties declared in the body of the class.
Modifiers
If your ad contains several modifiers, always put them in the following order:
public / protected / private / internalexpect / actualfinal / open / abstract / sealed / constexternaloverridelateinittailrecvarargsuspendinnerenum / annotationcompanioninlineinfixoperatordata
Place all annotations before modifiers:
@Named("Foo")privateval foo: Foo
If you are not working on the library, omit redundant modifiers (for example, public).
Annotation Formatting
Annotations are usually placed on separate lines before the declaration to which they are attached, and with the same indentation:
@Target(AnnotationTarget.PROPERTY)annotationclassJsonExclude
Annotations without arguments can be placed on one line:
@JsonExclude@JvmFieldvar x: String
One annotation without arguments can be placed on the same line as the corresponding declaration:
@Testfunfoo() { ... }
File annotations
Annotations to files are placed after the comment to the file (if any), before the package instruction and separated from the package with an empty line (to emphasize the fact that they are aimed at the file, not the package).
/** License, copyright and whatever */@file:JvmName("FooBar")
package foo.bar
Formatting Functions
If the method signature does not fit on one line, use the following syntax:
funlongMethodName(
argument: ArgumentType = defaultValue,
argument2: AnotherArgumentType
): ReturnType {
// body
}
Use regular indent (4 spaces) for function parameters.
Rationale: Consistency with Designer Parameters
It is preferable to use an expression without braces for functions consisting of one line.
funfoo(): Int { // badreturn1
}
funfoo() = 1// good
Formatting a single line expression
If the body of a single-line function does not fit in the same line as the declaration, put the = sign in the first line. Indent the body of the expression by 4 spaces.
funf(x: String) =
x.length
Property formatting
For simple read-only properties, it is preferable to use single-line formatting:
val isEmpty: Booleanget() = size == 0
For more complex properties, always use get
and set
on separate lines:
val foo: String
get() { ... }
For properties with initialization, if the initializer is too long, add a line break after the equal sign and an indent of four spaces for the initialization string:
privateval defaultCharset: Charset? =
EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)
Formatting instruction instructions
If the condition is in a control instruction if
or when
multi-line, always use curly braces around the operator's body. Indent each subsequent line of the condition by 4 spaces relative to the beginning of the operator. Put the closing condition brackets together with the opening brace on a separate line:
if (!component.isSyncing &&
!hasAnyKotlinRuntimeInScope(module)
) {
return createKotlinNotConfiguredPanel(module)
}
Rationale: neat alignment and a clear separation of body conditions and body conditions
Put keywords else
, catch
, finally
and a keyword while
do / while loop on the same line as the previous closing brace:
if (condition) {
// body
} else {
// else part
}
try {
// body
} finally {
// cleanup
}
If the conditions of the when
instruction consist of several blocks, it is recommended to separate each other with an empty line:
privatefunparsePropertyValue(propName: String, token: Token) {
when (token) {
is Token.ValueToken ->
callback.visitValue(propName, token.value)
Token.LBRACE -> { // ...
}
}
}
Place the short when
instruction blocks on a single line without curly brackets.
when (foo) {
true -> bar() // goodfalse -> { baz() } // bad
}
Formatting Method Calls
When using a long list of parameters, place the line break after the parenthesis. Use an indent in 4 spaces and group the arguments related logically into one line.
drawSquare(
x = 10, y = 10,
width = 100, height = 100,
fill = true
)
Use spaces around the equal sign between the parameter name and its value.
Formatting chained function calls
When using chain calls, place .
or ?.
operators on a new line with one indentation in 4 spaces:
val anchor = owner
?.firstChild!!
.siblings(forward = true)
.dropWhile { it is PsiComment || it is PsiWhiteSpace }
The first call in the chain should usually have a line break before it, but this is normal, not to do it if the code is better read and it makes sense.
Formatting lambda expressions
In lambda expressions, spaces should be used around braces and around the arrow separating parameters from the body. If the call accepts one lambda symbol, it should be used, if possible, outside of parentheses.
list.filter { it > 10 }
When assigning a label to a lambda expression, do not put a space between the label and the opening brace:
funfoo() {
ints.forEach lit@{
// ...
}
}
When declaring parameter names in a multi-line lambda expression, place the names on the first line, then the arrow, and on the new line, the beginning of the function body:
appendCommaSeparated(properties) { prop ->
val propertyValue = prop.get(obj) // ...
}
If the parameter list does not fit on one line, put the arrow on a separate line:
foo {
context: Context,
environment: Env
->
context.configureEnv(environment)
}
Paperwork
When using multi-line documentation, put /**
on a separate line, and begin each subsequent line with an asterisk:
/**
* This is a documentation comment
* on multiple lines.
*/
Short documentation can be placed on one line:
/** This is a short documentation comment. */
As a rule, avoid using param and return tags . Instead, include a description of the parameters and return values directly in the comment to the documentation and add references to the parameters wherever they are mentioned. Use param and return only when you need a long description that does not fit into the meaning of the main text.
// Avoid doing this:/**
* Returns the absolute value of the given number.
* @param number The number to return the absolute value for.
* @return The absolute value.
*/funabs(number: Int) = ...
// Do this instead:/**
* Returns the absolute value of the given [number].
*/funabs(number: Int) = ...
Avoiding unnecessary constructs
Many syntactic constructions in Kotlin are optional and are highlighted by the development environment as unnecessary; you should not use them in your code just to give "clarity" to your code.
Use of the keyword Unit
In functions, the use of the keyword Unit should not be used:
funfoo() { // ": Unit" is omitted here
}
Semicolon
Avoid using a semicolon at every opportunity.
String patterns
Do not use curly braces when inserting a simple variable into a template string. Use curly braces only for long expressions.
println("$name has ${children.size} children")
Idiomatic use of language features
Immutability
It is preferable to use immutable data before changing. Always declare local variables and properties as val
, rather than var
if they do not really change.
Always use immutable collections interfaces ( Collection
, List
, Set
, Map
) to collections of ads that do not change. Whenever possible, when using factory methods to create a collection, use an implementation that returns immutable collections:
// Bad: use of mutable collection type for value which will not be mutatedfunvalidateValue(actualValue: String, allowedValues: HashSet<String>) { ... }
// Good: immutable collection type used insteadfunvalidateValue(actualValue: String, allowedValues: Set<String>) { ... }
// Bad: arrayListOf() returns ArrayList<T>, which is a mutable collection typeval allowedValues = arrayListOf("a", "b", "c")
// Good: listOf() returns List<T>val allowedValues = listOf("a", "b", "c")
Translator's note: there must be extremely compelling reasons for using mutable collections.
Default Parameter Value
It is always preferable to declare functions with default parameter values instead of declaring overloaded functions.
// Badfunfoo() = foo("a")
funfoo(a: String) { ... }
// Goodfunfoo(a: String = "a") { ... }
Creating aliases [Type alias]
If you have a functional type or a type with parameters of a type that is used several times in the code, it is better to define an alias for it:
typealias MouseClickHandler = (Any, MouseEvent) -> Unittypealias PersonIndex = Map<String, Person>
Parameter naming in lambda expressions
In lambda expressions that are short and not nested, it is recommended to use the it
naming convention instead of explicitly declaring a parameter. In nested lambda expressions with parameters, parameters should always be explicitly named.
Return values from lambda expressions
Avoid using multiple marked return points in lambda. Consider restructuring lambda expressions so that they have a single exit point. If this is impossible or not sufficiently clear, consider converting a lambda expression into an anonymous function.
Do not use the flagged return ( @
) for the last operator in the lambda.
Calling methods with naming arguments
Use the syntax of named arguments when a method takes several parameters of the same primitive type, or for type parameters boolean
, if the meaning of all parameters is not completely clear from the context.
drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)
Rules for control structures with conditions
Prefer to use design returns try
, if
and when
, before explicit use with the keyword return
:
returnif (x) foo() else bar() // Такая конструкция предпочтительнее, чем конструкция нижеif (x)
return foo()
elsereturn bar()
//returnwhen(x) {
0 -> "zero"else -> "nonzero"
} // Такая конструкция предпочтительнее, чем конструкция нижеwhen(x) {
0 -> return"zero"else -> return"nonzero"
}
if
vs when
It is better to use if
when using two possible conditions instead ofwhen
when (x) {
null -> ...
else -> ...
}
if (x == null) ... else ... // Такой синтаксис понятнее и короче
If there are more than two possible conditions, it is better to use the when
construction.
Use Boolean values? in conditional statements
If you need to use a Boolean?
value in a conditional operator, use comparisons with the keyword if (value == true)
or if (value == false)
, instead of type checks if (value ?: false)
or , to use comparisons if (value != null && value)
.
Use cycles
It is preferable to use high-level functions such as filtet
, map
etc. instead of cycles. Exception: forEach
(it is preferable to use a normal loop for
if the iteration source is not null orforEach
not part of a large chain of transformations)
When choosing between a complex expression that uses several high-level functions and a cycle, it is necessary to understand and take into account the cost of each operation, and to make a choice from considerations of performance and readability.
Range loops
Use until
for cycles excluding borders (open intervals):
for (i in0..n - 1) { ... } // badfor (i in0 until n) { ... } // good
Formatting strings
It is preferable to use string patterns to connect strings.
Instead of embedding \n
escape sequences in regular string literals, use a multiline record type.
To preserve indenting in multi-line lines, use trimIndent
when the resulting line does not require internal indent, or trimMargin
when internal indenting is required:
assertEquals(
"""
Foo
Bar
""".trimIndent(),
value
)
val a = """if(a > 1) {
| return a
|}""".trimMargin()
Functions vs. properties
In some cases, functions with no arguments can be interchanged with read-only properties. Although their semantics is similar, there are some stylistic agreements about when to prefer one to another.
Prefer the property of the function with the basic algorithm:
- throws no exceptions
- easy to calculate (or cached when first running)
- returns the same result for each call if the state of the object has not changed
Using Extension Functions
Use the extension functions easily and freely. Every time you have a function that works in the main object, consider making it an extension function that accepts this object as a receiver. To minimize API contamination, limit the visibility of the extension functions as much as possible. If necessary, use local extension functions, member expansion functions, or top-level expansion functions with closed visibility.
Using infix functions
Declare a function as infix
only when it works with two objects that play a similar role. Good examples: and
, to
, zip
. Bad example add
.
Do not declare a method as infix
if it changes the recipient object.
Factory Functions
If a factory function is declared for a class, you should not give it the same name as the class itself. Prefer to use a separate name, making it clear why the behavior of the factory function is special. Only if there is really no special semantics, can you use the same name as the class.
classPoint(val x: Double, val y: Double) {
companionobject {
funfromPolar(angle: Double, radius: Double) = Point(...)
}
}
If there is an object with several overloaded constructors that do not call various superclass constructors and cannot be reduced to a single constructor with default argument values, it is preferable to replace the overloaded constructors with functions of the factory.
Platform types
Translator's note: platform types are the receipt of an object of type from any code written not on Kotlin and which can return both anull
value and notnull
public
A function / method that returns a value of the platform type must explicitly declare the Kotlin type:
funapiCall(): String = MyJavaApi.getProperty("name")
Any property (package-level or class-level) initialized by a call to the platform type must be explicitly indicated by the Kotlin type:
classPerson{
val name: String = MyJavaApi.getProperty("name")
}
Local variables that are initialized by platform types can be declared without explicitly specifying Kotlin type:
funmain() {
val name = MyJavaApi.getProperty("name")
println(name)
}
Using functions apply
/ with
/ run
/ also
/let
Kotlin provides many functions for executing a block of code in the context of a given object. To select the correct function, consider the following:
- Do you call methods of multiple objects in a block or pass an instance of a context object as an argument? If so, use one of the functions that allows you to access the context object as a
it
, rather than tothis
(also
orlet
). Usealso
if the receiver is not used at all in the unit.
// Context object is 'it'classBaz{
var currentBar: Bar?
val observable: Observable
val foo = createBar().also {
currentBar = it // Accessing property of Baz
observable.registerCallback(it) // Passing context object as argument
}
}
// Receiver not used in the blockval foo = createBar().also {
LOG.info("Bar created")
}
// Context object is 'this'classBaz{
val foo: Bar = createBar().apply {
color = RED // Accessing only properties of Bar
text = "Foo"
}
}
- What should be the result of the call? If the result should be a context object, use
apply
oralso
. If you need to return a value from a block, usewith
,let
orrun
.
// Return value is context objectclassBaz{
val foo: Bar = createBar().apply {
color = RED // Accessing only properties of Bar
text = "Foo"
}
}
// Return value is block resultclassBaz{
val foo: Bar = createNetworkConnection().let {
loadBar()
}
}
- Is the context object null or computed as a result of a call chain? If so, use
apply
,let
orrun
. Otherwise, usewith
oralso
.
// Context object is nullable
person.email?.let { sendEmail(it) }
// Context object is non-null and accessible directly
with(person) {
println("First name: $firstName, last name: $lastName")
}
Rules when creating libraries
When writing libraries, it is recommended to follow an additional set of rules to ensure the stability of the API:
- Always explicitly indicate the visibility of the member (to avoid accidentally disclosing the ad as a public API)
- Always explicitly specify return types from functions and property types (to avoid accidental changes in the type of data returned when the implementation changes)
- Providing KDoc documentation for all public api, with the exception of overrides, which have their own documentation and do not require updating / refining