Java vs. Kotlin для Android. День 3: Android высшего порядка
Ну что же, настало время погрузиться в самые интересные разделы документации. Базовый синтаксис, и не только, был озвучен в предыдущей статье, а сейчас настало время пройтись по «функционалу». В свое жалкое оправдание могу сказать, что до встречи с Kotlin не особенно следил за трендом Functional Programming (FP).
Анонимные функции с удовольствием использовал в JS, но это было слишком давно, и к Android-разработке, к сожалению, не применимо. У разработчиков сервер-сайда есть выбор, например, Scala или Groovy, а у Android-разработчиков — его нет. Да, знаю о том, что в Java 8 появились первые наработки по FP, но это все применимо больше к сервер-сайду и под Android этого еще очень долго не будет в нормальном виде! (Ах, как хорошо-то строка ложилась пока Гугль не решил нативно поддерживать Java 8 в Android). Но это не означает, что Kotlin потерял свою актуальность, так ведь?! ;)
Поэтому как-то не было необходимости в это дело погружаться. Да, мне стыдно, нельзя так надолго отставать от тренда, обещаю, что такого больше не повторится (вот уже по горячим следам восстанавливаю свои познания в Python и JS (в него с 2011 года не глядел)). Итак, от слов к делу...
Functions
Когда постоянно варишься в объектно-ориентированном коде, забываешь о тех удобствах, которые предоставляет FP, а о многих можешь просто и не знать! Взять к примеру Local Functions. Бывали у вас случаи, когда вы сделали Extract Method куска кода, но совершенно не хочется его выносить в отдельный метод класса, потому что он используется только в одном каком-то специфическом месте, но несколько раз? Меня всегда это расстраивает/раздражает, ведь в том же Pascal(простите мне мою навязчивую ностальгическую сентиментальность) можно было объявлять локальные (nested) функции/процедуры. И вот, пожалуйста, Kotlin предлагает такую возможность — круто! Более того, локальные функции имеют доступ к переменным объявленным во внешнем контексте. Дважды круто!
Как давно вы использовали TextUtils.isEmpty()? Для тех, кто не в теме, она делает две вещи:
- проверяет переданную строку на null
- проверяет длину строки
Меня всегда раздражало, что я должен использовать для этих двух простых действий сторонний класс и не могу этого сделать с помощью myStringVariable.isEmpty(). Ведь я получу NPE, если в myStringVariable будет null.
Все эти лишние телодвижения не делают код красивее. Как эта проблема решается в Kotlin? Очень просто — Extension Functions, с помощью них можно расширять уже существующие классы без необходимости наследования от них. Да, да, можно взять любой класс из Android SDK (и не только) и расширить его своим набором методов. Вот это крутотень! Я не слишком часто говорю слово «круто»? Я сразу бы пошел расширять класс String и добавлять в него правильный метод isEmpty(), вот только он уже там есть (называется isNullOrEmpty()) и работает как надо, а реализация null safety в Kotlin защитит нас от NPE.
Разберемся, как это дело работает. К примеру, вы всегда хотели знать какой длины окружность получилась бы для произвольного целого числа будь оно радиусом этой окружности. В Java-мире я бы создал класс Circle, к примеру, и метод в нем getLength(), или статический класс с отдельным методом. Некрасиво и громоздко. А теперь посмотрим, как это правильно делается в мире Kotlin:
fun Int?.circleLength() = if (this != null) 2 * this * PI else 0; println(10.circleLength()) // prints 62.83185307179586 println(null.circleLength()) // prints 0
Не хотите заморачиваться с nullable stuff — убираете ? и проверки на null. Красиво, просто, быстро. Или, например, всегда хотели быстро посчитать кол-во пробелов в строке не прибегая к ухищрениям? Опять опоздали — такая функция уже есть String.count({lambda}).
А как насчет функций, которые синтаксически выглядят как операции? Тоже не проблема, нужно в описание функции в начало добавить ключевое слово infix.
infix fun String.mix() = {...} "Hello " mix "World" "Hello ".mix("World")
А вот пример из повседневной жизни на Android:
//this is how we inflate layout to view LayoutInflater inflater = LayoutInflater.from(getContext()); view = inflater.inflate(R.layout.item_user, container, false); //and this is how we can do it in a smarter way fun ViewGroup.inflate(layoutId: Int, attachToRoot: Boolean = false): View { return LayoutInflater.from(context).inflate(layoutId, this, attachToRoot) } view = viewGroup.inflate(R.layout.item_user, false)
И эту функцию можно использовать во всем проекте! Добавим к этому, что функции можно просто описывать в файле и не обязательно они должны быть внутри какого-то класса. Скажем «До свидания» *Utils-классам — теперь все вспомогательные функции можно хранить вместе! Хотя это уже больше дело вкуса.
Even More Functions
Вернемся в са-а-а-мое начало моего повествования. Кажется я там писал нуднейшую функцию обхода массива с выборкой нужных элементов, удовлетворяющих какому-то условию. Да, это задачка совершенно тривиальная, и такие вот задачи приходится делать достаточно часто, в результате чего классы толстеют, читабельность кода снижается, и вообще тратится куча лишнего времени на создание структуры вместо бизнес-логики. Создаются helper-классы с однотипными методами, где разница лишь в условии выборки. Если вам надоело писать однотипный ugly код, тогда Kotlin идет к вам!
На ваш выбор предлагаются:
- функции высшего порядка (higher-order functions(HOF))
- анонимные функции (anonymous functions)
- лямбды (lambdas)
Все они тесно связаны между собой и по сути нельзя использовать HOF не используя lambdas или anonymous functions.
Для тех, кто не знаком с матчастью: HOF позволяют принимать другие функции в качестве параметров или возвращать функции как результат работы. JS-разработчики пользуются этими вещами практически каждый день, когда делают AJAX-запрос, и в качестве success/error колбеков передают анонимные функции. В Python это тоже реализовано с незапамятных времен. Это удобно и просто. Теперь и под Андроид можно так делать!
А если у вас уже есть готовая функция и вы хотите ее передать в качестве параметра, это тоже запросто можно сделать, поставив ‘::’ перед ее именем. Более того, в Kotlin 1.1 добавили возможность передачи методов экземпляра класса таким же образом! Разберем простой пример, посмотрим, как это работает. Задача следующая: есть список строк, нужно из них выбрать только те, длина которых четная. Нам тут поможет уже знакомая с прошлой статьи функция расширения — isEven().
fun Int?.isEven() : Boolean = this?.rem(2) == 0 //in Kotlin 1.1 mod got deprecated and you need to use rem instead //just a dummy func used for an experiment fun check(str : String) : Boolean = str.length.isEven() //Java-style implementation, just to show how you can supply a function into a function! OMG, what am I saying?!!! //@param validator - it’s a function that takes String param and returns Boolean value fun sortOutStrings(list : List<String>, validator : (String) -> Boolean) : List<String> { val result = arrayListOf<String>() for (item in list) if (validator(item)) result.add(item) return result } //sending reference of our check() function inside sortOutStrings() println(sortOutStrings(listOf("A", "AB", "ABC"), ::check)) //prints [AB]
Подобным образом работает функция filter для Collections — принимает на вход функцию/лямбду, результатом которой будет Boolean, чтобы выбрать искомые данные и вернуть коллекцию с ними.
На самом деле данный пример выглядит все еще громоздко, попробуем упростить с помощью анонимной функции и лямбды:
//anonymous function println(sortOutStrings(listOf("A", "AB", "ABC"), fun(str) = str.length.isEven())) //prints [AB] //lambda with the chain of other functions that process the result of each other println(listOf("abc", "ab", "a").filter{!it.length.isEven()}.sortBy{it}.map{it.toUpperCase})//prints [A, ABC] //lambdas println(sortOutStrings(listOf("A", "AB", "ABC"), {it.length.isEven()})) //prints [AB] println(sortOutStrings(listOf("A", "AB", "ABC")){it.length.isEven()}) //prints [AB]
Последние две строчки особо интересны — по сути это одна и та же структура, вот только синтаксис немного отличается. it — неявное имя единственного параметра (а в Kotlin 1.1 в лямбдах можно делать деструктуризацию (destructure) параметра!), таким образом упрощается обращение к параметру в лямбде. Eсли последним параметром в вызове идет функция, то ее тело можно описать вне круглых скобок, а сразу за ними в фигурных. И вот почему это здорово: таким образом можно описывать блоки, состоящие из нескольких функций, и реализовывается это с помощью function literal with a specified receiver object. С одной стороны это очень похоже на функции расширения, потому что мы можем вызывать методы этого объекта без каких-либо дополнительных qualifiers, только теперь еще можно передать набор методов этого объекта, которые мы хотим вызвать. Возьмем пример сверху с view inflation и изменим его, заточив под использование с TextView:
inline fun textview(parent: ViewGroup, setup: TextView.() -> Unit) : TextView { val view = TextView(parent.context) view.setup() parent.addView(view) return view }
Функция textview() принимает последним параметром лямбду setup с явно указанным типом объекта-приемника (receiver object) — TextView. Это можно использовать вот так:
textview(container) { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) setText(R.string.app_name)// OR text = getString(R.string.app_name) setTextColor(Color.RED) textSize = 12f; setOnClickListener { Toast.makeText(context, "That's Me!", Toast.LENGTH_LONG).show() } }
Т.е. весь код, который находится внутри скобок будет выполнен в том месте, где происходит вызов view.setup(). Симпатично, не правда ли? Думаю, вы уже догадались, где можно и нужно использовать данный подход, правильно — в билдерах & древовидных структурах.
В описании функции textview() присутствует ключевое слово inline. Оно говорит компилятору о том, что код данной функции (т.е. ее тело) нужно вставить непосредственно в место вызова этой функции в чистом виде. Таким образом мы экономим память (т.к. не создается еще одна абстракция), не теряем в быстродействии, НО немного растет количество кода на выходе. Об этом не нужно забывать и по возможности использовать inline максимально и с пользой. К примеру, extension functions для базовых типов реализованы именно так.
Совсем недавно у нас на проекте пришлось рефакторить код, который отвечал за создание, отображение и колбеки диалогов. Вернее как, рефакторить — этого кода вообще не было, а большинство диалогов были сами по себе, хотя по сути выполняли одну и ту же задачу: получали какой-то инпут от пользователя и результат работы возвращали в receiver — вот только каждый делал это по-своему. А тут как раз technical debt подкрался — ну как такой возможностью не воспользоваться? Базовый набор диалогов был такой:
- обычные информационные (с тайтлами и без, с сообщениями и без);
- с подтверждениями(confirmations);
- с единичным выбором (single-choice);
- с множественным выбором (multi-select);
- с полями для firstname/lastname/nickname.
В итоге получилась типичная фабрика на Java, но совершенно не гибкая и со скрепами;). Возьмем, к примеру, процесс создания диалога в Android:
- создаем инстанс нашего DialogFragment класса
- создаем Bundle для аргументов
- наполняем его(bundle) значениями
- отправляем его(bundle) в диалог
- устанавливаем listeners
- отображаем диалог
И так каждый раз, когда нужно показать диалог. Да, это можно и нужно обернуть в метод и это будет выглядеть как-то так:
static public void showSimpleDialog(FragmentManager fManager, String title, String message) { MyDialog dialog = new MyDialog(); Bundle args = new Bundle(); args.putString(ARG_TITLE, title); args.putString(ARG_MESSAGE, message); dialog.setArguments(args); ...//some other dialog methods calls, i.e. callbacks for the buttons dialog.show(fManager, MyDialog.class.getSimpleName()); }
Но есть тут одно НО: как только у нас меняется кол-во аргументов, передаваемых в диалог (дизайнеры ж захотят для некоторых из них, к примеру, кастомные текстовки для кнопок влепить), приходится либо рефакторить этот метод, либо создавать новый. А если еще набор этих аргументов может варьироваться, а именно так у нас на проекте и было, то становится совсем грустно. А теперь посмотрим, как можно решить подобную задачу на Kotlin в первом приближении:
//inline function with receiver object to wrap dialog creation and showing inline fun mydialog(fragManager: FragmentManager, args: Bundle, init: MyDialog.() -> Unit) : Unit { val dialog = MyDialog(); dialog.arguments = args dialog.init() dialog.show(fragManager, dialog.javaClass.simpleName) } //inline function with receiver object that helps to wrap Bundle initialisation inline fun bundle(init: Bundle.() -> Unit) : Bundle { val result = Bundle() result.init() return result } //this is how we create and show our dialog mydialog(fragmentManager, bundle { putString(ARG_TITLE, "Hello?") putString(ARG_MESSAGE, "Is There Anybody In There?") //here we may put as many params as we want … }) { ...//some other dialog methods calls, i.e. callbacks for the buttons }
Выглядит фантастически не так ли? Фантастически просто, практично и красиво. И не нужно писать монстро-классы, ведь все можно решить двумя функциями. Если вас зацепило так же как и меня, то советую посмотреть в документации отдельный раздел по Type-Safe Builders, там приведен отличнейший пример построения HTML-разметки, используя подход function with receiver object — настоятельно рекомендую к ознакомлению, фанатам Groovy он точно придется по вкусу.
Something borrowed, Something new in Kotlin 1.1
К набору функций для работы с коллекциями добавился такой замечательный метод как groupingBy(), который позволяет выполнять группировки коллекций и потом их обрабатывать. Такая штука может запросто пригодиться, если вы не хотите (ну да, просто лениво) хранить данные с сервера в локальной базе, но нужно их группировать и делать какие-то агрегации.
Еще добавились такие функции общего назначения как takeIf()/takeUnless(). takeif() подобна filter(), но работает с единичным значением, а не с коллекцией. Она проверяет получателя на соответствие условию, и если все ОК, то возвращает его, иначе — null. А функция takeUnless() работает наоборот. В сочетании с elvis-оператором, можно делать подобные конструкции:
val dummyVal = listOf("a", "ab", "abc", "abcd").get(2).takeIf { it.length.isEven() } ?: "Sorry!" println(dummyVal)//prints “Sorry!”
И не забываем, что в такие функции можно запросто передать ссылку на функцию:
//very useful extension function ;) fun String.containsC() : Boolean = this.contains("C", true) //and here we send this function into takeIf() val dummyVal = listOf("a", "ab", "abc", "abcd").get(2).takeIf (String::containsC) ?: "Sorry!" println(dummyVal)//prints “abc”
Выше я уже не удержался и упомянул про деструктуризацию в лямбдах, что само по себе уже здорово. Но в них еще добавили возможность пропуска ненужных параметров с помощью подчеркивания «_».
//this is our distracted data class data class DistractedDataClass(val id:Int, val text:String, val weight:Int) val threshold = 6; val dummyList = listOf(DistractedDataClass(0, "Hello", 1), DistractedDataClass(1, "World", 2)) val goodList = dummyList.filter { (_, txt, w) -> txt.length + w > threshold} if (goodList.size > 0) println("${goodList.get(0).toString()}")//prints “DistractedDataClass(id=1, text=World, weight=2)”
Coming back to DAY 1
А теперь вернемся к первому абзацу из «DAY 1» и попробуем переработать Java-решение в красивое Kotlin-решение. Я приведу маленький кусочек кода, и его будет достаточно для сравнения:
//Java code public class Message { private String text; private boolean isRead; private String author; public Message(String text, boolean isRead, String author) { this.text = text; this.isRead = isRead; this.author = author; } public String getText() {return text;} public boolean isRead() {return isRead;} public String getAuthor() {return author;} } // public class MessageHelper { //getting read messages static public List<Message> getReadMessages(List<Message> messages) { List<Message> result = new ArrayList<>(); for(Message message : messages) { if (message.isRead()) { result.add(message); } } return result; } //getting messages by specific author static public List<Message> getMessagesByAuthor(List<Message> messages, String author) { List<Message> result = new ArrayList<>(); for(Message message : messages) { if (!TextUtils.isEmpty(message.getAuthor()) && message.getAuthor().equals(author)) { result.add(message); } } return result; } } //usage MessageHelper.getReadMessages(messages); MessageHelper.getMessagesByAuthor(messages, "Roger"); ------ // Kotlin code data class Message(val text: String, val isRead: Boolean, val author: String?) //usage messages.filter { it.isRead } messages.filter { it.author == "Roger" }
Лично мне больше нечего добавить, кроме как сделать
Заключение
Kotlin — отличная альтернатива Java как для сервер-сайд разработки, так и для Android. Непосредственно для Андроид-разработчиков я бы рекомендовал свой следующий проект писать именно на нем потому что:
Java под Android все еще хватается за соломинки 6/7-ой версий, иВосьмерочку завезли и это здорово, но все-таки она не такая вкусная как Kotlin, IMHO;8-ка только на полпути к нам- Kotlin на 100% совместим с Java: можно перемешивать код Java и Kotlin вместе, использовать классы Kotlin в Java-коде и наоборот + опять-таки Kotlin 1.1 поддерживает Java 8;
- ООП в Kotlin содержит дофига вкусняшек, которые я описал в первой статье (+ в версии 1.1 там еще добавили массу всего);
- Null safety реализована на уровне системы типов;
- вы сможете использовать приемы функционального программирования (higher-order functions, lambdas, anonymous functions, function references) практически без потери производительности (во всяком случае нам так говорят JetBrains) И, как я понимаю, теперь не нужно использовать RetroLambda;
- Kotlin умеет в умные преобразования типов(smart casts);
- Kotlin умеет в функции расширения(extension functions);
- можно использовать Android extension plugin — теперь вам не нужен ButterKnife;
- вы будете создавать меньше кода, чтобы получить результат, и на выходе вы получите лаконичный хорошо читаемый код;
- вы хотите создавать бизнес-логику, а не инфраструктуру для нее;
- теперь вы сможете подумать о проблеме/задаче с другой стороны;
- вы сыты по горло классами-помощниками и этими бесконечными статическими методами;
- вам всегда было интересно, что это за штука такая «лямбда», и почему все так от них
торчаттащатся, но не были уверены на пуркуа вам это надо; - вы хотите выучить что-то новое и интересное, но при этом желательно, чтобы потраченные усилия того стоили;
- вы можете просить повышения ЗП, так как теперь вы знаете еще один язык программирования!
- вы просто....а придумайте-ка за меня еще парочку пунктов;).
Полезные ресурсы:
- Kotlin reference.
- Kotlin online.
- IDEA & Kotlin.
- Getting started with Android and Kotlin.
- Walktrough tutorials.
- Kotlin for Android Developers.
- Android Development with Kotlin — Jake Wharton.
- Android Coroutines.
ПЫ.СЫ. При подготовке статей ни один зеленый человечек не пострадал.