Руководство по стилю 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 // отключен по умолчанию

Заключение

Данная статья получилась довольно большая, надеюсь вам было полезно прочитанное.

В следующей статье: именование, специальные конструкции и документация.

Полезные ссылки:

Ждите следующей части!

Let’s block ads! (Why?)

Read More

Recent Posts

VK купила 40% билетной платформы Intickets.ru

VK объявляет о приобретении 40% компании Intickets.ru (Интикетс). Это облачный сервис для контроля и управления продажей билетов на мероприятия. Сумма…

18 часов ago

OpenAI готовится запустить поисковую систему на базе ChatGPT

OpenAI готовится запустить собственную поисковую систему на базе ChatGPT. Информацию об этом публикуют западные издания. Ожидается, что новый поисковик может…

1 день ago

Роскомнадзор рекомендовал хостинг-провайдерам ограничить сбор данных с сайтов для иностранных ботов

Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…

2 дня ago

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

7 дней ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

1 неделя ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

1 неделя ago