Выразительный Kotlin. Extensions

habr.png

Никто не любит повторяемый код. Тем не менее, существуют конструкции, которые прижились и укореннились в программировании довольно давно, не смотря на эту самую повторяемость.
Есть такая часто используемая конструкция биндинга данных в 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() = PairUnit>(
	R.layout.layout_car_cell, {
        carcell_title.text = name
        carcell_icon.setImage(icon)
    })

fun Data.motoAdapter() = PairUnit>(
	R.layout.layout_moto_cell, {
        moto_icon.setImage(icon)
    })


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

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

© Habrahabr.ru