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()? Для тих, хто не в темі, вона робить дві речі:

Мене завжди дратувало, що я повинен використовувати для цих двох простих дій сторонній клас і не можу цього зробити за допомогою 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 way smarter
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 йде до вас!

На ваш вибір пропонуються:

Всі вони тісно пов'язані між собою і по суті не можна використовувати 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 of our reference 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 process that 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)параметра !), таким чином спрощується звернення до параметру в лямбда. Якщо останнім параметром у виклику йде функція, то її тіло можна описати поза круглих дужок, а відразу за ними в фігурних. І ось чому це здорово: таким чином можна описувати блоки, що складаються з декількох функцій, і реалізовується це з допомогою 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 підкрався — ну як такою можливістю не скористатися? Базовий набір діалогів був такий:

В результаті склалася типова фабрика на Java, але зовсім не гнучка і зі скріпами;). Візьмемо, приміром, процес створення діалогу в Android:

І так кожен раз, коли потрібно показати діалог. Так, це можна і потрібно обернути в метод і це буде виглядати якось так:

public static 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. Безпосередньо для Android-розробників я б рекомендував свій наступний проект писати саме на ньому, тому що:

Корисні ресурси:

ПИ.СИ. При підготовці статей жоден зелений чоловічок не постраждав.

Опубліковано: 11/04/17 @ 07:00
Розділ Різне

Рекомендуємо:

iOS дайджест #17: Що нового у Swift 3.1, User Notifications, Method Swizzling у Swift
DOU Books: 5 книг, які радить Сергій Бондаренко, СЕО Skywell
DOU Ревізор в Харкові: «Брутальна студія Plarium» + ВІДЕО
Частичный редирект для robots.txt для Nginx
DOU Проектор: Navizor — розумний мобільний навігатор та система моніторингу якості доріг