Om du någonsin har känt att din Android-app fryser när den laddar data, är chansen stor att du kämpar med huvudtråden. Kotlin-koroutiner har varit en livräddare för många utvecklare, vilket gör att vi kan skriva asynkron kod som ser sekventiell och linjär ut , vilket eliminerar de oändliga återuppringningarna som fick koden att likna en trappa.
För att dra nytta av denna superkraft räcker det inte att bara skriva ordet suspend före en föreställning. Det som verkligen är viktigt är att veta var den utförs varje kodbit, och det är där Dispatcherna kommer in i bilden, de agerar som dirigenter för en orkester som bestämmer vilken tråd som ansvarar för varje uppgift.
Vad i helvete är en dispatcher?
I grund och botten ansvarar en dispatcher för att tilldela en coroutine till en specifik tråd eller grupp av trådar . Det är viktigt att komma ihåg att även om coroutines är lätta och icke-blockerande, körs de fortfarande under huven. Att konfigurera rätt dispatcher förhindrar att appen kraschar eller att prestandan försämras kraftigt.
När vi lanserar en coroutine med launchDen ärver normalt sett det sammanhang i vilket den skapades. Men om vi behöver flytta en specifik del av logiken till en annan tråd kan vi uttryckligen ange detta. För att kontrollera vilken tråd vi faktiskt befinner oss i är ett mycket användbart knep att skriva ut Tråd.nuvarandeTråd().namn, vilket gör att vi kan se magin hända i realtid.
Den dynamiska trion: Main, IO och Default
Kotlin erbjuder flera alternativ, men det finns tre som du kommer att använda 99 % av tiden. Först finns Dispatchers.Main , som är UI-tråden. Den bör bara användas för mycket snabba uppgifter, som att uppdatera text på skärmen eller interagera med UI-komponenter. Om du börjar utföra beräkningar här kommer appen att krascha och användaren kommer att bli frustrerad.
Sedan har vi Dispatchers.IO , som är arbetshästen för allt som rör I/O. Den är idealisk för att läsa filer, göra API-förfrågningar eller fråga databasen med Room. Denna dispatcher använder en mycket större trådpool eftersom I/O-uppgifter ofta lägger ner mycket tid på att vänta på ett externt svar utan att förbruka CPU.
Slutligen har vi Dispatchers.Default . Detta är optimerat för CPU-intensiva uppgifter . Om du behöver analysera en massiv JSON-fil, sortera en enorm lista eller utföra komplexa matematiska beräkningar är detta verktyget för dig. Dess trådar matchas vanligtvis mot antalet CPU-kärnor för att undvika överbelastning av systemet.
Det finns också Dispatchers.Unconfined, men sanningen är det nästan oanvänd I verkliga projekt börjar den var den än anropas och kan återupptas i vilken tråd som helst, vilket gör den ganska oförutsägbar i de flesta fall.
Konsten att byta trådar med withContext
Det är här magin händer huvudtrådssäkerhet (huvudsäkerhet). Funktionen withContext Det låter dig pausa den aktuella korutinen, hoppa till en annan avsändare, exekvera kodblocket och, när det är klart, automatiskt återgå till den ursprungliga avsändaren.
Det bästa med det här mönstret är att funktionens avsändare inte behöver oroa sig för vilken tråd den ska anropa den på. En bra vana är att ha själva funktionen pausad... hantera ditt eget sammanhangTill exempel en funktion getDatos() borde ringa internt withContext(Dispatchers.IO)På så sätt kan ViewModel anropa den från huvudtråden utan rädsla för att blockera skärmen.
När det gäller prestanda, withContext Det är extremt effektivt. Det lägger inte till någon extra belastning jämfört med återanrop, och ibland kan Kotlin göra det. optimera trådbyte Om den upptäcker att vi redan är i önskad dispatcher undviker den onödiga hopp.
Kontroll av livscykeln: Omfattningar och jobb

Att starta coroutines slumpmässigt är ett recept för katastrof och minnesläckor. För att undvika detta använder vi CoroutineScopessom definierar uppgiftens livslängd. I Android har vi viewModelScope för ViewModels och lifecycleScope För aktiviteter eller fragment; när komponenten dör, avbryts korutinerna automatiskt.
Varje gång vi använder launch o async, genereras en JobbobjektDet här jobbet är som en biljett som låter oss kontrollera korrutinen: vi kan använda job.join() att vänta på att det ska ta slut eller job.cancel() att stoppa det om resultatet inte längre är av intresse för oss. Detta är grunden för strukturerad samtidighet: barnen är kopplade till fadern och det finns inga föräldralösa processer kvar i minnet.
Parallell exekvering och skillnaden mellan start och asynkron
Ibland vill vi göra flera saker samtidigt för att spara tid. För detta använder vi async, vilket returnerar ett objekt Deferred. Till skillnad från launch (vilket i grunden är en Skjut och glöm.), async Det låter oss hämta ett värde med hjälp av funktionen await().
Var uppmärksam här: att du använder async Det betyder inte automatiskt att det finns parallellism. Om du lanserar två async en Dispatchers.MainDe kommer att exekveras en efter en i samma tråd. För att uppnå en verklig parallellismDu måste tilldela dem Dispatchers.Default o IOså att de kan distribueras över de olika CPU-kärnorna.
För att hantera grupper av parallella uppgifter, byggverktyget coroutineScope Detta är grundläggande. Det säkerställer att funktionen inte avslutas förrän alla underordnade koroutiner har slutfört sitt arbete, och fångar också upp eventuella undantag som kan uppstå längs vägen så att de inte går vilse i limbo.
För att sluta cirkeln är det viktigt att inte överanvända GlobalScope eftersom det är mycket svårt att testa och avbryta. Helst bör man alltid förlita sig på Android-omfattningshierarki och använd rätt koordinator beroende på arbetsbelastningen. Genom att kombinera viewModelScope, withContext för tunga uppgifter och async För samtidighet uppnår vi robusta applikationer som upprätthåller ett flytande gränssnitt samtidigt som de bearbetar data i bakgrunden utan krångel.