Всем привет! Меня зовут Николай Попов. Сегодня я поделюсь с вами опытом использования одной из самых классных фишек языка 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 {
   binding.imageView.visibility = View.GONE
}

Новая реализация:

// Функции расширения
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 { predecate }

Задание оттенка компоненту, цвета для заднего фона или текста, то же рекомендую выносить в 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<String, View.OnClickListener>, 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 ${e.message}")
       }
   }
   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 = "&#8381".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
}

Конец

Это далеко не все варианты удачного использования таких функций, но напоследок хотел бы дать несколько советов по их использованию:

  1. Не увлекайтесь перенося всю логику в extensions, только необходимую реализацию для данного класса. Чем больше конструкция расширения, тем больше необходимость выделение под нее отдельной сущности.

  2. Не забывайте про такие модификаторы как inline, это позволит повысить производительность.

  3. Структурируйте функции расширения по файлам, внутри одного package. Именуйте файлы в честь класса, который он расширяет, например ViewExt.

Спасибо за внимание!