Kotlin Coroutines в Android

Работата с паралелността на Android винаги е била предизвикателство. Въпреки това, с все по-популярното приемане на Kotlin за разработка на Android, възникват нови възможности за писане на асинхронен код, като съпрограмми.

В тази публикация в блога, след кратко представяне на това какво представляват съпрограммите и как работят вътрешно, ще разгледаме няколко практически примера за използване на съпрограмми за писане на лесен за четене асинхронен код за Android.

Корутини в Kotlin

Концепцията за съпрограмми е въведена във версия 1.1 на Kotlin. В този момент те все още са експериментална функция на езика и е много вероятно вътрешната им реализация да се промени.

Корутините по същество са лека алтернатива на нишките. Те позволяват изпълнението на асинхронен код по същия начин, по който обикновено изпълнявате синхронен. Това елиминира необходимостта да се справяте със сложен и подробен синтаксис при писане на паралелен код, което е толкова типично при работа с приложения в Java и други езици за програмиране.

Съпрограмите на езика Kotlin са внедрени на най-ниското възможно ниво, което позволява на други библиотеки да надграждат предоставените от езика API. В същото време езикови конструкции на съпрограма от по-високо ниво, като шаблон async и await, се предоставят от библиотеката на съпрограма на Kotlin.

Съществуващите реализации са налични под формата на библиотеки, базирани на RxJava, CompletableFuture, NIO, Swing, Android и т.н. Всички те могат да бъдат намерени в библиотеката kotlinx.coroutines (страница GitHub).

Вътрешни корутини

Съпрограммите в Kotlin обикновено са много леки и начинът, по който се компилират, не зависи от операционната система. По същество всяка спираща функция се трансформира от компилатора в държавна машина със състояния, представляващи спиращи повиквания.

Точно преди извикване на спиране, следващото състояние се съхранява в поле на клас, генериран от компилатора, заедно със съответния контекст. Възобновяването от спиране възстановява запазеното състояние и машината на състоянието продължава от точката след извикването за спиране. Спряна съпрограма може също да се предава като обект от тип Continuation, съдържащ нейното състояние и локален контекст.

Това позволява стартирането на стотици хиляди съпрограми по едно и също време, за разлика от ограниченията за работа с нишки, които са доста скъпи за сравнение. Популярен пример показва код, стартиращ 100 000съпрограми и стартиращ същия брой нишки. Примерът за съпрограма обикновено би работил добре, докато стартирането на такъв голям брой нишки най-вероятно ще доведе до срив на приложението ви с грешка OutOfMemory.

Корутини в Android

Активирането на съпрограммите на Kotlin в Android включва само няколко прости стъпки. Първо, добавете зависимостта на съпрограмата на библиотеката на Android във вашия файл на проекта build.gradle.

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5"

Тъй като съпрограммите все още се считат за експериментална функция в текущата версия на Kotlin (версия 1.2.30 към момента на писане), ще трябва да добавите следния ред към вашия gradle.properties, в противен случай ще видите предупреждение в изхода на компилатора.

kotlin.coroutines=enable

В следващите раздели ще разгледаме писането на нашите собствени функции за спиране, обработката на резултатите от спирането на функциите в нишката на потребителския интерфейс, обработката на изключения в съпрограмите и ограничаването на изпълнението на съпрограмата до жизнения цикъл на компонентите на Android.

Вашата първа функция за спиране

Функцията за спиране е просто обикновена функция на Kotlin с допълнителен модификатор suspend, който показва, че функцията може да спре изпълнението на съпрограма. Функциите за спиране могат да извикват всякакви други редовни функции, но за да спре действително изпълнението, това трябва да е друга функция за спиране.

От друга страна, функция за спиране не може да бъде извикана от обикновена функция, поради което са осигурени няколко така наречени конструктори на съпрограмми, които позволяват извикване на функция за спиране от обикновен обхват без спиране.

Библиотеката на съпрограмми Kotlin предоставя няколко конструктора на съпрограмми:

  • runBlocking: стартира нова съпрограма и блокира текущата нишка до завършването й,
  • launch: стартира нова съпрограма и връща препратка към нея като Job обект, който няма резултатна стойност, свързана с него,
  • async: стартира нова съпрограма и връща препратка към нея като Deferred<T> обект, който позволява връщане на стойност от нея (също съхранява изключение). Трябва да се използва заедно с функцията за спиране await, която чака резултата, без да блокира нишката.

Нека демонстрираме част от теорията, която научихме досега, на прост пример от реалния свят. Следващият откъс от код води до това, че докосването на FAB трябва да промени текста на екрана след определено забавяне, без да блокира нишката, в която работи, докато чака.

И така, какво се случи тук? В OnClickListener използваме конструктора на launch сърутина и предаваме UI от CoroutineDispatcher тип като параметър към него. Параметърът UI гарантира, че блокът от код се изпълнява в основната нишка.

Вътре в блока launch ние извикваме нашата функция за спиране setTextAfterDelay, която е дефинирана по-долу. В нашата спираща функция се извиква друга спираща функция delay, която забавя съпрограмата, без да блокира нишка. След като изтече определеното време, функцията продължава изпълнението си и задава текста на TextView.

Обработка на резултатите в основната нишка

Тъй като Android позволява достъп до йерархията на изгледи само от оригиналната нишка, която ги е създала (обикновено основната нишка), съпрограмите идват с опция за указване на нишката, върху която да се изпълнява, чрез предаване на CoroutineDispatcher на създател на съпрограма.

Библиотеката на сърутинна програма Kotlin предоставя няколко диспечера, напр. CommonPool (което ограничава изпълнението на съпрограма до набор от фонови нишки) или newSingleThreadContext (което създава нов контекст на съпрограма с една нишка). Ако не е посочено нито едно, се използва DefaultDispatcher (равно е на CommonPool, но може да се промени в бъдеще, така че се препоръчва винаги да се посочва изрично).

Вече знаем UI CoroutineDispatcher, осигурен за изпълнение на съпрограмен код в основната нишка на Android от предишния пример.

Нека да разгледаме следната част от кода, която демонстрира изпълнение на мрежово повикване във фонова нишка и показване на резултата в основната нишка, използвайки прост синтаксис на съпрограма.

Същото като в предишния пример, ние използваме launch конструктор на съпрограмма с UI CoroutineDispatcher, за да стартираме сърутина. Вътре в него се извиква async стартер заедно с await функцията за спиране в очакване на очаквания резултат от мрежовото повикване.

Кодовият блок async извиква обикновена функция downloadDataBlocking, която, както подсказва името, изтегля JSON низ от мрежата по блокиращ начин. Това обаче е добре, защото ограничаваме блока async до споделен пул от фонови нишки, като му предаваме CommonPool като параметър. По този начин основната нишка не се блокира по време на изпълнение на мрежовото повикване.

Върнатият резултат от мрежовото повикване незабавно се предава на TextView в главната нишка.

Обработка на изключения

Предишният пример прави обработката на мрежови повиквания лесна и ни позволява да пишем код по последователен начин, без да е необходимо да мислим за нишки твърде много.

Това обаче е просто идеален случай. Какво ще се случи, ако възникне грешка по време на изпълнението? Както при всяко необработено изключение, приложението обикновено се срива.

Следният примерен код демонстрира как съпрограмите улесняват обработката на изключения. Както при традиционния неконкурентен код, простото опаковане на повикванията в блок try/catch върши работата.

Обвиване на обратно извикване

Обработката на асинхронно изпълнение на код обикновено включва внедряване на някакъв вид механизми за обратно извикване. Например, с асинхронно мрежово повикване, вероятно искате да имате onSuccess и onFailure обратни извиквания, така че да можете да обработвате двата случая по подходящ начин.

Такъв код често може да стане доста сложен и труден за четене. За щастие съпрограммите осигуряват начин за обвиване на обратните извиквания, за да скриете сложността на обработката на асинхронния код далеч от повикващия.

Нека да разгледаме пример за асинхронно мрежово повикване с обратно извикване за обработка на резултата. Ще обвием обратното извикване в съпрограма и значително ще опростим мрежовото повикване.

Както в предишните примери, започваме нашата сърутина с launch, преминавайки UI CoroutineDispatcher. Вътре извикваме нашата спираща функция downloadDataAsync, която сме дефинирали няколко реда по-долу.

Забележете, че нашата функция за спиране връща резултата от suspendCoroutine функция за спиране, предоставена от библиотеката на съпрограмата. Той улавя текущия екземпляр Continuation и спира текущо изпълняваната съпрограма.

Обектът Continuation предоставя две функции, с които можем да възобновим изпълнението на съпрограмата. Извикването на resume възобновява изпълнението на съпрограмата и връща стойност, докато resumeWithException хвърля отново изключението веднага след последната точка на спиране.

Отмяна на сърутина

Както при всяка многонишкова концепция, жизненият цикъл на компонентите на Android може да се превърне в проблем. Трябва да спрем всички потенциално дълготрайни фонови задачи, когато дейност (или фрагмент) достигне определени събития от жизнения цикъл (обикновено onStop или onDestroy), за да предотвратим изтичане на памет или сривове, когато резултатът стане достъпен, след като дейността е била унищожена.

За да разрешат това, съпрограмите предоставят прост механизъм за отмяна. Когато се стартира нова съпрограма с помощта на конструктор на съпрограма, се връща екземпляр на Job обект, представляващ работещата съпрограма.

За да отмените съпрограма, просто извикайте метода cancel на обекта Job, върнат от конструктора на съпрограма. Наличен е и join метод, който спира изпълнението на съпрограмата, докато отменената задача не приключи. За удобство е предоставен и cancelAndJoin метод, комбиниращ двата.

Анулирането на сърутина е кооперативно. Това означава, че функцията за спиране трябва да си сътрудничи, за да поддържа анулирането. На практика функцията за спиране трябва периодично да тества свойството isActive, което е зададено на false, когато съпрограмата е отменена.

Това обаче се отнася за вашите функции за спиране. Всички функции за спиране, осигурени от библиотеката за съпрограми на Kotlin, вече поддържат анулиране.

Следващата част от кода показва пример за неблокиращ брояч, който увеличава стойността си всяка секунда. Броячът може да бъде отменен чрез докосване на FAB.

Заключение

Съпрограмите ни позволяват да пишем паралелен код по традиционен императивен начин, без да се налага да се тревожим твърде много за нишки, обработка на обратни извиквания или изключения.

Научаването на основните концепции, за да започнете да използвате съпрограмми, е много лесно в сравнение с други често използвани библиотеки като RxJava. От друга страна, ако вече познавате RxJava и го използвате широко във вашата кодова база, вероятно няма да видите причина да преминете към съпрограмми, тъй като ще загубите финия контрол върху нишките и огромната колекция от функционални оператори, които RxJava предлага.

Като цяло съпрограммите изглеждат като хубав и прост подход за писане на асинхронен код на Android, особено за разработчици, които не са запознати с RxJava или не искат да използват RxJava или подобна библиотека в своите проекти. Ако не сте ги пробвали, опитайте ги и ми кажете в коментарите какво мислите.

В тази статия ние само надраскахме повърхността на това какво представляват корутините и как да ги използвате. За по-задълбочена информация вижте „документ с описание на неформална сърутина“.