Încă de la începutul apariției sistemului de operare Android, cele mai bune practici privind designul aplicațiilor și arhitecturile acestora au fost determinate exclusiv de comunitate, aceasta având un rol decisiv asupra modului în care dezvoltatorii și-au construit roadmapul către produsul finit. Putem observa cu ușurință o creștere foarte mare a comunității Android, care a condus inevitabil la dezvoltarea ecosistemului care s-a creat în jurul frameworkului.
Cu toate acestea, modul în care s-a realizat dezvoltarea aplicațiilor pentru platforma Android a evoluat lent. Au fost introduse noi cadre și au existat dezbateri permanente în acest sens, dar comunitatea a evitat să își asume impunerea unor seturi de reguli și standarde cu privire la modul în care ar trebui proiectate și dezvoltate aplicațiile. Este bine cunoscut faptul că de-a lungul anilor, ideologia celor de la Google în calitatea lor de companie, a fost să creeze produse de top, inovatoare, care să rezolve anumite probleme ale societății în care trăim, cu accent pe satisfacția utilizatorilor, fără să ofere întotdeauna îndrumarea necesară asupra direcției pe care ar trebui să o urmărească dezvoltatorii, lucrurile fiind lăsate să evolueze de la sine.
Nevoia existenței unui set de reguli care să facă dezvoltarea aplicațiilor mai ușoară, i-a împins pe cei de la Google să își schimbe filozofia și să găsească soluții pentru rezolvarea unor probleme comune cu care se confruntă dezvoltatorii. Contextul actual a permis ca în anul 2017, Google să introducă conceptul de Android Architecture Components (componente de arhitectură în Android), reprezentat de un set de librării ghidate de principiul separării diferitelor aspecte ale funcționalității (engl. SoC principle - Separation of Concerns), care să permită proiectarea și dezvoltarea unor aplicații modulare, codul fiind mai ușor de testat și de întreținut.
Acest concept a fost repede adoptat de comunitate, iar în ultimii doi ani, au fost rezolvate numeroase probleme introducându-se, totodată, funcționalități noi. Pentru a elimina codul redundant și pentru a simplifica complexitatea unor procese, dar și pentru a păstra compatibilitatea între diferitele versiuni de dispozitive și sisteme de operare existente, Google a decis să reorganizeze componentele de arhitectură și librăriile deja existente, într-o nouă colecție, numită Android Jetpack.
Android Jetpack înglobează un set de componente, instrumente și soluții arhitecturale bine definite, care au ca scop accelerarea procesului de dezvoltare, reprezentând o actualizare majoră asupra modului în care se realiza dezvoltarea aplicațiilor mobile până la momentul respectiv. Fiecare dintre componentele regăsite în cadrul acestui concept arhitectural, a fost proiectată astfel încât să poată fi folosită independent, nefiind condiționată de utilizarea celorlalte componente sau de o anumită versiune a sistemului de operare, având astfel un grad foarte mare de flexibilitate și adaptabilitate.
Pornind de la introducerea componentelor de arhitectură și continuând cu Jetpack, cei de la Google au făcut un pas important în stabilirea unor reguli și principii asupra direcțiilor pe care fiecare dezvoltator trebuie să le urmeze. Având la bază aceste principii și aplicabilitatea lor, putem observa că Jetpack accelerează anumite etape din ciclul de viață al unui produs, precum dezvoltarea sau întreținerea codului. În cadrul echipelor și al comunităților de Android, procesle de internship sau onboarding au devenit facile, persoanele noi putând să dobândească mai ușor o viziune amplă asupra culturii din mediul respectiv, și asupra a ceea ce a devenit dezvoltarea aplicațiilor Android în zilele noastre.
Figura 1. Android Jeptack
După cum se poate observa în Figura 1, Android Jetpack este divizat în patru mari concepte:
Architecture. În cadrul acestuia regăsim componente care ne permit manipularea ciclului de viață al aplicațiilor, control avansat pentru optimizarea taskurilor, dar și pentru manipularea și persistența datelor;
Foundation. Acesta conține componente de arhitectură folosite pentru a asigura compatibilitatea dintre diferite versiuni ale sistemului de operare, testare, dar și pentru a oferi suport limbajului de programare Kotlin și a asigura interoperabilitatea acestuia cu Java;
Behavior. El oferă contextul pentru integrarea unor servicii Android, precum notificări și permisiuni de sistem, media sau capabilități de sharing;
UI. Acesta grupează componentele utilizate pentru dezvoltarea interfeței cu utilizatorul, precum Fragment sau Layout, dar și widgeturi, emoji sau alte extensii ale acestora pentru aplicații pentru Auto, TV sau wearables.
Navigarea între componentele vizuale ale unei aplicații Android a reprezentat întotdeauna o dificultate, fie că era vorba de legături multiple și complexe între modulele unei aplicații, fie că era vorba de gestionarea corectă a succesiunii pe stiva de activități. Cu scopul de a rezolva aceste probleme, a fost introdusă componenta de arhitectură Navigation, care vine în ajutorul dezvoltatorilor, oferindu-le un cadru vizual complex și manevrabilitate ridicată în ceea ce privește tranziția și transmiterea datelor între componente.
Pentru a facilita o navigare consistentă și predictibilă a utilizatorilor finali, la baza adoptării componentei Navigation, stă o serie de principii care oferă un cadru extins asupra direcției care ar trebui urmate de către dezvoltatori, printre care regăsim:
O aplicație Android trebuie să aibă un singur punct de intrare, independent. Acest punct de intrare poate fi reprezentat de prima componentă vizuală afișată utilizatorului atunci când aplicația este lansată;
Stiva de navigare trebuie să aibă la bază o structură de tip LIFO (engl. Last In First Out - Ultimul intrat, primul ieșit), unde la baza stivei regăsim punctul inițial de intrare, iar în vârful acesteia regăsim componenta vizuală afișată utilizatorului la momentul curent;
Stiva de navigare trebuie să fie consistentă în toate situațiile, chiar și atunci când aplicația a fost lansată prin intermediul unor surse externe (deeplinkuri, notificări sau alte aplicații), dar și atunci când aceasta a fost lansată prin acțiune explicită din partea utilizatorului;
În cadrul Navigation, regăsim două concepte noi și anume cel de destinație (engl. destination) și acțiune (engl. action). O destinație reprezintă fiecare loc în care utilizatorul poate ajunge navigând prin aplicație. Mai multe destinații pot fi interconectate prin intermediul unor acțiuni. În general, o destinație este reprezentată de o componentă vizuală, fie ea activitate sau fragment.
Puse cap la cap, aceste elemente dau naștere unei structuri de graf orientat în care nodurile sunt reprezentate de destinații, iar drumurile dintre noduri de către acțiuni. Acest graf poartă numele de graf de navigare (engl. navigation graph) și poate fi manipulat utilizând editorul de navigare (engl. Navigation Editor), introdus în Android Studio 3.2 Canary.
Figura 2. Graf de navigare cu trei destinații Interconectate
În Figura 2 se poate observa un exemplu de dispunere vizuală a unui graf de navigare care conține trei destinații, interconectate prin acțiuni. Pe lângă vizualizare, ne este permis să manipulăm tranziția între anumite stări ale grafului, să adăugăm deeplink-uri în funcție de care aplicația să reacționeze, dar și să manipulăm argumentele care se doresc a fi trimise de la o destinație la alta. Toate aceste îmbunătățiri fac ca navigarea în cadrul unei aplicații Android să se realizeze mult mai ușor, eliminând codul redundant, având ca rezultat final o experiență plăcută a utilizatorilor finali.
O problemă des întâlnită în cadrul aplicațiilor Android este reprezentată de solicitarea și încărcarea cantităților mari de date pe ecran, aceasta putând avea un impact negativ asupra performanței. Soluțiile folosite inițial: secvențierea și consumarea unor cantități mai mici de date de la sursă, utilizarea unor liste specializate în cadrul aplicațiilor client (precum RecyclerView) și încărcarea graduală a datelor pe ecran manipulând capabilitățile de scroll ale acestora, s-au dovedit dificile pentru menținerea eficienței. Mai mult, deoarece logica care stă în spatele interacțiunii cu utilizatorul și a manipulării datelor devine foarte complicată, aceasta tinde să conducă pe termen lung la apariția problemelor de depanare și testare.
Componenta de arhitectură Paging vine ca o rezolvare pentru a prelua și a consuma eficient cantități convenabile de date, din surse diferite, economisind resursele de sistem și lățimea de bandă. Ideea centrală a acestei componente este de a manipula datele în structuri, numite pagini și de a le afișa cât mai rapid utilizatorului.
Cele mai importante concepte în jurul cărora a fost proiectată această componentă, sunt:
DataSource - responsabilă pentru preluarea și consumarea datelor de la o anumită sursă;
PagedList - care reprezintă o structură de date folosită pentru încărcarea datelor gradual în pagini;
Fiind proiectată având la bază design patternul Observer și cu aplicabilitate directă cu alte concepte de interes și actualitate precum Room, LiveData și RxJava, componenta Paging, abstractizează comunicarea dintre anumite substraturi ale aplicației, făcând codul testabil și ușor de întreținut. S-a reușit standardizarea și eficientizarea procesului de preluare a datelor de la anumite surse și prezentarea lor utilizatorului, într-o manieră secvențială, cu scopul de a îmbunătăți experiența acestuia.
O direcție foarte importantă care ar trebui urmată și care stă la baza unei funcționări optime a oricărei aplicații mobile, este reprezentată de execuția asincronă în fundal, a anumitor taskuri de lungă durată, dereferențiate de firul principal de execuție responsabil de interfața grafică și de interacțiunea cu utilizatorul (engl. main thread - UI thread). Necesarul decuplării acțiunii propriu-zise de firul principal e dat de obligativitatea păstrării unui nivel de execuție cât mai scăzut pe acesta, pentru a nu periclita experiența utilizatorului. În caz contrar, dacă main threadul este încărcat cu diferite operații costisitoare, poate conduce la performanțe slabe și ulterior la blocarea aplicației.
Pentru operațiuni care nu afectează direct interfața cu utilizatorul și care ar putea deveni costisitoare în materie de timp, se recomandă folosirea unor fire de execuție separate. De-a lungul timpului, au fost introduse diferite mecanisme responsabile atât de realizarea în fundal a anumitor activități (ex: JobSchedulers, Services, Loaders, AlarmManager), cât și de eficientizarea consumului resurselor de sistem (ex: Doze mode, App standby, Background services limitation).
Componenta de arhitectură WorkManager a fost introdusă ca o soluție optimă pentru rezolvarea proceselor care trebuie executate cert, dar care pot fi amânate până la satisfacerea anumitor condiții.
Cele mai importante caracteristici care se regăsesc în cadrul componentei WorkManager, sunt:
A fost dezvoltată în strânsă legătură cu restricțiile puse de sistem pentru activitățile care au loc în fundal;
Oferă compatibilitate cu dispozitivele care au integrate Google Play Services sau nu. Pentru dispozitivele care nu au integrate aceste servicii, folosește capabilitățile interne ale componentelor AlarmManager și BroadcastReceiver;
Acționează ca un intermediar între componentele folosite pentru execuția anumitor operații în fundal, deja existente, precum JobScheduler sau Firebase JobDispathcer, și versiunea sistemului de operare;
Oferă suport extins pentru execuția unor calcule complexe, secvențial sau în paralel, care pot fi constrânse de anumite condiții;
Oferă un API public care permite interogarea constantă a stării unei anumite activități și a progresului curent;
Un obiect de tip WorkManager poate să execute sarcinile pentru care a fost proiectat, o singură dată sau periodic. Aceste două modalități de execuție sunt abstractizate folosind clasele OneTimeWorkRequest sau PeriodicWorkRequest. Mai multe obiecte de tip WorkRequest pot fi dispuse în paralel, astfel încât timpii de execuție a sarcinilor pe care trebuie să le ducă la bun sfârșit, poate fi redus exponențial. Dacă se dorește o execuție secvențială, atunci acestea pot fi înlănțuite, rezultatele obținute anterior putând fi pasate ca date de intrare pentru următorul obiect de tip WorkRequest din lanț. În continuare, am prezentat un simplu exemplu, scris în Kotlin, prin care mai multe obiecte de tip WorkRequest pot fi construite și trimise către execuție în paralel sau secvențial.
val request1 = OneTimeWorkRequest.Builder(DownloadImageWorker::class.java).build()
val request2 = OneTimeWorkRequest.Builder(ProcessImageWorker::class.java).build()
val request3 = OneTimeWorkRequest.Builder(DownloadImageWorker::class.java).build()
val request4 = OneTimeWorkRequest.Builder(ProcessImageWorker::class.java).build()
WorkManager.getInstance().beginWith(request1, request2).then(request3).then(request4).enqueue()
Figura 3. Exemplu de execuție în paralel și secvențială a unor sarcini de lucru, folosind WorkManager
Flexibilitatea și adaptabilitatea componentei WorkManager a permis ca anumite sarcini complexe, precum descărcarea unor fișiere de dimensiuni mari, procesarea imaginilor sau sincronizarea datelor în mod offline, să se poată realiza facil. Aceste sarcini pot fi combinate după bunul plac, astfel încât timpii de procesare să devină optimi, iar utilizatorul final să nu aibă de suferit în proces.
Putem spune că odată cu introducerea Jetpack și adoptarea lui rapidă de către comunitate, s-a realizat un pas foarte important pentru validarea și valorificarea ideologiei care stă la baza succesului pe care îl are frameworkul Android. Deși au fost introduse seturi de reguli și standarde noi cu privire la modul în care ar trebui proiectate și dezvoltate aplicațiile, acestea erau necesare, deoarece au standardizat procesele și au schimbat concepția dezvoltatorilor, deschizând noi orizonturi, care în trecut nu erau posibile.
Chiar dacă la început dezvoltatorii sunt reticenți cu privire la adoptarea tehnologiilor din cadrul Jetpack, iar dezvoltarea aplicațiilor poate părea greoaie, putem afirma că această trecere va reprezenta o etapă benefică în cariera oricărui programator. Experimentarea graduală a fiecăreia dintre componentele introduse, și nu numai, va permite dobândirea unor cunoștințe noi și va accelera procesul de dezvoltare, prin reducerea complexității și a dimensiunii codului, având un impact direct asupra utilizatorului final.
https://developer.android.com/jetpack/
https://www.raywenderlich.com/5376-introduction-to-android-jetpack
https://android-developers.googleblog.com/2018/05/android-studio-3-2-canary.html
https://www.i-programmer.info/news/193-android/11792-jetpack-and-android-studio-32-not-much-new.html
https://www.raywenderlich.com/6040-workmanager-tutorial-for-android-getting-started