Есть такая часто используемая конструкция биндинга данных в 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() = 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)
qwert_ukg
18.01.2018 16:18По моему, вы перемудрили. Все в кучу. Там вроде и так все просто. А у вас как-то все сложно, при этом даже без дженериков. Еще с возвратом экстеншн лямды.
Вы в следующий раз спросите себя перед тем как писать код: "а нужно ли мне это?", я вот так с корутинами вляпался.Guitariz Автор
18.01.2018 16:51Любой функционал (а тем более такой сложный) требует отличного понимания, зачем оно нужно и почему выгодно использовать именно его. Еще раз, в этой статье (как, в прочем, и ни в какой другой статье) я никого не склоняю к определенному стилю — я показываю возможности языка.
lazard105
19.01.2018 10:30Если цель просто иметь доступ к переменным двух классов, то чем вас не устроил «apply»?
fun Data.bindCell1(view: View) = view.apply{ cell1_icon.setImageResource(icon) cell2_text.setText(titleId) }
Guitariz Автор
19.01.2018 10:31Всем, но тогда пример бы не получился. В аннотации я указал, что статья не стиля программирования ради, а для описания возможностей языка.
OneeL
22.01.2018 15:46Нельзя оценить выразительность языка в полной мере, если рассматривать неудачные примеры. К сожалению, у вас в статье описанные возможности и правда используются не по назначению. Единственного apply было бы достаточно.
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 }
Но опять же, для наглядности написал свой.OneeL
22.01.2018 15:52Почти уверен, что если разобрать реализацию существующих функций, перед этим показав, как их применять, статья будет намного более полезной.
areht
19.01.2018 18:33> cell2_icon.setImageResource(icon)
А только меня смущает, что из кода теперь невозможно понять к какому классу относится «icon», ко view или к data?
Если эта информация просто теряется — это не выразительность, это наоборот, обфускация. И потенциальный источник ошибок.
Guitariz Автор
19.01.2018 19:55В Kotlin, если использовать подобные конструкции, однозначно нужно использовать преффикс (вроде cell2_ в cell2_icon) в именах элементов лайаута. Потому что как раз в них крайне легко накосячить с импортом сгенерированного обращения и получить NullPointer. Одновременно с этим он и позволяет избежать обфучкации конкретно в моем примере.
В целом, в своих проектах объединение нескольких пространств имен я практикую только в заранее безопасных вариантах — model (набор переменных) и view (набор переменных с префиксами), модель и сервис (набор методов), модель логику (набор методов вроде process_ и show_).
Объединение пространств имен, как и множественное наследование, однозначно не является рекомендуемой практикой и должно подкрепляться пониманием программиста, что и зачем он делает. Тот самый случай, когда проще совсем не значит лучше.
Именно мой пример я считаю безопасным в использовании, т.к. префиксы более я нигде не использую, кроме имен отображаемых элементов.
Beholder
Зачем делать свойство-расширение с типом лямбды? Почему не просто метод-расширение?
Guitariz Автор
В этом случае свойство-расширение включает два пространства имен (в моем случае и View, и Data)
Beholder
Ну это почти то же самое что и
Ради чего? Избавиться от явных имён переменных? Зато усложнить само объявление функции. И теперь вместо простого метода у вас в байткоде добавится новый анонимный класс.
Какой-то перебор во всём.
Guitariz Автор
Я в аннотации написал, что данная статья исключительно о языковых возможностях, никак не о стиле программирования.