Om du någonsin har känt att din Android-app fryser eller slutar svara när du laddar ner något, kämpar du förmodligen med huvudtråden. I mobilutvecklingsvärlden är det den heliga graalen att hålla din app igång smidigt medan du utför krävande uppgifter, och det är där Kotlin-koroutiner kommer in . De låter oss i huvudsak skriva asynkron kod utan att fastna i oändliga återanrop.
Tänk dig koroutiner som lätta trådar ; de skapar inte en ny tråd i operativsystemet varje gång, utan utnyttjar istället grupper av befintliga trådar och ett mycket smart schemaläggningssystem. Detta gör dem otroligt effektiva, och koden läses nästan sekventiellt, trots att en hel dans av pauser och återupptagningar pågår bakom kulisserna så att användaren inte märker någon fördröjning på skärmen.
Hemligheten med avstängning: avstängning och återupptagande
Till skillnad från vanliga funktioner introducerar coroutines två nyckelbegrepp. För det första finns det `suspend` , som låter dig pausa körningen av en uppgift medan du sparar dess nuvarande tillstånd. För det andra finns det `resume` , som återupptar körningen exakt där den slutade. Intressant nog blockerar inte en pausad funktion tråden den körs på; den kliver helt enkelt åt sidan för att låta andra uppgifter fortsätta medan den väntar på ett svar, till exempel från en databas.
Det är avgörande att förstå att användning av nyckelordet `suspend` inte automatiskt flyttar koden till en sekundär tråd. För att uppnå main- safety måste vi kombinera dessa funktioner med lämpliga dispatchers, vilket säkerställer att eventuella resurskrävande operationer inte stryper användargränssnittet.
Dispatchers: Var sker magin?
För att tala om för Kotlin vilken tråd som ska användas för att exekvera varje kodstycke använder vi Dispatchers . Alla Dispatchers tjänar inte samma syfte, så du behöver veta hur du väljer rätt:
- Dispatchers.MainDet här verktyget ansvarar för att interagera med användargränssnittet. Det bör endast användas för snabba visuella ändringar eller för att interagera med LiveData, eftersom allt som är komplicerat här kommer att resultera i ett fel. Applikationen svarar inte (ANR).
- Dispatchers.IODen är optimerad för in-/utdatauppgifter. Det är den ideala platsen för API-anrop, läsa filer från disken eller hantera rumsdatabasen.
- Dispatchers.DefaultUtformad för CPU-intensivt arbete, till exempel när du behöver bearbeta en gigantisk JSON eller sortera en enorm lista med saker.
En mycket vanlig och rekommenderad teknik är att använda `withContext` . Den här funktionen låter oss ändra dispatchern inom en pausad funktion utan att behöva återanrop. På så sätt behöver utvecklaren som anropar funktionen inte oroa sig för vilken tråd den är på, eftersom funktionen i sig säkerställer sin egen säkerhet genom att internt växla till IO- eller Default-tråden.
Starta kontra asynkron: Vilken ska man välja?
När vi vill starta en coroutine har vi två huvudsakliga vägar beroende på om vi behöver att uppgiften ska returnera något eller inte.
Operatorn `launch` är vad vi kallar "fire and forget". Den används för uppgifter som inte returnerar ett resultat, till exempel att uppdatera en färg på skärmen eller spara data i cachen. Den returnerar ett `Job`- objekt , vilket i huvudsak är en fjärrkontroll som låter oss avbryta korrutinen om vi inte längre behöver den.
Å andra sidan används `async` när vi behöver ett returvärde. Denna operator returnerar ett `Deferred<T>` , vilket är som ett löfte om att det kommer att finnas ett resultat i framtiden. För att få detta värde måste vi anropa funktionen `await()` , som pausar exekveringen tills resultatet är klart. En styrka med `async` är att den tillåter parallella anrop ; om vi gör tre `async`-förfrågningar kommer alla tre att gå till servern samtidigt, vilket optimerar den totala väntetiden.
Strukturerad samtidighets- och livscykelhantering
För att förhindra att coroutines fortsätter att köras efter att användaren har stängt skärmen (vilket skulle orsaka minnesläckor) använder Kotlin structured concurrency . Allt detta hanteras via CoroutineScope , som spårar alla coroutines som startas inom den.
I Android är det vanligast att använda fördefinierade omfång som `viewModelScope` eller `lifecycleScope` . Dessa är utmärkta eftersom de automatiskt avbryter allt väntande arbete när `ViewModel` förstörs eller `Activity` avslutas. Om du behöver skapa ditt eget omfång kan du kombinera ett `Job` och en `Dispatcher`, men kom alltid ihåg att anropa ` scope.cancel()` i din klass rensningsmetod.
Hanteringsfel och specialfall
Undantagshanteringen varierar beroende på vilken konstruktor du använder. I `launch` , om ett fel uppstår och inte upptäcks, stängs applikationen omedelbart. I `async` lagras dock undantaget i `Deferred`-objektet och kommer inte att utlösas förrän ` await()` anropas , vilket ibland kan göra att fel går obemärkt förbi om du inte är försiktig.
För att hantera detta naturligt är det bäst att linda in anropen i try-catch -block , vilket gör att asynkron kod känns lika enkel som synkron kod. Dessutom, om vi vill att en korutin ska vara immun mot annullering av sin förälder, kan vi använda `NonCancellable` med `withContext`, även om detta bör göras mycket noggrant för att undvika att bryta samtidighetshierarkin.
Att integrera allt detta med bibliotek som Retrofit är enkelt, eftersom moderna versioner har nativt stöd för suspenderade funktioner. I slutändan är det ideala arbetsflödet att ViewModel startar coroutinen, Repository hanterar affärslogiken och Data Source gör begäran till Dispatchers.IO , med alltid ett flytande gränssnitt och ren kod.
Att använda coroutines transformerar Android-utveckling och ersätter komplexiteten hos traditionella trådar med ett effektivt upphängningssystem och livscykelbundna omfång. Genom att förstå skillnaden mellan den snabba utlösningen av "launch" och den parallella kraften hos "async" med "await", tillsammans med rätt val av dispatchers, kan du skapa robust programvara som maximerar enhetsresurser utan att kompromissa med användarupplevelsen. Dela den här guiden så att andra användare kan lära sig om den.