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.
 
ПЫ.СЫ. При подготовке статей ни один зеленый человечек не пострадал.