Руководство по стилю 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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *