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

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

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

Похожие статьи:
Весь липень ІТ-спільнота обговорювала гроші та Дія City. Ви не залишили нам вибору: у цьому випуску подкасту ми говоримо саме на ці теми...
Ever since the UK voted to exit the EU, there has been great volatility in the value of the pound and the euro. The markets don’t like uncertainty and there has been nothing but this since the referendum vote. Once the UK finally leaves the EU,...
Не всі українські інтернет-провайдери можуть забезпечити фіксовану мережу під час вимкнень світла. У такому разі користувачі...
Если вы хотите научится оценивать проекты, используя agile подходы, а также перенять опыт сертифицированного скрам мастера,...
Українська ІТ-компанія N-iX, яка об’єднує понад 1800 фахівців в Україні та за кордоном, відкриває новий офіс розробки. Улітку...
Яндекс.Метрика