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 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 йде до вас!
На ваш вибір пропонуються:
- функції вищого порядку (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 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 підкрався — ну як такою можливістю не скористатися? Базовий набір діалогів був такий:
- звичайні інформаційні (з тайтлами і без, з повідомленнями та без);
- з підтверджень(confirmations);
- з одиничним вибором (single-choice);
- з множинним вибором (multi-select);
- з полями для firstname/lastname/nickname.
В результаті склалася типова фабрика на Java, але зовсім не гнучка і зі скріпами;). Візьмемо, приміром, процес створення діалогу в Android:
- створюємо інстанси нашого DialogFragmentкласу
- створюємо Bundleдля аргументів
- наповнюємо його(bundle) значеннями
- відправляємо його(bundle) в діалог
- встановлюємо listeners
- відображаємо діалог
І так кожен раз, коли потрібно показати діалог. Так, це можна і потрібно обернути в метод і це буде виглядати якось так:
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-розробників я б рекомендував свій наступний проект писати саме на ньому, тому що:
- 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;
- ви будете створювати менше коду, щоб отримати результат, і на виході ви отримаєте лаконічний добре читається код;
- ви хочете створювати бізнес-логіку, а не інфраструктуру для неї;
- тепер ви зможете подумати про проблеми/задачі з іншого боку;
- ви ситі по горло класами-помічниками і цими нескінченними статичними методами;
- вам завжди було цікаво, що це за штука така «лямбда», і чому всі так від них стирчать тащаться, але не були впевнені на пуркуа вам це треба;
- ви хочете вивчити щось нове і цікаве, але при цьому бажано, щоб витрачені зусилля того варті;
- ви можете просити підвищення ЗП, так як тепер ви знаєте ще одна мова програмування!
- ви просто....а придумайте-ка за мене ще парочку пунктів;).
Корисні ресурси:
- 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 .
ПИ.СИ. При підготовці статей жоден зелений чоловічок не постраждав.
Опубліковано: 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 — розумний мобільний навігатор та система моніторингу якості доріг