Sviluppare un'applicazione Android su CouchDB

Questo documento riporta gli elementi fondamentali di un'Android App che accede ad un database CouchDB.
Android e' il diffusissimo sistema operativo per dispositivi Mobile che permette di sviluppare App sofisticate. CouchDB e' un database document-oriented scritto in Erlang, sotto licenza Apache. A differenza dei database SQL tradizionali, CouchDB salva i dati in documenti, ognuno con uno schema libero e indipendente dagli altri documenti.

Lo sviluppo di un'applicazione Android su CouchDB e' particolarmente interessante poiche' lo stack necessario per gestire i dati di CouchDB e' molto leggero e la struttura dei documenti e' molto flessibile: sono due caratteristiche molto vantaggiose per lo sviluppo di applicazioni su Mobile.

Questo documento e' organizzato nei seguenti capitoli: App Android, Architettura applicativa, Metodi principali (cURL, Java), Analisi della risposta, Applicazione d'esempio, ...
La lettura di questo documento richiede alcune conoscenze di base su Java, CouchDB ed Android. Documenti introduttivi sono Ditelo con... Java, Introduzione a CouchDB, Introduzione ad Android.

Android App

Un'Android App e' un'applicazione sviluppata per il sistema operativo Android.

Per sviluppare applicazioni in grado di girare su sistemi Android, e' necessario installare sul proprio PC un apposito kit di sviluppo (SDK), che sia completo di emulatore, librerie e documentazione. L'Android SDK e' disponibile gratuitamente per sistemi Windows, Linux e MacOS X. L'installazione e' semplice: l'unica cosa di cui bisogna accertarsi e' soddisfare i requisiti di base, come Java SDK.

Per facilitare lo sviluppo delle applicazioni e' reso disponibile un add-on chiamato Android Development Tools for Eclipse (ADT) per la piattaforma di sviluppo Open Source Eclipse. Grazie all'ADT e' possibile configurare rapidamente nuovi progetti Android, creare un'interfaccia utente, effettuare il debug dell'applicazione ed esportare il file .apk al fine di distribuire l'applicazione.

Architettura applicativa

L'interazione con il database CouchDB e' possibile grazie ai comandi HTTP ed agli oggetti JSON. Utilizzando i comandi HTTP, e' facile far interagire qualsiasi applicazione con il database. Infatti tali programmi sono disponibili su Java attraverso il pacchetto org.apache.http.client, disponibile a partire da API Level 1.
Invece i dati, cio' vale sia per le risposte ad una determinata richiesta che per i dati che vogliamo salvare all'interno del database, sono scritti in oggetti JSON. Anche in questo caso e' disponibile un pacchetto su Java org.json anch'esso disponibile a partire da API Level 1.

Nel capitolo successivo analizzeremo i metodi necessari all'implementazione di un'applicazione Java completa.

Metodi principali

Di seguito sono riassunti le principali azioni che si possono fare su questo tipo di database, confrontando la sintassi in Java con quella utilizzata con lo strumento cURL: creazione documento, eliminazione documento, aggiornamento documento, elenco di tutti i documenti, visualizzare un documento, visualizzare un allegato, visualizzare il risultato di una query.

Non sono presenti tutti i metodi disponibili ed utilizzabili, ma solamente quelli piu' utilizzati all'interno di un'applicazione Android.

Creazione di un documento all'interno del database

curl -X PUT http://couchdb.xenialab.it:5984/rubrica_app/[id_documento] -d '{}'

public void creaDocIdSpec (String id_documento) {
 HttpClient client = new DefaultHttpClient();
 HttpPut request = new HttpPut("http://couchdb.xenialab.it:5984/rubrica_app/id_documento");
 ResponseHandler<String> handler = new BasicResponseHandler();
 request.addHeader("Accept", "application/json");
 request.setHeader("Content-Type", "application/json");
 
 //Acquisizione dei dati da salvare
 EditText et_nome = (EditText) findViewById(R.id.et_nome);
 String nome = et_nome.getText().toString();
 ...
 
 try {
     request.setEntity(new StringEntity("{\"Nome\":\""+nome+"\",\"Cognome\":\""+cognome+"\",\"Email\":\""+email+
         "\",\"Ufficio\":\""+ufficio+"\",\"Casa\":\""+casa+"\",\"Cellulare\":\""+cellulare+"\"}"));
     String response = client.execute(request, handler);
 } catch (UnsupportedEncodingException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (ClientProtocolException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (IOException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
     }
} 

Tramite questa dicitura e' possibile creare un documento specificando l'id che si vuole assegnare.  Alternativamente, si puo' scegliere di omettere l'id (come nella dicitura che segue) chiedendo ad database si assegnare automaticamente un id casuale.

curl -X POST http://couchdb.xenialab.it:5984/rubrica_app/[nome_documento] -d '{}'

public void creaDocumento () {
 HttpClient client = new DefaultHttpClient();
 HttpPost request = new HttpPost("http://couchdb.xenialab.it:5984/rubrica_app/");
 ResponseHandler<String> handler = new BasicResponseHandler();
 request.addHeader("Accept", "application/json");
 request.setHeader("Content-Type", "application/json");
 
 //Acquisizione dei dati da salvare
 EditText et_nome = (EditText) findViewById(R.id.et_nome);
 String nome = et_nome.getText().toString();
 ...
 
 try {
    request.setEntity(new StringEntity("{\"Nome\":\""+nome+"\",\"Cognome\":\""+cognome+"\",\"Email\":\""+email+
        "\",\"Ufficio\":\""+ufficio+"\",\"Casa\":\""+casa+"\",\"Cellulare\":\""+cellulare+"\"}"));
    String response = client.execute(request, handler);
 } catch (UnsupportedEncodingException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (ClientProtocolException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (IOException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
     }
} 

In entrambi i casi, i dati che desideriamo salvare sul database li dobbiamo inserire all'interno delle parentesi graffe secondo il formato JSON.
Ad esempio: {"campo1":"elemento","campo2":["elemento1","elemento2"], ...}.

Per entrambi i metodo illustrati per l'inserimento di un documento all'interno del database, la risposta e' la stessa: oltre al risultato della richiesta il database restituisce all'utente i campi id e rev che vengono assegnati.

Eliminare un documento all'interno del database

curl -X DELETE http://couchdb.xenialab.it:5984/rubrica_app/[id_documento]?rev=[rev_documento]

public void cancellaDocumento (String id_documento, String rev_documento) {
 HttpClient httpclient = new DefaultHttpClient();
 HttpDelete request = new HttpDelete("http://couchdb.xenialab.it:5984/rubrica_app/"
     +id_documento+"?rev="+rev_documento);
 request.addHeader("Accept","application/json");
 String response;
 ResponseHandler<String> handler = new BasicResponseHandler();
 try {
     response = httpclient.execute(request, handler);
 } catch (ClientProtocolException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (IOException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 }
} 

La cancellazione di un documento prevede come risposta il risultato della richiesta.

Per questo metodo risulta necessario reperire il campo id e rev per ogni elemento, quindi e' opportuno immagazzinare tali dati all'interno di una struttura dati.

Aggiornare un documento all'interno del database

curl -X PUT http://couchdb.xenialab.it:5984/rubrica_app/[id_documento] -d '{"_rev":[rev_documento], ...}'

public void aggiornaDocumento (String  id_documento, String item_rev) {
 HttpClient client = new DefaultHttpClient();
 HttpPut request = new HttpPut("http://couchdb.xenialab.it:5984/rubrica_app/id_documento");
 ResponseHandler<String> handler = new BasicResponseHandler();
 request.addHeader("Accept", "application/json");
 request.setHeader("Content-Type", "application/json");
 String response;
 
 //Acquisizione dei dati da salvare 
 EditText et_nome = (EditText) findViewById(R.id.editText_nome);
 String nome_new = et_nome.getText().toString();
 ...
 
 try {
     request.setEntity(new StringEntity("{\"_rev\":\""+item_rev+"\",
         \"Nome\":\""+nome_new+"\",
         \"Cognome\":\""+cognome_new+"\",
         \"Email\":\""+email_new+"\",
         \"Casa\":\""+casa_new+"\",
         \"Uffico\":\""+ufficio_new+"\",
         \"Cellulare\":\""+cellulare_new+"\"}"));
     response = client.execute(request,handler);
 } catch (UnsupportedEncodingException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (ClientProtocolException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
 } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
 }
} 

Per aggiornare un documento all'interno di un database e' necessario inserire all'interno delle parentesi graffe, oltre i campi desiderati, inserire il campo rev con l'opportuna revisione del documento da aggiornare. Il documento verra' sovrascritto, assegnando un nuovo codice revisione per il documento.

Infatti la risposta e' identica a quella relativa all'inserimento di un nuovo documento all'interno del database.

Elencare tutti i documenti presenti all'interno di un database

curl -X GET http://couchdb.xenialab.it:5984/rubrica_app/_all_docs

public void elencaDocumenti () {
 HttpClient client = new DefaultHttpClient();
 HttpGet request = new HttpGet("http://couchdb.xenialab.it:5984/rubrica_app/_all_docs");
 ResponseHandler<String> handler = new BasicResponseHandler();
 String response;
 try {
     response = client.execute(request, handler);
     // Analisi della risposta
 } catch (ClientProtocolException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
 }
} 

Come risposta viene comunicato il numero di tutti i documenti presenti nel database, incluse le view, insieme all'elenco completo dei campi id e rev per ogni documento.

Visualizzare un documento

curl -X GET http://couchdb.xenialab.it:5984/rubrica_app/[id_documento]

public void visualizzaDocumento (String id_documento) {
 HttpClient client = new DefaultHttpClient();
 HttpGet request = new HttpGet("http://couchdb.xenialab.it:5984/rubrica_app/id_documento");
 ResponseHandler<String> handler = new BasicResponseHandler();
 String response;
 try {
     response = client.execute(request, handler);
     JSONObject obj = new JSONObject(response);
          
     //Salvataggio delle informazioni acquisite
     TextView tv_nome = (TextView) findViewById(R.id.tv_nome);
    tv_nome.setText(obj.getString("Nome"));
     ...
 
 } catch (ClientProtocolException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (IOException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 }
} 

Con questo metodo e' possibile ottenere l'elenco completo dei campi, con i rispettivi valori, del documento desiderato.

Visualizza un allegato

curl -X GET http://couchdb.xenialab.it:5984/rubrica_app/[id_documento]/[nome_allegato]

Il codice per questo metodo puo' variare a seconda del tipo di allegato.

Per esempio, se l'allegato e' un immagine e' possibile utilizzare metodi tradizionali, indicando solamente l'indirizzo web. Tuttavia potrebbe essere utile un metodo che indichi o meno la presenza di allegati: a tal scopo, occorre analizzare la risposta della visualizzazione di un documento controllando se la stringa contiene la parola "attachments".

String response;
HttpClient client = new DefaultHttpClient();  
HttpGet request = new HttpGet("http://couchdb.xenialab.it:5984/rubrica_app/id_documento");
ResponseHandler<String> handler = new BasicResponseHandler();
try {
    response = client.execute(request, handler);
    JSONObject obj = new JSONObject(response);
    if(response.contains("attachments")) {
        //Do something
    } else {
        //Do something    }
} catch (ClientProtocolException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (JSONException e) {
    // TODO Auto-generated catch block
    e.printStackTrace()
}
}

Visualizzare il risultato di una query

curl -X GET http://couchdb.xenialab.it:5984/rubrica_app/_design/[nome_design_doc]/_view/[nome_view]

public void visualizzaQuery (String nome_view) {
 HttpClient client = new DefaultHttpClient();
 HttpGet request = new HttpGet("http://localhost:5984/rubrica_app/_design/rubrica/_view/nome_view");
 ResponseHandler<String> handler = new BasicResponseHandler();
 String response;
 try {
     response = client.execute(request, handler);
     //Analisi della risposta
 } catch (ClientProtocolException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 } catch (IOException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
 }
} 

La risposta di tale metodo e' simile alla richiesta dell'elenco dei documenti all'interno del database.

Pero' e' preferibile questo metodo perche' e' piu' veloce.

Analisi della risposta

Come visto precedentemente, ad ogni richiesta segue una risposta da parte del database. Anche se sono metodi diversi, le risposte si possono raggruppare in due insiemi diversi: esito di un'operazione o visualizzazione di campi.

Anche se la risposta viene memorizzata inizialmente in una stringa puo' essere utile salvare i dati utili in JSONObject oppure in JSONArray tramite i metodi disponibili getJSONObject e getJSONArray.

In pratica e' possibile ottenere un JSONObject se l'elemento desiderato e' racchiuso tra parentesi graffe mentre si ottiene un JSONArray se e' racchiuso tra parentesi quadre. E' disponibile anche il metodo getString ottenendo una stringa a partire da un JSONObject o da un JSONArray.

E' utile apprendere come popolare una ListView a partire da una query e come reperire, quindi memorizzare, i campi necessari (id e rev) per l'utilizzo del database.

Come popolare una ListView e memorizzare i campi necessari

Per popolare una ListView viene utilizzata come struttura dati una List, precisamente un ArrayList di HashMap avente sia come chiave che come valori delle stringhe.

Nell'applicazione presa in considerazione (Rubrica), la query restituisce i seguenti campi: id come key e come value rev, nome e cognome.

L'idea base e' quella di allocare spazio per una nuova HashMap per ogni elemento presente nella risposta ed aggiungerla alla lista; solo al termine dell'analisi della risposta sara' possibile selezionare solamente i campi desiderati da visualizzare nell'applicazione.

Di seguito e' riportato il metodo corrispondente ed e' necessario richiamarlo in seguito a qualsiasi operazione sul database.

public void visualizza_rubrica () {
 data = new ArrayList<HashMap<String,String>>();
 ListView rubrica = (ListView) findViewById(R.id.listView1);
 HttpClient client = new DefaultHttpClient();
 HttpGet request = new HttpGet("http://couchdb.xenialab.it:5984/rubrica_app/_design/rubrica/_view/show_rubrica");
 ResponseHandler<String> handler = new BasicResponseHandler();
 String response;
 try {
     response = client.execute(request, handler);
     JSONObject obj = new JSONObject(response);
     JSONArray array = obj.getJSONArray("rows");
     for (int i = 0; i < array.length(); i++) {
         HashMap<String, String> map = new HashMap<String, String>();
         map.put("id", array.getJSONObject(i).getString("key"));
         JSONArray contatto = array.getJSONObject(i).getJSONArray("value");
         String nome_cognome = contatto.getString(0) + " " + contatto.getString(1);
         map.put("nome", nome_cognome);
         map.put("rev", contatto.getString(2));
         data.add(map);
     }
 String[] from = {"nome"};
 int[] to = {android.R.id.text1};
 SimpleAdapter adapter = new SimpleAdapter(getApplicationContext(), data,
         android.R.layout.simple_list_item_1, from, to);
 rubrica.setAdapter(adapter);
 } catch (ClientProtocolException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
 } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
 } catch (JSONException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
         }
 }

Esempio di applicazione : Rubrica

Di seguito sono riportate le schermate principali dell'applicazione Rubrica, da cui e' tratto il codice sorgente trascritto precedentemente.

Android App con CouchDB - AddNew   Android App con CouchDB - Details


Titolo: Sviluppare un'applicazione Android su CouchDB
Livello: Esperto (4/5)
Data: 10 Luglio 2012
Versione: 1.0.3 - 15 Agosto 2012
Autori: andrea.bianco90 [AT] gmail.com, mail [AT] meo.bogliolo.name