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-ой версий, и 8-ка только на полпути к нам Восьмерочку завезли и это здорово, но все-таки она не такая вкусная как Kotlin, IMHO;
  • 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;
  • вы будете создавать меньше кода, чтобы получить результат, и на выходе вы получите лаконичный хорошо читаемый код;
  • вы хотите создавать бизнес-логику, а не инфраструктуру для нее;
  • теперь вы сможете подумать о проблеме/задаче с другой стороны;
  • вы сыты по горло классами-помощниками и этими бесконечными статическими методами;
  • вам всегда было интересно, что это за штука такая «лямбда», и почему все так от них торчат тащатся, но не были уверены на пуркуа вам это надо;
  • вы хотите выучить что-то новое и интересное, но при этом желательно, чтобы потраченные усилия того стоили;
  • вы можете просить повышения ЗП, так как теперь вы знаете еще один язык программирования!
  • вы просто....а придумайте-ка за меня еще парочку пунктов;).

Полезные ресурсы:

ПЫ.СЫ. При подготовке статей ни один зеленый человечек не пострадал.

Похожие статьи:
Летом прошлого года американская продуктовая компания Globality открыла в Киеве R&D-центр. Перед тем как это сделать, она изучала...
[Об авторе: Руслан Дмитракович — разработчик ПО и предприниматель, в ИТ-индустрии с 1994 года. Пионер интернет-рекламы...
Голова «фінкомітету» Верховної Ради Данило Гетманцев відповів на відкритий лист-звернення представників малого...
Леон Воловик, ныне СТО в финансовой компании NexPay, переехал из Украины в Австралию шесть лет назад. Выбрал Сидней...
В выпуске: новые выпуски JavaScript Air, интервью с создателем CSS и материалы по React Native, Meteor и Angular 2, а также новые Vue.js,...
Яндекс.Метрика