Autore Topic: [medio/avanzato]Content Providers + CursorLoaders: the modern way  (Letto 6979 volte)

Offline Phate

  • Utente junior
  • **
  • Post: 123
  • Respect: +6
    • Mostra profilo
  • Dispositivo Android:
    Samsung galaxy S
  • Sistema operativo:
    Windows 7
+6
Livello di difficolta: medio/avanzato
Versione SDK utilizzata: 2.3 (api 9, gingerbread)
Versione minima sdk: 1.6 (eh si praticamente non ci sono limiti!)
Link al file compresso del progetto eclipse: file in allegato

Presentazione

Sono sempre di più le app che hanno bisogno di un database per memorizzare i dati utenti e, ogni volta puntualmente, ci ritroviamo con tutta una serie di problemi/scelte implementative:

  • Quando creiamo il db?
  • Come lo rendiamo accessibile alle nostre activities?(singleton, instanza,ecc.)
  • Quando lo chiudiamo?
  • Incubo per definizione: come gestiamo le listview?Quando le popoliamo?Quando le riaggiorniamo?Come?

Oltre ai problemi esposti a volte c'è anche la necessità di fare in modo che i dati di un'app siano accessibili ad altre app.

Pre-requisiti
  • Conoscere sqllite e saper creare/interrogare un database.
  • Conoscere listview e cursoradapters.
Se non conoscete queste cose tornate successivamente, visto che le darò per scontate.

Ok, fate il pieno di snack e iniziamo!  ;-)

I content provider
Un content provider fornisce la risposta ai primi 3 punti della lista, oltre che permettere eventualmente l'esportazione dei dati verso altre app.
E' un'entità che vive indipendentemente dall'app, nel senso che verrà avviato da android quando richiesto la prima volta ed eliminato in automatico quando non sarà più richiesto (non conosco i dettagli implementativi, ma immagino venga eliminato in situazioni di poca memoria o quando passa parecchio tempo dall'ultimo utilizzo).
Come dice il nome fornisce i meccanismi per accedere a dei contenuti, nel corso del tutorial vedremo come crearne uno, collegarlo ai contenuti dell'app (sotto forma di un database sqllite) e utilizzarlo nelle activity che lo richiedono.
Nota: nonostante i content providers vengano usati quasi sempre per permettere l'accesso a un db (e in questo tutorial faremo proprio questo), in realtà questi potrebbero, in teoria, riferirsi a qualsiasi altra cosa, come ad esempio dei files sulla sd, un sito web remoto, ecc.

La classe DB
Ho utilizzato una versione Singleton dell'helper, ma come vedremo non è strettamente necessario, visto che ci sarà sempre una sola connessione al db e questa verrà usata dal content provider.
C'è una sola tabella, notes, che raccoglie delle note inserite dall'utente.
Codice (Java): [Seleziona]
public class DBHelper extends SQLiteOpenHelper{

        public static final String TABLE_NAME = "notes";
        public static final String COLUMN_ID = "_id"; //è necessario usare questo nome per la colonna id, altrimenti
                                                                                                //i corsor adapter non funzioneranno
       
        private static final String DATABASE_NAME = "myDb.db";
        private static final int DATABASE_VERSION = 1;
       
        private static final String DATABASE_CREATE_QUERY = "create table "
                      + TABLE_NAME + " (" + COLUMN_ID
                      + " integer primary key autoincrement, note text default '')";
       
        //sigleton: solo un'istanza dell'helper
        private static DBHelper helper;
       
        public static DBHelper getHelper(Context c){
                if(helper == null)
                        helper = new DBHelper(c);
               
                return helper;
        }
       
        private DBHelper(Context c){
                super(c,DATABASE_NAME,null,DATABASE_VERSION);
        }
       
        @Override
        public void onCreate(SQLiteDatabase db) {
                db.execSQL(DATABASE_CREATE_QUERY);
               
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                // TODO Auto-generated method stub
               
        }

}
Fin qui tutto semplice! Vediamo adesso il piatto forte! :)

Il nostro content provider
Come nella miglior tradizione android e' necessario estendere la classe ContentProvider ed implementare qualche metodo.
Codice (Java): [Seleziona]
public class MyContentProvider extends ContentProvider{
costanti e attributi
I content providers sono basati sul concetto di Uri, ovvero i nostri contenuti saranno individuati da un'URI univoco che, in questo caso, dovrà iniziare per "content://" ed essere nella forma "content://autority/risorsa".
Autorithy deve essere una stringa univoca che individua il nostro content provider in mezzo a tutti gli altri ed è consigliato farle avere lo stesso nome del package dell'applicazione (così sicuramente è unico!;)), mentre risorsa individua una possibile risorsa, nel nostro caso le tabelle del db.
Siccome abbiamo una sola risorsa, nella classe definiamo le seguenti costanti:
Codice (Java): [Seleziona]
private static final String AUTHORITY = "phate.example.cursorLoaders.contentprovider"; //ho aggiunto contentprovider alla fine per precisione
private static final String NOTE_BASE_PATH = DBHelper.TABLE_NAME; //la nostra risorsa, l'ho fissata uguale al nome della tabella anche se avrei potuto scegliere un nome qualsiasi
public static final Uri NOTE_CONTENT_URI = Uri.parse("content://" + AUTHORITY+"/" + NOTE_BASE_PATH); //ecco l'uri base che punta alla risorsa tabella!
Se avessimo avuto più tabelle avremmo dovuto definire un content uri per ognuna, variando l'ultima parte.
E' possibile, anche se noi non lo useremo, aggiungere ancora un'altra cosa all'uri base e cioè l'id della risorsa, nel nostro caso l'id di riga della tabella. L'uri avrebbe una forma di questo tipo "content://authority/tabella/#", dove "#" è un qualsiasi numero (ad es. tabella/1 identificherebbe la riga con id pari a 1).

Nel seguito avremo bisogno di distinguere tra i vari uri, per capire l'utente a quale risorsa vuole accedere (parlando genericamente, è chiaro che qui ce n'è una sola!XD), quindi nella classe ci definiamo un uri matcher che, in sostanza, dato un uri restituisce un int che lo rappresenta
Codice (Java): [Seleziona]
public static final int NOTES = 100; //con 100 identifichiamo l'uri content://authority/tabella
public static final int NOTE_ID = 110; //con 110 l'uri content://authority/tabella/id

private static final UriMatcher sURIMatcher = new UriMatcher(
                        UriMatcher.NO_MATCH);
        static {
                sURIMatcher.addURI(AUTHORITY, NOTE_BASE_PATH, NOTES); //colleghiamo gli uri ai tipi, es. /notes = 100
                sURIMatcher.addURI(AUTHORITY, NOTE_BASE_PATH + "/#", NOTE_ID);
        }

Infine è richiesto, per il content provider, esporre che tipo di dati fornisce. Il tipo dovrà essere nella forma tipo/sottotipo, ad esempio "image/jpg".
Dal momento che il nostro restituisce cursori a tabelle definiamo queste costanti:
Codice (Java): [Seleziona]
public static final String NOTE_CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.example.notes";
public static final String NOTE_CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.example.noteelement";
La prima parte è obbligatoria, così come il "vnd", dopo il punto invece possiamo mettere quello che vogliamo.

metodi
Dovremo implementare i seguenti metodi
  • public String getType(Uri uri): dovrà restituire il content type appropriato:
Codice (Java): [Seleziona]
@Override
        public String getType(Uri uri) {
                switch(sURIMatcher.match(uri)){ //per adesso supportiamo solo le note
                case NOTES:
                        return MyContentProvider.NOTE_CONTENT_TYPE;
                case NOTE_ID:
                        return MyContentProvider.NOTE_CONTENT_ITEM_TYPE;
                default:
                        throw new IllegalArgumentException("Invalid Uri!");
                }
        }
questa struttura la useremo sempre: usiamo il metodo match di uri matcher che ci restituirà un intero a partire dall'uri, a seconda di quale intero restituisce faremo una cosa diversa.

  • onCreate() : concettualmente identico a quello di DBHelper, invocato alla creazione, ci serve per inizializzare il content provider
Codice (Java): [Seleziona]
@Override
        public boolean onCreate() {
                db = DBHelper.getHelper(getContext()).getWritableDatabase();
                return true;
        }
    da notare che non c'è nessun onclose(): il db verrà chiuso in automatico da android quando deciderà che il content provider non è più necessario![/li]

  • metodi query,insert,update,delete : in sostanza sono metodi wrapper all'interno dei quali dovremo interrogare il db interno e restituire il risultato. Vi farò vedere solo query e insert, gli altri sono più o meno la stessa cosa e li capirete facilmente!
query:
Codice (Java): [Seleziona]
@Override
        public Cursor query(Uri uri, String[] projection, String selection, String[] selArgs,
                        String sortOrder) {

                String tableName = null;
                switch(sURIMatcher.match(uri)){ //per adesso supportiamo solo le note
                case NOTES:
                        tableName = DBHelper.TABLE_NAME;
                        break;
                default:
                        throw new IllegalArgumentException("Invalid Uri!");

                }

                Cursor cursor = db.query(tableName, projection, selection,
                                selArgs, null, null, sortOrder);
                cursor.setNotificationUri(getContext().getContentResolver(), uri);

                return cursor;
        }
se avessi voluto supportare anche l'id uri avrei dovuto mettere una condizione al where della query, siccome mi scocciavo si fa tutto con l'uri base, la modifica è semplice ;)
La cosa importante è l'istruzione setNotificationUri che avviserà eventuali ascoltatori (ad esempio una listview associata a questa tabella) che il contenuto dei cursori è cambiato, non dimenticatela mai!

insert
Codice (Java): [Seleziona]
@Override
        public Uri insert(Uri uri, ContentValues cv) {
                String tableName = null;
                switch(sURIMatcher.match(uri)){ //per adesso supportiamo solo le note
                case NOTES:
                        tableName = DBHelper.TABLE_NAME;
                        break;
                default:
                        throw new IllegalArgumentException("Invalid Uri!");

                }

                long id = db.insert(tableName, null, cv);

                Uri retUri = ContentUris.withAppendedId(NOTE_CONTENT_URI, id);
                getContext().getContentResolver().notifyChange(retUri, null);

                return retUri;

        }
L'insert deve restituire l'uri alla nuova riga creata e lo facciamo semplicemente chiamando il metodo onAppendedId sull'uri base, anche qui un'istruzione notifyChange notificherà gli ascoltatori che i dati sono cambiati.


Update e delete devono restituire il numero di righe modificate/cancellate, ma per il resto il loro codice è identico a insert e query!
Modifiche al manifest
E' necessario informare android del nostro content provider:
Codice (XML): [Seleziona]
<provider android:exported="false"
           android:authorities="phate.example.cursorLoaders.contentprovider"
           android:name=".MyContentProvider"/>
l'exported false indica che intendiamo usare questo content provider solo all'interno della nostra app, occhio che il campo authorities deve corrispondere a quello definito nel nostro content provider.

Interagire col db
Molto semplice: in qualunque app, in qualunque activity, vi basterà fare questa semplice istruzione:
Codice (Java): [Seleziona]
getContentResolver().insert(MyContentProvider.NOTE_CONTENT_URI, values); //su getContentResolver() si possono richiamare tutti i metodi del db
niente più inizializzazione db, niente più attributo database, il db esiste da qualche parte e basta: noi conosciamo l'uri e tanto ci basta!:)

Nota: getContentResolver() è un metodo della classe Context, quindi lo potrete richiamare all'interno di activities e services e in generale di tutte le classi che estendono Context. Nelle altre classi dovrete ottenere in qualche modo un riferimento al context corrente.

Cursor loaders
Capite perché vi ho detto di fare il pieno di snacks? Le cose sono lunghe ;)
I cursor loaders sono praticamente degli observer che incapsulano un cursore. Si occupano di aprirlo/chiuderlo/aggiornarlo in automatico, quindi voi non dovrete fare NIENTE (o quasi! :D) e tutto avverrà come per magia: la lista si popola, si aggiorna, si svuota a seconda delle operazioni sul db, fatte da qualsiasi activity o app.
Come se non bastasse, a differenza del metodo requery(), i cursor loaders sono totalmente asincroni il che vuol dire che l'interfaccia principale non verrà rallentata dall'aggiornamento della listview. Che dire?Il sogno di ogni programmatore!

Attenzione: usare le classi del package android.support.v4, visto che sono le uniche supportate su qualsiasi sdk a partire dall'1.5!

Inizializzazione
Per supportare il tutto la vostra classe dovrà estendere FragmentActivity invece di Activity, non cambia assolutamente nulla a parte qualche metodo in più che è possibile usare. A partire da honey comb anche Activity implementa questi metodi, tuttavia io vi consiglio di usare sempre questo approccio in modo da poter installare le vostre app su una varietà maggiore di dispositivi.
Per supportare i Loaders, inoltre, la classe dovrà implementare l'interfaccia LoaderCallbacks, la firma diventa:
Codice (Java): [Seleziona]
public class MainActivity extends FragmentActivity implements LoaderCallbacks<Cursor>{
private SimpleCursorAdapter dataSource; //il nostro cursor adapter automatico, yay!:D

ora, nel metodo onCreate dovrete inizializzare il loader, oltre che settare l'adapter per la listview:
Codice (Java): [Seleziona]
getSupportLoaderManager().initLoader(LOADER_ID, null, this); //id, bundle loadercallback

dataSource = new SimpleCursorAdapter(getApplicationContext(),R.layout.list_item,
                        null,new String[]{"_id","note"},new int[]{R.id.idLabel,R.id.noteLabel},SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
   
        ListView myList = (ListView)findViewById(R.id.myList);
        myList.setAdapter(dataSource);
La cosa da notare è che, per ora, passiamo null come oggetto cursore al nostro cursor adapter.

Fase finale, i metodi da implementare:
Codice (Java): [Seleziona]
@Override
        public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) { //chiamato alla creazione, collega il cursore alla query che ci interessa
                CursorLoader cl = new CursorLoader(getApplicationContext(),MyContentProvider.NOTE_CONTENT_URI,
                                new String[]{"_id","note"},null,null,null);
               
                return cl;
        }

        @Override
        public void onLoadFinished(Loader<Cursor> arg0, Cursor c) { //chiamato ogni volta che c'è un cambiamento, sostituiamo il vecchio cursore
                dataSource.swapCursor(c);                                              //col nuovo
               
        }

        @Override
        public void onLoaderReset(Loader<Cursor> arg0) { //chiamato sul reset, nullifichiamo il cursore
                dataSource.swapCursor(null);
        }
Tutto qua: se accederete sempre al db tramite content provider le liste si updateranno da sole, senza più pensieri.

Che faticata eh?:D
Però vi garantisco che se avete app complesse (e un content provider base da copiarvi e modificare) questo approccio vi risparmierà un sacco di problemi ed errori nel codice!
Spero vi sia d'aiuto, alla prossima!
« Ultima modifica: 16 Febbraio 2013, 16:48:03 CET da Phate »

Offline mcatta

  • Nuovo arrivato
  • *
  • Post: 7
  • Respect: 0
    • Marco_Cattaneo
    • Mostra profilo
    • FloatDesign
  • Dispositivo Android:
    HTC Desire
  • Sistema operativo:
    OS X 10.7.1
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #1 il: 29 Ottobre 2012, 22:40:07 CET »
0
Ciao, intanto ti ringrazio per il tutorial che mi è stato utilissimo per capire come funzionano i content provider applicati ai cursor, però ho una domanda: nel tuo esempio lavori con un solo cursor che popola la tua listactivity, ma nell'ipotesi che io nella mia activity ho ad esempio due Spinner da popolare con due cursori differenti (perchè sono dati provenienti da due tabelle diverse) come potrei fare?

Codice (Java): [Seleziona]
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) { //chiamato alla creazione, collega il cursore alla query che ci interessa
   CursorLoader cl = new CursorLoader(getApplicationContext(),MyContentProvider.NOTE_CONTENT_URI,
      new String[]{"_id","note"},null,null,null);
               
   return cl;
}

Nel onCreateLoader alla fine lavori con un content_uri perchè la tabella è una, ma io nel mio caso dovrei creare due CursorLoader differenti.

Grazie!
Marco

Offline Ricky`

  • Amministratore
  • Utente storico
  • *****
  • Post: 3489
  • Respect: +506
    • Github
    • Google+
    • rciovati
    • Mostra profilo
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #2 il: 30 Ottobre 2012, 08:46:10 CET »
+2
Se ti può interessare nella mia app ho fatto proprio una cosa del genere:

https://bitbucket.org/rciovati/museums-in-lombardy/src/01e3843d558df3c7506791c6efe85688b2d41ee5/MuseumsInLombardy/src/it/rciovati/mappamusei/fragments/FilterMuseumsListFragmentDialog.java?at=master

(Si potrebbe rifattorizzare un po' meglio, ma dovrebbe farti capire l'idea).
« Ultima modifica: 30 Ottobre 2012, 08:52:10 CET da Ricky` »

Offline mcatta

  • Nuovo arrivato
  • *
  • Post: 7
  • Respect: 0
    • Marco_Cattaneo
    • Mostra profilo
    • FloatDesign
  • Dispositivo Android:
    HTC Desire
  • Sistema operativo:
    OS X 10.7.1
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #3 il: 30 Ottobre 2012, 09:23:00 CET »
0
Se ti può interessare nella mia app ho fatto proprio una cosa del genere:

https://bitbucket.org/rciovati/museums-in-lombardy/src/01e3843d558df3c7506791c6efe85688b2d41ee5/MuseumsInLombardy/src/it/rciovati/mappamusei/fragments/FilterMuseumsListFragmentDialog.java?at=master

(Si potrebbe rifattorizzare un po' meglio, ma dovrebbe farti capire l'idea).
Perfetto, a qualcosa di simile ci avevo pensato, in pratica nel loadFinished con i risultati della query generi l'array dei risultati che poi va a finire come ArrayAdapter nello spinner. Ti ringrazio dell'aiuto!

Offline ozzem

  • Nuovo arrivato
  • *
  • Post: 18
  • Respect: 0
    • Mostra profilo
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #4 il: 02 Novembre 2012, 18:36:31 CET »
0
intanto ti ringrazio per l'utilissimo Tutorial..davvero ben fatto.

vorrei sapere una cosa..se ho bisogno di una serie di metodi che lavorano sul db..ad esempio una insert differente..

avevo pensato di creare un altro metodo insert2 al cui interno passavo dei parametri all'insert del contentprovider.. cioè..mi spiego meglio

vorrei poter fare

Codice (Java): [Seleziona]
 


public Uri insertPerson(ContentValues values){
                Uri uri = DbContentProvider.PERSON_CONTENT_URI;
                ContentValues valori= values;
                Uri ret;
                getContentResolver().insert(uri, valori);
               
                return ret;
       
        }
       
       

tralasciando la banalità del metodo che ho descritto..

è possibile servirsi dei metodi del contentresolver all'interno di altre classi?

questa porzione di codice mi da problemi non riconoscendo proprio il metodo getContentResolver().

grazie in anticipo

Offline Ricky`

  • Amministratore
  • Utente storico
  • *****
  • Post: 3489
  • Respect: +506
    • Github
    • Google+
    • rciovati
    • Mostra profilo
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #5 il: 03 Novembre 2012, 10:12:56 CET »
+1
è possibile servirsi dei metodi del contentresolver all'interno di altre classi?

getContentResolver() è un metodo della classe Context.
La classe Activity (e altre) estendono Context quindi puoi chiamare il metodo direttamente, mentre in altre classi ti serve passare un'instanza appunto della classe Con text.

Es:

Codice (Java): [Seleziona]
public Uri insertPerson(Context context, ContentValues values){
                Uri uri = DbContentProvider.PERSON_CONTENT_URI;
                ContentValues valori= values;
                Uri ret;
                context.getContentResolver().insert(uri, valori);
               
                return ret;
       
        }

Offline Giak

  • Utente junior
  • **
  • Post: 52
  • Respect: 0
    • Mostra profilo
  • Dispositivo Android:
    transformer tf101
  • Sistema operativo:
    ubuntu 12.10
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #6 il: 25 Novembre 2012, 15:32:56 CET »
0
ho un piccolo dubbio su queste due righe.

Codice (Java): [Seleziona]
private static final String NOTE_BASE_PATH = DBHelper.TABLE_NAME; //la nostra risorsa, l'ho fissata uguale al nome della tabella anche se avrei potuto scegliere un nome qualsiasi
public static final Uri NOTE_CONTENT_URI = Uri.parse("content://" + AUTHORITY+"/" + NOTE_BASE_PATH); //ecco l'uri base che punta alla risorsa tabella!

nel primo commento dici che il nome della risorsa può essere un nome qualsiasi.
invece nel metodo Uri.parse() deve essere obbligatoriamente il nome della tabella vero?

quello che mi sfugge è quale sia la riga di codice dove andiamo ad associare il content provider al nostro database e più precisamente alla nostra tabella. [/code]

Offline Phate

  • Utente junior
  • **
  • Post: 123
  • Respect: +6
    • Mostra profilo
  • Dispositivo Android:
    Samsung galaxy S
  • Sistema operativo:
    Windows 7
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #7 il: 16 Febbraio 2013, 16:35:15 CET »
0
Scusate per il ritardo, da un certo punto in poi ho avuto un po' da fare e sono stato totalmente assente da questo forum:)
Ringrazio per i complimenti e per le risposte date in mia vece :)

Per l'ultima domanda, se serve ancora, semplicemente l'associazione col db la fai nell'onCreate, facendo in modo che l'instanza di db sia del db che tu vuoi, con le tabelle te la giochi con gli uri. Se hai più tabelle definisci uri per ogni tabella e poi nei metodi insert/update/ecc. usi gli switch per scegliere quella giusta (se hai notato in questo esempio, essendoci una sola tabella, i costrutti switch sono inutili sono là proprio per l'espansione con più tabelle).

Offline idrone

  • Nuovo arrivato
  • *
  • Post: 14
  • Respect: 0
    • Mostra profilo
  • Dispositivo Android:
    HTC One S
  • Sistema operativo:
    Mac OS X 10.7.5
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #8 il: 14 Settembre 2013, 19:55:50 CEST »
0
Codice (Java): [Seleziona]
private static final String NOTE_BASE_PATH = DBHelper.TABLE_NAME;E se il nome della tabella fosse dinamico...cioè le tabelle vengono create dall'utente stesso?

Offline Rickyc81

  • Nuovo arrivato
  • *
  • Post: 23
  • Respect: 0
    • Mostra profilo
  • Dispositivo Android:
    Nexus4
  • Sistema operativo:
    Win8
Re:[medio/avanzato]Content Providers + CursorLoaders: the modern way
« Risposta #9 il: 20 Ottobre 2013, 15:01:24 CEST »
0
Ciao, nella mia app stò implementando una rubrica interna, che deve visualizzare in listview "nome" cognome" e sotto essi il relativo "numero telefono" ...
è possibile adattarlo al mio uso? oppure per questa funzione è sprecato visto che è abbastanza complesso?

Ciao
Ric