În Tips & Tricks

Integrarea aplicațiilor la nivel de date – partea I

integrarea_aplicatiilor

 

Dacă în articolul anterior, ne-am concentrat asupra modului în care o aplicație poate să stocheze informație într-o bază de date, astăzi vom vorbi despre cum putem folosi aceste concepte pentru a integra mai multe aplicații.

Integrarea

Integrarea aplicațiilor nu este ceva nou în lumea Android. Am putea descrie mecanismul de Intent-uri ca și o integrare la nivel de interfață, similară arhitecturilor bazate pe servicii. Dar una dintre cele mai cunoscute și folosite tipuri de integrare rămâne cea la nivelul datelor. Martin Fowler, un expert în Object-Oriented Design, numește acest mod de integrare Database Integration. Practic, prin folosirea unui data store comun, se elimină necesitatea unui strat intermediar care să gestioneze integrarea. Totul se reduce la comunicarea de date, prezente în data store-uri și o modularizare mai eficientă a arhitecturii.

Conceptul de integrare la nivel de date nu este unul nou, nici chiar pentru Android, ci este o parte importantă din platformă. Acest concept este puternic folosit în aplicațiile de bază ale platformei (Calendar, Contacts etc.), iar SDK-ul oferă un API complet pentru crearea și folosirea bazelor de date de integrare și poartă denumirea de Content Provider.

Content Providers

Un content provider este responsabil cu oferirea conținutului aferent unei solicitări. Dacă în articolul anterior menționam că bazele de date SQLite sunt dedicate doar contextului aplicației în care sunt gestionate, content provider-ul oferă posibilitatea expunerii acestor date spre alte aplicații într-un mod controlat și astfel, facilitând integrarea.

În mare, un content provider gestionează accesul la o bază de date (dar nu numai). Content provider-ul e o componentă de bază a unei aplicații, dar nu oferă niciun fel de UI pentru interacțiunea utilizatorului cu datele. Scopul său principal este să expună datele spre alte aplicații prin intermediul unui client. Împreună, content provider-ul și clienții săi oferă o interfață standardizată și consistentă către date, având în același timp grijă de comunicarea inter-procesuală și securitatea datelor.

Cum funcționează?

Să vedem cum funcționează un provider. În primul rând, un content provider este o componentă a unei aplicații, asemănătoare cu serviciile și activitățile și trebuie declarată în fișierul de manifest. Pentru că un content provider oferă access la o resursă, schema folosită pentru identificare este URI. Astfel, atunci când declarăm un provider în AndroidManifest.xml, trebuie specificată baza pentru URI prin intermediul atributului android:authorities. Declararea se face în interiorul tag-ului de application. Iată un exemplu:

<provider
        android:authorities="de.vogella.android.todos.contentprovider"
        android:name=".contentprovider.MyTodoContentProvider" >
</provider>

Este destul de evident că URI-ul folosit trebuie să fie unic, altfel Android nu va putea identifica singular content provider-ul. Se poate observa că avem trecut în declarație un alt atribut, android:name. Acesta este numele clasei care implementează funcționalitatea prin extinderea clasei abstracte ContentProvider.  Această clasă trebuie să implementeze o serie de metode prin care va accesa efectiv datele. Metodele sunt după modelul CRUD: create, read, update, delete și au denumiri foarte sugestive: query(), insert(), update() și delete(). În cazul în care nu dorim să oferim accesul la o anumită operație, de exemplu, cea de ștergere pentru a ne proteja datele, putem arunca o excepție la apelare: UnsupportedOperationException.

Am amintit la început că acest mecanism este un mod de a expune date în mod securizat. Odată cu înaintarea în versiune, Android a modificat politica de acces la resurse. Astfel, dacă până la versiunea 4.2 content provider-ii erau expuși implicit spre alte aplicații, începând cu 4.2 aceștia trebuie exportați explicit. Acest lucru se face prin specificarea în tag-ul de manifest a atributului android:exported și setarea lui pe true sau false. Această măsură nu înseamnă că este descurajată expunerea de date spre alte aplicații, ci ea vizează doar o sporire a siguranței. Unele aplicații mai complexe pot avea o arhitectură puternic modularizată și doresc folosirea de content provideri doar în interiorul ei, pentru a facilita integrarea modulelor și nu doresc neapărat expunerea lor spre exterior.

Un alt aspect important este cel de concurență. Am vorbit de firele de execuție într-un articol anterior. Pentru că accesul la baza de date poate avea loc din mai multe direcții, aspectul concurenței devine și mai important pentru că aceasta poate fi accesată și din alte aplicații, de pe procese diferite. Este foarte important, atunci când creăm un content provider, să ne asigurăm că el este thread-safe. Cel mai simplu ar fi să folosim synchronized în toate metodele, astfel încât un singur fir să poată accesa metoda la un moment dat. O altă soluție este să setăm atributul de android:multiprocess pe true atunci când definim provider-ul în manifest. Aceasta permite ca o instanță a provider-ului să fie creată în fiecare proces client, eliminând nevoia de comunicare inter-procesuală.

Utilizarea

Acum că avem o idee despre cum funcționează un content provider, să vedem efectiv cum îl putem folosi. După cum spuneam, acesta oferă acces la bazele de date ale unei aplicații.

schema_dev_androidDin acest motiv, modul de folosire este foarte asemănător cu cel de acces direct la acestea. Astfel, un content provider va returna un Cursor (vezi articolul anterior) care va conține datele cerute. Să luăm, de exemplu, o aplicație care dorește să citească contactele stocate în telefon. Metoda ar arăta cam așa:

private Cursor getContacts() {
        // Run query
        Uri uri = ContactsContract.Contacts.CONTENT_URI;
        String[] projection = new String[] { 
ContactsContract.Contacts._ID,
           ContactsContract.Contacts.DISPLAY_NAME };
        String selection = ContactsContract.Contacts.IN_VISIBLE_GROUP + " = '"
           + ("1") + "'";
        String[] selectionArgs = null;
        String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
           + " COLLATE LOCALIZED ASC";
           return managedQuery(uri, projection, selection, selectionArgs,
           sortOrder);
}

După cum se observă, modul de acces la date este similar cu cel de la SQLite, dar până la un punct. Metoda managedQuery() preia o serie de parametri pentru a putea obține datele dorite. Cel mai important parametru de acolo este primul: URI. El reprezintă content provider-ul de la care dorim să obținem informația. În cazul nostru, el are o valoare dintr-o constantă, oferită de SDK: ContactsContract.Contacts.CONTENT_URI, care identifică content provider-ul de contacte. Restul codului este foarte asemănător cu o interogare SQL și nu vom insista asupra lui.

Două precizări merită făcute legat de codul de mai sus. Metoda managedQuery()din clasa Activity este depășită începând cu API Level 11 și este încurajată folosirea clasei CursorLoader. În același timp, accesul la informație sensibilă, cum e cea de contacte, necesită permisii speciale în cadrul manifestului, utilizatorul fiind informat că aplicația va avea acces la acestea. Pentru a primi permisia de acces, trebuie adaugat tag-ul aferent în manifest:

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

După cum putem observa, unul dintre avantajele principale de a face integrarea la nivel de date este că se renunță la codul suplimentar. Pentru a putea accesa date, este de ajuns pentru o aplicație să folosească content provider-ul aferent. Acest content provider este un strat de abstractizare peste o bază de date SQLite. De aici vine și nevoia de a lucra în continuare cu cursori. În afară de familiaritatea acestui concept, putem beneficia și de restul facilitatilor SQL, putând include clauze cum ar fi LIKE în query-uri.

În partea a doua a acestui articol, vom vedea cum putem lucra cu un content provider dedicat.