*Текстът е предоставен от Devexperts, автор: Иван Димитров, Software Developer в Devexperts
Екипът от Android разработчици на Devexperts мигрира по-голямата част от кода си от RxJava към Kotlin Coroutines. В следващите редове може да видите основните причини за миграцията и предимствата на Kotlin Coroutines.
Плюсове и минуси на RxJava
RxJava се използва повече от 10 години за разработването на Android и като цяло има голям принос в областта на асинхронното програмиране. RxJava е стабилна и изпитана технология, на която много компании и разработчици са разчитали за големи проекти. Друго основно предимство на RxJava е съвместимостта ѝ с Java.
RxJava обаче има един голям недостатък: тя е невероятно сложна, особено за разработчици без опит. Представете си, че Вашият екип има двама или трима нови колеги без предишен опит в RxJava и вместо да се заемат направо с писането на код, те трябва да изгубят няколко месеца, просто за да се запознаят с RxJava. Това би било предизвикателство дори за хора, които учат и възприемат информация бързо. Това е и основната причина, поради която нашият екип организира вътрешна дискусия и инициира миграция към Kotlin coroutines.
Накратко за Kotlin Coroutines
Coroutines са начин за извършване на задачи, който може да бъде поставян на пауза и възобновяван по-късно. В Kotlin, coroutines са вградени в езика, за да улеснят асинхронното програмиране. По принцип може да гледате на една Kotlin coroutine като фина нишка, като една нишка може да съдържа множество coroutines.
Основната идея зад coroutines е да дефинирате точки на прекъсване в кода, където coroutine може да постави на пауза изпълнението си и да освободи основната нишка, за да изпълнява други задачи. Това позволява множество coroutines да се изпълняват едновременно, което подобрява оползотворяването на системните ресурси.
За да използвате coroutines, трябва да създадете coroutine scope. След това можете да стартирате coroutines с помощта на функции като launch или async и в отделна coroutine ще започне да се изпълнява фрагмент от код. Вътре в coroutine можете да използвате ключови думи като suspend, за да маркирате функции, които могат да бъдат спирани. След като спряната операция завърши, coroutine се възобновява от мястото, където е прекъснала.
Flows в Kotlin coroutines
Потоците при Kotlin coroutines са тип, който представлява последователност от стойности, които могат да бъдат асинхронно произвеждани и събирани. В общи линии те са reactive streams, използвани във всеки реактивен проект като RxJava и Reactor.
Основната характеристика на flows е, че те са студени (cold). Това означава, че те не започват да произвеждат стойности, докато не бъдат изрично събрани. Когато събирате flow, върху излъчените стойности можете да извършвате операции като филтриране, съпоставяне или преобразуване.
Предимства на Kotlin coroutines + flows за разработчици
Нека разгледаме по-подробно причините, поради които нашият екип реши да мигрира към Kotlin coroutines от гледна точка на техните предимства пред RxJava.
По-опростен API
Голямо предимство на Kotlin coroutines е техният по-прост API. В RxJava първото нещо, което новият разработчик ще види, са много непознати нови типове като Observable, Flowable, Single, Maybe, или Completable. Вместо тези пет нови термина Kotlin coroutines въвежда само два: flow и suspend.
Нека навлезем и по-дълбоко. В RxJava използвахме или Observable, или Flowable, когато искахме да наблюдаваме много събития. От тези два типа само Flowable има изрично управление на backpressure handling. В Kotlin Coroutines можем да заменим Observable и Flowable с flow, който има автоматично управление на backpressure handling. Така че вече не е нужно да мислите как да се справите с backpressure handling – flow прави всичко вместо вас.
Типът Single винаги връща една стойност или грешка. В примера се връща един string или грешка. Но с Kotlin coroutines връщането е просто обикновен string или един обект, който бихте искали да върнете. Единственото нещо, което трябва да направите, е да маркирате функцията си със suspend.
Ако искате да върнете нула или само един обект, трябва да използвате Maybe като тип на връщане. Това е още един изцяло нов тип връщане, който трябва да научите. В това отношение в Kotlin можете да използвате тип, позволяващ стойност null (отбелязан с ?), и целият метод трябва да бъде обозначен със „suspend“. Прави почти същото като RxJava, но по много по-удобен начин.
Поради някаква причина RxJava използва отделен тип Completable за методи като run, които трябва да завършат без конкретен тип връщане. В Kotlin сoroutines можете да маркирате целия метод като suspend и да го използвате като всеки нормален метод, който изпълнява операция и връща void или unit.
Избягване на оператори
След като екипът ни започна да използва Kotlin Coroutines, много от операторите, които използвахме в RxJava, изчезнаха, защото вече не бяха необходими.
Представете си, че има елемент, който трябва да се преобразува в друг тип. Например, можем да използваме функцията map. Ако искате да извършите същата операция само че асинхронно, ще Ви е необходим друг оператор, т.е. имате нужда от два различни оператора за извършването на една операция. В Kotlin можем да използваме само един оператор (map), да го маркираме като suspend и тогава ни интересува дали операцията е синхронна или асинхронна. Ще работи и в двата случая. По същество елиминирахме половината от операторите, необходими за конкретен случай.
Structured concurrency
Друга интересна за разработчиците област е structured concurrency. Нека разгледаме следния кодов фрагмент: имаме състояние от ViewModel, което наблюдаваме в основната нишка. След това се абонираме, обновяваме потребителския интерфейс и после трябва да добавим всичко към compositeDisposable. Преди това обаче трябва да дефинираме compositeDisposable и да го изчистим в подходящ момент, за да избегнем изтичане на памет. Това се случва с RxJava, която не поддържа structured concurrency.
Нека да разгледаме и малко по-реален пример. Представете си, че отваряте приложение, за да проверите какво е времето в момента. След това затваряте приложението, но ако не сте настроили както трябва управлението на паметта на устройството си, приложението ще продължи да търси информация. Ето защо трябва да изчиствате всеки път, когато затваряте фрагмента.
В Kotlin това е уредено по доста по-елегантен начин: има дефинирани scopes за стартиране на coroutine. В същия кодов фрагмент ще има GlobalScope, LifeCycleScope и ViewModelScope. Хубавото е, че всъщност не можете да стартирате coroutine без scope. Но ако имате scope и го затворите, изчистването ще стане автоматично.
Четливост
Представете си сценарий в RxJava, където имаме фрагмент, който получава списък с данни (stocks) от ViewModel. Нашата цел е да се абонираме за io Scheduler, да наблюдаваме основната нишка и да обновим UI със stocks, ако всичко върви гладко. Въпреки това, ако възникне изключение, трябва да обновим UI с грешката. И след като приключим, не трябва да забравяме да изчистим compositeDisposable, за да предотвратим изтичане на памет.
Изглежда объркващо, нали? Особено за онези разработчици, които никога преди не са работили с RxJava.
Когато преминем на Kotlin coroutines и опитаме същата операция с фрагмента, става много по-просто. Получаваме stocks от ViewModel и директно обновяваме UI със stocks, които сме получили.
Нашият код сега изглежда изцяло последователен, без никакви асинхронни операции. Лесно е: вземаме данни, обновяваме UI. Ако има изключение, можем просто да използваме блок try-catch, за да обновим UI без изключението. Блокът try-catch всъщност е най-сложният аспект на този код и е добре познат на всеки с основни познания по програмиране. Така че кодът, който имаме тук, е много по-четлив.
Производителност
Ето как измерихме производителността с помощта на три различни имплементации Scrabble, разработен от José Paumard.
Като отправна точка ще използваме имплементацията Sequence, която представлява най-бързата възможна реализация за този показател. Една операция на показателя Scrabble отнема около 10 ms за операция.
Сравнителният тест на RxJava отне повече от два пъти повече време на операция (24 ms/оп.), което е голяма разлика. За Kotlin coroutines и flows времето за показателя беше около 14 ms: не толкова бързо, колкото базовата последователност, но това е нормално, като се има предвид, че имаше управление на backpressure handling. Въпреки това натоварването е много по-ниско, отколкото при RxJava, и тези 10 ms могат да направят огромна разлика по отношение на производителността на Вашата система.
Cъвместимост
Миграцията от RxJava към Kotlin coroutines е лесна, защото двете технологии са напълно съвместими. Много е лесно RxJava код да се конвертира в Kotlin coroutines и обратно.
Резултати
Към този момент екипът на Android е мигрирал над 80% от своя код с помощта на Kotlin Coroutines. Екипът е доволен от получения код и очаква да продължи миграцията до 100% (или дори 101% 🙂 )