Данная статья охватывает не только эстетические вопросы форматирования, но и другие типы соглашений и стандартов, которые необходимо знать Android разработчику.
Основной фокус, в первую очередь, на жестких правилах, которым следуют Google разработчики повсеместно!
Сначала я думал, что статья будет небольшой, но из-за слишком колоссального количества примеров кода она достаточно выросла.
Поэтому я решил разделить её на две части.
Обе части содержат описание стандартов кода на языке прораммирования Kotlin.
Что покрывают обе части:
Именование файлов, переменных, классов, свойств и т.д.
Структура исходного файла
Форматирование - строки, пробелы, скобки, специальные конструкции, переносы и др.
Документация
В первой части я затрону исходные файлы и форматирование (неполностью).
Ну что ж пора начинать!
Исходные файлы
Поговорим сначала об исходных файлах, о их структуре и других важных вещах.
Кодировка
Все исходные файлы должны иметь UTF-8 кодировку.
Именование
Все исходные файлы, которые содержат высокоуровневые определения классов, должны именоваться следующим образом: имя класса + расширение файла .kt
Если файл содержит несколько высокоуровневых определений (два класса и один enum
к примеру) выбирается имя файла, которое описывает его содержимое:
// PhotoAdapter.kt
class PhotoAdapter(): RecyclerView.Adapter<PhotoViewHolder>() {
// ...
}
// Utils.kt
class Utils {}
fun Utils.generateNumbers(start: Int, end: Int, step: Int) {
// ...
}
// Map.kt
fun <T, O> Set<T>.map(func: (T) -> O): List<O> = // ...
fun <T, O> List<T>.map(func: (T) -> O): List<O> = // ...
Структура
Kotlin файл .kt включает в себя:
Заголовок, в котором указана лицензия и авторские права (необязательно)
Аннотации, которые объявлены на уровне файла
package объявление
import выражения
высокоуровневые объявления (классы, интерфейсы, различные функции)
Заголовок должен быть объявлен выше остальных определений с использованием многострочных комментариев:
/*
* Copyright 2021 MyCompany, Inc.
*
*
*/
Не используйте однострочные и KDoc комментарии:
/**
* Copyright 2021 MyCompany, Inc.
*
*/
// Copyright 2021 MyCompany, Inc.
//
Аннотация @file
, которая является use-site target должна быть помещена между заголовком и package
объявлением:
/*
* Copyright 2021 MyCompany, Inc.
*
*/
@file:JvmName("Foo")
package com.example.android
Оператор package
и import
никогда не переносятся и всегда размещаются на одной строке:
package com.example.android.fragments // переносы запрещены
import android.view.LayoutInflater // так же и здесь
import android.view.View
Выражения import
группируются для классов, функций и свойств в сортированные списки.
Импорты с подстановочным знаком не разрешены:
import androidx.room.* // так делать не нужно
Kotlin файл может содержать объявление одного или нескольких классов, функций, свойств или typealias
выражений.
Контент файла должен относится к одной теме. Например у нас есть публичный класс и набор extension функций, которые выполняют некоторые операции.
Нет явного ограничения на количество и порядок содержимого файла
Файлы обычно читаются сверху вниз, поэтому верхние части кода должны помогать нам понять нижние.
Важен логический порядок, который может объяснить сам разработчик.
Например: новые функции были добавлены в конец файла, не потому что мы используем хронологический порядок, а потому что они являются вспомогательными и не зависят от других
Для членов класса применимы те же правила, что и для высокоуровневых определений.
Специальные символы
В исходном коде используется только ASCII горизонтальный пробельный символ (0x20).
Это означает, что:
Все другие пробельные символы в строчных и символьных литералах должны экранироваться
Tab символы не используются для отступов
Для любого символа, который имеет экранированную последовательность (\b, \r, \t, \\
) используется эта последовательность, а не Unicode (например: \u000a
).
Для оставшихся символов, которые не принадлежат ASCII, используется либо Unicode символ (∞), либо Unicode последовательность (\u221e
).
Выбор зависит лишь от того, что облегчает чтение и понимание кода:
// Лучшая практика: понятно без комментариев
val symbol0 = "∞"
// Плохо: нет причины не использовать символ вместо Unicode последовательности
val symbol1 = "\u221e" // ∞
// Плохо: читатель не сможет понять, что это за символ
val symbol2 = "\u221e"
// Хорошо: использование Unicode последовательности для непечатаемого символа
return "\ufeff" + content // неразрывный пробел нулевой ширины
Форматирование
Ближе к коду!
Скобки
Скобки не требуются дляwhen
и if
которые помещаются на одной строке (оператор if
не имеет else
ветки):
if (str.isEmpty()) return
when (option) {
0 -> return
// …
}
В другом случае скобки обязательно требуются для if, for, when
ветвлений и do
и while
выражений:
if (str.isEmpty())
return // так делать нельзя!
if (str.isEmpty()) {
return // OK
}
Скобки следуют стилю Кернигана и Ритчи для непустых блоков и блочных конструкций:
Нельзя делать разрыв строки перед открывающей скобкой
Разрыв строки после открывающей cкобки
Разрыв строки перед закрывающей скобкой
Разрыв строки после закрывающей скобкой только в том случае, если она заканчивает выражение или тело функции, конструктора, класса.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// ...
}
}
Пустые блоки тоже должны быть в стиле K&R:
try {
val response = fetchDogs("https://api.dog.com/dogs")
} catch (e: Exception) {} // неправильно
try {
val response = fetchDogs("https://api.dog.com/dogs")
} catch (e: Exception) {
} // OK
if/else
выражение может быть без скобок, если помещается на одной строке:
val value = if (str.isEmpty()) 0 else 1 // OK
val value = if (str.isEmpty()) // неправильно
0
else
1
val value = if (str.isEmpty()) { // OK
0
} else {
1
}
С каждом новым блоком отступ увеличивается на 4 пробела. Когда блок закрывается отступ возвращается на предыдущий уровень (это применимо и для комментариев).
Переносы
Каждое выражение разделяется переносом на новую строку (;
не используется)
Строка кода имеет ограничение в 100 символов.
Исключения:
Строки, которые невозможно перенести (например: длинный URL)
package
иimport
выраженияКоманды в документации, которые можно вставить в shell
Правила для переноса на новую строку:
Перенос после оператора или infix функции.
Если строка завершается следующими операторами, то перенос осуществляется вместе с ними:
точка (
.
,.?
)ссылка на член (
::
)
Имя метода или конструктура находится на одной строке с открывающей скобкой
Запятая
(,)
связана с элементом и не переноситсяСтрелка (
->
) для lambda выражений связана с аргументами
Когда сигнатура функции не помещается, объявление параметров располагается на отдельных строчках (параметры должны иметь один отступ в 4 пробела):
fun makeSomething(
val param1: String,
val param2: String,
val param3: Int
) {
}
Когда функция содержит одно выражение можно сделать так:
override fun toString(): String {
return "Hello, $name"
}
override fun toString() = "Hello, $name"
Единственный случай, когда функция-выражение может переносится - это использование специальных блочных конструкций:
fun waitMe() = runBlocking {
delay(1000)
}
Когда инициализация свойства не помещается на одной строке можно сделать перенос после знака присваивания (=
):
val binding: ListItemBinding =
DataBindingUtil.inflate(inflater, R.layout.list_item, parent, false)
get
и set
функции должны быть на отдельной строке с обычным отступом (4 пробела):
val items: LiveData<List<Item>>
get() = _items
Read-only свойства могут иметь более краткий синтаксис:
val javaExtension: String get() = "java"
Пробелы
Пустая строка может быть:
Между членами классов: свойствами, функциями, конструкторами и другими
Пустая строка между двумя свойствами необязательна. Это нужно для создания логических групп (например для backing свойств)
Между выражениями для логического разделения
Перед первым членом функции или класса (необязательно)
Помимо требуемых правил для языка и литералов (строчных или символьных) одиночный ASCII пробел:
Разделяет зарезервированные слова, таких как:
if
,for
илиcatch
от круглой открывающей скобки:
// неправильно
for(i in 1..6) {
}
// OK
for (i in 1..6) {
}
Разделяет любые зарезервированные слова, таких как
else
иcatch
от закрывающей фигурной скобки:
// Неправильно
}else {
}
// OK
} else {
}
Ставиться перед любой открывающей фигурной скобкой:
// Неправильно
if (items.isEmpty()){
}
// OK
if (items.isEmpty()) {
}
Ставиться между операндами:
// Неправильно
val four = 2+2
// OK
val four = 2 + 2
// Это относится и к оператору лямбда выражения (->)
// Неправильно
items.map { item->item % 2 == 0 }
// OK
items.map { item -> item % 2 == 0 }
Исключение: оператор ссылка на член (
::
), точка (.
) или range (..
)
// Неправильно
val str = Any :: toString
// OK
val str = Any::toString
// Неправильно
item . toString()
// OK
item.toString()
// Неправильно
for (i in 1 .. 6) {
println(i)
}
// OK
for (i in 1..6) {
println(i)
}
Перед двоеточием (
:
) для указания расширения базового класса или интерфейса, а также вwhen
выражении для generic типов:
// Неправильно
class Worker: Runnable
// OK
class Worker : Runnable
// Неправильно
fun <T> min(a: T, b: T) where T: Comparable<T>
// OK
fun <T> min(a: T, b: T) where T : Comparable<T>
После двоеточия (
:
) или запятой (,
)
// Неправильно
val items = listOf(1,2)
// OK
val items = listOf(1, 2)
// Неправильно
class Worker :Runnable
// OK
class Worker : Runnable
По обеим сторонам двойного слеша:
// Неправильно
var debugging = false//отключен по умолчанию
// OK
val debugging = false // отключен по умолчанию
Заключение
Данная статья получилась довольно большая, надеюсь вам было полезно прочитанное.
В следующей статье: именование, специальные конструкции и документация.
Полезные ссылки:
Kotlin style guide (на английском)
K&R стиль
Книга: Чистый код (Боб Мартин)
Кратко о книге Боба Мартина
Ждите следующей части!