Расширения (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—2024 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 Extensions для Android проекта
Всем привет! Меня зовут Николай Попов. Сегодня я поделюсь с вами опытом использования одной из самых классных фишек языка Kotlin, а именно — функции расширения или Kotlin Extensions. Эти функции позволяют расширять базовый класс без необходимости наследования или использования шаблонов проектирования, таких как декоратор. Использование функций расширения позволяет избавиться от написания однотипного кода, также позволяет сделать его простым и лаконичным.
Управление видимостью View
В процессе разработки часто приходиться скрывать или показывать view компоненты, этот процесс можно упростить создав функции расширения, которые будут менять параметр visibility.
binding.imageView.visibility = View.VISIBLE
// Функции расширения fun View.show() < visibility = View.VISIBLE >fun View.invisible() < visibility = View.INVISIBLE >fun View.hide() < visibility = View.GONE >// Использование binding.imageView.show() binding.imageView.hide() binding.imageView.invisible()
Также можно сделать функцию, которая будет управлять видимостью по предикату, что упростит вечные if else в проекте.
if (predecate) < binding.imageView.visibility = View.VISIBLE >else
// Функции расширения inline fun View.showIf(condition: View.() -> Boolean) < if (condition()) < show() >else < hide() >> inline fun View.invisibleIf(condition: View.() -> Boolean) < if (condition()) < invisible() >else < show() >> inline fun View.hideIf(condition: View.() -> Boolean) < if (condition()) < hide() >else < show() >> // Использование binding.imageView.showIf < predecate >binding.imageView.hideIf < predecate >binding.imageView.invisibleIf
Задание оттенка компоненту, цвета для заднего фона или текста, то же рекомендую выносить в extensions.
binding.imageView.setBackgroundColor(context?.getColorCompat(R.color.white) binding.textView.setTextColor(context?.getColorCompat(R.color.white)) binding.imageView.imageTintList = ColorStateList.valueOf(activity.resources.getColor(R.color.gray))
// Функции расширения fun View.setBackgroundColorRes(@ColorRes color: Int) = setBackgroundColor(context.getColorCompat(color)) fun TextView.setTextColorRes(@ColorRes color: Int) = setTextColor(context.getColorCompat(color)) fun ImageView.setTint(@ColorRes colorRes: Int) < ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(ContextCompat.getColor(context, colorRes))) // Использование binding.imageView.setBackgroundColorRes(R.color.white) binding.textView.setTextColorRes(R.color.white) binding.imageView.setTint(R.color.white)
Создаем ссылки и переводим ед. измерения в TextView
В рабочей практике столкнулся с необходимостью созданием ссылки в рамках одного textView и кастомного обработчика этой ссылки. Такой функционал использовался часто в проекте, и чтоб не плодить сущности было принято решение вынести реализацию в extensions.
Работает makeLinks просто, сначала мы наполняем компонент текстом, потом обращаемся к функции расширения и передаем участок текста для создания ссылки и лямбду для обработчика.
// Функции расширения fun TextView.makeLinks(vararg links: Pair, colorLink: Int = R.color.orange) < val spannableString = SpannableString(this.text) var startIndexOfLink = -1 for (link in links) < val clickableSpan = object : ClickableSpan() < override fun updateDrawState(textPaint: TextPaint) < textPaint.color = ContextCompat.getColor(context, colorLink) textPaint.isUnderlineText = true >override fun onClick(view: View) < Selection.setSelection((view as TextView).text as Spannable, 0) view.invalidate() link.second.onClick(view) >> startIndexOfLink = this.text.toString().indexOf(link.first, startIndexOfLink + 1) try < spannableString.setSpan( clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) >catch (e: Exception) < println("error setSpan $") > > this.movementMethod = LinkMovementMethod.getInstance() this.setText(spannableString, TextView.BufferType.SPANNABLE) > // Использование binding.txtSendMessage.text = "Отправить код смс" binding.txtSendMessage.makeLinks(Pair("Отправить", View.OnClickListener < notification?.sendMessage() >))
Еще один пример использования функций расширения для TextView. Данная функция позволяет скрыть реализацию подстановки текста из ресурса и перевода формата метры\километры.
// Функции расширения fun TextView?.getDistanceText(distance: Double) < this?.text = when < distance < 0 ->this?.context?.getString(R.string.unknown_distance) distance > 1000 -> this?.context?.getString(R.string.distance_format_km, (distance / 1000.0)) else -> this?.context?.getString(R.string.distance_format_m, distance) > ?: "" > // Использование binding.txtLocationDistance.getDistanceText(5000) "Текущее расстояние до объекта 5.0 км"
Упрощаем методы для которых необходим Context
Контекст внутри проекта может использоваться во многих местах, вот несколько примеров как можно упростить использование контекста.
Показ тостов никогда не был таким простым. // Функции расширения fun Context.showToast(message: String?) < message?.let < Toast.makeText(this, message, Toast.LENGTH_LONG).show() >> // Использование this.showToast("Hello World")
Функция расширения, которая позволяет легко управлять вибрацией смартфона. Например, её можно использовать, если нужна вибрация по нажатию на кнопку.
// Функции расширения fun Context.vibratePhone(length: Long = 200) < val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator if (!vibrator.hasVibrator()) return if (Build.VERSION.SDK_INT >= 26) < vibrator.vibrate(VibrationEffect.createOneShot(length, VibrationEffect.DEFAULT_AMPLITUDE)) >else < vibrator.vibrate(length) >> // Использование context.vibratePhone()
Если ваше приложение требует много разрешений, то для упрощения кода и сокращение проверок, можно использовать следующее расширение, которое будет принимать список разрешений и возвращать истину в случае если разрешения даны.
// Функции расширения fun Context.hasPermissions(vararg permissions: String) = permissions.all < permission ->ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED > // Использование if(this?.hasPermissions(CAMERA, ACCESS_FINE_LOCATION)) < // TODO >
Парсим HTML в String
Следующая функция позволяет быстро декодировать html внутри любой строки. Я такую функцию использую для декодирования символа валюты внутри HTML.
// Функции расширения fun String?.htmlDecode(): Spanned? < return this?.let < if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) Html.fromHtml(it, Html.FROM_HTML_MODE_LEGACY) else HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY) > > // Использование binding.sign.text = "₽".htmlDecode()
Получение теста при редактировании в EditText
Последний пример относится к случаю, когда нужно обрабатывать ввод от пользователя и не использовать обратные вызовы afterTextChanged и beforeTextChanged. Такое часто необходимо при валидации данных от пользователя и последующей реакции на эти данные.
// Функции расширения fun EditText.onChange(textChanged: ((String) -> Unit)) < this.addTextChangedListener(object : TextWatcher < override fun afterTextChanged(s: Editable) <>override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) <> override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) < textChanged.invoke(s.toString()) >>) > // Использование binding.editTextUserNum.onChange < textChanged ->//TODO >
Конец
Это далеко не все варианты удачного использования таких функций, но напоследок хотел бы дать несколько советов по их использованию:
- Не увлекайтесь перенося всю логику в extensions, только необходимую реализацию для данного класса. Чем больше конструкция расширения, тем больше необходимость выделение под нее отдельной сущности.
- Не забывайте про такие модификаторы как inline, это позволит повысить производительность.
- Структурируйте функции расширения по файлам, внутри одного package. Именуйте файлы в честь класса, который он расширяет, например ViewExt.
Спасибо за внимание!
- kotlin
- kotlin lessons
- android
- android development
Настройка среды
Прежде чем приступить к созданию своего первого приложения для работы как на iOS, так и на Android, начните с настройки среды для Kotlin Multiplatform Mobile разработки:

- Если вы собираетесь работать с общим кодом или кодом, специфичным для Android, вы можете работать на любом компьютере с операционной системой, поддерживаемой Android Studio. Если вы также хотите написать код, специфичный для iOS, и запускать приложение на моделируемом или реальном устройстве, используйте Mac с macOS. Работать с iOS в других операционных системах, таких как Microsoft Windows, нельзя. Это связано с требованием Apple.
- Установите Android Studio 4.2 или 2020.3.1 Canary 8 или выше. Вы будете использовать Android Studio для создания своих мультиплатформенных приложений и запуска их на имитируемых или аппаратных устройствах.
- Если вам нужно написать код для iOS и запускать приложения, установите Xcode версии 11.3 или выше. Большую часть времени Xcode будет работать в фоновом режиме. Вы будете использовать его для добавления кода на Swift или Objective-C в свое приложение.
- Убедитесь, что у вас установлен совместимый плагин Kotlin. В Android Studio выберите Tools | Kotlin | Configure Kotlin Plugin Updates и проверьте текущую версию плагина Kotlin. При необходимости обновите до последней Stable версии.
- Установите плагин Kotlin Multiplatform Mobile. В Android Studio выберите Preferences | Plugins, найдите плагин Kotlin Multiplatform Mobile в Marketplace и установите его. Посмотрите Kotlin Multiplatform Mobile plugin release notes.
© 2015—2024 Open Source Community