Kotlin, созданный всего 5 лет назад, с 2019 года считается приоритетным языком программирования под Android. И все же этот язык достаточно молод и продолжает развиваться, поэтому иногда бывает непонятно, каким образом лучше написать код. У нас в команде часто бывают обсуждения на тему чистого Kotlin-кода, и на их основе мы составили свои best practices. Хотим поделиться этими рекомендациями и ждем ваших вопросов.

Ну что ж, приступим! В первую очередь, в Котлине много синтаксического сахара, и если им злоупотреблять, то читать такой код становится затруднительно. Следующие несколько пунктов можно отнести к борьбе между краткостью и читаемостью

Не пишите объявление класса в одну строчку

Даже если сегодня объявление вашего класса уместилось в одну строчку и не выходит за поля – завтра может добавиться еще один аргумент в конструктор или еще один интерфейс в список наследуемых типов. В таком случае этот самый список может уехать вправо и пропасть из поля видимости.

class ChannelViewModel(
       val conversationId: String,
       getChannelUseCase: GetChannelUseCase,
) : ViewModel() {

Размещая каждый параметр конструктора на отдельной строке, мы также получаем бонусы:

  • Если при рефакторинге надо поменять местами параметры, сделать это будет быстрее (в Android Studio сдвигаем строчку кода вверх/вниз с помощью сочетания клавиш Shift+Command+Up/Down)

  • В гите изменения параметров будут отображаться более наглядно.

Вложенный return

Рассмотрим такой пример:

data class MyClass(val number: Int, val flag: Boolean)

fun create(numberParam: Int?, flag: Boolean?): MyClass? {
   return MyClass(numberParam ?: return null, flag == true)
}

Обычно если мы доходим до выражения с return, то это такая точка, в которой завершается выполнение функции. В этом же примере в случае, когда numberParam равен null,  мы мысленно проходим сперва через return MyClass(...) и затем через return null. Лучше в данном случае использовать простой if, чтобы поток выполнения программы выглядел понятнее:

fun create(numberParam: Int?, flag: Boolean?): MyClass? {
   if (numberParam == null) {
       return null
   }
   return MyClass(numberParam, flag == true)
}

Анонимный параметр it

Как давно замечали на Хабре, цепочка из вложенных методов с анонимным параметром it читается тяжело, однако такие злоупотребления по-прежнему регулярно встречаются в коде:

values?.filterNot { selectedValues?.contains(it) == true }
   ?.let {
       selectedValues?.addAll(it)
       result[key]?.values = selectedValues?.filter { it.isChecked }
   }

Именованный параметр следует добавить как минимум в функцию let:

values?.filterNot { allSelectedValues?.contains(it) == true }
   ?.let { newValues ->
       allSelectedValues?.addAll(newValues)
       result[key]?.values = allSelectedValues?.filter { it.isChecked }
   }

В этом коде it – не единственная проблема. В данном примере обнуляемые типы можно заменить на не-null, в результате код станет опрятнее. Сравните:

val newValues = values.filterNot { selectedValues.contains(it) }
selectedValues.addAll(newValues)
result[key]?.values = selectedValues.filter { it.isChecked }

Сокращайте цепочки безопасных вызовов ?. заменой обнуляемых типов на не-null типы

Продолжим затронутую в предыдущем пункте тему с обнуляемыми типами и рассмотрим такой пример:


private var animatedView: FrameLayout? = null
...
animatedView?.animate()?.alpha(1f)?.setDuration(500)?.interpolator = AccelerateInterpolator()

Здесь фактически значение null могло бы появиться только в том случае, когда animatedView равен null. Добавление простой проверки if (animatedView != null) избавит нас от цепочки безопасных вызовов. Но в данном примере вообще нет необходимости в том, чтобы  animatedView принимало значение null. Поэтому лучше его сделать lateinit переменной, и тогда код вообще не будет содержать проверок на null:

private lateinit var animatedView: FrameLayout
...
animatedView.animate().alpha(1f).setDuration(500).interpolator = AccelerateInterpolator()

Некоторые проблемы появляются при автоматическом переводе файлов с Java на Kotlin, особенно если разработчик поторопился и оставил код “как есть”. Обычно такой код пестрит восклицательными и вопросительными знаками и требует пересмотра. При переходе с Java какие-то if можно заменить на when, можно использовать функции области видимости (let, apply, also, with, run), функции для работы с коллекциями, переписать классы Utils на extension функции.

Избавляемся от !!

Использование оператора !! считается дурным тоном, так как оно означает игнорирование потенциального NullPointerException, да и сам код с обилием знаков !! выглядит “костыльно”. 

От !! можно избавиться следующими способами: 

  • заменой обнуляемых типов на не-null типы (как было сделано в примере с lateinit animatedView)

  • функцией let с безопасным вызовом ?.let { … }

  • элвис-оператором ?:

  • и наконец, если мы действительно допускаем, что в приложении должно произойти исключение, когда переменная приняла значение null, то можно воспользоваться функцией checkNotNull или requireNotNull. Они отличаются только типом выбрасываемого исключения: IllegalStateException и IllegalArgumentException соответственно. 

Для того, чтобы предупредить появление !! при переводе кода с Java на Kotlin, можно добавить аннотации @NonNull в Java-код.

Отдаем предпочтение when перед if

Сравните:

val price = if (priceData.isWeightPrice) {
   priceData.minDiscountPrice.toInt()
} else if (priceData.discountPrice != 0.0) {
   priceData.discountPrice.toInt()
} else {
   priceData.price.toInt()
}

и вариант с when:

val price = when {
   priceData.isWeightPrice -> priceData.minDiscountPrice.toInt()
   priceData.discountPrice != 0.0 -> priceData.discountPrice.toInt()
   else -> priceData.price.toInt()
}

С when мы можем не писать лишние круглые и фигурные скобки, но код все равно выглядит структурированным, так как условия отделены от результатов стрелками. 

Преимущества when заметны в том случае, когда проверяется более двух условий. Конечно, нет смысла использовать when для проверки значений булевского флага.

Заменяем классы Util на функции расширения

Если статические вспомогательные функции можно отнести к какому-то определенному классу, лучше оформить их в виде функций extension. Тогда код будет выглядеть более идиоматичным. 

Еще один плюс функций расширения в том, что автокомплит сам их предлагает. Не надо держать в голове, реализована ли уже на проекте та или иная вспомогательная функция, и в каком файле она лежит.

Это не значит, что от объектов Util в Kotlin надо полностью избавляться (обратите внимание: статические методы в Java содержатся в классах, а в Kotlin – в объектах). Если вспомогательная функция не относится к какому-то конкретному типу, не стоит оформлять ее в виде статической функции верхнего (package) уровня. Иначе такие функции будут постоянно появляться в автокомплите (extension функцию автокомплит предложит после того, как мы напечатаем название переменной определенного типа и точку, и это нормально; а функция верхнего уровня появится сразу после того, как мы начнем что-то печатать). Кроме того, код будет лучше организован и структурирован, если вспомогательные функции помещать внутри объекта, а не просто внутри файла.

Котлин периодически обновляется, и иногда разработчик может не уследить за отдельными новшествами (и это не говоря о случаях “прочитал и забыл”). Приведем несколько полезных возможностей языка, которые мы советуем использовать.

Замыкающие запятые (trailing commas) 

Замыкающие запятые появились в версии языка 1.4. Мы рекомендуем их использовать в сочетании с приведенной выше рекомендацией располагать параметры конструктора (и вообще метода) на отдельных строках по той же причине: с замыкающей запятой аккуратнее выглядит diff в гите при добавлении параметра в конец. 

Single Abstract Method interface (Fun interface)

Тоже появились в Kotlin 1.4.0. Сравните, как выглядит создание анонимного объекта для обычного и fun интерфейса:

this.actionClickListener = object : BubbleView.ClickListener {
   override fun onBubbleViewClick() {
           ...
       }
   }

и

 this.actionClickListener = BubbleView.ClickListener {
      ...
  }

Во втором случае к объявлению интерфейса добавилось только  fun:

fun interface ClickListener {
   fun onBubbleViewClick()
}

Таким образом, вместо одного интерфейса в Java, в Kotlin можно использовать обычный интерфейс, SAM-интерфейс или лямбду. Предлагаем в этом выборе руководствоваться следующим правилом:

  • для простых колбеков и мапперов используем функциональный тип (T) -> R;

  • если для наглядности мы хотим иметь название для типа или для вызываемого метода, то SAM-интерфейс;

  • если должно быть несколько методов, то обычный интерфейс. 

Функции для работы с коллекциями

Kotlin содержит огромное множество функций для сортировки, фильтрации, поиска, преобразования коллекций. Каждый раз, когда в коде встречается цикл для обхода коллекции, велика вероятность, что его можно переписать с использованием уже существующей функции. Поэтому если вы регулярно пишете циклы, рекомендуем вам изучить/освежить в памяти документацию

Как можно улучшить представленный код?

val itemIdsSet: Set<String> = ...
val currentItemIds: Set<String> = ...
for(itemId in itemIdsSet) {
   if(!currentItemIds.contains(itemId)) {
       repository.exclude(itemId)
   }
}

Как вариант:

val itemIdsSet: Set<String> = ...
val currentItemIds: Set<String> = ...
for (itemId in itemIdsSet subtract currentItemIds) {
   repository.exclude(itemId)
}

Или если отредактировать API репозитория так, чтобы функция exclude могла принимать коллекции, тогда мы сможем написать:

repository.exclude(itemIdsSet subtract currentItemIds)  

Согласуйте code style

Иногда стиль написания кода у разработчиков в команде может настолько различаться, что проект рискует превратиться в “кашу”. Поэтому лучше согласовывать такие моменты: 

  • принципы наименования переменных (например, запрет на использование префикса _ для теневых полей или написание констант enum только заглавными буквами)

  • порядок следования функций внутри классов

  • положение companion object внутри класса (в начале или в конце) и т.п. 

Заключение

В этой статье мы поделились несколькими правилами, которые показали свою эффективность в нашей команде. Однако, мы не претендуем на их истинность и рекомендуем вам обсуждать и формировать свои наборы best practices.

Как пример, больше идей по улучшению Kotlin-кода можно почерпнуть здесь:

https://developer.android.com/kotlin/style-guide https://proandroiddev.com/an-opinionated-guide-on-how-to-make-your-kotlin-code-fun-to-read-and-joy-to-work-with-caa3a4036f9e 
https://developer.android.com/kotlin/coroutines/coroutines-best-practices