SPECIALIZAREA CALCULATOARE ȘI TEHNOLOGIA INFORMAȚIEI [307495]

UNIVERSITATEA BUCUREȘTI

FACULTATEA DE MATEMATICĂ ȘI INFORMATICĂ

SPECIALIZAREA CALCULATOARE ȘI TEHNOLOGIA INFORMAȚIEI

LUCRARE DE LICENȚĂ

Dezvoltarea unei aplicații sociale pentru sistemul de operare Android

BUCUREȘTI

Iunie 2017

[anonimizat], oferă comunității sale o cale de a [anonimizat] a impune limite sau a [anonimizat]. [anonimizat] a [anonimizat] a o rula sunt o versiune adecvată a sistemului de operare și o conexiune stabilă la Internet.

Am folosit o [anonimizat] a dezvolta o [anonimizat]. Cele două componente ale arhitecturii comunică între ele prin intermediul cererilor HTTP. Partea de backend se află pe un server virtual privat ce rulează pe o [anonimizat], iar proiectul de frontend.

[anonimizat], [anonimizat] a cererilor, oferă și un sistem de securitate inedit ce este format din proceduri complexe de criptare a datelor și un serviciu de autentificare rapid și sigur.

[anonimizat], iar acest lucru este suficient pentru a inspira un sentiment de securitate și încredere față de aplicație. [anonimizat] a [anonimizat].

[anonimizat], după care va selecta acele părți tehnice ce reprezintă implementări proprii ale unor idei sau arhitecturi ce aduc un plus de valoare aplicației în vederea accentuării avantajelor pe care aceasta le oferă și punând în evidență motivele pentru care ea reprezintă candidat: [anonimizat], [anonimizat] a [anonimizat]. [anonimizat], and the only requirements for running it are an adequate version of the Android operating system and a stable connection to the Internet.

In order to develop a scalable, fast and secured application, I have used a client-[anonimizat]. The backend resides on a Cloud dedicate machine inside a [anonimizat].

The server project uses a [anonimizat], which, [anonimizat]ting of both strong data encryption procedures and fast, but reliable authentication service.

On the other hand, the client project uses the Android platform, created and maintained by Google, and this fact by itself offers a great sense of security and trust towards the application. It uses components that were newly introduced in the recent version of the operating system and have received backwards compatibility support, thus allowing owners of older mobile phone to use the application and have the same user experience as the owners of newer devices.

This paper will go in depth about the modern technologies and frameworks used to develop the application, and afterwards, it will specifically select the technical parts which represent my implementation of ideas or architectures that add value to the application highlighting the advantages it offers and the reasons why it is an ideal choice as a social platform for any user around the world.

Legendă

Secțiune de cod ce poate fi rulată într-un mediu corespunzător.

Într-un mediu ce poate rula SQL:

CREATE TABLE users (

id integer NOT NULL PRIMARY KEY,

email varchar(100) NOT NULL UNIQUE,

first_name varchar(30),

last_name varchar(30),

password text NOT NULL,

);

Într-un mediu ce poate rula Python:

def hello_world(request):

return Response('Hello %(name)s!' % request.matchdict)

Într-un mediu ce poate rula Java:

class HelloWorldApp {

public static void main(String[] args) {

System.out.println("Hello World!");

}

}

Referințe către obiecte sau date din cod.

Se face referire la tabela users ce conține colona email care are constrângerile de UNIQUE si NOT NULL.

Se face referire la funcția hello_world precedată de cuvântul cheie def.

Se face referire la clasa HelloWorldApp care conține o metodă statică numită main.

Atenționări

Sfaturi

1. Introducere

1.1. Contextul lucrării

Internetul a cunoscut o dezvoltare copleșitoare de la începutul acestui secol, iar odata cu acesta, a crescut și dorința oamenilor de a socializa, de a accesa diverse informații si de a fi la curent cu evenimentele din jurul lumii. Acest lucru trebuie să fie posibil în orice moment, indiferent de locația în care se află utilizatorul sau activitatea pe care o desfășoară. Din acest motiv, aplicațiile ce pot fi rulate pe un sistem de operare mobil au devenit foarte populare, datorită accesibilității pe care o oferă.

Înainte de a discuta de beneficiile pe care le oferă o aplicație socială, utilizatorii doresc să știe daca conversațiile pe care aceștia le au sunt sau nu protejate. Pentru o persoană ce nu are cunoștințe în domeniul securității, modul în care este implementată aceasta nu este important, atâta timp cât ea există, iar informațiile pe care acesta le scrie nu sunt expuse unui atac cibernetic. Însă, utilizatorii cu experiență în domeniu pot pune la încercare securitatea unei aplicații pentru a expune eventualele slăbiciuni. Dacă acestea există și sunt făcute publice, rezultatul final este renunțarea la utilizarea aplicației de către marea parte a utilizatorilor.

Pentru a ridica interesul unei astfel de aplicații, este importantă întelegerea modului în care un utilizator o folosește pentru a îi arăta subiecte de interes ce îl vor determina să intre în conversații. Acest lucru trebuie realizat într-un mod dinamic pentru a se adapta la interesele mereu în schimbare ale unui utilizator.

De asemenea, nu trebuie neglijat nici posibilitatea de customizare a aplicației, în special a subiectelor pe care utilizatorul nu dorește să le vadă deloc, dar și a alotor elemente precum interfața grafică sau profilul personal. Cu toate că acestea din urmă nu au un impact foarte mare asupra performanței sau utilizării aplicației, ele reprezintă un mod de a personaliza experiența fiecărui utilizator.

O altă problemă cu care se confruntă utilizatorii platformelor sociale sunt restricțiile impuse de acestea cu privire la subiectele discutate, respectiv limbajul folosit. Este de înteles faptul că există persoane care nu doresc să participe la conversații în cadrul acestor subiecte, însă, acest motiv nu ar trebui să fie folosit pentru a impune granițe cu privire la dezbaterile unei comunități.

1.2. Ideea si scopul aplicației

Aplicația Ask Around îsi propune în primul rând să atace aceste restricții ale subiectelor de comunicare, oferind în același timp un mediu foarte securizat de transmitere a informațiilor pentru a satisface nevoia de siguranță a utilizatorilor. De asemenea, tehnologiile moderne folosite în dezvoltarea acesteia asigură o performață sporită și un grad de accesibilitaet foarte ridicat, fiind o aplicație dezvoltată pe sistemul de operare Android, singurele restricții fiind posesia unui dispozitiv ce poate rula o versiune a sistemului de operare mai mare decât 4.0 și o conexiune stabilă la Internet.

Funcționalitatea aplicației constă în posibilitatea de purta o discuție deschisă despre orice subiect cu ceilalți utilizatori. Discuțiile sunt purtate într-un mediu sigur, iar datele stocate sunt securizate prin protocoale de comunicare și serializare a datelor moderne, menite să crească încrederea utilizării aplicației.

Subiectele ce stârnesc interesul comunitații vor putea fi votate de către cititori pentru a fi mai ușor vizualizate, în timp ce, subiectele care nu sunt considerate atractive, vor primi voturi negative, iar în final nu vor mai fi afișate sub decât în urma unor căutari specifice pe subiectul repsectiv. Creatorul unei postări (”original poster” – OP), nu o va putea șterge, indiferent de concluzia la care ajunge discuția sau direcția către care aceasta se îndreaptă, lucru menit să încurajeze dreptul la opinie al celorlalți.

Aplicația va putea fi customizată local prin alegerea unei teme preferate de către utilizator și alegerea subiectelor de interes pentru ca acestea să poată fi învățate în mod dinamic de aplicație în vederea filtrării subiectelor de interes.

1.3. Structura lucrării

Capitolul 2 al lucrării va incepe prin enumerare tehnologiilor folosite împreună cu avantajele utilizării acestora și o scurtă prezentare generală, urmate de o introducere ce descrie modul de folosire a tehnologiei respective, prin expemple de sine stătătoare ce pot fi rulate în mediul aferent.

În capitolul 3 este prezentată arhitectura generală a aplicației pentru e reprezenta modalitatea de transmitere a informației de la client la server și pentru a oferi o întelegere mai amănunțită a celor două entități care formează aplicația Ask Around.

Continuând cu capitolul 4, sunt aprofundate explicațiile prezentate anterior, punând accent pe detaliile de implementare ale aplicației, atât pe partea de server, cât și pe partea de client, prin care acestea sunt optimizată și modularizată.

În final, este prezentat modul în care aplicația poate fi folosită, punând în evidență diverse scenarii în care se pot regăsi utilizatorii.

2. Tehnologii folosite

2.1. Baza de date PostgreSQL

2.1.1. Scurt istoric

În anul 1986, la Universitatea Berkley din California, profesorul Michael Stonebraker a implementat un nou sistem de baze de date relaționare, numit POSTGRES, ce avea urmatoarele scopuri:

Suport mai bun pentru obiecte complexe.

Suport pentru tipuri de date noi, operatori si metode de acces.

Facilități specifice bazelor de date active (de exemplu, declanșatoare).

Simplificarea codului rulat în cazul opririi neașteptate a sistemului.

Proiectarea unui plan care să se foloseasca de unitățile optice si sistemele cu multi-procesoare.

Cât mai puține schimbări cu privire la modelul relaționar.

În primele două versiuni de lansare ale POSTGRES-ului, acesta a suferit multe modificări ale regulilor sistemului, pană la apariția celei de-a treia versiuni care a fost cea mai stabilă. Următoarele versiuni până la apariția lui Postgres95 s-au concentrat pe portabilitatea și fiabilitatea sistemului.

Postgres95 a luat naștere în anul 1994, când Andrew Yu și Jolly Chen au adăugat un interpretor al limbajului de interogare structurat (Structured Query Language – SQL) în POSTGRES care a înlocuit limbajul PostQUEL. Acest lucru a permis multor concepte din lumea SQL-ului să fie folosit si in Postgres95 si a condus la denumirea finală a pachetului, PostgreSQL, adoptată în anul 1996, ce reflectă relația dintre POSTGRES și capabilitățile SQL-ului.

Cu două decenii de dezoltare în spate, PostgreSQL este astăzi unul dintre cele mai avansate SGBD-uri din lume.

2.1.2. Avantaje PostgreSQL

În mod tradițional, SGBD-urile suportă un model de date format dintr-o colecție de relații ce conțin atibute de un anumit tip specific: numere întregi, șiruri de caractere si date calendaristice. Acest model este recunoscut pentru simplitatea lui, însa, din cauza acesteia, de mult ori, anumite aplicații întâmpina dificultăți în dezvoltare.

PostgreSQL este un sistem de gestiune a unei baze de date (SGBD) relaționale ce vine în ajutorul acestor aplicații prin implementarea următoarelor concepte:

Clase

Moștenire

Tipuri de date

Funcții

De asemenea, următoarele funcționalități asigură mai multă flexibilitate în dezvoltare:

Constrângeri

Declanșatoare

Reguli

Integritatea tranzacțiilor

PostgreSQL funcționeaza pe modelul client-server, avand o sesiune formata din următoarele componente:

Un proces server, numit postgres, reprezentând backend-ul, ce administrează fișierele bazei de date, acceptă conexiuni de la client si execută diverse acțiuni în numele acestuia.

Aplicația de fontend ce dorește să consume serviciile serverului si să execute operații in baza de date.

Protocolul de comunicare folosit între frontend si backend este cel Transmission Control Protocol / Internet Protocol (TCP/IP). Serverul PostgreSQL poate manipula mai multe conexiuni de la clienți într-un mod concurent. Pentru a realiza acest lucru, pornește câte un proces nou pentru fiecare conexiune inițiată. Din acel moment, clientul și serverul comunică direct, fără intervenția procesului postgres.

Astfel, procesul master rulează mereu, așteptând noi conexiuni de la clienți. Acest proces este transparent clienților.

În universul bazelor de date, este bine cunoscut faptul că atunci când o operație scrie date într-un tabel, aceasta blochează resursa folosită pentru a evita problemele de concurență. Acest lucru poate deveni o problemă din punctul de vedere al performanței SGBD-ului, în momentul în care mai multe operații trebuie sa lucreze în același timp pe date din mai multe tabele deoarece există șanse mari ca acestea să aiba nevoie de resurse ce sunt deja utilizate.

Tehnica de tranzacționare a apărut pentru a facilita desfășurarea acestor operații. Tranzacțiile sunt una dintre cele mai fundamentale concepte ale bazelor de date. În esență, acestea împachetează o multitudine de pași, intr-o operație de tipul ”totul sau nimic”. Pașii respectivi nu sunt vizibili altor tranzacții care se realizează în mod concurent și este suficient un singur eșec al unuia dintre pașii respectivi pentru a preveni completarea cu succes a tranzacției. Astfel, toți pașii executați până în momentul respectiv sunt anulați, iar baza de date nu suferă nicio modificare. O tranzacție trebuie să fie atomică din viziunea altor tranzacții; ori se desfășoară complet, ori deloc.

De asemenea, o tranzacție care a fost completată cu succes trebuie să fie permanent înregistrată în sistem și să existe asigurarea că aceasta nu va fi pierduta chiar și în eventualitatea apariției unei erori la scurt timp după terminarea tranzacției.

PostgreSQL se conformează setului de proprietăți de atomicitate, consistență, izolare si durabilitate (atomicity, consistency, isolation, durability – ACID). Orice operație se desfăsoară în mediul ei privat, iar după terminarea acesteia, schimbările se regăsesc garantat în baza de date. Acestă metodă poartă numele de Controlul Concurenței Multi-Versiune (Multi-Version Concurrency Control – MVCC) și reprezintă adevărata sursă de putere a Postgres-ului.

Majoritatea SGBD-urilor oferă funcționalități precum: partiționarea logica a tabelelor, compresarea automată a datelor, așa numitele servicii (serviciu – job) și posibilitatea de a stoca fișiere de mărime nelimitată, însă, acestea necesită anumite licențe speciale plătite. PostgreSQL este un SGBD open source, prin urmare oferă funcționalitățile mai sus menționate gratuit, fiind o alternativă mult mai bună din punct de vedere financiar.

2.1.3. Utilizare

Această parte descrie modul de utilizare a limbajului de interogare structurat (Structured Query Language – SQL) în cadrul bazei de date PostgreSQL, începând cu modul de creare a structurilor ce vor conține datele, modalități de populare al bazei de date și interogare a acesteia și continuând cu optimizarea performanțelor.

2.1.3.1. Bazele tabelelor

Noțiunea de ”tabel” în contextul bazelor de date relaționale este asemănătoare cu cea a unui tabel obișnuit. Numărul de rânduri – sau înregistrări – existente la un moment de timp reprezintă cantitatea de date stocată până atunci, iar coloanele reprezintă valorile asociate înregistrării respective. Mulțimea posibilă de valori pe care o poate lua o coloană este nemărginită, însă, de cele mai multe ori, avem nevoie să impunem restricții asupra valorilor stocate în baza de date. Astfel, asociem fiecărei coloane un tip de date care restrânge mulțimea posibilă de valori.

PostgreSQL suportă tipurile standard de date din SQL, anume int, smallint, double precision, char(N), varchar(N), date, time, timestamp, interval, precum si multe tipuri geometrice, de exemplu cube, earth și polygon.

Pentru crearea unui tabel se folosește comanda CREATE TABLE:

CREATE TABLE users (

name text,

age integer

);

Acestă instrucțiune crează un tabel cu numele users ce are două coloane: name și age. Un posibil set de înregistrări ale acestei tabele arată sub următoarea formă:

După cum se poate observa, limbajul SQL nu impune nicio restricție asupra unicității înregistrărilor din tabel, făcând astfel posibilă apariția duplicatelor. În practică, pentru a folosi bazele de date, este necesar ca fiecare înregistrare să poată fi identificată în mod unic. Pentru realizarea acestui lucru, fiecare tabelă folosește așa numitele chei primare (cheie primară – primary key – PK) ce introduc un element de diferențiere între înregistrări.

ALTER TABLE users

ADD id integer

Acum, înregistrările din tabela users au forma următoare:

După actualizarea tabelei cu noua coloană, fiecare înregistrare poate fi determinată în mod unic.

În SQL se pot executa diverse operații asupra tabelelor care pot modifica structura acestuia sau pot redenumi coloanele. Aceste operații sunt executate folosind comanda: ALTER TABLE:

Inserarea unei coloane:

ALTER TABLE users ADD COLUMN email text;

Noua coloană este populată inițial cu valoarea implicită, în acest caz, valoarea NULL, sau cu o valoare implicită setată de utilizator.

ALTER TABLE users ADD COLUMN email text DEFAULT "";

Ștergerea unei coloane:

ALTER TABLE users DROP COLUMN email;

Orice date care existau în coloana respectivă sunt șterse. Același lucru se întâmplă și cu constrângerile legate de aceasta, singura excepție fiind atunci când există o constrângere de cheie străină către coloana în cauză. În această situație, se poate folosi opțiunea CASCADE pentru a șterge și constrângerea de cheie străină.

ALTER TABLE my_table DROP COLUMN column_used_as_foreign_key CASCADE;

Adăugarea unei constrângeri:

ALTER TABLE users ADD COLUMN group_id integer;

ALTER TABLE users ADD CHECK (name <> '');

ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE email;

ALTER TABLE users ADD FOREIGN KEY (group_id) REFERENCES user_groups;

ALTER TABLE users ALTER COLUMN email SET NOT NULL;

Introducerea unei constrângeri presupune ca datele existente să satisfacă condițiile impuse de constrângerea ce urmează să intre în efect. În caz contrar, este ridicată o eroare, iar aplicarea constrângerii este anulată.

Ștergerea unei constrângeri:

Mai sus, a fost introdusă o constrângere pe coloana email a tabelei users, ce impune unicitatea valorilor sale, cu numele unique_email. Pentru ștergerea acesteia se va folosi următoarea comandă:

ALTER TABLE users DROP CONSTRAINT unique_email;

Similar cu ștergerea unei coloane, este necesară folosirea opțiunii CASCADE dacă există dependențe pe coloana legată de constrângerea ștearsă.

Schimbarea valorilor implicite ale unei coloane:

ALTER TABLE users ALTER COLUMN age SET DEFAULT 18;

Este important de reținut faptul că introducerea acestei constrângeri nu afectează valorile deja existente, ci doar pe cele care vor fi inserate ulterior.

Valoare implicită se poate șterge chiar dacă nu este cunoscut numele constrângerii folosind comanda:

ALTER TABLE users ALTER COLUMN age DROP DEFAULT;

Această comandă este echivalentă cu setarea valorii implicită a sistemului. Prin urmare, chiar dacă nu a fost definită o valoare implicită de către utilizator, nu se va ridica o eroare.

Schimbarea tipului de date al unei coloane:

ALTER TABLE users ALTER COLUMN age TYPE numeric(10,2);

Această comandă va rula cu succes doar dacă toate valorile din coloana age pot fi convertite cu succes la noul tip de date specificat. În cazul în care este necesară o operație mai complexă pentru a converti valorile la noul tip de date se poate folosi optiunea USING prin care se definește operția ce va fi utilizată.

Redenumirea coloanelor:

ALTER TABLE products RENAME COLUMN product_no TO product_number;

Redenumirea tabelului:

ALTER TABLE products RENAME TO items;

Daca se dorește ștergerea tabelului, acest lucru se poate realiza folosind comanda DROP TABLE.

DROP TABLE users;

În cazul în care se incearcă ștergerea unei tabele inexistente, este ridicată o eroare, iar execuția comenzii SQL este întreruptă.

2.1.3.2. Privilegii

În momentul creării unui obiect – spre exemplu, o tabelă – acesta este asignat proprietarului. Pentru majoritatea obiectelor, starea inițială este aceea în care doar propietarul sau un super-utilizator poate executa operații pe ele. Pentru a permite acest lucru și altor utilizatori, ei trebuie să primească drepturile necesare.

Permisiunile existente în PostgreSQL sunt următoarele: SELECT, INSERT, UPDATE, TRUNCATE, REFERENCES, TRIGGER, CREATE, CONNECT, TEMPORARY, EXECUTE și USAGE. Unui obiect îi pot fi atribuite diverse permisiuni, în funcție de natura acestuia – acesta poate fi un tabel sau o funcție –, însă, drepturile de a fi distrus sau modificat aparțin întotdeauna proprietarului.

Propietarul unui obiect se poate schimba prin folosirea comenzii ALTER TABLE. Aceasta poate fi folosită doar de către super-utilizatori sau utilizatorii normali care sunt proprietarii obiectului și fac parte și din noul rol care va deveni proprietar.

Pentru a acorda privilegii asupra unui obiect se folosește comanda GRANT. De exemplu, se presupune existența unui utilizator pe nume lyris și a tabelei users a cărui propietar este administratorul bazei de date. Acesta poate rula următoarea comanda pentru a acorda dreptul de UPDATE utilizatorului lyris:

GRANT UPDATE ON users TO lyris;

Daca se dorește acordarea tuturor drepturilor, se poate folosi cuvântul cheie ALL.

GRANT ALL ON users TO lyris;

Pentru a revoca dreputrile pentru un anumit obiect se poate folosi comanda REVOKE:

REVOKE ALL ON users FROM lyris;

Există posibilitatea de a acorda drepturi unui alt utilizator sau grup de utilizatori cu opțiunea WITH GRANT OPTION care premite acestora, la rândul lor, să acorde privilegiile primite mai departe. Însă, daca dreptul primit este revocat, acesta va fi revocat tuturor persoanelor sau grupurilor de persoane care au primit dreptul din sursa respectivă.

2.1.3.3. Operații cu date

După crearea unui tabel, acesta este gol; nu conține nicio înregistrare. El trebuie să fie populat prin inserarea datelor. La nivel conceptual, datele sunt inserate rând cu rând, indiferent daca informațiile înregistrarii respective sunt complete sau nu. Pentru orice informație lipsa, sistemul inserează valoarea DEFAULT implicită sau cea setată de utilizator, atâta timp cât toate constrângerile tabelei sunt satisfăcute.

Mai jos este definită o variantă mai complexă a tabelei users folosită ca exemplu la subcapitolele 2.1.3.1. Bazele tabelelor și 2.1.3.2. Privilegii.

CREATE TABLE users (

id integer NOT NULL PRIMARY KEY,

email varchar(100) NOT NULL UNIQUE,

first_name varchar(30),

last_name varchar(30),

password text NOT NULL

);

În continuare, tabelul users va fi populat cu diverse date care, la rândul lor, vor suferi și ele modificări:

Inserarea datelor:

INSERT INTO users VALUES (1, 'lyris@askaround.com', 'lyris', 'titanborn', '=Q1qF…');

În exemplul de mai sus datele sunt introduse în ordinea în care coloanele apar în baza de date. Dezavantajul acestei sintaxe este faptul că utilizatorul trebuie să cunoască ordinea în care acestea apar în tabel. Pentru combaterea acestui dezavantaj, se poate folosi următoarea comandă ce mapează fiecare valoare pe coloana precizată de către utilizator:

INSERT INTO users (email, id, password)

VALUES ('lyris@askaround.com', 1, '=Q1qF…');

De asemenea, pot fi inserate mai multe înregistrări în cadrul aceleași comenzi SQL:

INSERT INTO users (id, email, password) VALUES

(1, 'lyris@askaround.com', '=Q1qF…'),

(2, 'nocturnal@askaround.com', 'io3Wd…'),

(3, 'yuno@askaround.com', 'mniKki…');

Actualizarea datelor:

Modificarea datelor unei tabele poate fi efectuată pe toate înregistrările sale, pe o anumită înregistrare, pe o anumită coloană a unei înregistrări sau orice combinație dintre aceste variante. Pentru acest luctru se folosește comanda UPDATE:

UPDATE users SET first_name = 'yuno';

Comanda de mai sus setează valoarea coloanei first_name cu șirul de caractere ”yuno” pentru fiecare înregistrare a tabelului users. Însă, de cele mai multe ori, avem nevoie să modificăm datele unei singure înregistrări. Acest lucru este posibil prin folosirea opțiunii WHERE, care specifică o condiție, selectând astfel doar acele înregistrări care o îndeplinesc.

UPDATE users SET first_name = 'yuno' WHERE id=3;

Pot exista cazuri în care nicio înregistrare nu satisface condiția data, însă acest lucru nu va ridica o eroare. Condiția unei comenzi poate consta și în expresii, nu numai în valori constante.

Se pot actualiza mai multe coloane în aceeași operație de UPDATE:

UPDATE users SET first_name = 'yuno', last_name = 'gasai' WHERE id=3;

Ștergerea datelor

Pentru a elimina o înregistrare dintr-o tabelă, se poate folosi comanda DELETE.

DELETE FROM users WHERE id = 2;

2.2. Framework-ul Pyramid

2.2.1. Scurt istoric

Înaintea anului 2010, pe piața Interfețelor Portalurilor cu Severe Web (Web Server Gateway Interface – WSGI) existau foarte multe cadre (cadru – framework) care își propuneau să ușureze viața dezvoltatorilor în scrierea aplicațiilor web.

Proiectele repoze.bgf și Pylons făceau parte din acele framework-uri. Ele erau simpliste, în sensul că nu cuprindeau funcționalități neimportante care le-ar fi încetinit, foloseau același model de mapare a localizatorului uniform de resurse (uniform resource locator – URL) pe cod și erau concurente pentru același eșantion de dezvoltatori.

Astfel, în anul 2010, cele două proiecte au decis să își unească forțele sub numele de Pyramid.

2.2.2. Prezentare generală

Pyramid este un framework web minimalistic scris în Python și bazat pe WSGI, ce folosește o arhitectură de tip Model-Vedere-Controler (Model-View-Controller – MVC) – prezentată amănunțit în capitolul 2.4.2. Arhitectura MVC și aplicarea acesteia în dezvoltarea Android. Este integrat cu limbajul SQL, motorul SQLAlchemy și multe baze de date non-relaționale.

Prin construcție, Pyramid nu este un framework foarte pretențios. Nu conține multe funcționalități ce ar putea să îl încetinească, nu are o mapare de tip obiect-relație (object relational-mapping – ORM) construită în el, nu conține generări de formulare și nici nu are o interfață web pentru administrare. Toate aceste lucruri considerate de nivel înalt sunt lăsate pentru a fi tratate de celelalte aplicații și framework-uri ce folosesc Pyramid la baza lor.

Acest framework oferă însă alte funcționalitați specifice ce pot fi folosite cu ușurință și pot ajuta la dezvoltarea oricărui tip de aplicație:

Opțiunea de a mapa URL-uri cu diverse clase sau funcții folosind metoda ”Traversal” sau ”URL Dispatcher”.

Un sistem de autorizare si autentificare declarativ.

Posibilitatea de a crea diverse ”fabrici” (fabrică – factory) de traduceri pentru internaționalizare si localizare ( Internationalization and localization – I18N)

Pyramid folosește doua metode pentru a rezolva cererile primite de la consumatori: ”Traversal” și ”URL Dispatcher”.

Metoda Traversal presupune transpunerea URL-ului într-un arbore de resurse, în care fiecare nod este reprezentat de o parte a URL. Astfel, se poate forma o structură mai bună a aplicației, deorece aceasta nu se bazează pe o ordonare fixă a potrivirii URL-urilor. Acestă metodă este specifică mai mult bazelor de date non-relaționale, motiv pentru care este folosită mai rar.

A doua metodă, URL Dispatcher, folosește un dispecer ce rezolvă calea URL către o vedere (view), prin executarea unor operațiuni de potivire a URL-ului cu anumite definiții de rute. Aceste definiții sunt examinate în ordinea apariției lor, iar prima care se potrivește este și executată.

Pyramid furnizează un sistem de securitate declarativ opțional. Acesta este separat în două module: cel de autentificare și cel autorizare. Acestea comunică prin identificatori ”principali” (principal – un șir de caractere sau un obiect Unicode[17] ce reprezintă o entitate (de obicei un utilizator sau un grup)).

Autentificarea este un mecanism care rezolvă credențialele trimise de un utilizator într-unul sau mai multe identificatoare principale. Acestea reprezintă utilizatorii și grupurile care sunt ”active” în sesiunea curentă.

Autorizarea este procesul care determină posibilitatea accesului unei resurse în funcție de identificatorii de pe sesiunea curentă și identificatorii de pe resursa în cauză.

La un nivel abstractizat, aceștia sunt pașii urmați de sistemul de autorizare pentru a decide dacă unui utilizator îi este permis accesul pe o resursă:

Un utilizator poate să fi accesat aplicația în trecut și să fi furnizat credențialele sale, caz în care aplicația iși va aminti de acestea.

O cerere (request) este creată atunci când un utilizator accesează aplicația.

Pe baza cererii, se localizează un contex în care este încadrată.

Este localizată resursa cerută, impreună cu contextul ei.

Dacă există o politică de autentificare în efect, aceasta primește cererea și returnează identificatorii ei unici.

Dacă există o politică de autentificare în efect, aceasta primește resursa cerută, contextul ei și identificatorii request-ului, după care decide dacă permite accesul sau nu.

Dacă accesul este permis, atunci se apelează view-ul resursei.

Dacă accesul nu este permis, atunci se apelează view-ul interzis (forbidden view).

2.2.3. Utilizare

2.2.3.1. Structura de bază a unei aplicații

Începem prin a prezenta scheletul ce stă la baza unei aplicații Pyramid ce descrie modul de construire a configurației server-ului și înregistrarea rutelor.

from wsgiref.simple_server import make_server

from pyramid.config import Configurator

from pyramid.response import Response

def hello_world(request):

return Response('Hello %(name)s!' % request.matchdict)

return Response('Hello %(name)s!' % request.matchdict)

config = Configurator()

config.add_route('hello', '/hello/{name}')

config.add_view(hello_world, route_name='hello')

app = config.make_wsgi_app()

server = make_server('localhost', 8080, app)

server.serve_forever()

După rularea programului de mai sus dintr-un terminal, în consolă este afișat un mesaj informator ce precizează faptul că serverul a fost pornit și ascultă pe URL-ul http://localhost și portul 8080.

from wsgiref.simple_server import make_server

from pyramid.config import Configurator

from pyramid.response import Response

Pyramid, la fel ca multe alte web framework-uri, folosește protocolul WSGI pentru a conecta un server cu o aplicație. În cazul acesta, este folosit modulul wsgiref pentru simplitate, deoarece acesta vine odată cu librăria standard a limbajului Python.

În continuare sunt importate clasele necesare pentru configurarea serverului din modulul pyramid. Clasa Configurator este folosită pentru a adăuga rute și pentru a defini funcționalitățile lor aferente în cadrul aplicației. Clasa Response este folosită pentru a returna răspunsul unei cerere primite.

config = Configurator()

config.add_route('hello', '/hello/{name}')

config.add_view(hello_world, route_name='hello')

Prin configurația de mai sus, server-ul este instruit ca atunci când primește o cerere pe ruta /hello/{name} să execute funcția hello_world și să întoarca cererii rezultatul obținut. Parametrul {name} semnifică faptul că URL-ul poate fi rezolvat indiferent de ce se află în locul acestuia, fiind practic o variabilă.

Astfel, dacă este vizitat URL-ul http://localhost:8080/hello/Yuno într-un browser web, răspunsul primit va fi Hello Yuno!. În această situație Yuno este valoarea atribuita parametrului name din rută, însă se putea folosi orice în locul acestei valori.

def hello_world(request):

return Response('Hello %(name)s!' % request.matchdict)

Funcția hello_world este logica asociată rutei ce poartă numele hello și aceasta va fi executată de fiecare data când aplicația primește o cerere pe un URL ce poate fi protrivit cu URL-ul configurat, anume /hello/{name}. Ea primește un singur argument, request, reprezentând cererea inițiată de utilizator, și returnează un obiect de tip Response.

Deoarece această funcție a fost asociată unei rute, este numită o ”vedere apelată” (view callable), motiv pentru care trebuie să întoarcă mereu un obiect de tip Response, ce reprezintă cererea HTTP trimisă către Pyramid de către server-ul WSGI. Acest obiect conține toate informațiile necesare pentru a putea fi transformat într-un răspuns HTTP, după care este convertit în format text de căatre server-ul WSGI și este trimis către browser-ul de unde a fost primită cererea.

app = config.make_wsgi_app()

După terminarea tuturor configurțiilor, este creat server-ul WSGI folosind funcția pyramid.config.Configurator.make_wsgi_app(). Această funție returnează un obiect de tip WSGI ce poate fi folosit de server-ul WSGI formănd aplicația finală ce servește toate solicitările primite. În acest moment, orice altă configurație nu mai are efect asupra server-ului, iar singurul lucru rămas este pornirea acestuia.

server = make_server('0.0.0.0', 8080, app)

server.serve_forever()

Se folosește funcția make_server pentru a construi server-ul propriu-zis. Primul parametru reprezintă adresa la care vor fi ascultate cererile. În cazul acesta, adresa 0.0.0.0 înseamnă practic toate interfețele TCP. În mod implicit, acest parametru are valoarea 127.0.0.1, însă aceasta nu permite accesul server-ului de la distanță, ceea ce poate crea probleme dacă server-ul folosit în dezvoltare se află pe un alt calculator. Al doilea parametru setează portul TCP pe care vor fi ascultate cererile, iar ultimul parametru reprezintă aplicația ce va fi servită.

În final, se apelează funcția serve_forever ce pornește server-ul, iar acesta va rula la infinit și va așteapta cereri din mediul extern.

2.2.3.2. Procesarea cererilor

Serverul, odată pornit, este pregătit să accepte cereri de la diverși consumatori, să le proceseze și să returneze răspunsurile corespunzătoare. Cu toate că răspunsul este returnat foarte rapid, înainte de a fi generat, cererea sursă din care provine trece prin mai multe modificări, sisteme de autentificare și evaluari.

O aplicație poate fi customizată astfel încât să poată fi controlat gradul de complexitate al procesului de generare a unui răspuns dintr-o cerere dată. La un nivel abstractizat, în figura de mai jos este reprezentată schema asociată pașilor prin care trece o cerere din momentul în care este acceptată până când este returnat un răspuns:

Un utilizator inițiază o cerere din browser către locația formată din adresa și portul asociat server-ului WSGI.

Serverul pasează mediul WSGI către funcția __call__ din obiectul router.

Un obiect cerere este creat pe baza mediul WSGI trimis.

Obiectele application registry și request create sunt împinse pe o stivă locală pentru a permite framework-ului Pyramid să folosească funcțiile get_current_request() și get_current_registry().

Un eveniment de tipul NewRequest este trimis către toți abonații (subscribers).

Dacă există cel puțin o singură rută configurată în aplicație, router-ul Pyramid apelează un dispecer URL numit ”route mapper”. Sarcina lui este de a examina cererea pentru a determina dacă se potrivește cu una din rutele definite.

În cazul în care există o rută care să trateze cererea, mapper-ul adaugă atributele matchdict, ce conține un dicționar în care sunt reprezentate toate elementele care formează calea, și matched_route, unde este stocat un obiect de tip IRoute ce reprezinta ruta propiru zisă pe care s-a potrivit cererea.

Un eveniment de tipul BeforeTraversal este trimis tuturor abonaților.

În continuare, se generează un obiect de bază, root, asociat cu ruta respectivă. Dacă configurația rutei care s-a potrivit a primit un argument factory, atunci acesta este folosit pentru a genera obiectul de bază, altfel este folosit în mod implicit argumentul root factory.

Router-ul apelează funcția traverser() cu argumentele root și request. Aceasta încearcă să traverseze obiectul root folosindu-se de funcția __getitem__ de pe toate proprietățile și subproprietățile sale pentru a găsi un context. În cazul în care niciuna dintre proprietăți nu are o funție __getitem__, atunci se consideră ca însuși obiectul root este contextul. Funcția traverser() returnează un dicționare ce conține numele view-ului găsit împreună cu contextul asociat acestuia.

Obiectul request este decorat cu proprietățile obținute de la funția traverser() cum ar fi context și view_name.

Un eveniment de tipul ContextFound este trimis către toți abonații.

Pyramid caută un view callable folosind contextul, obiectul request și numele view-ului. Dacă nu există un asemenea view, este ridicată o excepție de tipul HTTPNotFound.

Dacă a fost găsit un view callable, atunci acesta este apelat. Dacă se utilizează o politică de autorizare și view-ul este protejat print-o permisie, Pyramid va determina dacă apelarea acestuia poate continua bazându-se pe credențialele oferite de obiectul request și informația atașată contextului. Dacă execuția este permisă, Pyramid continuă apelarea view-ului pentru a genera un răspuns, altfel este generată o excepție de tipul HTTPForbidden.

Atunci când este ridicată o excepție – cum ar fi HTTPNotFound sau HTTPForbidden, router-ul o atașează obiectului request pe proprietatea exception, după care incearcă să găsească un view callble particular, numit exception view, care să trateze excepția respectivă. Dacă un asemenea view este găsit, acesta este apelat, iar răspunsul generat este trimis mai departe, altfel este generat un răspuns implicit.

Următorii pași au loc doar în cazul în care un răspuns a fost generat cu succes de către un view callable sau un exception view. Pyramid va incerca să execute orice funcție de retroapelare (callback) adăugată pe obiectul response prin intermediul funcției add_response_callback(), după care un nou eveniment de tipul NewResponse este trimis tuturor abonaților. Funția __call__ a obiectului response este folosită pentru a genera un răspuns WSGI, răspuns care este trimis mai apoi server-ului WSGI.

Pyramid va incerca să execute orice callback adaugat pe obiectul response prin intermediul funcției add_finished_callback().

Stiva locală este eliberată de obiectele application registry și request.

2.3. Motorul SQLAlchemy

2.3.1. Scurt istoric

Una dintre principalele părți ale oricărei aplicații web este construirea unui backend solid. În trecut, dezvoltatorii erau nevoiți să scrie comenzi SQL manual, să le trimită către motorul bazei de date și să parseze rezultatul. Astăzi, există programe de mapare obiect-relație (object-relational mapping – ORM) care elimină necesitatea scrierii acestor instrucțiuni SQL manual, oferind alternative ușor de citit și de menținut.

Tehnica de programare ORM convertește datele transmise dintr-un format incompatibil intr-unul orientat pe obiecte. De obicei, limbajele orientate pe obiecte conțin tipuri nescalare, mai exact tipuri ce nu pot fi exprimate prin primitive cum ar fi șirurile de caractere.

SQLAlchemy este un set de instrumente și un ORM ce oferă dezvoltatorilor întreaga putere si flexibilitate a SQL-ului. Acesta asigura mai multe modele de persistență, construite pentru acces rapid și eficient în baza de date, și adaptate pentru limbajul Python.

2.3.2. Prezentare generală

SQLAlchemy este împărțit în mai multe zone de funcționalitate ce pot fi folosite individual sau împreună:

SQLAlchemy ORM

SQLAlchemy Core – conține scheme, tipuri, limbajul expresilor SQL (SQL Expression Language)

DBAPI – Database Application Programming Interface

ORM-ul SQLAlchemy prezintă o metodă de a asocia clasele scrise în Python de către dezvoltatori cu tabele din baza de date, și instațe ale acestor clase cu rânduri corespunzătoare tabelelor respective. Sincronizarea dintre obiecte si răndurile aferente din tabelele din baza de date se face în mod transparent, la fel și în cazul interogărilor bazei de date în ceea ce privește clasele si relațiile definite de dezvoltator.

Acest ORM oferă o perspectivă de nivel înalt a utilizării limbajului SQL, oferind posibilitatea utilizării acestuia fără a fi necesare cunoștințe avansate. Pe de altă parte, în anumite situații, poate fi necesar un control mult mai exact asupra operațiilor SQL executate, moment în care se pot folosi expresiile SQL integrate in nucleul SQLAlchemy-ului.

Unul dintre conceptele de bază din SQLAlchemy este sesiunea. O sesiune stabilește și menține toate conversațiile dintre aplicație și baza de date. Ea reprezintă o zona intermediară pentru toate modele obiectelor Python care au fost încărcate și este punctul de intrare a tuturor execuțiilor interogărilor bazei de date. Rezultatele generate sunt mapate pe obiecte unice în interiorul sesiunii.

O sesiune are urmatorul ciclu de viață:

Sesiunea este contruită, moment în care nu are asociat niciun model de obiecte.

Sesiunea primește cereri cu instrucțiuni, a căror rezultate persistă în sesiune și sunt asociate cu aceasta.

Modelele obiectelor sunt construite și adăugate sesiunii, iar aceasta începe să le mențină și să le managerieze.

Când toate schimbările obiectelor au fost efectuate, acestea pot fi comitate (commited) în baza de date sau derulate (rollback).

Sesiunea se închide și eliberează toate obiectele asociate.

Pe durata existenței sesiunii, obiectele pot avea una din următoarele stări:

Tranziție (Transient): o instanță care nu este inclusă într-o sesiune și nici nu a fost încă persistată în baza de date.

În așteptare (Pending): o instanță care a fost atașată unei sesiunei, dar nu a fost încă persistată în baza de date. Aceasta va persista la urmatoarea acțiune de commit.

Persistentă (Persistent): o instanță care este atașată unei sesiuni și a fost și persistată în baza de date.

Detașată (Detached): o instanță care a fost persistată în baza de date și nu face parte din nicio sesiune.

Un alt element important al SQLAlchemy-ului este limbajul expresiilor SQL. Acesta permite dezvoltatorilor să specifice instrucțiuni SQL în blocuri de cod scrise în Python și să le folosească pentru executarea unor instrucțiuni mult mai complexe. Aceste expresii nu țin cont de backend-ul folosit și acoperă în detaliu toate aspectele SQL-ului.

2.3.3. Utilizare

2.3.3.1. Conectarea motorului

SQLAlchemy permite utilizarea unei baze de date localizată în memoria fizică. ce are aceeleași funcționalități ca una normală, și care poate fi accesată folosind o instanță a motorului din modulul sqlalchemy. Acest lucru se realizează prin următoarele linii de cod:

from sqlalchemy import create_engine

engine = create_engine('sqlite:///:memory:', echo=True)

Argumentul echo trimis funcției create_engine este o scurtătură ce face setările de baza ale sistemul de logare al SQLAlchmey-ului în concordanță cu modulul logging din librăria standard Python astfel încât cererile SQL executate să fie afișate în consolă.

O importanță deosebită o are funcția create_engine ce returnează o instanță a clasei Engine din modulul sqlalchemy.engine, instanță ce reprezintă interfața cu baza de date adaptată printr-un dialect ce ascunde detaliile de utilizare ale acesteia. În particular, dialectul SQLite va interpreta instrucțiunile primite folosind modulul sqlite3 care este deja integrat în Python.

În momentul în care se va apela pentru prima data o metodă ce execută operații în baza de date – de exemplu engine.execute() sau engine.connect() – se stabilește legătura între motor și aceasta. În general, obiectul engine nu este folosit în mod direct în dezvoltare. Acesta este folosit în spatele scenelor de către ORM.

2.3.3.2. Crearea mapării

Pentru a persista informații într-o bază de date, această trebuie sa conțină tabele ce vor servi ca modele ce descriu informațiile stocate. Crearea acestor tabele se realizează printr-un sistem declarativ în SQLAlchemy ce permite descrierea tabelelor ca pe niște clase, unde membrii acestora reprezintă coloanele din tabela respectivă.

Clasele mapate folosind acest sistem declarativ trebuie să fie descendente ale unei clase de baza oferită de SQLAlchemy, și anume declarativ_base.

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

Clasa Base este folosită mereu ca un singur obiect global în aplicție, urmând modelul de proiectare singleton[16] (model de proiectare – design pattern), ce serverște la definirea tuturor claselor ce urmează a fi mapate folosind sistemul declarativ. Se va exemplifica prin crearea unei clase User ce va fi mapată pe un tabel, users, în baza de date salvată în memorie.

from sqlalchemy import Column, Integer, String

class User(Base):

__tablename__ = 'users'

id = Column(Integer, primary_key=True)

name = Column(String)

password = Column(String)

Definirea unei clase în sistemul declarativ presupune faptul că acea clasă are cel puțin un membru, __tablename__, a cărui valoare reprezintă numele tabelului ce va fi mapat pe clasa respectivă, și cel puțin un membru de tipul Column din modulul sqlalchmey care să fie definită ca fiind cheie primară (primary key – PK) pentru tabelul respectiv.

Când clasa User este construită, sistemul declarativ înlocuiește toți membrii de tip Column cu niște descriptori speciali (accessors) printr-un proces ce poartă numele de instrumentare. Clasa rezultată va permite referențierea tabelului users într-un context SQL, precum și inserarea și persistarea informațiilor în coloanele din tabelă.

2.3.3.3. Crearea schemei

Toate informațiile ce descriu clasa User se află în membru __table__ al acesteia. Acesta este un obiect ce aparține unei colecții numite MetaData, iar acest obiect se află în membrul metadata din clasa Base a sistemului declarativ.

MetaData este un registru ce oferă posibilitatea de a emite un set finit de comenzi de generare a unei scheme pentru o bază de date. Astfel, se poate emula comanda CREATE TABLE din SQL pentru a crea tabelul users definit în clasa User de mai sus. Pentru acest lucru se folosește funcția MetaData.create_all() ce primește ca parametru obiectul de tip Engine care conține informațiile despre conectarea la baza de date.

Base.metadata.create_all(engine)

# output

# ––––- omitted output –––––

PRAGMA table_info("users")

()

CREATE TABLE users (

id INTEGER NOT NULL, name VARCHAR,

name VARCHAR,

password VARCHAR,

PRIMARY KEY (id)

)

()

COMMIT

În acest moment, se poate crea o instanță a clasei User:

user_obj = User(name='Yuno Gasai', password='mirainikki')

user_obj.name  # 'Yuno Gasai'

user_obj.password  # 'mirainikki'

user_obj.id  # None

Se observă faptul că proprietatea id a obiectului user_obj are valoarea None, deși, în mod normal, Pyhton ar fi ridicat o eroare de tipul AttributeError deoarece există un atribut nedefinit. Acest lucru se datorează faptului că operația de instrumentare produce această valoare implicită pentru atributele mapate unei coloane atunci când ele sunt accesate prima dată.

2.3.3.4. Crearea și utilizarea sesiunii

Sesiunea este clasa care se ocupă cu crearea legăturilor dintre aplicație și baza de date folosind un motor ce manageriează aceste conexiuni. Pentru a obține o instanță a unui astfel de obiect se folosește funția sessionmaker() din modulul sqlalchemy.orm.

from sqlalchemy.orm import sessionmaker

session = sessionmaker(bind=engine)

Imediat după creare, obiectul session nu are nicio conexiune deschisă. În momentul în care va fi folosită pentru prima dată, motorul asociat îi va oferi una din conexiunile sale disponibile.

Pentru a persista obiectul user_obj din capitolul 2.3.3.3. Crearea schemei se folosește funcția add() ce aparține de sesiune.

session.add(user_obj)

În acest moment, instanța clasei User se află intr-o stare de așteptare deoarece nu a fost emisă nicio instrucțiune SQL, iar user_obj nu este reprezentat în tabela users. Sesiunea va emite instrucțiunile SQL necesare prin folosirea unui proces de ”evacuare” (flush). Acest proces poate fi apelat manual sau se poate executa automat în anumite circumstanțe. Spre exemplu, interogarea bazei de date prin intermediul sesiunii, va declanșa procesul de evacuare înaintea executării instrucțiunii SQL.

db_user = session.query(User).filter_by(name='Yuno Gasai').first()

db_user  # <User(name='Yuno Gasai', password='mirainikki')>

Sesiunea poate fi interogată prin funcția query() care acceptă ca parametru o clasă derivată din clasa Base, creată în capitolul 2.3.3.2. Crearea mapării, și returnează toate rândurile găsite în tabela respectivă. În continuare, rezultatele sunt filtrate folosind funcția filter_by() având unul sau mai multi parametrii cu nume. După filtrare, se apelează funcția first() pentru a returna doar primul rezultat găsit. Alte posibilități pentru returnarea rezultatelor sunt funțiile one() și all().

În acest moment, proprietatea id din user_obj are o valoare asignată automat datorită faptului că a fost marcată ca fiind o coloană ce este cheie primară pentru tabela users.

user_obj.id  # 1

După persistarea obiectului user_obj se obervă faptul că acesta este unul și același cu obiectul db_user extras din baza de date.

user_obj is db_user  # True

Acest lucru se datorează unui concept numit relație de identitate care se asigură că toate operațiile ce au loc asupra unui rând din interiorul unei sesiuni folosesc același set de date. Odata ce un obiect este prezent în sesiune și deține o cheie primară, toate instrucțiunile SQL pe acea sesiune vor returna același obiecte pentru cheia primară respectivă, iar în cazul în care se va încerca inserarea unui alt obiect cu o cheie primară identică cu primul, se va ridica o eroare.

Sesiunea observă în timp real dacă se modifică un obiect care face referire la un rând dintr-o tabelă a bazei de date. Aceste informații sunt accesibile prin proprietatea dirty a sesiunii.

db_user.password = 'new_password'

session.dirty  # IdentitySet([<User(name='Yuno Gasai', password='new_password')>])

Pentru a persista noile schimbări în baza de date se foloșeste funcția commit() a sesiunii.

session.commit()

Aceasta reprezintă de fapt modalitatea executării manuale a procesului de evacuare din sesiune. Dupa executarea funcției, sesiunea execută comanda UPDATE ce actualizează informația din tabel.

Sesiunea lucrează la nivel de tranzacții care, conform capitolului 2.1.2. Avantaje PostgreSQL, sunt atomice, iar orice schimbare făcută poate fi derulată, daca se dorește acest lucru, înainte de a fi persistată.

fake_user = User(name='fakeuser', password='12345')

session.add(fake_user)

session.query(User).filter_by(name='fakeuser').first() # <User(name='fakeuser', password='12345')>

fake_user in session  # True

session.rollback()

session.query(User).filter_by(name='fakeuser').first() # None

fake_user in session  # False

2.3.3.5. Construirea relațiilor

Aplicațiile complexe necesită cu siguranță mai mult de un tabel în care informațiile pot fi stocate, iar aceste informații sunt, de cele mai multe ori, legate între ele prin dependențe. În cazul de față, se presupune că mai este adăugat încă un tabel, numit addresses, ce va stoca adresele utilizatorilor.

from sqlalchemy import ForeignKey

from sqlalchemy.orm import relationship

class Address(Base):

__tablename__ = 'addresses'

id = Column(Integer, primary_key=True)

email_address = Column(String, nullable=False)

user_id = Column(Integer, ForeignKey('users.id'))

user = relationship("User", backref=backref("addresses"))

În codul de mai sus este introdus conceptul de cheie străină folosit în SQLAlchemy prin intermediul construcției ForeignKey(). Aceasta este o directivă aplicată funcției Column ce constrânge elementele din această coloană să ia doar valori care sunt prezente în coloana dată ca argument pentru ForeignKey(), adica coloana id din tabela users. Prin această funcționalitate se realizează referențierea instanțelor din alte tabele și se crează legaturile propriu-zise.

O altă directivă, relationship(), instruiește ORM-ul să lege proprietatea user de instanța corespunzătoare din tabela users. În mod implicit, relationship() folosește relațiile definite de directiva ForeignKey() pentru a deduce natura legăturii dintre cele două tabele, însă, atunci când există mai multe chei străine care se referă la același tabel, acest lucru nu este posibil și se folosește proprietatea foreign_keys de pe relationship() pentru a specifica un vector ce cuprinde toate cheile primare.

De cele mai multe ori, este nevoie ca o relație să fie oglindită în celălalt tabel. Prin folosirea directivei relationship() acest lucru poate fi specificat ușor prin proprietatea backref. Această proprietate specială primește ca parametru funcția backref() care instruiește ORM-ul să creeze o proprietate numită addresses pe tabela users care reprezintă oglinda relației deja existente.

Pentru a crea propiu zis tabela addresses, trebuie folosită funcția create_all() din clasa Base încă o dată.

Base.metadata.create_all(engine)

În acest moment, atunci când se crează un obiect de tip User, acesta va avea un membru, addresses, ce reprezinte o colecție, inițial goală.

maya = User(name='Maya', password='n3wp4s$')

maya.addresses  # []

Implicit, colecția addresses este reprezentată ca un vector, deci, poate fi tratat ca atare.

maya.addresses.append(Address(email_address='maya@askaround.com'))

maya.addresses.append(Address(email-address='word_email@askaround.com'))

Deoarece există o relație bidirecțională între tabelele users și addresses, elementele adăugate în orice parte a relației devin imediat vizibile și la polul opus. Acest comportament se datorează evenimentelor ce se declanșează la schimbarea atributelor.

address = DBSession.query(Address).filter(email_address='maya@askaround.com')

address.user  # <User(name='maya', password='n3wp4s$')>

Pentru a adăuga noul utilizator în baza de date se folosește obiectul sesiune:

session.add(maya)

session.commit()

2.4. Sistemul de operare Android

2.4.1. Informații generale

Rolul unui sistem de operare este de a comunica cu hardware-ul, de a furniza servicii pentru aplicații și de a gestiona execuția software-ului printr-un set de reguli ce trebuiesc urmate pentru a interacționa cu diverse părți ale sistemului. De asemenea, sistemul de operare mai are și următoarele responsabilități:

Gestionarea memoriei – determinarea capacității maxime de memorie alocată pentru o aplicație și partajarea memoriei totale a unui dispozitiv astfel încât aceasta să poată fi utilizată cât mai optim de toate aplicațiile.

Multitasking – oferă posibilitatea ca mai multe aplicații să ruleze în același timp.

Permisiuni – administrarea permisiunilor pentru diverse fișiere și zone de memorie.

Accesul la internet

Securitate

Interfața cu utilizatorul – responsabilitatea de a desena pe ecranul dispozitivului elementele grafice ale aplicației curente, precum și alte elemente ale unor aplicații aflate în plan secundar, în cazul în care acestea ar trebui să fie vizibile utilizatorului.

Sistemul de operare Android are la bază o distrbuție Linux. El a fost construit și optimizat pentru a rula pe dizpozitive mobile – smartphone-uri, tablete, etc. – motiv pentru care oferă un suport mult mai mare în ceea ce privește interacțiunea printr-un ecran tactil, spre deosebire de alte sisteme de operare care se bazeaza mai mult pe folosirea unor componente de intrare sau ieșire pentru a interacționa cu un utilizator.

Acest sistem de operare a devenit foarte popular în ultimul deceniu, atât pentru dinamicitatea sa, cât și pentru mediul de dezvoltare deschis (open source) ce permite dezvoltatorilor să creeze cu ușurință diverse aplicații și să le distribuie prin intermediul unui magazin virtual. De asemenea, aplicațiile dezvoltate pot fi monetizate cu o foarte mare ușurință, lucru care, din nou, atrage foarte mulți dezvoltatori.

Orice aplicație este îmbachetată într-un fișier ce poartă extensia .apk, prin intermediul căruia poate fi instalată pe orice dispozitiv care rulează sistemul de operare Android și care respectă cerințele impuse de aplicație.

Odată instalate, aplicațiile trăiesc într-un ecosistem propriu, ce oferă un mediu securizat în care acestea să poată rula, cu următoarele caracteristici:

Implicit, sistemul de operare atribuie oricărei aplicații un identificator unic (ID) care nu este cunoscut acesteia și, cu ajutorul căruia, sistemul de operare poate decide drepturile pe care aplicația le are asupra celorlalte fișiere.

Orice proces are o mașină virtuală proprie. Astfel, codul acesteia nu poate interacționa cu cel ale altor procese.

Un proces poate încapsula o singură aplicație. Astfel, sistemul de operare se asigură de faptul că, atunci când o componentă a aplicației respective trebuie pornită, există un singur proces ce trebuie rulat.

Prin urmare, orice aplicație rulează într-un mediu izolat care nu îi influențează performanța, însă, renunță la controlul oricărei entități aflate în afara mediului din care aceasta face parte.

Dezvoltarea unei aplicații complexe implică totuși necesitatea interacționării cu diverse elemente – cum ar fi fișiere sau date ale altei aplicații. Spre exemplu, unul dintre cele mai mari avantaje ale dezvoltării pe Android este reutilizabilitatea componentelor. Nu este necesar ca o aplicație să includă funcționalități complete pentru a fi utilizată, ci, se poate folosi de alte aplicații mai mici pentru a realiza obiective generale.

Prin urmare, un jurnal electronic poate permite utilizatorilor să încarce poze în înregistrările lor, dar nu trebuie neapărat să include funcționalitatea ce face posibilă capturarea unei imagini. În schimb, se poate baza pe existența unei alte aplicații deja instalată, adresându-i o cerere. Componenta responsabilă cu capturarea imaginilor primește cererea, o procesează și întoarce rezultatul primei aplicații care avea nevoie de imaginea respectivă.

Pentru a facilita comunicația dintre doua aplicații, sunt puse la dispoziție următoarele metode:

Cele doua aplicații pot avea același ID, caz în care, deși rulează în cadrul unor procese diferite, sunt tratate ca fiind o singură entitate, deci pot accesa fișierele pe care le dețin. Este posibil și ca cele doua aplicații să fie rulate în interiorul aceluiași proces și chiar ca acestea să foloseasca aceeași mașină virtuală. Pentru acest lucru, este necesar ca aplicațiile să dețină aceeași semnătură.

O aplicație poate cere permisiuni asupa unor date, urmând ca utilizatorul să iși exprime în mod explicit acordul pentru acordarea lor.

2.4.2. Arhitectura MVC și aplicarea acesteia în dezvoltarea Android

Creșterea complexității aplicațiilor a determinat apariția problemelor legate de diversele dependențe ale componentelor din care acestea erau compuse. De multe ori erau necesare modificări doar în părți din cod, însă, datorită agregării modulelor, acest lucru ducea la modificarea tuturor componentelor care depindeau într-un mod sau altul de cel modificat din simplul fapt că acestea erau prea strâns legate.

Arhitectura MVC soluționează această problemă prin implementarea conceptului de separare a grijilor în încercarea de a separa într-un mod cât mai clar și eficient părțile din care este alcătuită o aplicție. Astfel, se crează trei categorii de componente: model, vedere și controler –. În mod convențional, fiecare din aceste categorii sunt responsabile pentru a îndeplini un rol bine stabilit fiind, în contrast cu vechea arhitectură, foarte slab legate unele de celelalte:

Prin urmare, rolul modelului este de a stoca și administra datele aplicației, de a stabili conexiunea cu rețeaua sau cu o interfață de programare a unei aplicații (application programming interface – API).

Vederea reprezintă o vizualizare a datelor ținute în model, fiind practic interfața cu utilizatorul.

În cele din urmă, controlerul reprezintă stratul de logică al aplicației, iar responsabilitatea sa este de a reacționa atunci când au loc evenimente declanșate de comportamentul unui utilizator și de a actualiza datele stocate în model atunci când acest lucru este necesar.

Într-o arhitectură MVC tradițională, vederea este responsabilă cu preluarea acțiunilor efectuate de către un utilizator și propagarea lor către controler, care le prelucrează și eventual modifică datele din model, dacă este necesar. Vederea este informată de schimbarea datelor de către modelul propriu-zis.

În sistemul de operare Android, arhitectura MVC există sub forma unui caz particular, aceasta purtând numele de model-vedere-prezentator (model-view-presenter – MVP). Acest caz poate fi implementat în diverse metode, însă cea prezentată aici, și una dintre cele mai importante de altfel, este cea a vederii pasive.

Metoda vederii pasive separă ocupațiile de prezentare și logică în două componente specializate – vedere și controler –, unde controlerul este responsabil pentru răspunderea evenimentelor declanșate de utilizator și de logica aplicației, iar vederea este componenta vizuală.

În figura 2.4.2.2 – Arhitectura MVC în contextul Android se observă faptul că nu mai există o legătură directă între vedere și model. Comunicarea dintre cele două are loc prin intermediul prezentatorului, acestuia revenindu-i rolul de a decide când este necesar să actualizeze vederea. În consecință, vederile în arhitectura MVC conțin mai multă logică pentru a putea răspunde la notificări și la procesări de date. În cazul arhitecturii MVP, această logică este localizată în prezentator, reducând rolul vederilor la simple containere ce afișează informațiile pe care le primesc.

Datorită faptului că toate aplicțiile Android sunt construite după implementarea de vedere pasivă a arhitecturii MVP – care presupune ca orice obiect al acesteia să fie un model, o vedere sau un controler – se pot face următoarele afirmații:

Obiectele model nu are cunoștintă de contextul în care se află în aplicație, scopul său fiind doar stocarea și administrarea datelor. În sistemul de operare Android, majoritatea claselor care nu extind o clasă nativă – cum ar fi Activity sau Fragment – sunt obiecte model, iar toate aceste clase constituie stratul modelelor.

Obiectele vedere sunt folosite pentru a reprezenta interfața cu utilizatorul. Acestea au funcționalități ce le permit să deseneze componentele proprii. Toate clasele care pot fi vizualizate pe ecran fac parte din această categorie și formează stratul vederilor. Acestea poartă numele de scheme (schemă – layout).

Obiectele controler au rolul de a lega cele două tipuri de obiecte precizate mai sus. Ele conțin de fapt logica aplicației și sunt concepute pentru a răspunde la diverse evenimente declanșate de obiecte vedere și pentru a administra circulația datelor între vederi și modele.

2.4.3. Rolul activităților în sistemul de operare Android

În general, aplicațiile Android conțin o multitudine de activități care sunt slab legate – chiar independente în mare parte – între ele, iar fiecare activitate se specializează în execuția unei singure funcționalități.

Una dintre aceste activități este desemnată ca fiind punctul de intrare în aplicație, aceasta fiind prima executată la pornirea aplicației. În urma navigării utilizatorului prin colecția de activități disponibile, sistemul de operare crează o stivă ce are ca scop menținerea unui istoric al activităților accesate pentru a permite utilizatorului să revină la oricare din ele atunci când dorește.

O clasă ce va reprezenta o activitate trebuie să extindă clasa Activity oferită de librăria nativă și să implementeze cel puțin metoda onCreate(bundle) din ciclul de viață al unei activități. Această metodă are practic rolul de constructor, deoarece în corpul ei se realizează operațiile de inițializare a variabilelor activității.

Conform teoriei specificate în capitolul 2.4.2. Arhitectura MVC și aplicarea acesteia în dezvoltarea Android, activitățile au de fapt rolul de prezentator în aplicație, fiind intermediarul dintre vedere și model. Datorită faptului că modelele sunt tot clase care doar descriu o structură a informațiilor, acestea pot fi accesate ușor, însă, în cazul vederilor, procedura este mai complexă, deoarece trebuie specificată manual legătura dintre ele și prezentatori. Acest proces este simplificat prin folosirea unei funcții ce execută toți pașii necesari fără a fi nevoie de intervenția dezvoltatorului. Acestă metodă poartă numele de setContentView(int). Ea primește ca argument identificatorul unic al unei resurse layout care va reprezenta interfața cu utilizatorul, după care, în continuarea corpului metodei onCreate(), se apelează funcția findViewById(int) pentru a reține toate referințele componentelor existente.

Sistemul de operare administrează activitățile prin intermediul unei stive. Atunci când o nouă activitate este creată, aceasta este împinsă în capul stivei, fiind pusă în starea de execuție, însă activitățile existente până la momentul respectiv nu dispar, ci rămân în stivă în starea de așteptare. O activitate se poate afla într-una din următoarele patru stări:

Activă (sau în execuție) – aceasta este starea de execuție asumată de activitatea aflată în capul stivei.

Pauză – o activitate nu mai este în prim plan (focus), însă este încă vizibilă – mai exact, dacă a apărut o altă activitate ce nu ocupă tot ecranul, dar se află deasupra activității în cauză. O activitate afltă în această stare rămâne în viață – adica, își menține informțiile despre membrii pe care îi are și rămâne atașată managerului de ferestre –, însă poate fi distrusă în cazul în care sistemul de operare rămâne fără memorie.

Oprită – în momentul în care o activitate este complet ascunsă, aceasta se află în starea oprită. Această stare este similară cu cea de pauză, cu excepția faptului că activitatea nu mai este legată de managerul de ferestre și există o șansă mai mare ca aceasta să fie distrusă atunci când sistemul de operare are nevoie de resurse.

Figura 2.4.3.1 – Ciclul de viață al unei activități arată stagiile prin care trece o activitate pe durata existenței acesteia. Din analiza pașilor prezentați, se pot afirma următoarele:

Întreaga durată de viață unei activități are loc între funcțiile onCreate(), unde aceasta iși definește layout-ul și inițializează toate datele necesare, și onDestroy(), unde eliberează resursele și memoria alocată.

Durata de viață vizibilă din punctul de vedere al unui utilizator se află între funcțiile onStart() si onStop(). În această perioadă, activitatea este vizibilă.

Durata de viață din prim plan are loc între funcțiile onResume() și onPause(), unde utilizatorul poate interacționa cu componentele activității.

În general, o activitate urmează următoarea calea într-un ciclu de viață:

onCreate() – metoda apelată atunci când activitatea este creată. Aici se realizează legătura dintre prezentator și vedere prin apelarea funției setContentView(int) și se preiau referințele către elementele conținute de vedere. Obiectul de tip Bundle furnizat conține o stare salvată anterior prin metoda onSaveInstanceState(Bundle).

onRestart() – apelată după ce activitatea a fost oprită și urmează a fi repornită.

onStart() – apelată imediat după momentul în care activitatea devină vizibilă utilizatorului.

onResume() – apelată atunci când începe interacțiunea dintre activitate și utilizator. În acest moment, activitatea se află în capul stivei din sistemul de operare.

onPause() – apelată înainte ca activitatea să intre în starea de pauză sau de oprire. Aici se salvează infomațiile curente pentru a fi persistate în obiectul de tip Bundle ce va fi transmis ca parametru funcțiilor onResume() și onStop().

onStop() – apelată în momentul în care activitatea nu mai este vizibilă utilizatorului. Acest lucru se poate întâmpla dacă aceasta este distrusă sau o altă activitate este pornită sau este afișată deasupra celei în cauză. Dacă utilizatorul revine la aplicația inițială, se apelează metoda onRestart(), altfel activitatea este distrusă definitiv prin metoda onDestroy().

onDestroy() – ultima metodă apelată înainte ca activitatea să fie complet distrusă. Este responsabilă cu golirea memoriei și persistarea informațiilor nesalvate.

Sistemul de operare încearcă să mențină în viață un proces pentru o perioadă cât mai lungă de timp, însă, la un moment dat, va trebui să le distrugă pe cele vechi atunci când va avea nevoie de resurse. Decizia de a face acest lucru este strâns legată de natura interacțiunii dintre utilizator și procesul respectiv. Procesele sunt clasate în anumite cateogrii ce le oferă o prioritate de supraviețuire după cum urmează:

Activitatea din prim plan – este cea care se află în capul stivei și are prioritatea cea mai mare de a rămâne în viață. Aceasta este distrusă doar în ultima instanță, în cazul în care încearcă să utilizeze mai multă memorie decât este disponibilă.

Activitate vizibilă – de asemenea considerată foarte importantă. Este distrusă în aceleași condiții ca activitatea din prim plan.

Activitate de fundal – poate fi distrusă în siguranță pentru a recupera resurse. Dacă utilizatorul revine la activitatea respectivă, este apelată metoda onCreate(Bundle) cu obiectul de tip Bundle salvat în metoda onSaveInstanceState(Bundle) pentru a putea reface starea în care se afla când a fost distrusă.

Procesul gol – nu conține nicio activitate sau alt tip de componente. Din această categorie fac parte serviciile și clasele de tip BroadcastReceiver. Datorită faptului că aceste activități sunt primele distruse de către sistemul de operare, este indicată folosirea lor în contextul unei activități propriu-zise pentru a moșteni prioritatea acesteia.

2.4.4. Structura unei aplicații Android

După crearea unui nou proiect Android, acesta se regăsește sub următoarea formă:

Directorul (director – folder) Java conține toate fișierele .java care fac parte din proiect. În mod implicit, după crearea proiectului, acesta conține fișierul MainActivity.java care este folosită pentru a porni aplicația.

Folderul res conține toate toate resursele vizuale, fie ele scheme de vederi cu limbaj de marcare extensibil (eXtensible Markup Language – XML) sau diverse imagini. Cele mai importante sub-foldere și fișiere sunt:

drawable-Xdpi (X poate lua valorile l, m, h, xh, xxh, xxxh pentru a satisface o gamă cât mai largă de ecrane și rezoluții) – pe măsură ce sunt adăugate resurse vizuale, acestea pot fi împărțite pe cateogrii pentru a satisface varietatea mare de ecrane și rezoluții ale dispozitivelor care rulează sistemul de operare Android.

layout – în acest director se află fișierele XML care definesc interfața cu utilizatorul.

values – acest folder conține o colecție de resurse precum constante ce definesc siruri de caractere, culori sau dimensiuni.

AndroidManifest.xml – configurația întregii aplicații ce descrie caracteristicile ei, permisiunile necesare rulării și componentele utilizate.

build.gradle – un fișier generat automat ce conține diverse informații cum ar fi versiunea echipamentului de dezvoltare software (software development kit – SDK) folosită și versiunea, numele și descrierea aplicației.

Fișierul MainActivity.java conține o clasă cu același nume care reprezintă puncul de intrare al aplicației.

package com.example.helloworld;

import android.support.v7.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

}

Această clasă își va asuma rolul de prezentator din arhitectura MVP. Apelul funcției setContentView(R.layout.activity_main) construiește legătura propriu-zisă dintre prezentator și vedere. Obiectul R se referă la resursele existente în folderul res. Acestor resurse le este asociat un identificator unic, în particular chiar numele acestora, folosit pentru a putea fi referențiate în interiorul aplicației. Astfel, R.layout.activity_main este identificatorul ce reprezintă fișierul activity_main.xml din folderul res/layout. Acolo este definită interfața cu utilizatorul:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent" >

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_centerHorizontal="true"

android:layout_centerVertical="true"

android:padding="@dimen/padding_medium"

android:text="@string/hello_world"

tools:context=".MainActivity" />

</RelativeLayout>

Fișierul AndroidManifest.xml trebuie să declare toate componentele din care este formată aplicația. Aceasta reprezintă practic interfața dintre aplicație și sistemul de operare Android.

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.example.tutorialspoint7.myapplication">

<application

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:supportsRtl="true"

android:theme="@style/AppTheme">

<activity android:name=".MainActivity">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

</manifest>

Partea importantă a acetei configurații se află în interiorul etichetei <activity>. Ea este folosită pentru a declara activitățile aplicației, iar proprietatea name a acesteia specifică clasa care va fi considerată prezentatorul activității respective.

Fiecare activitate este pornită de sistemul de operare folosind un obiect de tip Intent[13]. Astfel, trebuiesc definite anumite filtre care specifică ce tip de intenție este necesară pentru a permite activității să ruleze. Filtrul definit are setată acțiunea de android.intent.action.MAIN, ce specifică faptul că activitatea în care a fost definit filtrul este punctul de intrare al aplicației și, categoria de android.intent.category.LAUNCHER, ce indică faptul că, atunci când aplicația este pornită, activitatea care va fi rulată este cea care conține filtrul.

3. Arhitectura aplicației

În continuare va fi prezentat modul în care se leagă componentele care au fost descrise până la acest pas în contextul utilizării acestora pentru aplicația Ask Around.

3.1. Descrierea arhitecturii globale a proiectului

În figura 3.1.1 – Arhitectura globală a aplicației este reprezentată funcționarea aplicației la un nivel abstractizat, începând cu mediul de stocare a datelor, conexiunea mediului respectiv cu partea de server (backend), backend-ul, comunicarea ce are loc între server și aplicația de client și, în final, aplicația propriu-zisă care va fi folosită de utilizatori.

Pentru stocarea informațiilor necesare este folosită baza de date PostgreSQL datorită capabilităților sale menționate în capitolul 2.1.2. Avantaje PostgreSQL ce îi oferă funcționalitățile în baza cărora a fost aleasă pentru acest proiect. Se observă faptul că tabelele principale ale bazei de date sunt users și questions, însă schema completă a bazei de date va fi discutată în detaliu în capitolul 3.2. Arhitectura serverului..

Comunicarea dintre SGBD și server are loc prin intermediul motorului SQLAlchemy care traduce instrucțiunile SQL scrise în limbajul Python pentru a fi înțelese de SGBD. Configurația acestui motor se realizează printr-un fișier de mediu (environment) local și este construit prin folosirea unui model de proiectare structural, anume cel singleton[16]. Acesta presupune existența unei singure instanțe a unui obiect în întreaga aplicațe la orice moment de timp.

Server-ul este responsabil cu preluarea cererilor trimise de către utilizatori, executarea lor și returnarea răspunsului către originea cererii. În vederea îndeplinirii acestor funcții, sunt declarate rute ale căror rol este de a asculta cereri HTTP, iar la primirea unor astfel de cereri, de a le înainta către controlerul aferent pentru a fi rezolvate. Aceste rute se adaugă URL-ului de bază configurat pe server din fișierul de environment. Spre exemplu, dacă server-ul este configurat pe adresa http://ask-around.ngenh.com și portul 6543, iar un utilizator dorește să facă o cerere pe ruta /api/auth/check, URL-ul final pe care se va trimite cererea este http://ask-around.ngenh.com:6543/api/auth/check.

Fiecare dintre aceste rute trebuie să fie unice prin perechea constituită din URL și metoda HTTP apelată, iar fiecărei astfel de combinații îi va fi asociat un controler, fie el o funcție sau o clasă, ce va fi executat în momentul primirii cererii. Prin urmare, existența rutelor:

http://ask-around.ngenh.com:6543/api/question cu metoda HTTP GET.

http://ask-around.ngenh.com:6543/api/question cu metoda HTTP POST.

Este permisă datorită faptului că metodele la care acestea răspund sunt diferite.

Securitatea aplicației este asigurată de sistemul de autentificare furnizat de framework-ul Pyramid prezentat în capitolul 2.2.2. Prezentare generală care se bazează pe un token de autentificare furnizat oricărui utilizator în momentul logării prin intermediul căruia determină drepturile pe care le are utilizatorul respectiv. Token-ul poate fi și el securizat prin folosirea unei metode specifice de criptare cu o cheie predefinită și setarea unei date de expirare a acestuia.

Pentru a beneficia de o securitate sporită, server-ul implementează conceptul de partajare a resurselor din origini diferite (Cross Origin Request Sharing – CORS). Astfel, se acceptă cereri doar din domenii predefinite și se diminuează riscul de a rula scripturi malițioase provenite din surse necunoscute.

Aplicația de client (frontend) reprezintă practic consumatorul ce se folosește de serviciile oferite de server prin intermediul cererilor HTTP. Informațiile sunt transmise prin intermediul acestor cereri sub formatul notației de obiect JavaScript (JavaScript Object Notation – JSON)[14]. Astfel, se transmite o cantitate minimă de infomații și aceasta este reprezentată cu ajutorul unui format standard folosit în dezvoltare, lucru ce ușurează utilizarea lor în orice limbaj de programare.

Responsabilitatea aplicației de client este de a furniza interfața cu utilizatorul prin care acesta poate interacționa cu informațiile ținute în baza de date și le poate modifica în mod dinamic. Datorită faptului că rulează în sistemul de operare Android, beneficiează în mod implicit de securitatea oferită de acesta. Prin urmare, ea rulează într-un mediu semi-izolat, cu permisiuni minimale, fiind ferită de activitatea altor aplicații.

Toate funcționalitățile principale ale aplicației se regăsesc într-o singură activitate, ce se folosește de două concepte puternice – comunicarea prin evenimente și utilizarea fragmentelor dinamice – pentru a administra într-un mod cât mai optim resursele diponibile și a modulariza într-un mod cât mai eficient aplicația. Aceste concepte vor fi detaliate în capitolul 4.2. Comunicarea prin evenimente în Android.

Pentru a putea construi cereri HTTP și a prelua răspunsul lor trimis de către server se va folosi librăria Retrofit. Aceasta expune decoratori și interfețe ce ajută la realizarea comunicării client-server utilizând un strat de securitate prin folosirea token-ului de autentificare generat de server. Acesta este atașat fiecărei cereri înainte de a fi trimisă pentru ca server-ul să poată identifica autenticitatea ei.

3.2. Arhitectura serverului.

3.2.1. Construirea a bazei de date din modele SQLAlchemy

Pe langă responsabilitățile legate de preluarea, executarea și returnarea unui răspuns pentru cererile inițiate de un utilizator, server-ul trebuie să construiască și baza de date printr-un proces ce poartă numele de migrare (migration). Acesta presupune crearea tabelelor fizice în baza de date prin migrarea informațiilor scrise prin clase după modelul explicat în capitolul 2.3.3.2. Crearea mapării.

În continuare, va fi prezentata modalitatea de creare a tabelelor folosite de aplicația Ask Around:

Clasa User, reprezentând tabelul users:

class User(Base, DictSerializable):

__tablename__ = 'users'

__table_args__ = dict(schema='user')

id = Column(types.Id, primary_key=True)

email = Column(types.String, nullable=False, unique=True)

first_name = Column(types.String, nullable=True, default="")

last_name = Column(types.String, nullable=False, default="")

password = Column(types.Password, nullable=False)

active = Column(types.Boolean, nullable=False, default=True)

is_admin=Column(types.Boolean, nullable=False, default=False)

Clasa UserQuerySearch, reprezentând tabelul user_query_searches:

class UserQuerySearch(Base, DictSerializable):

__tablename__ = 'user_query_searches'

__table_args__ = dict(schema='user')

id = Column(types.Id, primary_key=True)

user_id = Column(types.Id, ForeignKey(User.id))

query = Column(types.String)

times = Column(types.Number, default=1)

Clasa UserCategorySearch, reprezentând tabelul user_category_searches:

class UserCategorySearch(Base, DictSerializable):

__tablename__ = 'user_category_searches'

__table_args__ = dict(schema='user')

id = Column(types.Id, primary_key=True)

user_id = Column(types.Id, ForeignKey(User.id))

category_id = Column(types.Id, ForeignKey(Category.id))

times = Column(types.Number, default=1)

Clasa Question, reprezentând tabelul questions:

class Question(Base, DictSerializable):

__tablename__ = 'questions'

__table_args__ = dict(schema='question')

id = Column(types.Id, primary_key=True)

user_id = Column(types.Id, ForeignKey(User.id, ondelete="CASCADE",

onupdate="CASCADE"))

title = Column(types.Label, nullable=False)

content = Column(types.LongDescription, nullable=False)

created_at = Column(types.DateTime(timezone=True), nullable=False,

default=func.now(), server_default=func.now()

)

category_ids = Column(types.ARRAY(types.Integer))

user = relationship("User", foreign_keys=user_id,

backref=backref("questions", order_by=id,

single_parent=True, uselist=True,

cascade="all, delete-orphan",

passive_deletes=True)

)

Clasa Reply, reprezentând tabelul replies:

class Reply(Base, DictSerializable):

__tablename__ = 'replies'

__table_args__ = dict(schema='question')

id = Column(types.Id, primary_key=True)

user_id = Column(types.Id, ForeignKey(User.id, ondelete="CASCADE",

onupdate="CASCADE"))

question_id = Column(types.Id, ForeignKey(Question.id,

ondelete="CASCADE", onupdate="CASCADE"))

message = Column(types.UnicodeText, nullable=False)

created_at = Column(types.DateTime(timezone=True), nullable=False,

default=func.now(), server_default=func.now())

user = relationship("User", foreign_keys=user_id)

question = relationship("Question", foreign_keys=question_id,

backref=backref("replies",

order_by=created_at.desc(),

single_parent=True,

uselist=True,

cascade="all, delete-orphan",

passive_deletes=True)

)

Clasa Category, reprezentând tabelul categories:

class Category(Base, DictSerializable):

__tablename__ = 'categories'

__table_args__ = dict(schema='question')

id = Column(types.Id, primary_key=True)

name = Column(types.Label, nullable=False)

description = Column(types.LongDescription, nullable=False)

Relația dintre categorii și întrebări nu este vizibilă, deoarece este una de tipul ”mai mulți la mai mulți” (many-to-many) și necesită un tabel special de mapare a legăturii.

question_categories = Table('question_categories', Base.metadata,

Column('question_id', types.Integer,

ForeignKey(Question.id)),

Column('category_id', types.Integer,

ForeignKey(Category.id))

)

Question.categories = relationship(Category, secondary=question_categories,

backref=backref('questions'))

După ce tabelele propriu-zise sunt create, există posibilitatea de a insera niște date, fictive sau nu, pentru a putea fi utilizabilă. Acest proces poartă numele de ”însămânțare” (seeding) a bazei de date.

with transaction.manager:

exists = DBSession.query(User).first()

if exists is None:

DBSession.add(User(email='admin@askaround.com',

first_name='admin', last_name='askaround',

active=True, is_admin=True,

password='=Q1qF…'))

DBSession.add(User(email='user@askaround.com',

first_name='user', last_name='user',

active=True, is_admin=False,

password='io3Wd…'))

# ––––– code omitted –––––

Seed-ul bazei de date are loc în interiorul unui bloc ce folosește managerul de tranzacții al SQLAlchemy-ului, transaction.manager, pentru a putea executa la nivel atomic instrucțiunile SQL. Odată ajuns în acel bloc, trebuie să se verifice dacă există cel puțin un utilizator în baza de date. Dacă există, atunci procesul de seed a avut loc deja și nu mai este necesar.

Pentru a rula procesul de migrare, Pyramid expune în mod explicit un script care trebuie rulat într-un terminal ce va executa toate operațiile necesare.

initialize_[PROJECT_NAME]_db [ENVIRONMENT_FILE].ini ([OPTIONS])

În particular, script-ul pentru a inițializa baza de date pentru aplicația Ask Around este următorul:

initialize_ask-asround_db production.ini#ask-around

Fișierul production.ini conține toate informațiile necesare pentru a configura motorul SQLAlchemy și server-ul propriu-zis. În continuare, vor fi prezentate câteva informații relevante din acesta:

Configurarea URL-ului ce va fi folosit pentru conectarea motorului cu baza de date.

[app:ask-around]

# ––––- code omitted ––––––

sqlalchemy.url = postgresql+pypostgresql://ask-around:******@localhost/ask-around

# ––––- code omitted ––––––

Script-ul de inițializare a bazei de date utilizează țevi (țeavă – pipe) pentru a secționa codul din fișierul de mediu dat ca parametru. În mod implicit, acesta se uită după pipe-ul cu numele main, însă, daca se dorește utilizarea altei denumiri, acesta trebuie specificat după numele fișierului de mediu din comanda, delimitat de semnul diez (#).

În particular, datorită faptului ca pipe-ul principal din fișierul production.ini se numește ask-around, acest lucru este specificat în comandă: production.ini#ask-around.

Configurarea adresei și portului la care serverul va asculta cereri.

[server:main]

# ––––- code omitted ––––––

host = 0.0.0.0

port = 6543

3.2.2. Definirea rutelor și a controlerelor asociate

În acest moment, baza de date există fizic pe mediul de stocare și, în continuare, server-ul trebuie să definească rute pe care utilizatorii vor trimite cereri, iar controlerele asociate rutelor respective vor executa cererea și vor întoarce un răspuns.

Metoda folosită este identică cu cea prezentată în capitolul 2.2.3.1. Structura de bază a unei aplicații, folosindu-se același obiect de tip Configurator expus de framework-ul Pyramid. După apelarea metodei config.scan() sunt căutate toate rutele definite în aplicație și controlerul aferent este înregistrat.

În continuare va fi prezentată implementarea modulului de autentificare, ce utilizează funcții în postura de controler. Restul modulelor sunt implementate într-un mod diferit, utilizând clase, nu funcții, a cărui implementare va fi aprofundată în capitolul 4.1. Modularizarea codului din controlerele serverului..

Logarea unui utilizator

config.add_route('auth.login', 'api/auth/login', factory=context.Everyone)

Mai sus este definită ruta pe care utilizatorii vor putea trimite cererea de autentificare. Parametrul factory va fi discutat în capitolul 3.2.2. Contextul rutelor.

@view_config(route_name="auth.login", request_method='POST', renderer="json")

def auth_public(request):

assert(isinstance(request, Request))

assert(isinstance(request.response, Response))

json = request.json_body["credentials"]

try:

user_obj = DBSession.query(user.User) \

.filter_by(email=json['email'])\

.one()

except NoResultFound:

return HTTPForbidden("login failed")

# check password

if not verify_password(clear_text=json['password'],

encrypted=user_obj.password,

accept_clear_text=True):

return HTTPForbidden("login failed")

request.response.headerlist.extend(remember(request, user_obj.id))

return {'user': user_obj}

Funcția de mai sus este cea care reprezintă controlerul asociate rutei cu numele auth.login. Aceasta este decorată cu directiva @view_config() care specifică numele rutei, metoda HTTP pentru care va fi executat controlerul și protocolul folosit pentru a interacționa cu datele din cerere. În particular, ruta auth.login are un controler asociat definit prin funcția auth_public(), răspunde la o cerere HTTP de tip POST și folosește protocolul JSON.

Conform capitolului 2.2.3.1. Structura de bază a unei aplicații, această funcție este apelată cu un argument, request, ce reprezintă de fapt cererea propriu-zisă, însă, pentru a evita eventualele probleme, controlerul se asigură mai întâi de faptul că acel argument este într-adevăr o instanță a clasei Request, iar răspunsul asociat acesteia este o instanță a clasei Response.

În continuare, se interoghează baza de date folosind informațiile existente sub format JSON în corpul cererii pentru a găsi utilizatorul care urmează să fie autentificat. În caz de succes, se testează dacă valoarea câmpului password primit este egală cu cea stocată în baza de date. În final, dacă toate testele au fost trecute, se generează simbolul (simbol – token) de autentificare și se atașează pe răspunsul cererii. Un lucru important este faptul că token-ul generat va conține și identificatorul unic al utilizatorului care tocmai s-a autentificat.

Dacă oricare din testele de mai sus eșuează, execuția funcției se oprște și este returnat un răspuns informativ.

Verificarea autenticității unui utilizator:

config.add_route('auth.me, 'api/auth/me', factory=context.Everyone)

Modalitatea prin care se definește ruta este similară cu cea anterioră, exceptând valorile furnizate ca argumente.

@view_config(route_name="auth.me", request_method='GET', renderer="json")

def auth_me(request):

assert(isinstance(request, Request))

user_id = unauthenticated_userid(request)

if user_id is not None:

request.context._json_hints = json_hints

user_obj = DBSession.query(user.User) \

.filter_by(id=user_id) \

.one()

return user_obj

else:

return HTTPForbidden()

Această rută are rolul de a returna informații despre un utilizator logat, dacă token-ul de autentificare mai este încă valid. Funcția prin care se realizează acest lucru este unauthenticated_userid(). Aceasta primește ca parametru un obiect de tip Request și verifică valoarea variabilei cookie auth_tkt din antet (header) pentru a returna identificatorul unic al utilizatorului de la care a provenit cererea și îș folosește pentru a căuta informații referitoare la utilizatorul respectiv în baza de date, după care returnează rezultatul. Dacă token-ul respectiv nu există sau nu mai este valid, funcția returnează valoarea None și este ridicată o excepție de tipul HTTPForbidden.

Delogarea unui utilizator:

config.add_route('auth.logout', 'api/auth/logout', factory=context.Everyone)

# –––––––––––––––––––––––––

@view_config(route_name="auth.logout", request_method='GET', renderer="json")

def auth_logout(request):

assert(isinstance(request, Request))

request.response.headerlist.extend(forget(request))

return HTTPOk('Logged out', headers=request.response.headers)

Singura responsabilitate a aceste funcții este de a șterge token-ul de autentificare existent pe cerere folosind funcția de forget().

Verificarea conexiunii cu serverul:

config.add_route('auth.check', 'api/auth/check', factory=context.Everyone)

# –––––––––––––––––––––––––

@view_config(route_name="auth.check", request_method='GET', renderer="json")

def auth_check(request):

return HTTPOk('')

Singurul scop al funcției de mai sus este de a asigura un eventual client de faptul că există comunicare între acesta și el.

3.2.2. Contextul rutelor

Framework-ul Pyramid oferă posibilitatea granularizării accesului pe resursele server-ului printr-un sistem de autentificare care oferă posibilitatea definirii unui context și asociarea acestuia cu ruta ce se dorește a fi protejată.

Pentru a folosi acest sistem, el trebuie configurat pe obiectul de tip Configurator înainte de pornirea server-ului.

from pyramid.authentication import AuthTktAuthenticationPolicy

from pyramid.authorization import ACLAuthorizationPolicy

# ––––– configuration omitted –––––––

authentication_policy = AuthTktAuthenticationPolicy("4V3ry$3cr3tK3y", hashalg="sha512",

include_ip=True, debug=True, callback=get_user_permissions)

authorization_policy = ACLAuthorizationPolicy()

config.set_authentication_policy(authentication_policy)

config.set_authorization_policy(authorization_policy)

Principalele clase folosite sunt:

ACLAuthorizationPolicy care va permite utilizarea contextului pentru securizarea rutelor, motiv pentru care nu are nevoie de o configurare.

AuthTktAuthenticationPolicy care configurează politica de autentificare, cea responsabilă cu crearea token-ului de autentificare în urma logării cu succes a unui utilizator. Printre argumentele folosite se remarcă cel numit callback, reprezentând o funcție ce va fi apelată de fiecare dată când server-ul va primi o cerere. Acesta preia token-ul de autentificare de pe cerere, îl desface și folosește funcția get_user_permissions() pentru a crea contextul în care se află un utilizator.

def get_user_permissions(user_recv, request):

from .models import DBSession, user

from sqlalchemy.orm.exc import NoResultFound

if type(user_recv) is not int:

return None

user_obj = getattr(request, "user_object", None)

if user_obj is None:

try:

user_obj = DBSession.query(user.User).get(user_recv)

request.user_object = user_obj

except NoResultFound:

return None

if user_obj.is_admin is True:

return ['admin', user_obj.email, user_obj.id]

if user_obj.is_admin is False:

return ["user", user_obj.email, user_obj.id]

Funcția get_user_permissions() primește ca parametru identificatorul unic stocat în token-ul cererii, împreună cu cererea propriu-zisă și încearcă să determine contextul unui utilizator în funcție de anumite proprietăți. De asemenea, se poate menține un obiect de tip User pe request, tehnică supranumită ”depozitarea datelor” (caching), pentru a putea fi refolosit pe viitor.

Vectorul returnat în final reprezintă lista ce formează contextul propriu-zis în care se află utilizatorul și în baza căruia se va decide dacă acesta poate sau nu să acceseze o anumită resursă.

În particular, dacă se presupune un utilizator administrator – cu email-ul yuno.gasai@askaround.com și identificatorul unic 1 – trimite o cerere cu un token de autentificare valid, funcția get_user_permissions() va returna un vector conținând valorile admin, yuno.gasai@askaround.com și 1.

În capitolul 3.2.2. Definirea rutelor și a controlerelor asociate rutele declarate primeau un argument numit context, care se referă la contextul în care se va găsi aceasta.

config.add_route('auth.me, 'api/auth/me', factory=context.Everyone)

Pentru a putea utiliza aceste valori, este necesară definirea unor clase care vor reprezenta listele de acces pentru fiecare valoarea a contextului.

Aplicația Ask Around folosește următoarele posibilități de context:

from pyramid.security import Allow

class Public:

__acl__ = [

(Allow, "admin", ("search", "list", "get", "add", "update",

"delete", "lookup", "email")),

(Allow, "user", ("search", 'list', "get", 'add', 'update',

'delete', "lookup", "email")),

]

def __init__(self, request):

pass

class Private:

__acl__ = [

(Allow, "admin", ("search", "list", "get", "add",

"update", "delete", "lookup")),

]

def __init__(self, request):

pass

class Everyone:

__acl__ = [

(Allow, "system.Everyone", ("search", "list", "get", "add",

"update", "delete", "lookup")),

]

def __init__(self, request):

pass

class Self:

def __init__(self, request):

self.id = request.matchdict['user_id']

self.__acl__ = [

(Allow, "admin", ("search", "list", "get", "add",

"update", "delete", "lookup")),

(Allow, self.id, ("search", 'list', "get", 'add',

'update', 'delete', "lookup", "email")),

]

Fiecare din aceste clase reprezintă un context în care se poate afla o resursă. Contextul, la rândul lui, conține o listă de acces formată din mai multe roluri însoțite de permisiunile asociate fiecăruia. Ele pot fi asumate de orice cerere care conține cel puțin o valoare în vectorul de context asociat ce se potrivește cu una din cele definite în lista de acces a contextului.

Pentru a decide dacă o cerere poate accesa o resursă, acesta trebuie să aibă cel puțin un rol din contextul în care se află resursa. În caz contrar, este returnat un răspuns de tipul HTTPForbidden.

În particular, ruta auth.login se află în contextul Everyone, deci poate fi accesată de orice cerere, din moment ce nu necesită un rol special. Însă, ruta questions, care va fi prezentată în capitolul 4.1. Modularizarea codului din controlerele serverului., se află în contextul Public.

config.add_route('questions', 'api/question', factory=context.Public)

Prin urmare, pentru a fi accesată, cererea trebuie să provină de la un utilizator autentificat cu rolul de user sau admin.

3.3. Arhitectura clientului

Similar cu fișierul de mediu existent pe partea de backend a proiectului, configurația aplicației Android se gasește într-un fișier ce poartă numele de build.gradle. În acesta sunt definite proprietăți precum numele aplicației, versiunea sa, versiunea SDK-ului minimă pentru a utiliza aplicația și identificatorul unic pe care îl va avea în cazul în care va fi încarcată pe magazinul de aplicații al Android-ului, anume Play Store.

android {

compileSdkVersion 25

buildToolsVersion "24.0.0"

defaultConfig {

applicationId "com.ngenh.askaround"

minSdkVersion 15

targetSdkVersion 25

versionCode 1

versionName "1.0"

vectorDrawables.useSupportLibrary = true

}

//   ––– omitted configuration –––

}

Tot în fișierul build.gradle sunt găsite și dependețele necesare rulării aplicației.

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

testCompile 'junit:junit:4.12'

compile(

[group: 'com.fasterxml.jackson.core',

name: 'jackson-core', version: '2.4.1'],

[group: 'com.fasterxml.jackson.core',

name: 'jackson-annotations', version: '2.4.1'],

[group: 'com.fasterxml.jackson.core',

name: 'jackson-databind', version: '2.4.1']

)

compile 'com.android.support:appcompat-v7:25.0.1'

compile 'com.android.support:design:25.0.1'

compile 'com.android.support:cardview-v7:25.0.1'

compile 'com.android.support:recyclerview-v7:25.0.1'

compile 'com.android.support:percent:25.0.1'

compile 'com.android.support:support-vector-drawable:25.0.1'

compile 'com.android.support:animated-vector-drawable:25.0.1'

compile 'com.squareup.retrofit2:retrofit:2.1.0'

compile 'com.squareup.retrofit2:converter-jackson:2.1.0'

compile 'com.android.support:support-v4:25.0.1'

compile 'com.weiwangcn.betterspinner:library-material:1.1.0'

compile 'com.github.PhilJay:MPAndroidChart:v3.0.1'

}

Cele mai importante dependențe sunt cele care provin din librăria com.android.support, ce expun diverse componente de interfață cu utilizatorul ce pot fi folosite în aplicație, respectiv cele care provin din librăria com.fasterxml.jackson.core, responsabile cu parsarea datelor din formatul JSON în obiecte ce pot fi folosite în mediul Java.

3.3.1. Configurarea conexiunii cu server-ul

Pentru a putea realiza comunicarea dintre client și server, se va folosi librăria Retrofit. Rolul acesteia este de a mapa o interfață definită de dezvoltator pe un serviciu HTTP, pentru a putea genera cereri. Răspunsurile primite vor fi mapate pe clase definite tot în interiorul aplicației folosind librăria Jackson, ce convertește răspunsul din format JSON în obiecte utilizabile în contextul limbajului Java.

Orice serviciu HTTP trebuie creat folosind Retrofit, însă, pentru a evita multiplicarea codului, este construită o clasă generală ce primește un argument generic reprezentând clasa resursei pentru care se definește serviciul HTTP.

package com.ngenh.askaround.middleware;

import android.content.Context;

import com.ngenh.askaround.Global;

import com.ngenh.askaround.custom_controls.NetworkStatus;

import retrofit2.Retrofit;

import retrofit2.converter.jackson.JacksonConverterFactory;

public class ServiceCreator {

public static <T> T retrofitService(final Class<T> requiredClass, Context context) {

if(!NetworkStatus.getInstance(context).isNetworkAvailable()){

return null;

}

Retrofit.Builder retrofitBuilder = new Retrofit.Builder()

.baseUrl(Global.API_LOCATION)

.addConverterFactory(JacksonConverterFactory.create());

if (Global.authToken != null) {

retrofitBuilder.client(HttpInterceptor.authInterceptor());

}

return retrofitBuilder.build().create(requiredClass);

}

}

Clasa ServiceCreator are o singură metodă statică publică, retrofitService(), care primește două argumente – clasa pentru care se va crea serviciul și contextul ei. În acest proces iau parte clasele ajutătoare NetworkStatus și HttpInterceptor.

Structura clasei NetworkStatus este de forma următoare:

package com.ngenh.askaround.custom_controls;

import android.content.Context;

import android.net.ConnectivityManager;

import android.net.NetworkInfo;

import android.support.design.widget.Snackbar;

import com.ngenh.askaround.R;

public class NetworkStatus {

private static NetworkStatus instance = new NetworkStatus();

static Context context;

public static NetworkStatus getInstance(Context ctx) {

context = ctx.getApplicationContext();

return instance;

}

public boolean isNetworkAvailable() {

ConnectivityManager connectivityManager

= (ConnectivityManager) context.getSystemService(

Context.CONNECTIVITY_SERVICE

);

NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();

boolean connected = activeNetworkInfo != null

&& activeNetworkInfo.isConnected();

if(!connected){

SnackbarWrapper snackbarWrapper =

SnackbarWrapper.make(context.getApplicationContext(),

context.getResources().getString(R.string.no_internet),

Snackbar.LENGTH_SHORT

);

snackbarWrapper.show();

}

return connected;

}

}

Aceasta este responsabilă cu menținerea unei singure instanțe a propriei clase care să furnizeze informații despre starea curentă a conectivității la internet a dispozitivului. La apelare, setează contextul pe care l-a primit ca argument pentru ca, mai apoi, să îl folosească în scop informativ, afisând un mesaj utilizatorului în caz de eșec de conexiune.

Astfel, funcția retrofitService() se folosește de aceasta funcționalitate pentru a nu crea un serviciu invalid în cazul în care nu există conectivitate la internet prin următorul test:

if(!NetworkStatus.getInstance(context).isNetworkAvailable()){

return null;

}

Conform capitolului 3.2.2. Definirea rutelor și a controlerelor asociate, token-ul de autentificare trebuie să fie prezent în orice cerere care încearcă să acceseze o parte protejată a server-ului. Prin urmare, acest token trebuie atașat antetului oricărei cereri, înainte de a fi trimisă. Acest lucru se realizează prin clasa ajutătoare HTTPInterceptor care are următoarea structură:

package com.ngenh.askaround.middleware;

import com.ngenh.askaround.Global;

import java.io.IOException;

import java.util.concurrent.TimeUnit;

import okhttp3.Interceptor;

import okhttp3.OkHttpClient;

import okhttp3.Request;

import okhttp3.Response;

public class HttpInterceptor {

public static OkHttpClient authInterceptor(){

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

httpClient.connectTimeout(Global.connectionTimeout, TimeUnit.MILLISECONDS);

httpClient.readTimeout(Global.connectionTimeout, TimeUnit.MILLISECONDS);

httpClient.addInterceptor(new Interceptor() {

@Override

public Response intercept(Interceptor.Chain chain) throws IOException {

Request original = chain.request();

Request request = original.newBuilder()

.header("Cookie", Global.authToken)

.method(original.method(), original.body())

.build();

return chain.proceed(request);

}

});

return httpClient.build();

}

}

În interiorul ei este expusă public o singură metodă statică, authInterceptor(), cu rolul de a intercepta cererile HTTP înainte de a părăsi aplicația și de a le atașa token-ul menționat.

Clasa Global are rolul de a ține evidența tuturor variabilelor globale ale aplicației pentru a putea fi ușor accesate și modificate. Cea mai relevantă dintre aceste variabile poartă numele de API_LOCATION reprezentând adresa folosită de aplicație pentru a se conecta la server.

package com.ngenh.askaround;

// ––––––- imports omitted –––––––

public class Global {

public static String API_LOCATION = "http://ask-around.ngenh.com:6543/api/";

public static int connectionTimeout = 5000;

// ––––––– code omitted –––––––

}

Aceasta este folosită în funcția retrofitService() ca punct de plecare în construirea oricărui serviciu din cadrul aplicației.

Retrofit.Builder retrofitBuilder = new Retrofit.Builder()

.baseUrl(Global.API_LOCATION)

.addConverterFactory(JacksonConverterFactory.create());

De asemenea, tot aici este specificat faptul că se va folosi convertorul Jackson pentru a deserializa răspunul JSON.

În final, trebuie definite interfețele care vor fi utilizate pentru a crea serviciile HTTP.

Interfața AuthService:

package com.ngenh.askaround.interfaces;

import com.ngenh.askaround.models.Auth;

import com.ngenh.askaround.models.UserObj;

import retrofit2.Call;

import retrofit2.http.Body;

import retrofit2.http.GET;

import retrofit2.http.POST;

public interface AuthService {

@POST("auth/login")

Call<UserObj> login(@Body Auth auth);

@GET("auth/logout")

Call<String> logout();

@POST("auth/register")

Call<UserObj> register(@Body UserObj user);

@GET("auth/check")

Call<String> checkAvailability();

}

Interfața QuestionsService:

package com.ngenh.askaround.interfaces;

import com.ngenh.askaround.models.CategoryObj;

import com.ngenh.askaround.models.QuestionObj;

import com.ngenh.askaround.models.ReplyObj;

import com.ngenh.askaround.models.ResponseList;

import java.util.Map;

import retrofit2.Call;

import retrofit2.http.Body;

import retrofit2.http.GET;

import retrofit2.http.POST;

import retrofit2.http.PUT;

import retrofit2.http.QueryMap;

import retrofit2.http.Url;

public interface QuestionsService {

@GET("question")

Call<ResponseList<QuestionObj.Question>> allQuestions(

@QueryMap Map<String, String> options

);

@GET()

Call<QuestionObj> get(@Url String url);

@GET("category")

Call<ResponseList<CategoryObj.Category>> allCategories();

@POST

Call<QuestionObj> insert(@Body QuestionObj question, @Url String url);

@POST

Call<ReplyObj> insertReply(@Body ReplyObj reply, @Url String url);

@PUT

Call<String> vote(@Url String url);

}

Interfața CategoriesService:

package com.ngenh.askaround.interfaces;

import com.ngenh.askaround.models.CategoryObj;

import com.ngenh.askaround.models.ResponseList;

import java.util.Map;

import retrofit2.Call;

import retrofit2.http.Body;

import retrofit2.http.GET;

import retrofit2.http.POST;

import retrofit2.http.PUT;

import retrofit2.http.QueryMap;

import retrofit2.http.Url;

public interface CategoriesService {

@GET("category")

Call<ResponseList<CategoryObj.Category>> allCategories(

@QueryMap Map<String, String> options

);

@POST("category")

Call<CategoryObj> insert(@Body CategoryObj categoryObj);

@PUT

Call<CategoryObj> update(@Body CategoryObj categoryObj, @Url String url);

}

Interfața UsersService:

package com.ngenh.askaround.interfaces;

import com.ngenh.askaround.models.ResponseList;

import com.ngenh.askaround.models.UserObj;

import retrofit2.Call;

import retrofit2.http.Body;

import retrofit2.http.GET;

import retrofit2.http.POST;

import retrofit2.http.PUT;

import retrofit2.http.Url;

public interface UsersService {

@GET("user")

Call<ResponseList<UserObj.User>> allUsers();

@POST("user")

Call<UserObj> insert(@Body UserObj user);

@GET("auth/me")

Call<UserObj.User> getMe();

@PUT

Call<UserObj> update(@Body UserObj user, @Url String url);

}

Adnotările folosite – GET, POST, PUT – fac parte din librăria Retrofit și reprezintă metoda HTTP care va fi folosită în cererea respectivă. Opțional, în paranteză, se poate da și URL-ul folosit pentru cerere. Dacă acesta nu este specificat, funcția care va crea cererea trebuie să primească un argument precedat de adnotarea @Url reprezentând un șir de caractere ce conține ruta.

Argumentul precedat de adnotarea @QueryMap reprezintă o colecție de tupluri cheie-valoare ce vor fi transformate în parametrii de cerere.

În cazul în care este necesară trimiterea informațiilor pe cerere într-un format JSON, acest lucru se realizează furnizând un argument precedat de adnotarea @Body ce conține o instanță a unei clase Jackson ce poate fi serializată.

O funcție ce construiește cereri HTTP are tipul de întoarcere un obiect de tipul Call<[CLASS_NAME]>. Clasa Call reprezintă cererea propriu-zisă asupra căreia se pot apela funcții precum:

enqueue(Callback<T> callback) – trimite o cerere asincronă către server și notifică funcția primită prin argumentul callback când primește răspunsul.

clone() – crează o nouă cerere identică cu cea asupra căreia s-a executat apelul funcției.

cancel() – anulează cererea. Se poate executa numai pe cererile care nu au părăsit încă aplicația.

De asemenea, clasa Call acceptă un argument generic, reprezentând tipul de date pe care librăria Jackson va încerca să mapeze datele primite.

@GET()

Call<QuestionObj> get(@Url String url);

În cazul de mai sus, apelul funcției get() va returna un obiect de tipul Call, iar apelul funcției enque() al acestui obiect va lansa cererea asincronă către server. Răspunsul primit va fi mapat pe clasa QuestionObj.

În final, este nevoie de crearea unor clase serializabile de biblioteca Jackson care să permită transferul de informații. Aceste sunt simple mapări ale tabelelor din baza de date care reflectă structura unui obiect, însă, spre deosebire de tabelele respective, conțin și atributele construite prin directiva relationship().

Spre exemplu, clasa Question are următoarea structură:

@JsonIgnoreProperties(ignoreUnknown = true)

@JsonInclude(JsonInclude.Include.NON_NULL)

public static class Question {

@JsonCreator

public Question() {

category_ids = new ArrayList<>();

}

@JsonProperty("id")

private int id;

@JsonProperty("user_id")

private int userId;

@JsonProperty("title")

private String title;

@JsonProperty("content")

private String content;

@JsonProperty("created_at")

private String createdAt;

@JsonProperty("score")

private int score;

@JsonProperty("category_ids")

private List<Integer> category_ids;

@JsonProperty("votes")

private List<Votes> votes;

@JsonProperty("user")

private UserObj.User user;

@JsonProperty("replies")

private List<ReplyObj.Reply> replies;

@JsonProperty("categories")

private List<CategoryObj.Category> categories;

Se observă faptul că, deși proprietățile score, votes, user, replies și categories nu există în mod fizic în tabel, ele sunt declarate în clasa Question pentru a putea ține valorile apărute din directiva relationship() sau, în cazul atributului score, de @hybrid_propery, care va fi detaliată în capitolul 4.1.2. Utilizarea clasei de bază.

Librăria Jackson expune următoarele adnotări:

@JSONIgnoreProperties – definește anumite verbe care determină dacă o proprietate este ignorată sau nu în procesul de serializare. În particular, ignoreUnknown va ignora orice proprietate din obiectul JSON care nu este specificată în clasa pe care se realizaează maparea.

@JSONInclude – definește anumite constrângeri ce trebuie să fie îndeplinite de valoarea unei proprietăți pentru a include proprietatea respectivă în corpul cererii. În particular, JsonInclude.Include.NON_NULL permite doar proprietăților care au o valoare diferită de null să fie incluse în corpul cererii.

@JSONCreator – funcția atașată acestei adnotări va fi interpretată ca metoda constructor de librăria Jackson în momentul creării obiectului pentru serializare.

@JSONProperty – această adnotare specifică faptul că membrul imediat definit se va mapa cu o proprietate din obiectul JSON. În cazul în care se dorește a se utiliza o denumire diferită față de cea din JSON, acest lucru se specifică între paranteze după adnotare, urmând ca numele membrului să fie liber ales.

Serviciul Retrofit reprezintă doar comportamentul unei cereri HTTP, deci nu poate fi folosit de sine stătător. Prin urmare, trebuie implementată puntea dintre acesta și aplicație, utilizând o clasă ce va avea rol de consumator.

package com.ngenh.askaround.services;

import android.content.Context;

import com.ngenh.askaround.middleware.ServiceCreator;

// ––– omitted imports –––––

public class  QuestionsService {

private Context context;

private com.ngenh.askaround.interfaces.QuestionsService questionsService;

// ––– omitted variable declarations–––––

public QuestionsService(Context context){

this.context = context;

this.questionsService =

ServiceCreator.retrofitService(

com.ngenh.askaround.interfaces.QuestionsService.class, context

);

}

// ––– omitted class methods –––––

}

Mai sus este prezentată un model simplificat al unui serviciu, model urmat de toate celelalte servicii ale aplicației. Constructorul primește ca argument contextul din care este creat în aplicație și instanțiează un serviciu Retrofit pe care îl va folosi pentru a trimite cererile.

Acestă clasă este responsabilă de crearea metodelor de creare, actualizare, luare și ștergere a informațiilor de pe server, în funcție de necesitatea acestora. În particular, mai jos este prezentată o variantă simplificată a funcției de listare a tuturor întrebărilor.

package com.ngenh.askaround.services;

import android.content.Context;

import com.ngenh.askaround.middleware.ServiceCreator;

import com.ngenh.askaround.models.QuestionObj;

import com.ngenh.askaround.models.ResponseList;

import java.util.Map;

import retrofit2.Call;

import retrofit2.Callback;

import retrofit2.Response;

// ––– omitted imports –––––

public class QuestionsService {

private Context context;

private com.ngenh.askaround.interfaces.QuestionsService questionsService;

// ––– omitted variable declarations–––––

public QuestionsService(Context context){

this.context = context;

this.questionsService =

ServiceCreator.retrofitService(

com.ngenh.askaround.interfaces.QuestionsService.class, context

);

}

public void list(Map<String, String> options){

Call<ResponseList<QuestionObj.Question>> call =

questionsService.allQuestions(options);

call.enqueue(new Callback<ResponseList<QuestionObj.Question>>() {

@Override

public void onResponse(Call<ResponseList<QuestionObj.Question>> call,

Response<ResponseList<QuestionObj.Question>> response) {

int statusCode = response.code();

if(statusCode == 200) {

// success

}else {

// failure

}

}

@Override

public void onFailure(Call<ResponseList<QuestionObj.Question>> call,

Throwable t) {

// client failure

}

});

}

// ––– omitted class methods –––––

}

Metoda list() primește ca argument o colecție de tip Map ce reprezintă perechile cheie-valoare ce vor fi transformate în parametrii de cerere pentru a filtra rezultatele.

În continuare, se folosește serviciul questionsService pentru a crea cererea ce urmează a fi trimisă și aceasta se reține în obiectul call. Cererea este trimisă în urma apelului funcției enqueue(), iar obiectul de tip Callback dată ca parametru va fi apelată în momentul în care va fi primit un răspuns de la server. În cazul unei erori apărute pe partea de client, se apelează metoda onFailure().

Unul din argumentele metodei onSuccess() este response, un obiect de tipul Response<ResponseList<QuestionObj.Question>>. Acesta se poate despărți în trei componente:

Response<T> – clasă ce aparține de librăria Jackson.

ResponseList<T> – clasă generică pe care se mapează toate răspunsurile formate dintr-o colecție de obiecte. Structura acesteia este prezentată mai jos:

package com.ngenh.askaround.models;

import com.fasterxml.jackson.annotation.JsonCreator;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import com.fasterxml.jackson.annotation.JsonInclude;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)

@JsonInclude(JsonInclude.Include.NON_NULL)

public class ResponseList<T> {

@JsonCreator

public ResponseList(){}

@JsonProperty("items")

private List<T> items;

@JsonProperty("items")

public List<T> getItems() {

return items;

}

@JsonProperty("items")

public void setItems(List<T> items) {

this.items = items;

}

}

QuestionObj.Question – obiectul care va fi folosit pentru a deserializa elementele din colecția răspunsului.

În final, pentru a utiliza serviciul QuestionsService din interiorul aplicației sunt necesare următoarele apeluri:

QuestionsService questionsService =

new QuestionsService(getActivity().getApplicationContext());

questionsService.list(Global.options.get("questions"));

3.3.2. Arborele de activități și fragmente

Structura aplicației de client este definită în fișierul AndroidManifest.xml, unde sunt prezentate activitățile din care aceasta este compusă și permisiunile de care are nevoie pentru a rula.

<manifest package="com.ngenh.askaround"

xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>

<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<application

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:supportsRtl="true"

android:theme="@style/AppTheme"

android:windowSoftInputMode="adjustPan">

<activity android:name=".ServerActivity">

<intent-filter>

<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER"/>

</intent-filter>

</activity>

<activity android:name=".LoginActivity">

</activity>

<activity android:name=".RegisterActivity">

</activity>

<activity

android:name=".HomeActivity"

android:fitsSystemWindows="true"

android:windowSoftInputMode="adjustPan"

android:configChanges="keyboardHidden|orientation|screenSize">

</activity>

</application>

</manifest>

Se observă faptul că activitatea ServerActivity conține filtrul cu acțiunea android.intent.action.MAIN și categoria android.intent.category.LAUNCHER care, conform capitolului 2.4.4. Structura unei aplicații Android, marchează activitatea ce reprezintă punctul intrare în aplicație. Aceasta împreună cu următoarele activități definite formează aplicația propriu-zisă.

De asemenea, în cazul în care aplicația necesită anumite permisiuni speciale pentru a rula, acestea trebuie declarate în fișierul AndroidManifest.xml. În particular, acestea sunt:

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>

<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

android.permission.ACCESS_WIFI_STATE – aplicația necesită cunoașterea stării de conexiune Wi-Fi.

android.permission.CHANGE_WIFI_STATE – aplicația necesită dreptul de a schimba starea conexiunii Wi-Fi.

android.permission.INTERNET – aplicația necesită dreptul de a se conecta la internet pentru a trimite cereri.

android.permission.ACCESS_NETWORK_STATE – aplicația necesită cunoașterea stării rețelei la care este conectat dispozitivul.

Figura 3.3.2.1 – Structura aplicației Android reprezintă diagrama limbajului de modelare unificat (Uniform Modelling Language – UML) a activităților și fragmentelor din aplicația Ask Around. Se observă faptul că activitatea HomeActivity reprezintă nucleul aplicației, găzduind toate componentele funcționale principale.

Aceste componente sunt instanțe ale clasei Fragment oferită de Android și poartă denumirea de fragmente. Se preferă folosirea lor în locul activităților din motive de optimizare în momentul în care componentele sunt desenate pe ecran, dar și datorită gradului mare de modularizare oferit.

Datorită faptului că activitatea HomeActivity este doar un recipient pentru alte fragmente care realizează funcționalitatea dorită, structura interfeței vizuale a acesteia, activity_main.xml, este foarte simplă, ea reprezentând doar scheletul pe care vor fi construite fragmentele ce vor fi atașate.

<!– activity_main.xml –>

<android.support.v4.widget.DrawerLayout

xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

tools:context="com.ngenh.askaround.HomeActivity"

android:id="@+id/drawer_layout_home">

<android.support.design.widget.CoordinatorLayout

android:id="@+id/coordinator_home">

<LinearLayout>

<android.support.v7.widget.Toolbar

android:id="@+id/toolbar" />

<FrameLayout

android:id="@+id/contentFrame">

</FrameLayout>

</LinearLayout>

<android.support.design.widget.FloatingActionButton

android:id="@+id/fab"

android:src="@drawable/ic_plus" />

</android.support.design.widget.CoordinatorLayout>

<android.support.design.widget.NavigationView

android:id="@+id/navigation_view"

app:headerLayout="@layout/drawer_header"

app:menu="@menu/drawer"/>

</android.support.v4.widget.DrawerLayout>

Cel mai important element din schema activității HomeActivity este <FrameLayout> în care vor fi încărcate fragmentele în mod dinamic folosind legătura din controlerul asociat. Controlerul va putea face referire la elementul în cauză datorită faptului că acestuia îi este atribuit un identificator unic, contentFrame, prin care va putea fi identificat.

<FrameLayout android:id="@+id/contentFrame">

În continuare, este prezentată o structură simplificată a clasei HomeActivity care pune în evidență modalitatea de încarcare a unui fragment în elementul <FrameLayout>.

// ––––––– omitted imports ––––––––

public class HomeActivity extends AppCompatActivity {

// ––––––– omitted code ––––––––

private BroadcastReceiver receivedChangeFragment = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

Bundle bundle = intent.getExtras();

// ––––––– omitted code ––––––––

String fragmentType = bundle.getString("fragmentType");

// ––––––– omitted code ––––––––

changeFragment(fragmentType);

}

};

// ––––––– omitted code ––––––––

private void changeFragment(String fragmentType){

// ––––––– omitted code ––––––––

FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();

fragmentTransaction.setCustomAnimations(

R.animator.fragment_slide_left_enter,

R.animator.fragment_slide_left_exit,

R.animator.fragment_slide_right_enter,

R.animator.fragment_slide_right_exit

);

if(fragmentType.equals("questions")){

// ––––––– omitted code ––––––––

Questions questions = new Questions();

fragmentTransaction.replace(R.id.contentFrame, questions, fragmentType);

}

// ––––––– omitted code ––––––––

fragmentTransaction.commitAllowingStateLoss();

}

// ––––––– omitted code ––––––––

}

Aceast lucru se realizează prin utilizarea unei tranzacții. Pentru a administra acestă tranzacție se folosește un obiect de tip FragmentTransaction. Modul de operare cu un astfel de obiect este similar cu cel al tranzacțiilor din SQL prezentate în capitolul 2.1.2. Avantaje PostgreSQL. În momentul în care metoda beginTransaction() este apelată, se pot modela următoarele activități:

Metoda folosită pentru a executa tranziția prin care fragmentul va deveni vizibil.

fragmentTransaction.setCustomAnimations(

R.animator.fragment_slide_left_enter,

R.animator.fragment_slide_left_exit,

R.animator.fragment_slide_right_enter,

R.animator.fragment_slide_right_exit

);

Funcția setCustomAnimations() primește ca argumente patru elemente reprezentând animatori ce vor defini comportamentul desenării unui fragment care urmează să fie afișat sau ascuns.

Operația propriu-zisă care va injecta un anumit fragment într-un recipient predefinit.

fragmentTransaction.replace(R.id.contentFrame, questions, fragmentType);

Metoda replace() a obiectului fragmentTransaction() primește ca argumente identificatorul unic al recipientului în care va fi încărcat noul fragment, referința către fragmentul respectiv și, opțional, o etichetă ce va reprezenta operația respectivă.

În final, apelând funcția commit(), tranzacția este executată, iar noul fragment este încarcat în recipient. După încărcarea unui fragment în recipientul prezent din HomeActivity, sistemul de operare se va folosi de schema acestuia pentru a afișa componentele sale pe ecran, iar controlerul asociat va fi pus în vârful stivei de controlere. Astfel se realizează traversarea prin toate fragmentele, implicit funcționalitățile, aplicației.

4. Detalii de implementare

4.1. Modularizarea codului din controlerele serverului.

Executarea unei cereri HTTP în contextul serverului presupune existența unei rute pe care server-ul ascultă cererea respectivă, împreună cu controlerul asociat rutei. În exemplele utilizate în capitolul 2.2.3.2. Procesarea cererilor, controlerul a fost reprezentat de o funcție, însă, pentru cazuri mai complexe, folosirea doar a funcțiilor va duce la o creștere considerabilă a volumului de cod.

Prin urmare, este imperativă implementarea unui sistem ce generalizează operațiile comune, urmând ca funcționalitățile ce necesită un comportament specific să fie implementate separat. Pentru realizarea unui astfel de sistem se va folosi o clasa principală ce va fi responsabilă cu executarea operațiilor de creare, citire, actualizare și ștergere (Create, Read, Update, Delete – CRUD) ale unei resurse, iar clasele care vor reprezenta o resursă specifică vor fi descendente ale clasei de bază.

4.1.1. Prezentarea clasei de bază

În particular, clasa responsabilă cu implementarea logicii din spatele acestor operații este CRUDView. În continuare este prezentată o structură simplificată a acesteia:

class CRUDView(object):

filters = dict()

accept_order = dict()

json_hints = dict()

update_hints = dict()

context = None

target_type = object

target_name = "object"

request = None

update_lock = False

def __init__(self, request):

""":type request: Request"""

self.request = request

self.context = request.context

self.context._json_hints = self.json_hints

assert(isinstance(self.request, Request))

@property

def identity_filter(self):

# method body

pass

def sanitize_input(self):

# method body

pass

@property

def context_filter(self):

# method body

pass

def get_by_id(self, update_lock=False):

# method body

pass

@property

def query_filters(self):

# method body

pass

@property

def order_clauses(self):

# method body

pass

@property

def pager_slice(self):

# method body

pass

def get_base_query(self):

# method body

pass

def get_identity_base(self):

# method body

pass

def get_list_base(self):

# method body

pass

def get_search_results(self, query=None):

# method body

pass

def get(self):

return {self.target_name: self.get_by_id()}

def list(self):

# method body

pass

def apply_changes(self, obj, data, for_update=True):

# method body

pass

def update(self):

# method body

pass

def insert(self):

# method body

pass

def delete(self):

# method body

pass

Membrii definiți vor fi suprascriși de orice clasă copil a clasei CRUDView, ei având rolul de a impune constrângeri sau de a defini proprietăți specifice fiecărei resurse:

filters – această proprietate reprezintă un dicționar care va fi folosit pentru a impune obiectelor selectate din baza de date să îndeplinească anumite condiții înainte de a fi returnate. Un obiect va trece prin toate valorile existente în filtre, iar pentru oricare din acele valori, se va executa o funcție care va determina dacă obiectul curent îndeplinește sau nu criteriul respectiv.

accept_order – un dicționar ce definește coloanele după care obiectele selectate vor putea fi ordonate.

json_hints – un dicționar care are rolul de a specifica modul în care anumite proprietăți ale unui obiect selectat se comportă. În cadrul dicționarului json_hints se pot folosi proprietăți precum _include_ sau _exclude_ , acestea fiind colecții de elemente ale căror valori reprezintă proprietățile ce urmează a fi incluse, respectiv excluse din diverse contexte.

update_hints – un dicționar similar cu json_hints dar care acționează în momentul în care se realizează o operație de inserare sau de actualizare pe obiectul respectiv.

context – obiect în care se stochează contextul cererii.

target_type – referință către clasa specifică unei resurse.

target_name – șir de caractere reprezentând numele tabelei pe care este mapată resursa în cauză.

request – obiect în care este stocată cererea propriu-zisă.

update_lock – membru de tip boolean ce specifică dacă o operație va bloca, pe parcursul execuției ei, resursa respectivă.

target_type – membrul target_type reprezintă o referință către o clasă a unei resurse specifice, iar apelul ei, target_type(), este echivalent cu apelarea constructorului, ce va produce o nouă instanță a clasei respective.

Metodele rămase au rolul de a ajuta sau îmbunătăți procesul de executare al unei cereri:

__init__() – constructorul clasei. Are rolul de a intițializa variabilele request și context, de a seta valorile proprietății json_hints a contextului și de a verifica dacă cererea este una validă.

identity_filter() – funcția definește modalitatea prin care o instanță își identifică unicitatea în tabel.

sanitize_input() – validează formatul corpului JSON al unei cererei.

get_by_id() – returnează o resursă după identificatorul ei unic.

def get_by_id(self, update_lock=False):

try:

query = self.get_identity_base()

if update_lock:

query = query.with_for_update()

return query.filter(self.context_filter, self.identity_filter).one()

except NoResultFound:

raise HTTPNotFound()

În cazul în care locul din care este apelată funcția urmează să modifice valorile resursei pentru a o actualiza, interogarea query va specifica faptul că resursa respectivă va fi blocată prin transmiterea unui argument funcției with_for_update().

query_filters() – setează membrul filters al clasei CRUDView cu perechile de cheie-valoare primite pe cerere.

order_clauses() – ordonează rezultatele în funcție de proprietățile furnizate pe cerere.

pager_slice() – împarte rezultatele în grupuri egale, reprezentând pagini, pentru a preveni returnarea unui răspuns prea mare.

get_base_query() – interoghează baza de date folosind tabelul a cărui referință a fost suprascrisă de proprietatea target_type.

get_identity_base() – metodă care returnează o instanță a unei resurse folosindu-se de identificatorul ei unic.

def get_identity_base(self):

return self.get_base_query()

În cazul în care o resursă are nevoie să execute o operație specială în momentul interogării bazei de date, această funcție poate fi suprascrisă.

get_list_base() – metodă ce returnează toate înregistrările prezente într-un tabel.

def get_list_base(self):

return self.get_base_query()

Similar cu metoda get_identity_base(), dacă există necesitatea implementării unei funcționalități specifice, această funcție poate fi rescrisă.

get_search_results() – metodă apelată de operația de listare a tuturor înregistrărilor dintr-o tabelă. Ea execută pe rând, unde este necesar, filtrarea, paginarea și ordonarea rezultatelor înainte de a fi returnate.

apply_changes() – această funcție este folosită pentru inserarea sau actualizarea unei înregistrări.

def apply_changes(self, obj, data, for_update=True):

obj.update_conditional(data, self.update_hints, ignore_invalid=True)

Datorită faptului că toate enitățile unei baze de date sunt diferite, acestea au nevoie aproape de fiecare dată de o implementare specifică a acestei funcții.

Toate metodele și proprietățile mai sus menționate servesc ca pilon pentru executarea operațiilor CRUD de către următoarele funcții:

list() – interoghează baza de date și returnează o listă cu rezultatele obținute.

def list(self):

items, count = self.get_search_results()

return dict(items=[item._asdict() if isinstance(item, tuple)

and callable(getattr(item, '_asdict', None))

else item for item in items],

total=count)

Folosind metoda get_search_results() sunt returnate toate înregistrările corespunzătoare unei cereri, însă acestea vin sub o formă utilizabilă doar în limbajul Python. Prin urmare, este necesară convertirea lor la un format standart al aplicației. Ele sunt transformate într-un obiect cu două proprietăți:

items – colecție ce conține toate rezultatele găsite reprezentate într-un format ce poate fi transformat în JSON.

total – valoarea acestei proprietăți reprezintă numărul total de înregistrări găsite.

get() – interoghează baza de date în vederea găsirii a exact o singură înregistrare.

def get(self):

return {self.target_name: self.get_by_id()}

Se returnează un obiect cu o singură proprietate ce poartă numele clasei de care aparține resursa pentru care este executată cererea și are valoarea înregistrării găsite.

insert() – inserează o înregistrare în baza de date folosind datele existente pe cerere.

def insert(self):

obj = self.target_type()

values = self.sanitize_input()

with transaction.manager:

self.apply_changes(obj, values, False)

DBSession.add(obj)

obj = DBSession.merge(obj)

return {self.target_name: obj}

Variabilele obj și values rețin noua instanță a obiectului ce urmează a fi inserat în baza de date, respectiv valorile existente pe cerere din care se va forma noua înregistrare. În continuare, folosind managerul de tranzacții, se apelează metoda apply_changes() pentru a transfera valorile din cerere pe obiectul obj, după care acesta este adăugat pe sesiune și persistat astfel în tabel.

update() – actualizează o înregistrare deja existentă cu valorile existente pe cerere.

def update(self):

try:

with transaction.manager, DBSession.no_autoflush:

old = self.get_by_id(update_lock=self.update_lock)

values = self.sanitize_input()

try:

self.apply_changes(old, values, True)

except VersionCheckError as ex:

raise HTTPConflict(str(ex))

except StaleDataError as ex:

raise HTTPConflict(str(ex))

return self.get()

Folosind managerul de tranzacții din SQLAlchemy, se extrage din baza de date obiectul ce se dorește a fi modificat, se aplică noile valori pe acesta folosind funcția apply_changes(), după care se returnează obiectul actualizat.

delete() – șterge o înregistrare specifică din baza de date.

def delete(self):

with transaction.manager:

old = self.get_by_id()

DBSession.delete(old)

return HTTPOk()

Obiectul este găsit după identitatea sa, după care este șters din sesiune, urmând ca schimbările să fie persistate datorită faptului că această operație s-a desfășurat în interiorul managerului de tranzacții.

4.1.2. Utilizarea clasei de bază

Clasa CRUDView reprezintă scheletul pentru orice model al aplicației, conținând implementarea operațiilor de bază a oricărei resurse. Prin urmare, în acest moment, complexitatea implementării unei noi resurse se rezumă la crearea unui clase ce extinde clasa părinte, CRUDView, și care implementează minimul necesar de logică pentru a funcționa corect. Astfel, se realizează o arhitectură modulară, ușor utilizabilă și cu un grad de mentenanță ridicat.

În continuare, vor fi prezentate implementările claselor UsersView, controlerul asociat resursei user, și QuestionsView, controlerul asociate resursei question.

4.1.2.1. Implementarea clasei UsersView

Această clasă reprezintă controlerul resursei user și are responsabilitatea de a implementa toate funcționalitățile specifice ei.

@view_defaults(renderer='json')

@view_config(route_name='auth.register', request_method='POST',

attr='insert', permission='add')

@view_config(route_name='user.self', request_method='PUT',

attr='update', permission='update')

@view_config(route_name='user.self', request_method='GET',

attr='get', permission='get')

@view_config(route_name='user', request_method='GET',

attr='get', permission='get')

@view_config(route_name='users', request_method='GET',

attr='list', permission='list')

@view_config(route_name='user', request_method='PUT',

attr='update', permission='update')

@view_config(route_name='users', request_method='POST',

attr='insert', permission='add')

@view_config(route_name='user', request_method='DELETE',

attr='delete', permission='delete')

class UserView(CRUDView):

filters = dict(name_like=lambda val: or_(User.first_name.ilike("%"+val+"%"),

User.last_name.ilike("%"+val+"%")),

email_like=lambda val: User.email.ilike("%" + val + "%"),

)

accept_order = dict(email=User.email,

first_name=User.first_name,

last_name=User.last_name

)

update_hints = dict()

json_hints = dict(password=False, address=True, questions=True)

target_type = User

target_name = "user"

def __init__(self, request):

super().__init__(request)

if 'id' in self.request.matchdict:

self.id = self.request.matchdict['id']

if 'user_id' in self.request.matchdict:

self.id = self.request.matchdict['user_id']

@property

def identity_filter(self):

return User.id == self.id

def sanitize_input(self):

return self.request.json_body['user']

def apply_changes(self, obj, data, for_update=True):

obj.update_conditional(data,

dict(_exclude_=['id', 'questions'],

password=crypt_password_simple),

ignore_invalid=True)

def get_list_base(self):

return DBSession.query(User)

Directiva @view_config este folosită de mai multe ori pentru a specifica numeroasele rute asociate cu clasa UsersView. Spre deosebire de exemplul folosit în capitolul 3.2.2. Definirea rutelor și a controlerelor asociate, directiva primește încă doi parametrii, attr și permission, ce reprezintă metoda din cadrul clasei CRUDView ce va fi apelată pentru o cerere ce se potrivește descrierii, respectiv permisiunea necesară unui utilizator pentru a putea accesa ruta în cauză.

Spre exemplu, funcționalitatea de înregistrare este implementată prin decorarea clasei UserView astfel:

@view_config(route_name='auth.register', request_method='POST', attr='insert', permission='add')

Se observă faptul că membrii declarați în clasa CRUDView au fost suprascriși pentru a se potrivi cu nevoile specifice ale resursei user. Prin urmare:

Dicționarul filters a fost construit astfel încât să accepte următoarele filtre:

name_like – filtrarea după numele utilizatorului

email_like – filtrarea după emailul utilizatorului

Toate aceste proprietăți sunt funcții lambda care vor fi rulate de fiecare dată când o cerere necesită acest lucru pentru a evalua dacă o înregistrare din tabelul User îndeplinește condiția specificată de cerere.

filters = dict(name_like=lambda val: or_(User.first_name.ilike("%"+val+"%"),

User.last_name.ilike("%"+val+"%")),

email_like=lambda val: User.email.ilike("%" + val + "%"))

În exemplul de mai sus, valoarea argumentului trimis funcției lambda, val, reprezentând valoarea din cererea primită, este comparată cu valoarea din coloana email a clasei User, folosind metoda ilike().

În cazul în care valoarea după care se filtrează poate exista în mai multe coloane, se pot folosi funcțiile or_ și and_ furnizate de SQLAlchemy, similare operatorilor OR și AND din limbajul SQL.

filters = dict(name_like=lambda val: or_(User.first_name.ilike("%"+val+"%"),

User.last_name.ilike("%"+val+"%")),

Prin urmare, o cerere HTTP folosind metoda GET de forma http://ask-around.ngenh.com:6543/api/user?email_like=yuno va interoga baza de date pentru toate înregistrările din tabela users care au în coloana email un subșir de caractere egal cu valoarea yuno. Un potențial răspuns al acestei cereri are forma următoare:

{

"items": [

{

"id": 3

"email": "yuno@askaround.com",

"first_name": "yuno",

"last_name": "gasai",

"password": "mniKki…",

"active": true,

"is_admin": false,

}

],

"total": 1

}

Dicționarul accept_order definește propritățile clasei User care pot fi folosite pentru a ordona rezultatele înainte de a fi returnate:

accept_order = dict(email=User.email,

first_name=User.first_name,

last_name=User.last_name

)

Pentru a specifica faptul că se dorește ordonarea rezultatelor, trebuie să existe un parametru cerere de forma order=property[,{asc | desc}]. Prin urmare, cererea http://ask-around.ngenh.com:6543/api/user?order=email,asc va returna rezultatele în ordine crescătoare comparând lexicografic valoarea coloanei email a înregistrărilor.

Dicționarul json_hints specifică în acest caz faptul că se dorește returnarea instanțelor împreună cu relațiile lor din alte tabele. Acesta poate și să ascundă anumite proprietăți ce nu se doresc a fi returnate.

json_hints = dict(password=False, questions=True)

În particular, prorpietatea password primește direct valoarea False pentru a fi exclusă din răspuns, iar proprietatea questions, reprezentând o colecție cu toate întrebările legate de utilizatorul respectiv, primește valoarea True pentru a fi inclusă în răspuns. Aceasta nu este o coloană reală a tabelei users, ci o relație. Prin urmare, SQLAlchemy va rula automat instrucțiunea care va returna toate instanțele tabelului questions legate de obiectul user curent, le va transforma într-o colecție și o va atașa pe proprietatea questions a obiectului user.

Constructorul clasei a fost alterat astfel încât să poată reține valoarea unor variabile de cale, în cazul în care acestea există.

def __init__(self, request):

super().__init__(request)

if 'id' in self.request.matchdict:

self.id = self.request.matchdict['id']

if 'user_id' in self.request.matchdict:

self.id = self.request.matchdict['user_id']

Acest lucru este necesar datorită existenței rutelor de forma:

config.add_route('user.question', 'api/user/{user_id}/question/{id}'))

De cele mai multe ori, este necesară utilizarea valorilor variabilelor de cale user_id și id, iar pentru a ușura accesul către acestea, ele sunt salvate în membrii privați ai controlerului.

Filtrul de identidate implementează modalitatea de a determina unic o înregistrare a tabelei users. Coloana id a acestei tabele reprezintă cheia primară, deci ea este suficientă pentru a stabili unicitatea unei instanțe.

def identity_filter(self):

return User.id == self.id

Metoda sanitize_input() impune un standard pentru formatul corpului unei cereri pentru a putea folosi datele din interiorul acesteia.

def sanitize_input(self):

return self.request.json_body['user']

În particular, atunci când sunt făcute cereri legate de resursa user care conține un corp în care se află valorile ce vor fi utilizate în execuția cererii, aceasta trebuie să aibă forma următoare:

{

"user": { /* data */ }

}

Implementarea specifică pentru funcționalitatea de salvare sau actualizare a unei înregistrări se realizează prin suprascrierea metodei apply_changes():

def apply_changes(self, obj, data, for_update=True):

obj.update_conditional(data,

dict(_exclude_=['id', 'questions'],

password=crypt_password_simple),

ignore_invalid=True)

Cea mai importantă modificare este dicționarul transmis ca parametru funcției update_conditional() care conține următoarele proprietăți:

_exclude_ – colecție cu elemente ce reprezintă numele coloanelor care vor fi ignorate în momentul actualizării.

password – specifică faptul că proprietatea respectivă va fi actualizată într-un mod special definit prin funcția crypt_password_simple:

def crypt_password_simple(password):

if password == '' or password is None:

return None

import uuid, hashlib

salt = uuid.uuid4().hex

return hashlib.sha256(salt.encode() + password.encode()).hexdigest() + ":" + salt

Această funcție are rolul de a cripta un text folosind un algoritm de criptare modern. [7]

Metoda get_list_base() implementează funcționalitatea de selectare a tuturor înregistrărilor din tabela users:

def get_list_base(self):

return DBSession.query(User)

4.1.2.2. Implementarea clasei QuestionsView

Datorită arhitecturii modulare a serverului, controlerul resursei question are o structură similară cu cea a controlerului resursei user, cu mici excepții necesare pentru a trata particularitățile funcționalităților:

@view_defaults(renderer='json')

@view_config(route_name='questions', request_method='GET', attr='list', permission='list')

@view_config(route_name='question', request_method='GET', attr='get', permission='get')

@view_config(route_name='question.score', request_method='PUT', attr='update_question_score', permission='update')

@view_config(route_name='user.question', request_method='GET', attr='get', permission='get')

@view_config(route_name='user.questions', request_method='GET', attr='list', permission='list')

@view_config(route_name='user.questions', request_method='POST', attr='insert', permission='add')

class QuestionView(CRUDView):

# ––––– code omitted ––––––––-

json_hints = dict(replies=dict(user=dict(password=False)),

user=dict(password=False), categories=True,

_include_props_=['score', 'votes'])

# ––––– code omitted ––––––––-

def apply_changes(self, obj, data, for_update=True):

if obj.category_ids is None:

obj.category_ids = []

obj.user_id = self.user_id

obj.update_conditional(data, dict(_exclude_=['id', 'user_id', 'user',

'category_ids', 'categories',

'replies',

'question_scores', 'score',

'votes']),

ignore_invalid=True)

if 'category_ids' in data:

by_pk = {item: item for item in data['category_ids'] if item is not None}

to_remove = [item for item in obj.categories if item.id not in by_pk]

existing_by_pk = {item.id for item in obj.categories}

for item in to_remove:

obj.category_ids.remove(item.id)

obj.categories.remove(item)

for item_id in data['category_ids']:

if item_id not in existing_by_pk:

item = DBSession.query(Category).get(item_id)

if item is not None:

obj.category_ids.append(item.id)

obj.categories.append(item)

# ––––– code omitted ––––––––-

def update_question_score(self):

user_score = self.request.matchdict['score']

score = QuestionScore()

score.user_id = self.user_id

score.question_id = self.id

score.score = user_score

exists = DBSession.query(QuestionScore).\

filter_by(user_id=self.user_id).\

filter_by(question_id=self.id).first()

if exists is None:

DBSession.add(score)

else:

obj = dict(score=user_score)

exists.update_conditional(obj,

dict(_exclude_=['user_id', 'question_id']),

ignore_invalid=True)

return HTTPOk()

În ceea ce privește dicționarul json_hints, proprietatea replies reprezintă o colecție cu toate răspunsurile existente pe o întrebare, însă, de data aceasta nu primește o simplă valoare de True, ci un alt dicționar care la rândul lui specifică comportamentul altor proprietăți din obiectul relație. Prin urmare, se poate specifica comportamentul proprietăților în adâncime pentru a satisface orice condiție necesară.

Este de notat utilizarea proprietății _include_ a dicționarului json_hints care specifică faptul că obiectele de tip Question returnate vor include, pe lânga proprietățile uzuale, alte două poprietăți numite score, respectiv votes.

Pentru a discuta aceste două proprietăți este necesară prezentarea structurii proprietăților respective din cadrul clasei Question:

class Question(Base, DictSerializable):

# ––––––– code omitted ––––––––

@hybrid_property

def score(self):

res = 0

for I in self.scores:

res += i.score

return res

@hybrid_property

def votes(self):

res = []

for vote in self.scores:

obj = dict(user_id=vote.user_id, score=vote.score)

res.append(obj)

return res

Aceste două funcții se alătură implementării clasei Question prezentată în capitolul 3.2.1. Construirea a bazei de date din modele SQLAlchemy. Directiva @hybrid_property specifică faptul că funcțiile score, respectiv votes vor fi tratate ca fiind proprietăți ale clasei Question, dar nu vor exista în mod fizic în tabel. Atunci când una din ele este accesată, funcția asociată este rulată pentru a îi calcula valoarea la momentul apelului.

Revenind la clasa QuestionView, în continuare este prezentată funcția apply_changes() care este responsabilă cu persistarea informațiilor în tabel, funcție apelată indiferent de tipul operației efectuate, fie ea inserare sau de actualizare. Metoda update_conditional() este prezentă pe orice instanță a unui tabel și are rolul de a actualiza doar informațiile de tipuri simple din obiectul dat ca parametru – precum cele de tip int, string, boolean –, însă nu poate să manevreze date complexe cum ar fi obiecte sau colecții specifice proprietăților declarate cu ajutorul directivei relationship().

obj.update_conditional(data, dict(_exclude_=['id', 'user_id', 'user',

'category_ids', 'categories', 'replies',

'question_scores', 'score', 'votes']),

ignore_invalid=True)

Primul argument, data, este o colecție ce conține toate datele care vor fi utilizate pentru actualizarea celor din obiectul obj. Al doilea argument este un dicționar ce specifică opțiunile aferente operației de actualizare. În particular, este necesară excluderea mai multor coloane care nu trebuie modificate ori datorită faptului că reprezintă o informație importantă ori datorită faptului că ele sunt o relație a tabelei, nu o coloană simplă, deci trebuie actualizate manual. Prin urmare, dicționarul dat ca parametru exclude proprietăți de următoarele tipuri:

chei primare sau străine – id, user_id – aceastea sunt administrate de SQLAlchemy și nu ar trebui să fie modificate manual.

colecții și obiecte – category_ids, categories, replies, question_scores, user – operațiile posibile pe o colecție sunt inserarea unui noi element, actualizarea unuia deja existent sau ștergerea celor care nu mai sunt necesare. Mai mult, proprietățile provenite din folosirea directivelor relationship() și backref() nu trebuie modificate manual, ci tot folosind funcția update_conditional() pe proprietățile respective.

proprietăți care nu pot fi modificate – score, votes – valoarea acestora este calculată în momentul accesării lor folosind date ale altor proprietăți, prin urmare acestea nu pot fi modificate, iar SQLAlchemy ridică o eroare în cazul în care se încearcă acest lucru.

Din toate colecțiile existente în clasa Question, singura care nu provine din directive precum relationship() și backref() este category_ids. Prin urmare, aceasta poate fi actualizată manual în continuarea funcției apply_changes().

Metoda update_question_score() exemplifică implementarea unei funcționalități specifice. Aceasta nu se regăsește în clasa de baza, CRUDView, deci trebuie să execute cererea într-un mediu independent.

4.2. Comunicarea prin evenimente în Android

Activitățile și fragmentele ce alcătuiesc o aplicație conlucrează pentru buna funcționare a acesteia, însă, în același timp, ele trebuie să fie cât mai independente pentru a putea fi reutilizate din cât mai multe circumstanțe. Pentru a satisface această nevoie de flexibilitate este necesară implementarea unei arhitecturi care să permită transferul rapid și sigur al informațiilor în interiorul aplicației.

De asemenea, pe lângă aceste caracteristici, arhitectura implementată trebuie să trateze și una din marile probleme apărute în dezvoltarea aplicațiilor, și anume execuția codului asincron. Conceptul de cod asincron se referă la faptul că unele operații din cadrul programului se pot completa într-un timp nedeterminat, rezultând astfel un comportament nedeterministic al aplicației.

Spre exemplu, cererile HTTP, foarte des folosite în aplicația Ask Around, sunt transmise către server, unde sunt procesate, iar răspunsul rezultat este transmis înapoi. În mediul real, timpul necesar finalizării unei astfel de operație poate depinde de o multitudine de factori precum viteza transmisiei datelor prin internet, distanța dintre sursă și server sau calitatea comunicației. O astfel de cerere este considerată terminată în momentul în care aplicația primește răspunsul trimis de către server. Prin urmare, timpul necesar finalizării operației nu poate fi determinat. Pentru a nu bloca rularea unei aplicații până la sosirea răspunsului, o astfel de cerere este executată asincron.

4.2.1. Implementarea arhitecturii.

Aplicația Ask Around se adresează acestor probleme printr-un sistem de comunicare prin evenimente ce permite componentelor să se înștiințeze în diverse circumstanțe fără însă să creeze dependențe între acestea. Astfel, se crează un grad de modularitate foarte mare datorită faptului că activitățile și fragmentele se pot concentra pe responsabilitățile lor, fiind irelevant din ce parte a aplicației sunt folosite.

Pentru a putea utiliza sistemul mai sus menționat, sunt folosite librăriile android.content.BroadcastReceiver, android.content.Intent , android.content.IntentFilter și android.support.v4.content.LocalBroadcastManager, implementate după următoarea structură:

Se înregistrareză un obiect de tip BroadcastReceiver care va răspunde unor intenții ce sunt etichetate cu o denumire specifică.

BroadcastReceiver receiver= new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

// function body

}

};

LocalBroadcastManager.getInstance(getActivity()).registerReceiver(receiver,

new IntentFilter("event-name"));

Clasa LocalBroadcastManager expune un obiect singleton[16], accesat prin funcția getInstance(), care administrează toți receptorii înregistrați în aplicație.

Funcția registerReceiver() primește ca argument receptorul ce urmează a fi înregistrat împreună cu un filtru ce conține eticheta evenimentului pe care îl va asculta.

Declanșarea unui eveniment etichetat cu o denumire specifică.

Intent intent = new Intent("event-name");

LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);

Funcția sendBroadcast() va răspândi inteția creată în toată aplicația, urmând ca receptorii care ascultă după eticheta event-name să fie declanșați, iar funcțiile asociate lor să fie executate.

Prin folosirea metodei putExtra() a unui obiect de tip Intent, se pot atașa informații pe obiectul respectiv în vederea transmiterii lor către receptori.

intent.putExtra("my-key", "my-data");

Această arhitectură este folosită în două moduri principale în aplicația Ask Around:

Pentru a rezolva problema codului asincron.

Pentru a transmite informații în interiorul aplicației.

4.2.2. Adresarea problemei de cod asincron

Acest comportament se datorează cererilor HTTP datorită factorilor prezentați în capitolul 4.2. Comunicarea prin evenimente în Android. Pentru a iniția o astfel de cerere, este necesară o instanță a unui serviciu ce oferă diverse metode care vor crea, trimite și aștepta răspunsul cererii respective.

Orice serviciu al aplicației primește ca argument pentru constructorul ei contextul din care este creată instanța respectivă și un șir de caractere reprezentând un nume de eveniment. Componenta care inițiează cererea HTTP este responsabilă cu definirea unui receptor care va asculta pentru evenimente ce poartă eticheta cu care a fost instanțiat serviciul astfel încât să poată reacționa corespunzător.

public class AuthService {

public HashMap<String, Boolean> actions;

private Context context;

private com.ngenh.askaround.interfaces.AuthService authService;

private String eventName;

public AuthService(Context context, String eventName){

this.actions = new HashMap<>();

this.context = context;

this.eventName = eventName;

this.authService =

ServiceCreator.retrofitService(

com.ngenh.askaround.interfaces.AuthService.class, context

);

}

public void login(String email, String password){

if ((actions.containsKey("login") && actions.get("login")) return;

else

actions.put("login", true);

Auth authData = new Auth();

authData.getCredentials().setEmail(email);

authData.getCredentials().setPassword(password);

Call<UserObj> call = authService.login(authData);

call.enqueue(new Callback<UserObj>() {

@Override

public void onResponse(Call<UserObj> call, Response<UserObj> response) {

int statusCode = response.code();

actions.remove("login");

Intent intent = new Intent(eventName);

// –––– implementation detail ––––

LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

}

@Override

public void onFailure(Call<UserObj> call, Throwable t) {

actions.remove("login");

Intent intent = new Intent(eventName);

intent.putExtra("error", t.getMessage());

LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

}

});

}

}

Mai sus este prezentat serviciul de autentificare, AuthService, împreună cu una din funcționalitățile pe care le oferă, anume logarea unui utilizator în aplicația Ask Around. Se observă faptul că șirul de caractere eventName transmis ca argument constructorului este folosit pentru a eticheta o intenție înainte ca aceasta să fie trimisă la nivel local în toată aplicația. Acest lucru are loc prin apelul funcției sendBroadcast() în metodele onResponse() și onFailure() care sunt apelate atunci când este primit un răspuns de la server, respectiv când o eroare a apărut pe parcurs, iar cererea nu a putut fi procesată.

actions.remove("login");

LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

Răspândirea evenimentului în aplicație este independentă de celelelalte componente. Sursa nu știe dacă există un receptor asociat etichetei pe care o folosește, ci se bazează pe destinatar pentru a îl implementa, utiliza și dealoca din memorie atunci când nu mai este necesar.

private BroadcastReceiver receivedLogin = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

// response

}

};

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_login);

LocalBroadcastManager.getInstance(this).registerReceiver(receivedLogin,

new IntentFilter("received-login"));

}

@Override

public void onDestroy() {

LocalBroadcastManager.getInstance(this).unregisterReceiver(receivedLogin);

super.onDestroy();

}

Activitatea LoginActivity definește un receptor, receivedLogin, care va fi înregistrat în obiectul de tip LocalBroadcastManager, și va asculta pentru evenimente ce poartă eticheta received-login. La ieșirea din LoginActivity, receptorul este dealocat prin metoda unregisterReceiver().

4.2.3. Transmiterea informațiilor în interiorul aplicației

Mediul prin care informațiile sunt transmise între activități și fragmente trebuie să fie rapid, sigur, și în același timp să protejeze independeța componentelor care interacționează. Acest lucru este îndeosebi important în folosirea fragmentelor dinamice ce îmbunătățesc semnificativ performanțele aplicației.

În capitolul 3.3.2. Arborele de activități și fragmente activitatea HomeActivity a fost prezentată ca fiind scheletul aplicației, datorită faptului că servește ca recipient pentru toate fragmentele ce alcătuiesc majortiatea funcționalităților. Funcția changeFragment() din interiorul acesteia se bazează exclusiv pe comunicarea prin evenimente pentru a putea încărca fragmentele necesare pe baza datelor transmise printr-o intenție.

public class HomeActivity extends AppCompatActivity {

private NavigationView navigationView;

private Bundle fragmentBundle;

private BroadcastReceiver recvChangeFrag = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

// –––– code omitted ––––––

changeFragment(intent.getExtras().getString("fragmentType"));

}

};

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_home);

// –––– code omitted ––––––

LocalBroadcastManager.getInstance(this).registerReceiver(recvChangeFrag,

new IntentFilter("change-fragment"));

}

@Override

public void onDestroy() {

// –––– code omitted ––––––

LocalBroadcastManager.getInstance(this).unregisterReceiver(recvChangeFrag);

super.onDestroy();

}

private void changeFragment(String fragmentType){

FragmentTransaction ft = getFragmentManager().beginTransaction();

// –––– code omitted ––––––

if(fragmentType.equals("fragment-name")){

MyFragment fragment = new MyFragment();

ft.replace(R.id.contentFrame, fragment, fragmentType);

}

// –––– code omitted ––––––

ft.commit();

}

}

Mai sus este prezentată o structură simplificată a clasei HomeActivity ce subliniează modalitatea prin care sunt încărcate în mod dinamic fragmentele.

Receptorul recvChangeFrag ascultă pentru evenimente ce poartă eticheta change-fragment, iar atunci când este declanșat extrage proprietatea fragmentType aflată pe obiectul intent și apelează metoda changeFragment(). În corpul acestei metode, se crează o instanță a fragmentului dorit, după care este încărcată în recipientul cu identificatorul contentFrame aflat în schema activității HomeActivity.

Figura 4.2.3.1 – Traseul unui eveniment de schimbare a fragmentului prezintă o structură arborescentă a ecranului principal, unde rădăcina este formată din activitatea HomeActivity, ce conține un receptor care ascultă pentru evenimente cu numele de change-fragment, iar celelalte noduri reprezintă fragmentele aplicației. Orice apel al metodei sendBroadcast() trimite un mesaj unidirecțional în arbore, care se deplasează din nou în nod. În final, mesajul va ajunge în rădăcină, unde va declanșa receptorul prezent, care va apela metoda changeFragment().

4.3. Librăria suport a sistemului de operare Android

Versiunea 5.0 a sistemului de operare Android a adus schimbări semnificative și optimizări, în special pe partea grafică, care a fost modificată integral. Acest nou modul grafic poartă numele de Material Design și, pe lângă modificarea componentelor existente, introduce o serie de alte componente menite să ușureze implementarea structurii standard a unei aplicații Android.

Acest modul a avut un succes foarte mare pe noile versiuni de Android, însă, în prezent, o bună parte din utilizatorii dispozitivelor Android folosesc o versiune mai mică de 5.0. Prin urmare, pentru a alinia versiunile anterioare la noul standard a fost necesară dezoltarea unei noi librării de proiectare, android.support, ce face posibilă utilizarea modulului Material Design pe orice versiune Android mai mare sau egală decât 2.0.

În continuare, vor fi prezentate componentele principale expuse utilizării de către librăria mai sus menționată.

4.3.1. Componenta DrawerLayout

Aproape orice aplicație Android care nu face parte din categoria jocurilor necesită un meniu sub o formă sau alta care să permită utilizatorului să navigheze în interiorul acesteia. În versiunile de android mai mici decât 5.0, nu exista un standard al acestui meniu, prin urmare utilizatorii erau întâmpinați de imaginația dezvoltatorilor și erau, deseori, duși în eroare de meniuri neintuitive.

Pentru a rezolva această problemă, librăria android.support expune o componentă numită NavigationView, ce aparține de sub-biblioteca android.support.design.widget, care respectă standardul meniului din modulul Material Design și are schema următoare:

<android.support.design.widget.NavigationView

android:id="@+id/navigation_view"

app:headerLayout="@layout/drawer_header"

app:menu="@menu/drawer"/>

Acest element trebuie plasat în interiorul unui alt element, DrawerLayout, ce face parte din sub-biblioteca android.support.v4.widget, care controlează elementul NavigationView sub forma unei entitate unitară.

În cadrul aplicației Ask Around, componentele DrawerLayout și NavigationView sunt folosite în cadrul activității HomeActivity. Conform capitolului 3.3.2. Arborele de activități și fragmente, această activitate reprezintă scheletul aplicației, având rolul unui recipient ce încarcă diverse fragmente care oferă funcționalități diferite. Prin urmare, plasarea componentelor legate de meniu în această activitate expune funcționalitatea acestora în oricare dintre fragmentele existente, permitând utilizatorului să navigheze prin aplicație din orice punct al acesteia.

<!– –––- HomeActivity.java ––––– –>

<?xml version="1.0" encoding="utf-8"?>

<android.support.v4.widget.DrawerLayout

xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

tools:context="com.ngenh.askaround.HomeActivity"

android:id="@+id/drawer_layout_home"

android:layout_width="match_parent"

android:layout_height="match_parent">

<!– –––- omitted code ––––– –>

<android.support.design.widget.NavigationView

android:id="@+id/navigation_view"

android:layout_width="wrap_content"

android:layout_height="match_parent"

android:layout_gravity="start"

app:headerLayout="@layout/drawer_header"

app:menu="@menu/drawer"/>

</android.support.v4.widget.DrawerLayout>

Se observă faptul că în cadrul componentei NavigationView există următoarele două atribute:

headerLayout – opțional – în cazul în care nu este furnizată o valoare pentru acesta, se va folosi o schemă implicită existentă deja în sistemul de operare. În particular, atributul primește ca valoare calea către fișierul drawer_header.xml care conține o configurație a antetului împreună cu un element de tip TextView.

<!– –––- drawer_header.xml ––––– –>

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:app="http://schemas.android.com/apk/res-auto"

android:orientation="vertical"

android:layout_width="match_parent"

android:layout_height="150dp"

android:padding="16dp"

android:theme="@style/ThemeOverlay.AppCompat.Dark"

android:gravity="bottom"

android:background="@drawable/drawer_bg"

app:backgroundTint="?attr/colorPrimary">

<TextView

android:id="@+id/drawer_header_user"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:textColor="@color/black"

android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>

</LinearLayout>

menu – obligatoriu – fișierul schemă al acestui atribuit este mai special, în sensul că trebuie să definească o colecție de opțiuni ce vor forma meniul.

<!– –––- menu.xml ––––– –>

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android">

<group android:checkableBehavior="single">

<item

android:id="@+id/navigation_item_questions"

android:icon="@drawable/ic_question"

android:title="@string/nav_item_questions" />

<item

android:id="@+id/navigation_item_my_questions"

android:icon="@drawable/ic_book_open_page_variant"

android:title="@string/nav_item_my_questions" />

<item

android:id="@+id/navigation_item_ask_question"

android:icon="@drawable/ic_plus_circle_outline"

android:title="@string/nav_item_add_question" />

</group>

<item android:title="@string/nav_sub_menu">

<menu>

<item

android:id="@+id/navigation_item_profile"

android:icon="@drawable/ic_profile"

android:title="@string/nav_item_profile" />

<item

android:id="@+id/navigation_item_categories"

android:icon="@drawable/file_document_box"

android:title="@string/nav_item_categories" />

<item

android:id="@+id/navigation_item_settings"

android:icon="@drawable/ic_settings"

android:title="@string/nav_item_settings" />

<item

android:id="@+id/action_logout"

android:icon="@drawable/ic_close_circle_outline"

android:title="@string/logout" />

</menu>

</item>

<item

android:id="@+id/menu_none_checked"

android:title=""

android:visible="false"/>

</menu>

După definirea schemei meniului, acesta trebuie să fie configurat pentru a răspunde acțiunilor utilizatorului. Este de notat faptul că fiecare opțiune din fișierul menu.xml este acompaniată de un identificator unic prin care să poată fi referențiată într-un controler.

NavigationView navigationView = (NavigationView) findViewById(R.id.navigation_view);

navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {

@Override

public boolean onNavigationItemSelected(MenuItem menuItem) {

dlDrawer.closeDrawers();

if(previousItem != null && menuItem.equals(previousItem)) {

return true;

}

int id = menuItem.getItemId();

switch (id) {

case R.id.action_logout:

AuthService authService = new AuthService(getApplicationContext(), "received-logout");

authService.logout();

return true;

case R.id.navigation_item_ask_question:

changeFragment("add-question");

return true;

case R.id.navigation_item_profile:

changeFragment("profile");

return true;

case R.id.navigation_item_questions:

changeFragment("questions");

return true;

case R.id.navigation_item_my_questions:

changeFragment("my-questions");

return true;

case R.id.navigation_item_categories:

changeFragment("categories");

return true;

case R.id.navigation_item_settings:

changeFragment("settings");

return true;

}

return true;

}

});

Metoda setNavigationItemSelectedListener() setează un receptor ce va fi declanșat de fiecare dată când o opțiune a meniului este selectată, urmând ca metoda receptorului, onNavigationItemSelected(), să fie apelată cu un obiect de tip MenuItem reprezentând opțiunea selectată. Urmează o testare ce determină exact care opțiune a fost selectată, prin testarea identificatorului său, după care se execută comportamentul aferent opțiunii respective.

4.3.2. Componenta CoordinatorLayout

Această componentă, introdusă de sub-librăria android.support.design.widget, furnizează un control sporit al evenimentelor dintre două scheme ce au o relație de tip părinte-copil, lucru de care se folosesc multe dintre componentele librăriei de suport de proiectare. Elementul CoordinatorLayout este unul de bază, deci nu conține proprietăți de configurare.

<android.support.design.widget.CoordinatorLayout

android:id="@+id/coordinator_home">

În contextul aplicației Ask Around, funcționalitatea componentei CoordinatorLayout este folosită de către elementele:

FloatingActionButton – buton standard ce complementează funcționalitatea unui fragment prin definirea unei acțiuni specifice ce va fi executată la apăsarea acestuia.

RecyclerView – componentă ce prezintă o colecție de elemente customizabile.

SnackbarWrapper – clasă ce folosește componenta Snackback, furnizată de sub-librăria android.support.design.widget, pentru a afișa o notificare din orice punct al aplicației.

Pentru a exemplifica una din situațiile în care funcționalitatea oferită de componenta CoordinatorActivity, va fi prezentată mai întâi problema de comunicare dinre elementele FloatingActionButton și SnacbarWrapper.

Elementul de tip FloatingActionButton este așezat în schema activității HomeActivity sub forma următoare:

<?xml version="1.0" encoding="utf-8"?>

<android.support.v4.widget.DrawerLayout>

<android.support.design.widget.CoordinatorLayout

android:id="@+id/coordinator_home">

<!– –––––- omitted code ––––– –>

<android.support.design.widget.FloatingActionButton

android:id="@+id/fab"

android:src="@drawable/ic_plus" />

</android.support.design.widget.CoordinatorLayout>

<!– –––––- omitted code ––––– –>

</android.support.v4.widget.DrawerLayout>

Datorită acestei amplasări, elementul nu ar putea, în mod normal, să răspundă la evenimentele declanșate de fragmentele copil ale activității, prin urmare, notificările declanșate de clasa SnackbarWrapper ar fi acoperit butonul în cauză.

Folosirea componentei CoordinatorLayout rezolvă această problemă prin preluarea evenimentului declanșat de clasa SnackbarWrapper și permiterea butonului FloatingActionButton să răspundă la acesta înaintea execuției sale. Acest lucru se realizează prin transmiterea unui parametru funcției make(), expusă de clasa Snackbar, ce reprezintă referința către elementul de tip CoordinatorLayout.

Astfel, din oricare fragment al aplicației, se poate efectua o informare folosind clasa Snackbar cu următoarea linie de cod:

Snackbar.make(findViewById(R.id.coordinator_home),

"informative text", Snackbar.LENGTH_SHORT).show();

În ceea ce privește componenta RecyclerView, aceasta vine ca un înlocuitor al elementelor vechi precum ListView, GridView sau ScrollView, iar CoordinatorLayout oferă suport total pentru aceasta, fâcând-o alegerea logică în contextul aplicației Ask Around. Mai mult, pe langă funcționalitățile de bază, componenta RecyclerView oferă mai multe modalități de manipulare a datelor pe care le conține. Acestea vor fi discutate detaliat în capitolul următor.

4.3.3. Componenta RecyclerView

Această componentă înlocuiește modul tradițional de reprezentare a unei colecții de elemente de interfață. Una din cele mai importante optimizări introduse de ea este – așa cum insinuează și denumirea – reciclarea elementelor care fac parte din colecția sa.

În metoda folosită de componentele tradiționale, atunci când un element de interfață trebuia desenat, acesta trecea prin următoarele procese:

Se creează schema elementului și se face legătura dintre ea și controler.

Se încarcă în controler datele de intrare pentru elementul ce urmează a fi creat.

Se randează elementul grafic utilizând datele de intrare primite și se afișează pe ecran.

Acest proces se repeta pentru fiecare element de interfață creat. Problema apăruta în acest mod de proiectare este faptul că aceste elemente erau create de fiecare dată când trebuiau să apară pe ecran, iar în momentul în care trebuiau să dispară erau distruse complet. Acestă utilizare ineficientă a memoriei încetinea rularea aplicației, în special pentru cazurile în care elementele grafice necesitau multe resurse pentru a fi construite.

Soluția implementată de componenta RecyclerView este – precum sugerează și denumirea acesteia – de a recicla elementele, reducând astfel numărul de pași de la trei la unul singur, anume randarea elementului grafic utilizând datele de intrare primite.

Această componentă încarcă toate datele colecției deodată, după care se folosește de un model schelet al unei scheme pentru a crea elemente grafice ce sunt afișate pe ecran. În momentul în care unul din aceste elemente nu mai este vizibil, datele asociate lui sunt desprinse de schemă, însă aceasta nu este distrusă. În schimb, următorul element grafic care urmează să fie afișat preia schema respectivă și își încarcă datele proprii. Această operație este administrată de o clasă adaptor.

Pe lângă această optimizare importantă, componenta RecyclerView expune mai multe funcționalități ce ajută la manipularea datelor din colecția pe care o conține:

RefreshListener – furnizează posibilitatea de a reîncărca datele din componentă prin glisarea în jos a degetului pe componentă, atunci când primul element afișat este și primul element al colecției.

LoadMore – poate încărca date într-un mod “leneș” (lazy loading) pentru a optimiza cantitatea de date transferată dintre aplicație și server.

ScrollListeners – receptori ce pot reacționa la evenimentele de derulare a elementelor din colecție.

În continuare, va fi prezentată modalitatea prin care este utilizat elementul RecyclerView, împreună cu funcționalitățile mai sus menționate.

Mai întâi de toate, acesta trebuie să fie inclus în colecția de dependențe a aplicației localizată în interiorul fișierului build.gradle.

dependencies {

// –- omitted dependencies –-

compile 'com.android.support:recyclerview-v7:25.3.0'

// –- omitted dependencies –-

}

În acest moment, componenta RecyclerView poate fi folosită în orice fișier schemă al aplicației. Aceasta este utilizată pentru a reprezenta grafic o colecție de elemente, prin urmare este folosită în mai multe module ale aplicației Ask Around, însă cel mai notabil, și cel care va fi prezentat în continuare, este modulul questions care afișează o listă cu toate întrebările ce corespund criteriilor de căutare ale utilizatorului.

Acest modul este reprezentat într-un fragment ce va fi inclus în HomeActivity, având asociate ca schemă de vizualizare și controler fișierele fragment_questions.xml, respectiv Questions.java.

<!– fragment_questions.xml –>

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_height="match_parent"

android:layout_width="match_parent"

android:orientation="vertical">

<include android:id="@+id/no_results_found"

android:visibility="gone"

android:gravity="center"

layout="@layout/no_results_found"/>

<android.support.v4.widget.SwipeRefreshLayout

android:id="@+id/swipe_refresh"

android:layout_width="match_parent"

android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView

android:id="@+id/recycler_view_questions"

android:layout_height="wrap_content"

android:layout_width="match_parent"

android:visibility="invisible"

android:scrollbars="vertical">

</android.support.v7.widget.RecyclerView>

</android.support.v4.widget.SwipeRefreshLayout>

<FrameLayout

android:id="@+id/fallback_frame"

android:layout_width="match_parent"

android:layout_height="match_parent">

</FrameLayout>

</LinearLayout>

În acest fișier schemă se observă prezența elementului RecyclerView în care va fi încărcată colecția de întrebări. De asemenea, acesta este cuprins în interiorul elementului SwipeRefreshLayout a cărui scop este de a seta receptorii evenimentelor de glisare, în particular pentru funcționalitatea de RefreshListener.

Clasa Questions.java servește rolul de controler al schemei mai sus prezentate. Prin urmare, aceasta este responsabilă cu încărcarea colecției de întrebări în RecyclerView, dar și cu setarea receptorilor. Înainte însă, trebuie să creeze o legătură cu toate elementele schemei pentru a le putea referenția în interiorul ei.

Referințele către componente vor fi ținute în membrii privați ai clasei Questions:

private LinearLayoutManager llmRecycleLayout;

private RecyclerView rvRecycler;

private SwipeRefreshLayout srlSwipeRefresh;

private QuestionsAdapter questionsAdapter;

private QuestionsService questionsService;

llmRecycleLayout – definește modul de amplasare a elementelor din RecyclerView.

rvRecycler – elementul RecyclerView propriu-zis.

srlSwipeRefresh – folosit pentru a defini receptorii de glisare.

questionsAdapter – adaptorul ce mapează un obiect de tip Question pe schema schelet din RecyclerView.

questionsService – serviciu folosit pentru a solicita întrebări de la server.

Legătura dintre acești membrii și elementele corespunzătoare din schemă se realizează în funcția initUI(), apelată din metoda onViewCreated().

@Override

public void onViewCreated(View view, Bundle savedInstanceState) {

super.onViewCreated(view, savedInstanceState);

initUI(view);

// ––– omitted code –––

}

private void initUI(View view){

llmRecycleLayout = new LinearLayoutManager(getActivity());

llNoResultsFound = (LinearLayout) view.findViewById(R.id.no_results_found);

rvRecycler = (RecyclerView) view.findViewById(R.id.recycler_view_questions);

srlSwipeRefresh = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh);

// ––– omitted code –––

}

În acest moment, pregătirile sunt terminate și se poate începe configurarea fiecărui element.

rvRecycler.addOnScrollListener(new EndlessScrollListener(llmRecycleLayout) {

@Override

public void onLoadMore(int page, int totalItemsCount) {

if(Integer.parseInt(Global.options.get("questions").get("page"))>page)return;

Global.options.get("questions").put("page", String.valueOf(page));

questionsService.list(Global.options.get("questions"));

}

});

Mai sus este prezentată modalitatea de a implementa funcționalitatea de LoadMore pe elementul RecyclerView. Acestuia îi este adăugat un receptor de glisare de tipul EndlessScrollListener – care primește ca parametru schema după care vor fi aranjate elementele – ce îi permite utilizatorului să scroleze la infinit, cât timp există elemente de afișat.

Pentru a nu efectua operații lungi, serverul grupează întrebările și trimite unei cereri doar unul din aceste grupuri, al cărui index este specificat prin parametrul page al cerereii. Implementând funcționalitatea de LoadMore, elementul RecyclerView va primi mai întâi primul grup de rezultate, urmând ca, în momentul în care utilizatorul ajunge aproape de finalul acestuia, să fie trimisă automat încă o cerere pentru următorul grup.

În continuare, este setat receptorul pentru reactualizarea datelor din RecyclerView.

srlSwipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {

@Override

public void onRefresh() {

// –– omitted code ––

Global.options.get("questions").put("page", Global.defaultOptions.get("page"));

questionsService.list(Global.options.get("questions"));

}

});

Una din responsabilitățile funcției onRefresh() este de a reseta parametrul page al dicționarului Global.options și de a efectua o altă cerere pentru a lua noile informații.

În urma trimiterii unei astfel de cerere către server, după o perioadă de timp, este returnat un răspuns ce conține grupul de întrebări corespunzător, moment în care este apelată funcția showQuestions().

private void showQuestions(){

if(srlSwipeRefresh.isRefreshing()){

srlSwipeRefresh.setRefreshing(false);

}

// –– omitted code ––

rvRecycler.setVisibility(View.VISIBLE);

if(questionsAdapter == null) {

questionsAdapter = new QuestionsAdapter(getActivity(), Global.questions);

rvRecycler.setAdapter(questionsAdapter);

rvRecycler.setLayoutManager(llmRecycleLayout);

}else{

questionsAdapter.notifyDataSetChanged();

}

if(Global.questions.size() == 0){

llNoResultsFound.setVisibility(View.VISIBLE);

} else {

llNoResultsFound.setVisibility(View.GONE);

}

}

Aceasta are rolul de a afișa elementul RecyclerView și de a îl configura cu un adaptor care să încarce datele. În cazul în care nu există nicio întrebare în colecția din răspuns, este afișat elementul llNoResultsFound.

Adaptorul questionsAdapter este responsabil cu legătura dintre schema elementului RecyclerView și fiecare element al colecției cu care a fost instanțiat. Acesta este o instanță a clasei QuestionsAdapter ce are structura următoare:

public class QuestionsAdapter extends RecyclerView.Adapter<QuestionsAdapter.ViewHolder> {

private LayoutInflater inflater;

private List<QuestionObj.Question> questions;

private Context context;

private QuestionsService questionsService;

public QuestionsAdapter(Context context, List<QuestionObj.Question> questions){

inflater = LayoutInflater.from(context);

this.context = context;

this.questions = questions;

this.questionsService = new QuestionsService(context, "question-voted");

}

@Override

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

View view = inflater.inflate(R.layout.fragment_question, parent, false);

ViewHolder holder = new ViewHolder(view);

return holder;

}

// ––––––– code omitted –––––––

@Override

public void onBindViewHolder(final QuestionsAdapter.ViewHolder holder, int position) {

// ––––––– code omitted –––––––

}

@Override

public int getItemCount() {

return questions.size();

}

class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

// ––––––– code omitted –––––––

}

}

După cum se poate observa, ea extinde clasa RecyclerView.Adapter, iar clasa furnizată ca parametru tipului generic este QuestionsAdapter.ViewHolder. Cea din urma este practic recipientul pe care se mapează fiecare element din colecția dată în constructorul clasei QuestionsAdapter, anume questions. Este de notat suprascrierea următoarelor funcții:

onCreateViewHolder() – rolul acestei funcții este de a dicta modalitatea de creare a unui element de tip ViewHolder ce va fi afișat în interiorul elementului RecyclerView.

View view = inflater.inflate(R.layout.fragment_question, parent, false);

Linia de mai sus face legătura schemei cu identificatorul unic fragment_question și elementul abia insanțiat de tip ViewHolder.

Structura fișierului schema, într-o forma minimalistă, arată sub felul următor:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android">

<LinearLayout android:id="@+id/llContent">

<LinearLayout>

<TextView android:id="@+id/tvQuestion"/>

<TextView android:id="@+id/tvContent"/>

</LinearLayout>

<LinearLayout>

<TextView android:id="@+id/tvQuestionScore"/>

<ImageView android:id="@+id/ivUpVote"/>

<ImageView android:id="@+id/ivDownVote"/>

</LinearLayout>

</LinearLayout>

<LinearLayout android:id="@+id/loAuthor">

<TextView android:id="@+id/tvAuthor"/>

<TextView android:id="@+id/tvDate"/>

</LinearLayout>

</RelativeLayout>

onBindViewHolder() – aici sunt inițializate toate elementele copil ale instanței ViewHolder cu datele ce trebuie afișate.

public void onBindViewHolder(final QuestionsAdapter.ViewHolder holder, int position) {

final QuestionObj.Question question = questions.get(position);

holder.tvQuestion.setText(question.getTitle());

holder.tvContent.setText(question.getContent());

holder.tvAuthor.setText(QuestionObj.Question.getAuthor(question));

holder.tvDate.setText(QuestionObj.Question.getCreatedAtAsString(question));

holder.tvQuestionScore.setText(String.valueOf(question.getScore()));

holder.ivUpVote.setOnClickListener(new View.OnClickListener() {

// –– implementation detail –––

});

holder.ivDownVote.setOnClickListener(new View.OnClickListener() {

// –– implementation detail –––

});

holder.id = question.getId();

}

Se obține referința către elementul curent ce trebuie construit, acesta având poziția position în interiorul colecției questions. După care, informațiile relevante sunt încărcate în elementele copil ale instanței ViewHolder și este setat comportamentul butoanelor ivUpVote și ivDownVote.

getItemCount() – funcție responsabilă doar cu returnarea numărului de elemente prezente în adaptor.

O instantă a clasei ViewHolder, după cum este specificat și mai sus, reprezintă elementul ce va fi afișat utilizatorului. Prin urmare, acesta este responsabil cu crearea referințelor către elementele copil ale schemei și definirea comportamentului pentru reacționarea la evenimentul de clic pe acesta.

class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

private TextView tvQuestion;

private TextView tvContent;

private TextView tvAuthor;

private TextView tvDate;

private TextView tvQuestionScore;

private ImageView ivUpVote;

private ImageView ivDownVote;

private int id;

public ViewHolder(View itemView) {

super(itemView);

itemView.setOnClickListener(this);

tvQuestion = (TextView) itemView.findViewById(R.id.tvQuestion);

tvContent = (TextView) itemView.findViewById(R.id.tvContent);

tvAuthor = (TextView) itemView.findViewById(R.id.tvAuthor);

tvDate = (TextView) itemView.findViewById(R.id.tvDate);

tvQuestionScore = (TextView) itemView.findViewById(R.id.tvQuestionScore);

ivUpVote = (ImageView) itemView.findViewById(R.id.ivUpVote);

ivDownVote = (ImageView) itemView.findViewById(R.id.ivDownVote);

}

@Override

public void onClick(View view) {

Intent intent = new Intent("selected-question");

intent.putExtra("questionId", id);

intent.putExtra("question", tvQuestion.getText().toString());

intent.putExtra("content", tvContent.getText().toString());

intent.putExtra("author", tvAuthor.getText().toString());

intent.putExtra("date", tvDate.getText().toString());

intent.putExtra("questionScore", tvQuestionScore.getText().toString());

for(int i = 0; i < Global.questions.size(); i++){

if(Global.questions.get(i).getId() == id){

Global.questionPosition = i;

break;

}

}

LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

}

}

Crearea referințelor către elementele copil are loc în interiorul constructorului, iar metoda suprascirsă onClick() definește comportamentul mai sus menționat. Aceasta trimite o intenție în interiorul aplicației pentru a informa o anumită componentă de primirea evenimentului clic, astfel încât componenta respectivă să poată reacționa ca atare.

Se observă astfel ușurința utilizării componentei RecyclerView în cadrul aplicației, care oferă un nivel foarte mare de modularitate al codului, dar și un grad ridicat de încapsulare, împărțind responsabilitățile către mai multe componente secundare.

5. Utilizarea aplicației

5.1 Scurtă descriere

Aplicția Ask Around are scopul de a furniza un mediu deschis, necenzurat și customizabil ce permite utilizatorilor săi să participe în mod activ la discuții de orice tip, totodată oferind și posibilitatea ca aceștia să aleagă anumite teme sau cuvinte cheie pe care aceștia vor să le ignore.

De asemenea, datorită faptului că este o aplicație dezvoltată pentru sistemul de operare Android, utilizat pe majoritatea dispozitivelor mobile, ea satisface nevoia de accesibilitate a utilizatorilor, fiind disponibilă în orice moment, singura restricție fiind existența unei conectivități la Internet necesară pentru comunicarea cu ceilalți utilizatori.

5.2 Scenarii de utilizare

În continuare vor fi prezentate diverse scenarii pe care un utilizator le poate întâmpina în cursul folosirii aplicației Ask Around.

5.2.1. Autentificarea utilizatorilor

Odată pornită aplicația, un utilizator este întâmpinat de activitatea de autentificare, unde poate alege să se logheze cu un cont deja existent, sau, în cazul în care acesta nu deține încă credențialele pentru un cont, poate naviga către activitatea de înregistrare pentru a își crea unul nou.

Pentru logarea într-un cont existent, un utilizator trebuie să introducă email-ul și parola asociată contului respectiv. În cazul în care una dintre acestea este greșită, pe ecran apare un mesaj informativ ce aduce utilizatorului la cunsoștință faptul că nu a putut fi logat cu succes, fară a preciza care din câmpuri este cel greșit, din motive de securitate.

În cazul înregistrării unui cont nou, sunt necesare mai multe informații de la utilizator, pentru ca acesta să poată fi identificat mai ușor în aplicație. De asemenea, parola noului cont trebuie introdusă de două ori pentru a evita orice greșeală de tipografie.

De asemenea, aplicația se protejează împotriva introducerii datelor invalide, atât la logare, cât și la înregistrarea unui nou utilizator prin verificarea existenței acestora, dar și a corectitudinii.

5.2.2. Interogarea întrebărilor

Partea principală a aplicației o reprezintă interogarea întrebărilor create de alți utilizatori și participarea la discuțiile din cadrul acestora. Odată autentificat, un utilizator este întâmpinat de pagina de căutare a întrebărilor. Acesta poate naviga prin celelalte părți ale aplicației utilizând meniul accesibil prin apăsarea butonului din partea dreapta-sus a ecranului.

Pentru un utilizator nou, primele întrebări afișate sunt cele care au fost adăugate cel mai recent în aplicație, indiferent de natura acestora sau de categoriile din care face parte. Acest lucru se datorează faptului că aplicația încă nu are date despre preferințele noului utilizator. Pe măsură ce acesta va începe să caute subiecte de discuție interesante pentru el, aplicația va utiliza aceste date pentru a selecționa acele întrebări care se mulează cel ma bine pe necesitățile utilizatorului.

În cazul unui utilizator logat, foarte probabil acesta a mai avut căutări în trecut, prin urmare datele căutărilor sale există deja. Pe baza acestora, aplicația poate lua decizia de a activa anumite filtre fără necesitatea interacțiunii cu utilizatorul pentru a se asigura că acesta este întâmpinat de întrebări care au probabilitatea cea mai mare, statistic vorbind, să încurajeze utilizatorul să participe la o discuție în cadrul acestora.

Partea de sus a aplicației este reprezentată de o bară de instrumente care conține filtrele întrebărilor. Lupa, ,permite utilizatorului să caute întrebări pe baza unui cuvânt cheie.

Iconița localizată lângă lupă, ,are rolul de a afișa utilizatorului un dialog prin care acesta poate selecta până la cinci categorii de întrebări pe care acesta dorește să le vizualizeze.

Rolul ultimei iconițe, , este de a furniza o modalitate rapidă de a curăța toate filtrele, efectuând astfel o căutare similară cu cea a unui utilizator nou înregistrat, excepție făcând cazul în care utilizatorul curent are defnite preferințe proprii pentru filtrarea întrebărilor.

5.2.3. Preferințele utilizatorului

Partea de setări a aplicației prezintă utilizatorului posibilitatea de a filtra global toate întrebările pe baza unor criterii proprii, utilizând cuvinte cheie și categorii pentru a scăpa de întrebările nedorite.

Figura 5.2.3.2 – Preferințele utilizatorului reprezintă dialogul întămpinat de utilizator în momentul în care acesta dorește să schimbe tema aplicației.

5.2.4. Profilul utilizatorului

În această secțiune este prezentată modalitatea în care utilizatorul poate să își modifice datele personale. În cazul în care se dorește schimbarea parolei, aceasta trebuie introdusă de două ori, similar cu formularul de înregistrare.

5.2.5. Adăugarea unei noi întrebări

Pentru dezvoltarea comunității aplicației, este necesară deschiderea unor noi subiecte de discuție prin crearea întrebărilor la care să poată participa și ceilalți utilizatori.

Similar cu formularul de înregistrare al unui nou utilizator, acesta este și el protejat de introducerea datelor invalide prin validarea câmpurilor existente. De asemenea, unei întrebări îi pot fi atribuite până la cinci categorii pentru a putea fi mai ușor găsită de ceilalți utilizatori ai aplicației.

Concluzii

Dezvoltarea aplicației Ask Around a pus la încercare folosirea a unor multitudini de cunoștințe, atât din domeniul programării, cât și din cel al rețelisticii precum: protocoale de comunicație, programarea orientată pe obiecte, folosirea modelelor de proiectare, securitatea bazată pe token de autentificare, baze de date și programarea asincronă. Mai mult, a fost necesară aprofundarea cunoștințelor deja existente, dar și studierea unor tehnologii noi care să permită rularea cât mai optimă a aplicației și să creeze o experientă cât mai plăcută utilizatorului.

Obiectivul de a crea o aplicație cât mai rapida, cu un cod corect scris, ușor de menținut și modular a dus la folosirea arhitecturii MVC atât în cadrul aplicației server, cât și în cadrul celei client. De asemenea, urmărind același scop, a fost creat și sistemul de comunicare prin evenimente folosit în aplicația Android, care permite transmiterea rapidă și sigura a datelor fară ca aceastea să părăsească mediul în care rulează aplicația, și modularizarea codului din aplicația server prin construirea clasei CRUDView, care încapsulează toate operațiile principale HTTP, oferind o funcționalitate generală, dar completă claselor copil.

O mare problemă a reprezentat partea de experiență a utilizatorului în aplicație. Dat fiind faptul că dispozitivele Android sunt dotate cu un ecran mic, funcționalitățile trebuie să nu aglomereze partea vizuală, însă totodată trebuie să fie accesibile în orice moment sau printr-un efort cât mai mic. Acest lucru a condus la folosirea librăriei suport a sistemului de operare Android ce permitea utilizarea elementelor de Material Design, prezente doar în versiunile noi ale sistemului de operare, și în versiuni mai vechi. Aceasta a oferit elemente precum DrawerLayout și RecyvlerView familiare utilizatorilor, acestea fiind des întâlnite și în alte aplicații.

Comunicarea dintre client și server a condus la studierea protocolului de serializare a datelor, anume JSON, acesta fiind un mod modern, optim și ușor utilizabil de a transmite informații și de a le folosi în cadrul mai multor limbaje de programare, în particular Python și Java.

Rezultatul final al lucrării se manifestă prin aplicația Ask Around care îndeplinește toate obiectivele formulate inițial:

Oferă un mediu sigur de comunicare.

Scapă de granițele impuse de societate, fiind acceptat orice subiect de discuție.

Este foarte accesibilă datorită faptului că a fost dezvoltată pentru un sistem de operare mobil ce deține o cotă foarte mare de piață.

Se adaptează în mod dinamic la preferințele utilizatorului.

Oferă o interfață grafică familiară și accesibilă pentru orice utilizator.

Oferă un grad de customizare ridicat pentru subiectele de discuție.

Bibiliografie:

B. Hardy și B. Phillips, ”Android Programming – The Big Nerd Ranch Guide”, ISBN: 978-0321804334, Septembrie 2013, Editura Big Nerd Ranch, Inc

C. J. Date, "SQL and Relational Theory 3rd Edition", Ch 4, ISBN: 978-1-491-94117-1, Octombrie 2015, Editura O’Reilly

C. McDonough, ”The Pyramid Web Application Development Framework”, ISBN: 978-0615445670, Februarie 2011, Editura Agendaless Consulting

M. Bayer, ”SQLAlchemy Documentation”, Iunie 2016

Anexe

Android, ”Introduction to Android”, 19.05.2017

<https://developer.android.com/guide/index.html>

Aspiring Craftsmna, ”Interactive Application Arhitecture Patterns”, 19.05.2017

<http://aspiringcraftsman.com/2007/08/25/interactive-application-architecture/>

PostgreSQL, PostgreSQL 9.6 Documentation, 19.05.2017

<https://www.postgresql.org/docs/9.6/static/index.html>

Pylons Project, ”The Pyramid Web Framework”, 19.05.2017

<http://docs.pylonsproject.org/projects/pyramid/en/latest>

Python Central, ”Introductory Tutorial of Python’s SQLAlchemy”, 19.05.2017

<http://pythoncentral.io/introductory-tutorial-python-sqlalchemy/>

Python Central, ”Understanding Python’s SQLAlchemy session”, 19.05.2017

<http://pythoncentral.io/understanding-python-sqlalchemy-session/>

StackOverflow, ”Can SQLAlchemy’s session merge-update its result with newer data”, 09.05.2017

< https://stackoverflow.com/a/1851498/5619416 >

SQLAlchemy, ”Object Relational Tutorial – SQLALchemy”, 19.05.2017

<http://docs.sqlalchemy.org/en/latest/orm/tutorial.html>

Tech Your Chance, ”MVP and MVC in Android”, 19.05.2017

<http://www.techyourchance.com/mvp-mvc-android-1/>

The Design of Postgres – Michael Stonebraker and Lawrence A. Rowe, 19.05.2017

<http://dl.acm.org/citation.cfm?id=16888>

TutorialsPoint, ”Android Hello World Example ”, 19.05.2017

<https://www.tutorialspoint.com/android/android_hello_world_example.htm>

Upday, ”Android Arhitecture Patterns: Model-View-Controller”, 19.05.2017

<https://upday.github.io/blog/model-view-controller/>

Wikipedia, ”Intent (Android)”, 08.05.2017

<https://en.wikipedia.org/wiki/Intent_(Android)>

Wikipedia, ”JSON”, 08.05.2017

<https://ro.wikipedia.org/wiki/JSON>

Wikipedia, ”SHA-2”, 09.05.2017

<https://en.wikipedia.org/wiki/SHA-2>

Wikipedia, ”Singleton pattern”, 07.05.2017

<https://en.wikipedia.org/wiki/Singleton_pattern>

Wikipedia, ”Unicode”, 07.05.2017

<https://en.wikipedia.org/wiki/Unicode>

Similar Posts