Никто не любит повторяемый код. Тем не менее, существуют конструкции, которые прижились и укореннились в программировании довольно давно, не смотря на эту самую повторяемость.
Есть такая часто используемая конструкция биндинга данных в android:

fun bindCell1(view: View, data: Data) {
    view.cell1_text.setText(data.titleId)
    view.cell1_icon.setImageResource(data.icon)
}

Очевидный метод, у которого есть одна очень досаждающая мне неряшливость — каждый раз необходимо указывать ссылки view. и data. Каждая строка содержит 10 символов, которые очевидны.

И у Kotlin есть способ обойти данную неряшливость — экстеншны (Extensions). Более подробно о них можно почитать здесь.

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

Реализуем метод как расширение для класса данных.
Преобразуем нашу конструкцию в

fun Data.bindCell1(view: View) {
    view.cell1_icon.setImageResource(icon)
    view.cell2_text.setText(titleId)
}

Как так получается? метод bindSome теперь не сам по себе, а является расширением для класса data. Получается, что это метод ведет себя, как метод самого класса Data. Существует одно ограничение — protected и private сущности в расширениях не видны — что логично, так как в действительности экстеншн не прописан в самом классе. Однако, комбинируя internal и public свойства, можно получать достаточно безопасные комбинации. Соответственно и обращаться теперь можно напрямую к свойствам самого Data-экземпляра.

Теперь попробуем избавиться от префикса view.. для этого создадим неизменяемое свойство

val Data.bindMethod_cell_2: View.() -> Unit
    get() = {
        cell2_icon.setImageResource(icon)
        cell2_text.setText(titleId)
    }

Как же так?


Теперь свойство bindMethod является расширением для класса MediaData, и при этом же по типу данных — ресширение для View!

И что же дальше?


А дальше мы можем вызывать эту конструкцию как обычный метод, при этом передавая View в качестве аргумента!

data.bindMethod(view)

А если пойдем еще дальше, то сможем передавать View.()->Unit в качестве аргумента.

Что нам это дает?


Например, мы можем не типизировать объект RecyclerView от слова вообще, передавая в него только ID лайаута и полученную функцию биндинга. В самом начале, функция bindSome( view:View, data:Data) была строго типизирована, теперь же мы вообще не никак от этого типа данных не зависим. — тип данных (View.()->Unit) привязан только ко View.

А пересечение пространств имен?


Бывает, когда имена свойств внутри View и Data совпадают. Чисто технически все это просто обходится (в имени лайаутов можно добавить префикс), но можно и пойти по простому пути:

val Data.bindMethod_cell_1: View.() -> Unit
    get() = {
        this.cell1_icon.setImageResource(this@bindMethod_cell_1.icon)
        this.cell1_text.setText(this@bindMethod_cell_1.titleId)
    }

Разве что конструкция вышла длиннее.

А как же аргументы?


Если у bindMethod присутствуют аргументы, при вызове этого метода в качестве первого аргумента передастся объект View, после — остальные аргументы, как мы обычно и вызываем.

val Data.bindMethod: (View.(Int, String)->Unit) get() = { intValue, str ->
    view.numText.text = str.replace("%s", intValue.toString())
}

//--------------------------------------

data.bindMethod.invoke(view, 0, "str%s")


Данный метод позволит позволит нам собрать все методы биндинга в одном месте, и делать, например вот так:

Пример разделения на отдельные документы
class Data( val name:String, val icon:String)
//-----------------------------------
// DataExtensions.kt

fun Data.carAdapter() = Pair<Int, View.()->Unit>(
	R.layout.layout_car_cell, {
        carcell_title.text = name
        carcell_icon.setImage(icon)
    })

fun Data.motoAdapter() = Pair<Int, View.()->Unit>(
	R.layout.layout_moto_cell, {
        moto_icon.setImage(icon)
    })

Обратите внимание, carAdapter и motoAdapter не лежат внутри класса Data. они могут находиться вообще где угодно — хочешь, в экстеншны выноси, хочешь, вместе с классом оставляй. Вызывать их можно откуда угодно, экстеншны импортируются, как классы.

Материалы, используемые в статье, я скомпилировал в небольшой проект

Комментарии (13)


  1. Beholder
    18.01.2018 12:44

    Зачем делать свойство-расширение с типом лямбды? Почему не просто метод-расширение?


    1. Guitariz Автор
      18.01.2018 12:48

      В этом случае свойство-расширение включает два пространства имен (в моем случае и View, и Data)

      val Data.bindMethod_cell_2: View.() -> Unit
          get() = {
              //cell2_icon принадлежит View, icon принадлежит Data, а функция обращается к обоим свойствам без префиксов view. и data.
              cell2_icon.setImageResource(icon)
              cell2_text.setText(titleId)
          }


      1. Beholder
        18.01.2018 13:32

        Ну это почти то же самое что и

        fun Data.bindMethod_cell_2(view: View) {
            view.cell2_icon.setImageResource(icon)
            view.cell2_text.setText(titleId)
        }

        Ради чего? Избавиться от явных имён переменных? Зато усложнить само объявление функции. И теперь вместо простого метода у вас в байткоде добавится новый анонимный класс.

        Какой-то перебор во всём.


        1. Guitariz Автор
          18.01.2018 13:52

          Я в аннотации написал, что данная статья исключительно о языковых возможностях, никак не о стиле программирования.


  1. qwert_ukg
    18.01.2018 16:18

    По моему, вы перемудрили. Все в кучу. Там вроде и так все просто. А у вас как-то все сложно, при этом даже без дженериков. Еще с возвратом экстеншн лямды.
    Вы в следующий раз спросите себя перед тем как писать код: "а нужно ли мне это?", я вот так с корутинами вляпался.


    1. Guitariz Автор
      18.01.2018 16:51

      Любой функционал (а тем более такой сложный) требует отличного понимания, зачем оно нужно и почему выгодно использовать именно его. Еще раз, в этой статье (как, в прочем, и ни в какой другой статье) я никого не склоняю к определенному стилю — я показываю возможности языка.


  1. lazard105
    19.01.2018 10:30

    Если цель просто иметь доступ к переменным двух классов, то чем вас не устроил «apply»?

    fun Data.bindCell1(view: View) = view.apply{
        cell1_icon.setImageResource(icon)
        cell2_text.setText(titleId)
    }


    1. Guitariz Автор
      19.01.2018 10:31

      Всем, но тогда пример бы не получился. В аннотации я указал, что статья не стиля программирования ради, а для описания возможностей языка.


      1. OneeL
        22.01.2018 15:46

        Нельзя оценить выразительность языка в полной мере, если рассматривать неудачные примеры. К сожалению, у вас в статье описанные возможности и правда используются не по назначению. Единственного apply было бы достаточно.


        1. Guitariz Автор
          22.01.2018 15:50

          На самом деле apply внутри состоит из схожего метода.

          @kotlin.internal.InlineOnly
          public inline fun <T> T.apply(block: T.() -> Unit): T {
              contract {
                  callsInPlace(block, InvocationKind.EXACTLY_ONCE)
              }
              block()
              return this
          }
          

          Но опять же, для наглядности написал свой.


          1. OneeL
            22.01.2018 15:52

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


  1. areht
    19.01.2018 18:33

    > cell2_icon.setImageResource(icon)

    А только меня смущает, что из кода теперь невозможно понять к какому классу относится «icon», ко view или к data?

    Если эта информация просто теряется — это не выразительность, это наоборот, обфускация. И потенциальный источник ошибок.


  1. Guitariz Автор
    19.01.2018 19:55

    В Kotlin, если использовать подобные конструкции, однозначно нужно использовать преффикс (вроде cell2_ в cell2_icon) в именах элементов лайаута. Потому что как раз в них крайне легко накосячить с импортом сгенерированного обращения и получить NullPointer. Одновременно с этим он и позволяет избежать обфучкации конкретно в моем примере.
    В целом, в своих проектах объединение нескольких пространств имен я практикую только в заранее безопасных вариантах — model (набор переменных) и view (набор переменных с префиксами), модель и сервис (набор методов), модель логику (набор методов вроде process_ и show_).
    Объединение пространств имен, как и множественное наследование, однозначно не является рекомендуемой практикой и должно подкрепляться пониманием программиста, что и зачем он делает. Тот самый случай, когда проще совсем не значит лучше.
    Именно мой пример я считаю безопасным в использовании, т.к. префиксы более я нигде не использую, кроме имен отображаемых элементов.