В данном документе приведен набор соглашений по оформлению кода на языке Kotlin.
Этот список правил расширяет предложенные Google и командой разработки Kotlin гайды и пересматривает в них некоторые неоднозначные моменты.
- Длина строки
- Правила именования
- Форматирование выражений
- Функции
- Классы
- Структура класса
- Аннотации
- Использование условных операторов
- Template header
- Частые ошибки
Рекомендуемая длина строки: 100 символов.
Максимальная длина строки: 120 символов.
Пакеты именуются одним словом в стиле lowercase. Если необходимо использовать несколько слов, то просто склеиваем их вместе.
При объявлении констант, полей или аргументов функций рекомендуется дополнительно указывать размерность, если контекст или название функции не дает однозначного понимания их назначения:
// Bad
const val TIMEOUT = 1000L
const val PADDING = 24
// Bad
fun someFunction(timeout: Long)
// Bad
val defaultTimeout get() = 1000L
// Good
const val TIMEOUT_MILLIS = 1000L
const val PADDING_DP = 24
// Good
val TIMEOUT = 1000.milliseconds
val PADDING = 24.dp
// Good
fun preferGoodNames(timeoutMillis: Long)
// Good
val defaultTimeoutMillis get() = 1000L
При переносе на новую строку цепочки вызова методов символ .
или оператор ?.
переносятся на следующую строку, property при этом разрешается оставлять на одной строке:
val collectionItems = source.collectionItems
?.dropLast(10)
?.sortedBy { it.progress }
Элвис оператор ?:
в многострочном выражении также переносится на новую строку:
val throwableMessage: String = throwable?.message
?: DEFAULT_ERROR_MESSAGE
throwable.message?.let { showError(it) }
?: showError(DEFAULT_ERROR_MESSAGE)
Если перед элвис оператором ?:
многострочная лямбда, желательно перенести также и лямбду:
// Good
throwable.message
?.let { message ->
...
showError(message)
}
?: showError(DEFAULT_ERROR_MESSAGE)
// Not recommended
throwable.message?.let { message ->
...
showError(message)
}
?: showError(DEFAULT_ERROR_MESSAGE)
При описании переменной с делегатом, не помещающимися на одной строке, оставлять описание с открывающейся фигурной скобкой на одной строке, перенося остальное выражение на следующую строку:
private val promoItem: MarkPromoItem by lazy {
extractNotNull(BUNDLE_FEED_UNIT_KEY) as MarkPromoItem
}
Позволительно использовать функцию с одним выражением только в том случае, если она помещается в одну строку.
Если по контексту не понятно назначение аргумента, то следует сделать его именованным.
runOperation(
method = operation::run,
consumer,
errorHandler,
tag,
cacheSize = 3,
cacheMode
)
calculateSquare(x = 6, y = 19)
getCurrentUser(skipCache = false)
setProgressBarVisible(true)
Если именованные аргументы не помещаются на одной строке, то следует переносить каждый аргумент на новую строку (как в примере выше).
Именуем все лямбды, принимаемые функцией в качестве аргументов (кроме случаев когда лямбда вынесена за круглые скобки), чтобы во время чтения кода было понятно назначение и ответственность каждой лямбды.
editText.addTextChangedListener(
onTextChanged = { text, _, _, _ ->
viewModel.onTextChanged(text?.toString())
},
afterTextChanged = { text ->
viewModel.onAfterTextChanged(text?.toString())
}
)
Полезно именовать аргументы одинаковых типов, чтобы случайно не перепутать их местами.
val startDate: Date = ..
val endDate: Date = ..
compareDates(startDate = startDate, endDate = endDate)
Полезно именовать аргумент при передаче null
.
setAdditionalArguments(arguments = null)
Допускается вызов лямбды как с invoke
, так и сокращенный вариант ()
, если отсутствуют договоренности внутри проекта. Однако явный invoke
имеет ряд преимуществ:
Tip
Одной из основных причин использования явного invoke
является концептуальное разделение функции как члена класса и лямбды как входного параметра функции.
Используя invoke
явно, мы показываем, что используем лямбду, а не функцию.
При этом дополнительным аргументом к использованию invoke
является его заметность. Вызывая лямбду без invoke
, у нее можно потерять скобки в месте вызова, что приведет к некорректному поведению.
@Composable
fun ProfileScreenContent(
header: @Composable LazyItemScope.() -> Unit,
body: @Composable LazyListScope.() -> Unit,
footer: @Composable LazyItemScope.() -> Unit,
) {
LazyColumn {
item(content = header)
// Bad
body
// Good
body()
body.invoke(this@LazyColumn)
item(content = footer)
}
}
По возможности передавать метод по ссылке:
viewPager.adapter = QuestAdapter(quest, onQuestClickListener = ::onQuestClicked)
При написании лямбда-выражения более чем в одну строку всегда использовать именованный аргумент, вместо it
:
viewPager.adapter = QuestAdapter(
quest,
onQuestClickListener = { quest ->
Log.d(..)
viewModel.onQuestClicked(quest)
}
)
Неиспользуемые параметры лямбда-выражений всегда заменять символом _
.
Если описание класса не помещается в одну строку, и класс реализует несколько интерфейсов, то применять стандартные правила переноса, т.е. делать перенос только в случае, когда описание не помещается на одну строку, при этом продолжать перечисление интерфейсов на следующей строке.
class MyFavouriteVeryLongClassHolder : MyLongHolder<MyFavouriteVeryLongClass>(), SomeOtherInterface, AndAnotherOne,
OneMoreVeryLongInteface, OneMore{
fun foo() { /*...*/ }
}
Использование именованных аргументов аналогично с функциями
- Поля: abstract, override, public, internal, protected, private
- Блок инициализации: init, конструкторы
- Абстрактные методы
- Переопределенные методы родительского класса (желательно в порядке их следования в родительском классе)
- Реализации методов интерфейсов (желательно в порядке добавления интерфейсов в класс и следования методов в каждом интерфейсе)
- Методы класса (в логическом порядке. Например, метод располагается после того, в котором впервые упомянут). Можно перемешивать с методами из пунктов 3, 4, 5.
- inner классы
- companion object
Аннотации располагаются над описанием класса/поля/метода, к которому они применяются.
Если к классу/полю/методу применяется несколько аннотаций, размещать каждую аннотацию с новой строки:
@JsonValue
@JvmField
var promoItem: PromoItem? = null
Аннотации к аргументам в конструкторе класса или объявлении функции можно писать на той же строке, что и соответствующий аргумент.
При этом если аннотаций к одному аргументу несколько, то все аннотации пишутся с новой строки, и соответствующий аргумент отделяется от других сверху и снизу пустыми строками.
data class UserInfo (
@SerializedName("firstName") val firstName: String? = null,
@SerializedName("secondName") val secondName: String? = null
)
@Entity(tableName = "users")
data class UserInfo (
@PrimaryKey val id: Int,
@SerializedName("firstName")
@ColumnInfo(name = "firstName")
val firstName: String? = null,
@SerializedName("secondName")
@ColumnInfo(name = "secondName")
val secondName: String? = null
)
Не обрамлять if
выражения в фигурные скобки только если условный оператор if
помещается в одну строку.
По возможности использовать условные операторы, как выражение:
return if (condition) foo() else bar()
В операторе when
ветки, состоящие более чем из одной строки, обрамлять фигурными скобками и отделять от других case-веток пустыми строками сверху и снизу.
when (feed.type) {
FeedType.PERSONAL -> startPersonalFeedScreen()
FeedType.SUM -> {
showSumLayout()
hideProgressBar()
}
FeedType.CARD -> startCardFeedScreen()
else -> showError()
}
Не использовать Template Header для классов (касается авторства и даты создания файла).
В первом примере получится строчка "null"
, это плохо.
Необходимо сделать так, чтобы в таком случае возвращалась пустая строка ""
binding.authInputPassword.addTextChangeListener { editable: Editable? ->
// Bad
viewModel.onPasswordChanged(editable.toString())
// Good
viewModel.onPasswordChanged(editable?.toString().orEmpty())
}
Для коллекций и строк использовать orEmpty()
.
// Bad
nullableString ?: ""
nullableObject?.toString() ?: ""
someList ?: emptyList()
// Good
nullableString.orEmpty()
nullableObject?.toString().orEmpty()
someList.orEmpty()
При проверке nullable boolean вместо добавления ?: false
в условии явно проверять boolean == true
Это одна из общепринятных идиом Kotlin.
// Bad
val b: Boolean? = ...
if (boolean ?: false) {
...
} else {
// `b` is false or null
}
// Good
val b: Boolean? = ...
if (b == true) {
...
} else {
// `b` is false or null
}