Skip to content

Latest commit

 

History

History
365 lines (274 loc) · 16.1 KB

kotlin-code-style.md

File metadata and controls

365 lines (274 loc) · 16.1 KB

Kotlin Code Style

В данном документе приведен набор соглашений по оформлению кода на языке Kotlin.

Этот список правил расширяет предложенные Google и командой разработки Kotlin гайды и пересматривает в них некоторые неоднозначные моменты.

Длина строки

Рекомендуемая длина строки: 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() { /*...*/ }
}

Использование именованных аргументов аналогично с функциями

Структура класса

  1. Поля: abstract, override, public, internal, protected, private
  2. Блок инициализации: init, конструкторы
  3. Абстрактные методы
  4. Переопределенные методы родительского класса (желательно в порядке их следования в родительском классе)
  5. Реализации методов интерфейсов (желательно в порядке добавления интерфейсов в класс и следования методов в каждом интерфейсе)
  6. Методы класса (в логическом порядке. Например, метод располагается после того, в котором впервые упомянут). Можно перемешивать с методами из пунктов 3, 4, 5.
  7. inner классы
  8. 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

Не использовать Template Header для классов (касается авторства и даты создания файла).

Частые ошибки

Вызов toString() у nullable объектов

В первом примере получится строчка "null", это плохо. Необходимо сделать так, чтобы в таком случае возвращалась пустая строка ""

binding.authInputPassword.addTextChangeListener { editable: Editable? ->
    // Bad
    viewModel.onPasswordChanged(editable.toString())
    
    // Good
    viewModel.onPasswordChanged(editable?.toString().orEmpty())
}

Использование orEmpty() вместо ?:

Для коллекций и строк использовать orEmpty().

// Bad
nullableString ?: ""
nullableObject?.toString() ?: ""
someList ?: emptyList()

// Good
nullableString.orEmpty()
nullableObject?.toString().orEmpty()
someList.orEmpty()

Проверка nullable boolean

При проверке 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
}