Выразительный Kotlin. Extensions
Никто не любит повторяемый код. Тем не менее, существуют конструкции, которые прижились и укореннились в программировании довольно давно, не смотря на эту самую повторяемость.
Есть такая часто используемая конструкция биндинга данных в android:
fun bindCell1(view: View, data: Data) {
view.cell1_text.setText(data.titleId)
view.cell1_icon.setImageResource(data.icon)
}
Очевидный метод, у которого есть одна очень досаждающая мне неряшливость — каждый раз необходимо указывать ссылки view. и data. Каждая строка содержит 10 символов, которые очевидны.
И у Kotlin есть способ обойти данную неряшливость — экстеншны (Extensions). Более подробно о них можно почитать здесь.
Реализуем метод как расширение для класса данных.
Преобразуем нашу конструкцию в
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. они могут находиться вообще где угодно — хочешь, в экстеншны выноси, хочешь, вместе с классом оставляй. Вызывать их можно откуда угодно, экстеншны импортируются, как классы.
Материалы, используемые в статье, я скомпилировал в небольшой проект