Функции-расширения (Extension functions)
Java содержит множество классов с замечательными методами. Но разработчику всегда не хватает ещё одного метода, который ему нужен для собственного проекта. Функции-расширения позволяют создавать видимость внедрения в существующий класс нового метода. Это позволяет более элегантно решить проблему с классами Utils, которые создают разработчики для подобных целей. Кроме того, созданные функции будут выводиться в подсказках при наборе кода вместе с стандартными методами класса.
Рассмотрим пример на Java. Как узнать, является ли указанная дата субкотой (иногда люди используют слово «суббота»)? У класса Date нет соответствующего метода и мы можем определить субботу только по методу getDay(), который возвращает число 6 для субботы.
Создадим класс Utils.java и поместим свой метод.
static boolean isSaturday(Date date)
Вызываем метод по щелчку кнопки.
public void onClick(View view)
Теперь рассмотрим, как это можно сделать на Kotlin.
Добавим функцию в активность и вызовем её по щелчку кнопки.
fun Date.isSaturday(): Boolean < return getDay() == 6 >button_choose.setOnClickListener
Теперь, если смотреть на код, то создаётся ощущение, что isSaturday() является частью класса Date и мы вызываем его прямо из класса.
Предыдущий пример с функцией был написан в Java-стиле. В Kotlin можно заменить метод getDay() на свойство day.
fun Date.isSaturday(): Boolean
Впрочем и это не предел. Подобную функцию можно переписать в одну строку.
fun Date.isSaturday(): Boolean = day == 6
Если у вас будет несколько подобных функций-расширений, то удобнее их хранить в отдельном файле (не обязательно в классе). Создадим дополнительный пакет utils с файлом Utils.kt.
package ru.alexanderklimov.counter.utils import java.util.* fun Date.isSaturday() = day == 6 // другие функции-расширения
В активности при вызове функции следует импортировать её (возможно это сделает сама студия автоматически).
import ru.alexanderklimov.counter.utils.isSaturday // код для кнопки останется прежним
Можно переопределить имя функции при помощи as:
import ru.alexanderklimov.counter.utils.isSaturday as caturday val saturday = now.caturday()
Другие примеры из различных докладов и презентаций.
Функция для получения последнего символа из строки.
fun String.lastChar() = get(length - 1) // Применяем к строке val cat = "Мурзик" val c: Char = cat.lastChar() textview_info.text = cat.lastChar().toString() // длинный вариант
Знакомый нам пример. Мы добавляем в класс Context новую функцию toast(), которая вызывает метод Toast.makeText() с использованием параметров по умолчанию.
fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) < Toast.makeText(this, message, duration).show() >// вызываем в активности toast("Hello Kitty")
JetBrains добавила свой набор расширений для различных классов Java и Android. Самый показательный случай — набор расширений для коллекций — функции filter(), map(), count() и т.д. в функциональном стиле, даже используя старую Java 6.
Помните, что функции-расширения класса не могут обращаться к его членам с модификаторами protected или private.
Ключевое слово inflix
Если ваша функция-расширение использует только один аргумент, то можно вызвать функцию через ключевое слово infix.
Создадим функцию-расширение для Int, которое будет сравнивать число с аргументом, чтобы узнать, больше оно или меньше.
fun Int.isGreater(value: Int): Boolean < return this >value > // вызываем функцию 3.isGreater(9) // false
Перепишем функцию с ключевым словом infix.
infix fun Int.isGreater(value: Int): Boolean < return this >value > // вызываем функцию 12 isGreater 9 // true
Расширения (extensions)
Kotlin позволяет расширять класс путём добавления нового функционала без необходимости наследования от такого класса и использования паттернов, таких как Decorator. Это реализовано с помощью специальных выражений, называемых расширения.
Например, вы можете написать новые функции для класса из сторонней библиотеки, которую вы не можете изменить. Такие функции можно вызывать обычным способом, как если бы они были методами исходного класса. Этот механизм называется функцией расширения. Существуют также свойства расширения, которые позволяют определять новые свойства для существующих классов.
Функции-расширения
Для того чтобы объявить функцию-расширение, укажите в качестве префикса расширяемый тип, то есть тип, который мы расширяем. Следующий пример добавляет функцию swap к MutableList :
fun MutableList.swap(index1: Int, index2: Int) < val tmp = this[index1] // 'this' даёт ссылку на список this[index1] = this[index2] this[index2] = tmp >
Ключевое слово this внутри функции-расширения соотносится с объектом расширяемого типа (этот тип ставится перед точкой). Теперь мы можем вызывать такую функцию в любом MutableList .
val list = mutableListOf(1, 2, 3) list.swap(0, 2) // 'this' внутри 'swap()' будет содержать значение 'list'
`, and you can make it generic: —>
Следующая функция имеет смысл для любого MutableList , и вы можете сделать её обобщённой:
fun MutableList.swap(index1: Int, index2: Int) < val tmp = this[index1] // 'this' относится к списку this[index1] = this[index2] this[index2] = tmp >
Вам нужно объявлять обобщённый тип-параметр перед именем функции для того, чтобы он был доступен в получаемом типе-выражении. См. Обобщения.
Расширения вычисляются статически
Расширения на самом деле не проводят никаких модификаций с классами, которые они расширяют. Объявляя расширение, вы создаёте новую функцию, а не новый член класса. Такие функции могут быть вызваны через точку, применимо к конкретному типу.
Расширения имеют статическую диспетчеризацию: это значит, что вызванная функция-расширение определяется типом её выражения, из которого она вызвана, а не типом выражения, вычисленным в ходе выполнения программы, как при вызове виртуальных функций.
open class Shape class Rectangle: Shape() fun Shape.getName() = "Shape" fun Rectangle.getName() = "Rectangle" fun printClassName(s: Shape) < println(s.getName()) >printClassName(Rectangle())
Этот пример выведет нам Shape на экран потому, что вызванная функция-расширение зависит только от объявленного параметризованного типа s , который является Shape классом.
Если в классе есть и функция-член, и функция-расширение с тем же возвращаемым типом, таким же именем и применяется с такими же аргументами, то функция-член имеет более высокий приоритет.
class Example < fun printFunctionType() < println("Class method") >> fun Example.printFunctionType() < println("Extension function") >Example().printFunctionType()
Этот код выведет Class method.
Однако для функций-расширений совершенно нормально перегружать функции-члены, которые имеют такое же имя, но другую сигнатуру.
class Example < fun printFunctionType() < println("Class method") >> fun Example.printFunctionType(i: Int) < println("Extension function #$i") >Example().printFunctionType(1)
Обращение к Example().printFunctionType(1) выведет на экран надпись Extension function #1.
Расширение null-допустимых типов
Обратите внимание, что расширения могут быть объявлены для null-допустимых типов. Такие расширения могут ссылаться на переменные объекта, даже если значение переменной равно null и есть возможность провести проверку this == null внутри тела функции.
Благодаря этому метод toString() в Kotlin вызывается без проверки на null : она проходит внутри функции-расширения.
fun Any?.toString(): String < if (this == null) return "null" // после проверки на null, `this` автоматически приводится к не-null типу, // поэтому toString() обращается (ориг.: resolves) к функции-члену класса Any return toString() >
Свойства-расширения
Аналогично функциям, Kotlin поддерживает расширения свойств.
val List.lastIndex: Int get() = size - 1
Since extensions do not actually insert members into classes, there’s no efficient way for an extension > property to have a [backing field](properties.md#backing-fields). This is why _initializers are not allowed for > extension properties_. Their behavior can only be defined by explicitly providing getters/setters. —>
Поскольку расширения фактически не добавляют никаких членов к классам, свойство-расширение не может иметь теневого поля. Вот почему запрещено использовать инициализаторы для свойств-расширений. Их поведение может быть определено только явным образом, с указанием геттеров/сеттеров.
val House.number = 1 // ошибка: запрещено инициализировать значения // в свойствах-расширениях
Расширения для вспомогательных объектов (ориг.: companion object extensions)
Если у класса есть вспомогательный объект, вы также можете определить функции и свойства расширения для такого объекта. Как и обычные члены вспомогательного объекта, их можно вызывать, используя в качестве определителя только имя класса.
class MyClass < companion object < >// называется "Companion" > fun MyClass.Companion.printCompanion()
Область видимости расширений
В большинстве случаев вы определяете расширения на верхнем уровне, непосредственно в разделе пакетов.
package org.example.declarations fun List.getLongestString() < /*. */>
Для того, чтобы использовать такое расширение вне пакета, в котором оно было объявлено, импортируйте его на месте вызова.
package org.example.usage import org.example.declarations.getLongestString fun main()
См. Импорт для более подробной информации.
Объявление расширений в качестве членов класса
Внутри класса вы можете объявить расширение для другого класса. Внутри такого объявления существует несколько неявных объектов-приёмников (ориг.: implicit receivers), доступ к членам которых может быть произведён без квалификатора. Экземпляр класса, в котором расширение объявлено, называется диспетчером приёмников (ориг.: dispatch receiver), а экземпляр класса, для которого вызывается расширение, называется приёмником расширения (ориг.: extension receiver).
class Host(val hostname: String) < fun printHostname() < print(hostname) >> class Connection(val host: Host, val port: Int) < fun printPort() < print(port) >fun Host.printConnectionString() < printHostname() // вызывает Host.printHostname() print(":") printPort() // вызывает Connection.printPort() >fun connect() < /*. */ host.printConnectionString() // вызов функции-расширения >> fun main() < Connection(Host("kotl.in"), 443).connect() // Host("kotl.in").printConnectionString() // ошибка, функция расширения недоступна вне подключения >
В случае конфликта имён между членами классов диспетчера приёмников и приёмников расширения, приоритет имеет приёмник расширения. Чтобы обратиться к члену класса диспетчера приёмников, можно использовать синтаксис this с квалификатором.
class Connection < fun Host.getConnectionString() < toString() // вызывает Host.toString() this@Connection.toString() // вызывает Connection.toString() >>
Расширения, объявленные как члены класса, могут иметь модификатор видимости open и быть переопределены в унаследованных классах. Это означает, что диспечеризация таких функций является виртуальной по отношению к типу диспетчера приёмников, но статической по отношению к типам приёмников расширения.
open class Base < >class Derived : Base() < >open class BaseCaller < open fun Base.printFunctionInfo() < println("Base extension function in BaseCaller") >open fun Derived.printFunctionInfo() < println("Derived extension function in BaseCaller") >fun call(b: Base) < b.printFunctionInfo() // вызов функции расширения >> class DerivedCaller: BaseCaller() < override fun Base.printFunctionInfo() < println("Base extension function in DerivedCaller") >override fun Derived.printFunctionInfo() < println("Derived extension function in DerivedCaller") >> fun main() < BaseCaller().call(Base()) // "Base extension function in BaseCaller" DerivedCaller().call(Base()) // "Base extension function in DerivedCaller" - приемник отправки является виртуальным DerivedCaller().call(Derived()) // "Base extension function in DerivedCaller" - приемник расширения является статическим >
Примечание о видимости
Расширения используют те же модификаторы видимости как и обычные функции, объявленные в той же области видимости. Например:
- Расширение, объявленное на верхнем уровне файла, имеет доступ к другим private объявлениям верхнего уровня в том же файле;
- Если расширение объявлено вне своего типа приёмника, оно не может получить доступ к private или protected членам приёмника.
© 2015—2023 Open Source Community
Kotlin Android Extensions deprecated. Что делать? Инструкция по миграции
Безусловно, это было очень удобно, особенно если у вас проект полностью на Kotlin. Однако, мир меняется и теперь нужно искать альтернативы. В этой статье мы кратко рассмотрим, что такое плагин Kotlin Android Extension, какие были проблемы с ним и что теперь нам, Android-разработчикам делать. Частично, использовался материал этой статьи. Итак, поехали.
Кратко о Kotlin Android Extensions
Kotlin Android Extensions — это плагин для Kotlin, позволяющий восстанавливать view из Activities, Fragments, и Views без написания стандартного бойлерплэйт-кода типа findViewById.
Плагин генерирует дополнительный код, который позволяет получить доступ к view в виде XML, так же, как если бы вы имели дело с properties с именем id, который вы использовали при определении структуры.
Также он создаёт локальный кэш view. При первом использовании свойства, плагин выполнит стандартный findViewById. В последующем, view будет восстановлен из кэша, поэтому доступ к нему будет быстрее.
Если это всё так удобно, то зачем его сделали deprecated?
Проблемы Kotlin Android Extensions
- Используется глобальный нэйминг идентификаторов. Могут возникнуть ситуации, когда один и тот же идентификатор имеется у разных view в разных лэйаутах — соответственно только на этапе работы приложения вы узнаете о том, что использовали не тот id.
- Возможно использовать только в проектах на Kotlin (кэп)
- Отсутствует Null Safety. В случае, когда view представлена в одной конфигурации и отсутствует в другой — может возникнуть краш, т.к отсутствует обработка таких ситуаций
- Невозможно использовать в многомодульных проектах. Очень распространённый сценарий: у вас есть модуль UI Kit, хранящий общие UI-компоненты, которые вы хотите переиспользовать в других модулях. До сих пор висит issues которое вряд ли поправят. В таком сценарии обычно используют старый добрый findViewById 🙁
- Резюмируя приведённые недостатки, нетрудно понять, что этот подход не идеален — хотя, безусловно, очень удобен на небольших проектах. На больших проектах с многомодульной архитектурой и сотнями экранов — использование Kotlin Android Extensions уже не кажется идеальным решением.
Альтернативные способы
- Использование KotterKnife (кек, даже не думайте).
- Старый добрый FindViewById() — уже получше, но так себе.
- Использование AndroidAnnotations (привет из 2015)
- View Binding от Google — бинго!
View Binding от Google
Итак, победителем в этом списке выглядит ViewBinding от Google (не путайте с DataBinding). Давайте кратко рассмотрим, что это такое.
View Binding — это инструмент, который позволяет проще писать код для взаимодействия с view. При включении View Binding в определенном модуле он генерирует binding классы для каждого файла разметки (layout) в модуле. Объект сгенерированного binding класса содержит ссылки на все view из файла разметки, для которых указан android:id
Главные преимущества View Binding — это Null safety и Type safety.
Начало работы с View Binding
Начать работать с ViewBinding достаточно просто. Нужно добавить опцию в build.gradle:
android < . buildFeatures < viewBinding true >>
После этого можно уже использовать. Каждый сгенерированный binding класс содержит ссылку на корневой view разметки (root) и ссылки на все view, которые имеют id. Имя генерируемого класса формируется как «название файла разметки», переведенное в camel case + «Binding». Например, для файла разметки result_profile.xml:
Будет сгенерирован класс ResultProfileBinding, содержащий 2 поля: TextView name и Button button.
Использование в Activity
Например у вас вот такой layout:
Результат работы ViewBinding:
public final class ActivityMainBinding implements ViewBinding < @NonNull private final ConstraintLayout rootView; @NonNull public final TextView textView;
Использовать viewBinding можно так:
private lateinit var binding: ResultProfileBinding override fun onCreate(savedInstanceState: Bundle?)
И теперь, после того, как получили ссылки на view:
binding.name.text = viewModel.name binding.button.setOnClickListener
Если вы используете ViewBinding во фрагменте и держите ссылку на binding во фрагменте (а не только в методе onCreateView()) то не забывайте очищать ссылки в методе onDestroyView().
private var _binding: ResultProfileBinding? = null // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? < _binding = ResultProfileBinding.inflate(inflater, container, false) val view = binding.root return view >override fun onDestroyView()
Это необходимо делать из-за жизненного цикла фрагмента и view:
В целом, переключиться на ViewBinding достаточно не сложно, хотя и жаль, что Kotlin Android Extensions объявлен deprecated. Не забудьте присоединиться к нам в Telegram, а на платформе AndroidSchool.ru публикуются полезные материалы для Android-разработчика и современные туториалы.
- Туториалы в телеграм
- Документация по ViewBinding
- Статья о жизненном цикле фрагмента и view
- Статья о применении ViewBinding
- android development
- viewbinding
- kotlin
Нюансы перехода на Kotlin, или Руководство для Android-разработчика по предательству Java
Всем привет! Меня зовут Константин Михайловский, и я Android инженер в компании Genesis на проекте BetterMe. В этой статье я раскрою некоторые нюансы перехода с Java на язык программирования Kotlin в Android-проекте, с которыми наша команда успела столкнуться.

Итак, на дворе год. Если вы — Android-инженер и уже успели полностью или даже отчасти «пересесть» на язык программирования Kotlin, если полагаться на актуальные рекомендации Google и не беспочвенные восторги многих разработчиков от опыта использования языка, которые я и сам разделяю, вы на правильном пути. С большой вероятностью вы успели попасться на большинство «ошибок новичка», описанных в этой статье (и, что немаловажно, докопаться до причины их возникновения).
Однако, если вы ещё лишь задумываетесь о переходе на Kotlin — будь это в рамках разработки новых фич вашего проекта на Java и написания первых тестов к нему, либо разработка нового проекта с чистого листа — сейчас всё ещё прекрасное время для этого. Советы из статьи будут вам полезны на определённых этапах такого переходного периода.
Вероятно, самый закономерный вопрос, который первым делом придет вам в голову в процессе предательства Java в пользу Kotlin, — «с чего же начать?».
Открываем двери для Kotlin
В то время как на просторах интернета, в том числе в официальной документации языка и на платформе для разработчиков, можно найти доступные руководства по созданию Kotlin-проекта и/или настройки Kotlin в Android Studio и Gradle, в этом разделе я сфокусируюсь лишь на первых потенциальных ловушках на вашем пути предательства Джавы.
Если в своём существующем проекте вы используете annotation processing или библиотеки, которые на нём базируются (в большинстве случаев это вроде Dagger 2 и других, Data Binding, Butterknife), без предварительных правок в build.gradle файлах на уровне ваших модулей, они просто перестанут собираться без подключённого плагина kapt (собственный annotation processor у Kotlin):
apply plugin: 'kotlin-kapt'
Также чрезвычайно важный шаг — замена всех вхождений annotationProcessor конфигурации в вашем build.gradle на kapt .
Генерация стабов в данный момент поддерживается из-под-коробки.
Подготовка сознания к Null Safety
Важное отличие Kotlin от Java — поддержка первым nullable-типов null-safety «из-под-коробки». Поэтому не поленитесь начать знакомство с языком с раздела официальной документации, который довольно исчерпывающе раскрывает основные аспекты этого мощного механизма и работы с ним (в том числе при двухстороннем взаимодействии с Java-кодом).
Первое свидание с Kotlin: data classes
До того как Google объявил, что поддерживает Kotlin на официальном уровне, довольно часто можно было встретить среди рекомендаций — начать процесс перехода с написания unit-тестов на этом языке. И пусть этот совет имеет огромный смысл, ведь unit-тесты — действительно наименее агрессивный путь экспансии Kotlin на кодовую базу и при этом свободный от рисков наткнуться на странные ошибки, связанные с interop двух языков. Однако вряд ли вы испытаете катарсис, который способен принести «сладкий» и лаконичный синтаксис языка, подталкивающий к тем же приемам функционального программирования (unit-тесты в большинстве случаев — исключительно императивный стиль). Попробуйте параллельно начать делать вкрапления языка с написания POJO с помощью data-классов, о которых вы наверняка, даже не будучи знакомым с языком на практике, могли слышать раньше.
Предположим, вам нужно написать класс сущности для фильма Movie. Одной строчки кода будет достаточно!
data class Movie(val id: Long, val title: String, val director: String, val releaseDate: Date)
Помимо лаконичности здесь вы получаете:
— Переопределённые методы equals() , hashCode() и toString() под капотом;
— Immutable класс, неявно наследующийся от Any (в отличие от Object в Java) с immutable-полями (но это не точно) и неявными публичными геттерами и сеттерами для каждого. Создание экземпляра такого класса будет выглядеть так (обратите внимание на отсутствие ключевого слова new):
Movie(42L, "Isle of Dogs", "Wes Anderson", Date())
— Метод copy() , который позволяет клонировать экземпляр данного класса и может быть полезен в том случае, если вы, например, пожелаете создать новый неизменяемый объект на основе существующего, но с отличающимися значениями одного или нескольких полей (при условии, что они не private). Такой подход будет для вас первым шагом навстречу функциональному стилю:
val clonedMovie = existingMovie.copy(id = 43L)
Предупреждение № 1: при создании экземпляра такого класса на Java с помощью copy() вам придётся определить значения для каждого из полей.
Предупреждение № 2: на всякий случай предупрежу, что этот метод недоступен для экземпляров обычных (не data) классов.
— Поддержка значений по умолчанию, которой можно заменить использование Builder-паттерна.
data class Movie(val id: Long = 0L, val title: String = "", val director: String = "", val releaseDate: Date, val description: String? = null) . val movie = Movie(releaseDate = Date(), title = "The Darjeeling Limited")
Имейте в виду, что data-классы пока что не могут наследоваться друг от друга. Однако вы можете счесть полезными sealed-классы, неявно абстрактные. Например, в тех случаях, когда вам необходимо определить различные состояния загрузки данных или экрана. Или в любых других ситуациях, где ограниченные иерархии классов будут уместными.
data-классы + Parcelable
Начиная с версии 1.1.4, больше нет необходимости писать boilerplate-реализации методов parcelable для поддержки де/сериализации ваших объектов, так как за вас это сделает аннотация @Parcelize.
@Parcelize data class Movie(val id: Long, val title: String, val director: String, val releaseDate: Date)
Только не забудьте применить Android Extensions plugin:
apply plugin: 'kotlin-android-extensions'
И определить значение experimental-флага как true.
android < . androidExtensions < experimental = true >>
Экспериментальный статус расширения (к моменту написания этого материала) указывает на тот факт, что перед вами всё ещё не окончательный вариант этого API. Есть вероятность глубоко зарытых багов в его работе (пока что я с таковыми не сталкивался, но возможно всё). И обновление API в будущем потенциально способно «поломать» ваш код, и вам стоит использовать эту аннотацию в продакшн-коде на свой страх и риск.
data-классы в сочетании с часто используемыми библиотеками
Если вы используете замечательную Room Persistence Library, вы всё так же при подключённом kapt можете писать data-классы для сущностей вашей базы данных, которые прекрасно работают с Room-аннотациями.
@Entity(tableName = "movies") data class Movie( @PrimaryKey @ColumnInfo(name = "id") val id: Long, @ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "director") val director: String, @ColumnInfo(name = "date") val releaseDate: Long )
С Nullable-свойствами также нет никаких проблем. Но на тот случай, если вам вдруг станет любопытно так же, как нашей команде в своё время, поддерживает ли Room значения по умолчанию Kotlin, вас ждёт разочарование.
А что насчёт библиотек для сериализации данных?
В случае с одним из самых популярных в этой области GSON, необходимости в дополнительных конвертерах и плагинах нет. Библиотека будет сериализовать ваши POJO с таким же успехом, как и их Java-версии:
data class Movie( @SerializedName("id") val id: Long, @SerializedName("title") val title: String, @SerializedName("director") val director: String, @SerializedName("releaseDate") val releaseDate: Date )
Однако так же, как и в случае с Room, значения по умолчанию не поддерживаются.
Для совместимости Jackson c Kotlin вам необходимо добавить зависимость специального модуля.
Аналогичным образом дела обстоят и с Moshi, для совместимости с которым потребуется добавить зависимость:
implementation 'com.squareup.moshi:moshi-kotlin:1.x.y'
Сводим Kotlin с Java: Interoperability
Предположим, теперь вы готовы зайти дальше уровня data-классов и начать писать на Kotlin более сложные классы для ваших активити, фрагментов, presenter-, view model-, interactor-, repository- и других классов в зависимости от вашей архитектуры.
Разумеется, с некоторой вероятностью в перспективе вы планируете избавиться от Джавы в вашем проекте чуть более, чем полностью. Но перед этим вам придётся пройти длинный путь устранения конфликтов между языками, которые могут разрушить вашу жизнь (по крайней мере на пару часов).
Так как Kotlin был изначально спроектирован как JVM-язык, полностью совместимый с Java и наоборот, вы без труда можете наследоваться от существующих Java-классов, обращаться к ним и применять Java-аннотации к вашим Kotlin-классам и методам.
Существует несколько НО, образовавшихся под влиянием идеологических различий между двумя языками (о ключевых вы можете прочитать здесь).
Going Static
Например, в Kotlin нет ключевого слова static , просто смиритесь с этим.
Но не стоит отчаиваться, ведь в вашем распоряжении есть companion object . Это механизм объявления объекта внутри класса, способного содержать внутри константы и методы, синтаксически доступные со стороны Java в таком же виде, как и статические поля или методы Java. Но для того, чтобы при компиляции сгенерировались и статический метод класса, в котором находится этот объект, и метод этого объекта сам по себе, пометьте его как @JvmStatic (по умолчанию без этой аннотации метод companion-объекта будет вам доступен с помощью ссылки на Companion инстанс). Эта страница документации подробно раскрывает тему companion objects, создания синглтонов и object expressions в Kotlin.
Но прежде чем вы заключите все ваши константы в companion object-ы по всему проекту, возьмите во внимание тот факт, что такие объекты не так дешевы, как кажутся. Очень рекомендую всем, кто переходит на Kotlin, ознакомиться с циклом статей «Kotlin’s hidden costs» (part 1, part 2, part 3), открывающих обратную сторону (с точки зрения байткода) синтаксического сахара языка. Рекомендую обратить особое внимание на советы об использовании inline-функций для оптимизации производительности ваших лямбда-выражений, а также на советы по избежанию избыточной автоупаковки / автораспаковки «под капотом».
Коллекции
Когда речь идёт о коллекциях, Kotlin полностью полагается на классы стандартной библиотеки Java, расширяя их возможности с помощью дополнительных функций для их объявления ( listOf() , mapOf() , etc) и их модификаций и преобразований. Они довольно часто оказываются полезными и удобными в использовании, и с коллекциями как таковыми в Kotlin в целом всё довольно прозрачно. Ну. почти 😉 Обратите внимание на то, что List, Map и Set — это алиасы для неизменяемых JDK-реализаций коллекций, и попытки изменения их содержимого выльются в UnsupportedOperationException, что логично.
Generics
Обобщения в Kotlin тоже поддерживаются, но существуют важные различия в их реализации между двумя языками, пренебрежение которыми может привести вас к непредвиденным конфузам. Эта разница тщательно описана в документации и множестве тематических статей, поэтому вместо дублирования фактов просто оставлю ссылки на самые, на мой взгляд, ценные статьи, раскрывающие ключевые для дженериков в Kotlin концепции ковариантности, контравариантности и инвариантности:
- Kotlin Generics
- Understanding Generics and Variance in Kotlin
- An Illustrated Guide to Covariance and Contravariance in Kotlin
Возвращаемся к unit-тестам
Если вы работаете по TDD/BDD или хотя бы пишете unit-тесты к уже готовой бизнес-логике вашего приложения (ремарка: всегда, при малейшей возможности, покрывайте ваш код тестами), высока вероятность, что вы будете использовать для этих нужд Mockito.
Однако первым камнем преткновения при написании unit-тестов к Kotlin-классам может оказаться отсутствие поддержки со стороны Mockito из-под коробки создания моков к final-классам. В Kotlin классы всегда final по умолчанию, до тех пор, пока вы явно не обозначите их как open. Согласно «Effective Java», 3rd Edition, Item 19: Design and document for inheritance or else prohibit it, это вполне резонное решение. В данной ситуации вы можете поступить так:
- либо открыть тестируемый класс для наследования с помощью упомянутого модификатора open ;
- либо применить небольшой хак к Mockito, создав файл с названием org.mockito.plugins.MockMaker , обязательно в папке test/resources/mockito-extensions . Внутри он должен содержать строку: mock-maker-inline .
Она подключит плагин, который будет генерировать моки для final-классов, что позволит вам писать к ним жизнеспособные тесты.
Также от себя хочу порекомендовать библиотеку Ника Хаармана mockito-kotlin, которая предоставляет множество полезных вспомогательных функций. Они способны с помощью возможностей Котлина подсластить инициализацию моков, верификацию обращений к ним, etc, с помощью Mockito.
А например, вспомогательные inline-методы, возможностями которых воспользовался автор библиотеки, позволяют определять поведение мока сразу же при его инициализации. Например, можем сразу же задать возвращаемое значение для метода получения фильмов мок-объекта MoviesRepository:
val moviesRepo = mock < on < getMovies() >doReturn emptyList() >
Kotlin Android Extensions
Написание любого класса для Activity мы чаще всего начинаем с определения layout-а внутри onCreate метода. Если вы используете базирующиеся на annotation-processing альтернативы для получения вьюх из классическому findViewById -подходу или используете его же, посмотрите в сторону плагина Kotlin Android Extensions (не путайте с Android KTX 🙂 ), в который вы можете с некоторой вероятностью влюбиться с первых же строк кода.
С помощью него вы сможете ссылаться на вьюхи по их идентификаторам, указанным в xml, напрямую, так как доступные синтетические свойства для них уже сгенерированы.
override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setContentView(R.layout.activity_home) tvResult.text = "Random greetings text" btnFinish.setOnClickListener < Toast.makeText(this@BreathingActivity, "Voila!", Toast.LENGTH_LONG).show() >>
Больше об этом плагине и его подключении можно прочитать, перейдя по этой ссылке.
И сразу остановлюсь на опасном моменте при обращении к синтетическим свойствам, определяющим идентификаторы вьюх, с которым столкнулась вся наша команда. Если вы используете include-блоки в ваших убедитесь, что каждый из таких блоков НЕ переопределяет идентификатор корневого layout во включённом layout-файле.
Например, при такой конфигурации вас ждёт неминуемый креш:
import kotlinx.android.synthetic.main.activity_home.* class HomeActivity : AppCompatActivity() < override fun onCreate(savedInstanceState: Bundle?) < setContentView(R.layout.activity_home) randomTextView.text = "Hi!" // will lead to NPE >>
В данном случае исходный id включенного TextView (randomTextView) переопределён и таким образом не может быть найден. Оптимальным решением будет вообще при такой возможности оставить саму include-область без идентификатора, чтобы избежать конфузов.
Kotlin и Data Binding
Если вы используете в проекте DataBinding и не имеете возможности быстро мигрировать на Kotlin Android Extensions, вы можете обнаружить множество ошибок компиляции после перехода на kapt.
Чтобы вернуть поддержку совместимости Data Binding с Котлином, необходимо добавить в список зависимости в build.gradle зависимость компилятора:
kapt 'com.android.databinding:compiler:x.y.z'
X.y.z. здесь определяют текущую версию Gradle: они должны совпадать.
Потенциальные ловушки при общении с SDK, написанными на Java
В то время, как вам неизбежно придётся работать с SDK, написанными на Java (включая, собственно, Android SDK), нужно всегда оставаться на стороже, когда речь идёт о nullable аргументах (по умолчанию в Java), открытых для переопределения методов.
Предположим, вам нужно переопределить onActivityResult в вашем Activity.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent)
Замечаете что-нибудь подозрительное в этом сниппете?
Всегда есть шанс случайно упустить оператор ? после типа аргумента метода — в данном случае Intent, — который допускал бы null-значения. Здесь же, с точки зрения Kotlin-кода, data не может быть null ни при каких обстоятельствах, и, вне зависимости от того, укажете ли вы тип Intent как nullable или нет, вы не получите ни предупреждения, ни ошибки от компилятора, так как оба варианта сигнатуры допустимы. Но поскольку получение не пустой data не гарантировано, так как в случаях с SDK вы не можете это проконтролировать, получение null в данном случае приведёт к NPE. В случае, если вы укажете Intent?-тип, любые попытки вызвать метод такого объекта приведут к ошибке на этапе компиляции без должной проверки на null посредством ?-оператора.
Работаем с лямбдами: Android SDK
Как вы могли заметить в одном из сниппетов кода выше, с функции высшего порядка избавляют нас от большого количества бойлерплейт-кода — в том числе, когда речь идёт о реализации собственных click listenerов. Начать постижение дзен в написании красивых и производительных лямбд (и, к примеру, понять, в каких случаях уместно пренебречь фигурными скобками <> для достижения максимальной лаконичности), можно с этой страницы документации.
В этой статье мы сфокусируемся лишь на паре интересных кейсов, при которых лямбды могут значительно упростить (или наоборот, при неведении — усложнить) жизнь.
Предположим, что мы уже используем Kotlin Android Extensions в проекте. Давайте реализуем простые адаптер в связке с ViewHolder для списка фильмов на основе RecyclerView, каждый элемент в котором кликабелен, и выбор каждого должен быть каким-то образом обработан извне.
class MoviesAdapter( var movies: List, private val itemClick: (Movie) -> Unit ) : RecyclerView.Adapter() < override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieHolder < val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_movie, parent, false) return MoviesAdapter.MovieHolder(itemView) >override fun onBindViewHolder(holder: MovieHolder, position: Int) < val currentMovie = movies[position] holder.setup(currentMovie, itemClick) >override fun getItemCount(): Int = movies.size class MovieHolder( override val containerView: View ) : RecyclerView.ViewHolder(containerView), LayoutContainer < fun setup(movie: Movie, itemClick: (Movie) ->Unit) < with(containerView) < tvTitle = currentMovie.title tvDirector = currentMovie.director tvReleaseDate = currentMovie.releaseDate.asFormatted() setOnClickListener < itemClick.invoke(currentMovie) >> > > >
Вы наверняка заметили (Movie) -> Unit в качестве последнего параметра конструктора. В данном случае Movie выступает в качестве типа получателя, а Unit — в качестве результата выполнения функции и, таким образом, заменяет гипотетический click listener интерфейс, который вам бы пришлось реализовывать, имей вы дело с Java.
Ещё один участок, который мог привлечь ваше внимание, — класс MovieHolder сам по себе. Как видите, он реализует интерфейс LayoutContainer, который преобразовывает ViewHolder класс элемента списка в контейнер и таким образом активирует кеширование для вьюхи элемента (стратегию кеширования вы вполне можете кастомизировать).
Важно: не забудьте переопределить свойство containerView: View в конструкторе вашего ViewHolder.
И, наконец, вы определённо могли заметить выражение с with, которое, на самом деле, представляет из себя вызов одноимённой встроенной функции, позволяющей вам вызывать серию из методов текущего объекта.
Ещё одна ремарка: обратите внимание, что return внутри лямбды возвращает нас не из функции, в которой лямбда вызывается, а из самого лямбда-выражения.
Kotlin vs RxJava(2): And then. Nothing!
Kotlin значительно устраняет многословность RxJava в связке с Java (при условии, что вы не использовали Retrolambda или Jack) и полностью совместим с ней. Однако вселенная — место не самое идеальное, и на ловушки можно наткнуться и в этом тандеме.
Спойлер: всё из-за скобочек <>.
В своё время я стал жертвой печально известной проблемы с andThen оператором, которая подробно описана в статье «Kotlin and Rx2. How I wasted 5 hours because of wrong brackets». Передача лямбды в качестве параметра метода приведёт к разочаровывающему экзистенциальному ничему. Достаточно написать простой тест для выражения вроде
Completable.fromCallable < someRepository.removeData() >.andThen < anotherRepository.removeAnotherData() >.subscribe()
чтобы убедиться в этом воочию: содержимое andThen не выполнится. Всё потому, что пока в случае с большинством операторов вроде flatMap , defer , fromAction и огромного количества других, в качестве их аргументов ожидается действительно лямбда, при такой записи с andThen ожидается Completable/Observable/SingleSource . Проблема решается использованием обыкновенных круглых скобок () вместо фигурных <>.
К моменту публикации статьи этот баг всё ещё живёт застывшим в состоянии «under discussion».
Выводы
Эта статья покрывает лишь небольшой объём распространённых камней преткновения и вопросов, с которыми вы можете столкнуться в процессе перехода с Java на Kotlin. Без достаточного практического опыта он не всегда может оказаться столь же простым, каким кажется со стороны.
Однако, на мой взгляд, он того стоит. Даже решение открыть себя для новый язык программирования, выйти из зоны комфорта Java послужит важной вехой в вашем развитии как Android-разработчика. Так что при любой возможности — плавно, итеративно попробуйте перестроить своё сознание, свой проект на Kotlin. Помните, что все эти маленькие вкрапления боли, описанные в статье, — ничто по сравнению с тем, насколько вам приятно будет писать код на этом языке.
Полезные ссылки и ресурсы
- Посмотрите в сторону Android KTX. Это набор extension-функций Android SDK, которые значительно сэкономят ваше время на написание кода для заполнения Bundle, работы с Shared Preferences, bitmap-изображениями, анимациями, span-ами и многим другим. Помимо официальной документации, я бы порекомендовал ознакомиться с этой статьей-экскурсией по KTX — «Exploring KTX for Android».
- Обращайтесь время от времени к Kotlin Styleguide for Android.
- Книжка «Kotlin in Action» от создателей языка Светланы Исаковой и Дмитрия Жемерова. Вы точно не пожалеете о её прочтении!
Если у вас есть свои истории, связанные с Kotlin или interop между Kotlin и Java, повергшие вас в замешательство, или просто лайфхаки, подобные тем, что я описал в этой статье, вы очень поможете Android-сообществу, поделившись своим горьким опытом в комментариях.
Все про українське ІТ в телеграмі — підписуйтеся на канал DOU
Подобається Сподобалось 0
До обраного В обраному 1
