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
ITurchenko
Конечно необходимости нет. Пока однажды не отстрелите себе ногу из-за того, что инициализация почему-то не прошла либо метод вызвался до инициализации.
Может быть фрагмент сразу после создания в мусорку отправился, может быть переход слишком быстрым был, может быть ещё куча всяких условий.
lateinit не стоит бездумно лупить куда попало, обязательно нужно держать в голове порядок инициализации и использования переменной, иначе потом будет больное ой в виде UninitializedPropertyAccessException.
mobileSimbirSoft Автор
Конечно, вы правы. Обязательно нужно учитывать то, в какие моменты может произойти обращение к переменной, прежде чем делать ее lateinit.
Neikist
А кроме этого lateinit переменные фрагмента для вью прекрасное место для утечки памяти. Как и вообще переменные фрагмента вью хранящие если им null не присваивать.
mad_nazgul
ИМХО по возможности лучше использовать «by lazy», вместо «lateinit». :-)