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

    Заключение

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

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

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

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

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 10

      +3
      А в чем специфика kotlin-android кода? Это ж все просто кусками нарезаный официальный стайлгайд.
      Вообще для студия умеет в автовыравнивание + официальный стайлгайд. Почему не просто запомнить комбинацию клавиш и закрыть вопрос навсегда? Или кому то платят за форматирование кода (может еще и чужого)?
      Да, остальные правила можно подсветить detekt'ом. И повесить на коммиты хуком.
        0
        Здравствуйте. Я старался написать более понятным языком. К сожалению, а может быть и к счастью, я отношусь к тем программистам, которые считают, что нужно уметь писать красивый код самостоятельно и понимать: по каким правилам он написан.
          0
          Получилось ровно тоже самое, что в самом кодстайле котлина — только обрезано.
        0
        Для протокола: я на котлине не пишу и зашел исключительно из интереса, может быть, я чего-то очевидного не понимаю. Правила форматирования — это, конечно, хорошо, но хотелось бы аргументации. Читаемость, понятность и красота кода — вещи сугубо индивидуальные. Они даже от человека к человеку гуляют, не то что от организации к организации. Даже если эти правила сложились исторически, на момент введения они всё равно должны были иметь смысл. Взглядом же со стороны написанное воспринимается как вкусовщина, по большей части.
          0
          Спасибо за ответ! Данные правила использует одна из крупных и успешных компаний — Google, к тому же многие из этих правил поддерживаются опытными программистами. А форматирование скобок вообще следует правилам Unix создателей!
          Я считаю, что в программировании, как и в других областях, нужен какой-то общий стиль, чтобы программисты смогли быстро понять друг друга!
            +1
            Вам не кажется странным аргумент: «все должны делать X потому, что так делает Y»? Вот постановка или удаление пробелов вокруг скобок или операторов, лично на мой взгляд, не влияет абсолютно ни на что. Лишь бы в рамках проекта использовался один стиль. Любой (ну в разумных пределах, разумеется).
            Как пример логичного могу привести правило «одно условие — одна строка» в сочетании с указанием в многострочных выражениях оператора в начале, а не в конце, как иногда бывает:
            SELECT ...
            FROM table t
            WHERE t.field1 = 1
              AND t.field2 = 2
              AND t.field3 = 3
              AND t.field4 = 4
              AND t.field5 = 5
            

            Почему? Потому, что условие далеко справа легко не увидеть, даже если всё влезает на экран. Потому, что при отладке, зачастую, оператор важнее самих условий и можно быстро найти один маленький и злобный or среди горы условий, в то время как оператор в конце может быть выдавлен даже за пределы экрана.
            С этими аргументами можно согласиться или их оспорить, приводя другие. Это плодотворная дискуссия. А как оспаривать апелляцию к авторитету? Я не знаю.
            В целом, с современными средствами IDE вопросы форматирования кажутся надуманными. Код, перед загрузкой в репозиторий, должен быть отформатирован автоматическими инструментами по общим настройкам проекта, только и всего.
              0

              +
              В detekt есть правило по которому особо нет доводов за написание тех же логических операторов в if else в конце


              If (isFirst &&
                  isSecond &&
                  isThird ||
                  isForth
              ) { .. }

              Когда удобней читать код вначале


              If (isFirst
                  && isSecond
                  && isThird
                  || isForth
              ) { .. }

              Как с висящей запятой, при изменениях другим разработчиком будет затронута прошлая строка

          0
          Отступы пробелами… Ужасно…
            0
            Android Studio автоматически подставляет пробелы вместо табуляции)
              0
              Потому что там так в настройках по-умолчанию. Потому что это стандарт кодирования для гугла. Потому что у них сильно гетерогенная инфраструктура с огромным количество legacy-инструментов (да, вплоть до правки кода в «блокноте»).
              Почему эту глупость постоянно тащат в другие языки, на которых предполагается писать при помощи мощных IDE — для меня большая загадка.

          Only users with full accounts can post comments. Log in, please.