Руководство по стилю Kotlin для Android разработчиков (Часть I)

Данная статья охватывает не только эстетические вопросы форматирования, но и другие типы соглашений и стандартов, которые необходимо знать 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 // отключен по умолчанию
Заключение
Данная статья получилась довольно большая, надеюсь вам было полезно прочитанное.
В следующей статье: именование, специальные конструкции и документация.
Полезные ссылки:
Ждите следующей части!