Condividi tramite


Interazione con SQLite per Android con Visual Studio e Xamarin (it-IT)


Introduzione

In questo articolo vedremo come sviluppare una semplice soluzione Android mediante l'utilizzo di Visual Studio e Xamarin, il framework di sviluppo che consente la scrittura di codice multipiattaforma. Tale progetto esemplificativo ci consentirà di analizzare, più in generale, le particolarità di una soluzione Android, quali - ad esempio - l'organizzazione dei files del progetto (e quindi la sua struttura), e quelle che sono le specificità del framework stesso quanto alla sintassi. Faremo utilizzo del linguaggio C# per la scrittura del codice sorgente.

Definizione del progetto

Come di consueto, creiamo un nuovo progetto in Visual Studio. Se avremo installato il framework Xamarin (la cui configurazione di base non viene trattata nel presente articolo), avremo la disponibilità dei template Android. Nella fattispecie, selezioneremo la soluzione vuota, per poi aggiungere le classi e gli oggetti di interesse.

Più precisamente, in questo articolo tratteremo un caso banale: si desidera qui sviluppare una semplice app che, data una casella di testo, permetta l'inserimento di stringhe alfanumeriche qualsiasi, consentendone la memorizzazione su database SQLite. Tale finalità è volta a fornire sufficienti elementi per approfondire quelle che sono le tematiche più generali e basilari dello sviluppo di applicazioni Android: la struttura del progetto, la predisposizione della GUI ed il suo controllo mediante codice e - decisamente importante - l'interazione (per quanto essenziale) con una base dati, sfruttando qui un ORM disponibile come pacchetto NuGet.

La struttura del progetto

Nell'immagine seguente, viene mostrata l'organizzazione della soluzione già terminata, in modo da fornire un primo colpo d'occhio su come venga strutturato un progetto Android, e quale significato vada attribuito ai vari folder.

Al di là delle classi che definiscono le Activity della soluzione, e che non hanno particolari necessità di predisposizione, si notano le due cartelle Assets e Resources. La prima contiene tutti quei files di cui si necessita la copia su dispositivo. Nell'immagine, si nota in essa un file di nome test.db: si tratta della nostra base dati, di cui vedremo successivamente sviluppo. Qui ci basti considerare che, dovendo interagire appunto con un database, vogliamo che esso sia incluso tra i files di soluzione, per essere poi trasferito fisicamente sullo smartphone/tablet/dispositivo che eseguirà l'app.

La struttura della cartella Resources è più variegata, e contiene diversi sub-folders (la lista in esempio non è esaustiva), mediante i quali andare a definire costanti di stringa, oggetti grafici, icone ed immagini da utilizzare nel programma.

Nel nostro caso, notiamo la sottocartella drawable (che qui contiene il file Icon.png, ovvero l'icona da attribuire all'app), la directory layout - che contiene i files AXML, che definiscono mediante sintassi XML le videate dell'app ed i controlli definiti su di esse, e la directory values, nella quale poter definire files di costante, per esempio associando ad identificatori predefiniti da referenziare nell'app delle stringhe di testo fisso (è il caso del file Strings.xml).

Completano la lista, in questo caso, le classi C# MainActivity.cs (la classe che controllerà il nostro oggetto di avvio) ed SQLiteORM.cs, classe scritta appositamente per effettuare le banali operazioni su database che andremo a definire.

Il layout principale

Definiamo ora il layout relativo alla videata principale della nostra app. Aggiungiamo un nuovo elemento, e selezioniamo la tipologia Android Layout. Chiameremo tale layout Main.axml.

A livello grafico, ci preoccuperemo di creare tre oggetti di tipo LinearLayout (contenitori che consentono di riservare una certa percentuale di spazio a video al proprio contenuto, e di dare un'orientamento allo stesso), per popolare successivamente ciascuno di essi con i controlli desiderati: nel primo LinearLayout inseriremo una EditText (casella di testo editabile) ed un Button (oggetto cliccabile); sul secondo aggiungeremo una ListView (che ci servirà per elencare i record inseriti); sul terzo un Button per eseguire un'eventuale operazione di svuotamento massivo della nostra tabella. 

Il codice XML che realizza tale videata è come segue:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:p1="http://schemas.android.com/apk/res/android"
    p1:orientation="vertical"
    p1:layout_width="match_parent"
    p1:layout_height="match_parent"
    p1:id="@+id/linearLayout1">
    <LinearLayout
        p1:orientation="horizontal"
        p1:layout_width="match_parent"
        p1:layout_height="wrap_content"
        p1:id="@+id/linearLayout2"
        p1:layout_gravity="fill_horizontal">
        <EditText
            p1:id="@+id/editText1"
            p1:layout_width="290dp"
            p1:layout_height="wrap_content"
            p1:layout_gravity="fill_horizontal" />
        <Button
            p1:text="Aggiungi"
            p1:layout_width="wrap_content"
            p1:layout_height="match_parent"
            p1:id="@+id/button1" />
    </LinearLayout>
    <LinearLayout
        p1:orientation="vertical"
        p1:layout_width="match_parent"
        p1:layout_height="match_parent"
        p1:id="@+id/linearLayout3"
        p1:layout_weight="75">
        <ListView
            p1:minWidth="25px"
            p1:minHeight="25px"
            p1:layout_width="match_parent"
            p1:layout_height="match_parent"
            p1:id="@+id/listView1" />
    </LinearLayout>
    <LinearLayout
        p1:orientation="horizontal"
        p1:minWidth="25px"
        p1:minHeight="25px"
        p1:layout_width="match_parent"
        p1:layout_height="wrap_content"
        p1:id="@+id/linearLayout4">
        <Button
            p1:text="Svuota"
            p1:id="@+id/button2"
            p1:layout_width="match_parent"
            p1:layout_height="match_parent"
            p1:layout_gravity="fill_horizontal" />
    </LinearLayout>
</LinearLayout>

In esso si possono apprezzare alcuni parametri ricorrenti, per i quali annotiamo qui alcune importanti indicazioni. Anzitutto, il parametro id: è un settaggio fondamentale, in quanto definisce il nome di un controllo sul proprio layout. Senza di esso, non sarà possibile referenziare l'oggetto da codice, ed è quindi importante indicarlo/personalizzarlo (nella forma "@+id/nome") per tutti quei controlli che verranno poi effettivamente utilizzati. Le proprietà layout_width e layout_height definiscono rispettivamente la larghezza e l'altezza di un controllo. È possibile indicarne il valore anche tramite alcune parole riservate: nel caso di esempio, si nota spesso il ricorso a match_parent, che indica che la proprietà deve assumere il valore del suo contenitore, oppure wrap_content, ovvero che deve adattarsi al contenuto al suo variare; layout_weight indica invece una percentuale di occupazione di spazio. Si noti il linearLayout3: per esso è indicato un layout_weight di 75, ovvero si esplicita la volontà di ricavare un 75% dello spazio complessivo a tale controllo. Per quanto riguarda i LinearLayout, è inoltre presente la proprietà orientation, che indica come devono essere disposti i controlli al suo interno, ovvero rispettando un ordinamento affiancato, oppure impilato. Per i controlli che la prevedono, la proprietà Text indica il testo da mostrare su di essi.

Il lettore è invitato alla sperimentazione di queste ed altre proprietà, onde familiarizzare con esse.

Activity principale

Un layout viene referenziato e quindi utilizzato dalle opportune Activity, classi in cui risiede il codice operativo. In questo caso, definiremo una nuova activity, che chiameremo MainActivity.cs

Nel suo evento OnCreate, che definisce il codice eseguito alla creazione dell'Activity stessa, possiamo indicare quale file AXML usare, e quindi referenziarne - tramite il ricorso alla proprietà id descritta sopra - i controlli. Si consideri il seguente esempio:

public class  MainActivity : Activity
{
    EditText  t = null;
    ListView lv = null;
 
    protected override  void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);
        SetContentView(Resource.Layout.Main);
 
        Button b1 = FindViewById<Button>(Resource.Id.button1);
        Button b2 = FindViewById<Button>(Resource.Id.button2);
 
        lv = FindViewById<ListView>(Resource.Id.listView1);
        t  = FindViewById<EditText>(Resource.Id.editText1);
 
        b1.Click += B1_Click;
        b2.Click += B2_Click;
 
        t.KeyPress += (object sender, View.KeyEventArgs e) => {
            e.Handled = false;
            if (e.Event.Action == KeyEventActions.Down && e.KeyCode == Keycode.Enter)
            {
                // TO DO
                e.Handled = true;
            }
        };
    }
}

    
Tramite la funzione SetContentView, viene indicato il file AXML da utilizzare, specificandone il nome contenuto nella classe Resource.Layout. Da qui comprendiamo come la gerarchia di directory che formano il progetto vengano di fatto convertite in classi, attraverso cui è possibile referenziare gli elementi in esse contenuti. Una volta fatto ciò, la classe Resource.Id esporrà tutti gli id definiti su tale layout. Si noti allora come sia sufficiente dichiarare oggetti di una determinata classe (prendiamo ad esempio il Button b1) e, attraverso la funzione FindViewById (opportunamente castata), legare l'oggetto grafico al suo corrispettivo variabile. Di fatto, l'istruzione:

Button b1 = FindViewById<Button>(Resource.Id.button1);

            
Dice semplicemente: definisco un oggetto Button di nome b1, il quale deve riferirsi al controllo che, sul layout collegato a questa activity, è definito dall'id button1.
Di qui in poi, l'oggetto b1 ci permetterà di catturare i diversi eventi e proprietà dati occorrenti durante l'utilizzo dell'app.

Si noti per esempio come venga dichiarato l'evento KeyPress sulla EditText t: tramite una routine lambda, si dichiara cosa debba accadere alla pressione di un tasto mentre la casella di testo ha il focus (nel caso specifico, accadrà qualcosa solo se il tasto premuto sarà Enter). Inoltre, e lo si nota relativamente all'evento Click di b1 e b2, l'handler ad un evento può essere assegnato semplicemente indicando quale routine eseguire al verificarsi dell'evento stesso (routine che dovrà avere la stessa firma dell'evento chiamante, come da consueta sintassi di C#).

Il database: implementazione e controllo

Come dichiarato in apertura, questo applicativo si preoccuperà soltanto di memorizzare stringhe di testo, e pertanto la base dati sarà estremamente banale. Tuttavia, il principio di implementazione non cambia, ed è quindi utile analizzarlo in questa sede. Molte volte, si dà all'app il compito di creare una base dati, se inesistente. Altre volte, è possibile creare un modello di database predefinito e fare in modo che esso venga copiato dall'app nella locazione adatta. Vedremo qui questa seconda strada.

SQLite Expert Personal

Uno strumento utilissimo per creare databases di tipo SQLite è SQLite Expert Personal. È possibile scaricarlo qui: http://www.sqliteexpert.com/download.html.

Una volta avviato, possiamo creare un nuovo database, e - tramite la comoda GUI - andare a definire le tabelle ed i campi che lo compongono. L'utilizzo è veramente intuitivo, ragione per la quale non ci dilungheremo qui nell'illustrarlo.

Nell'esempio, creeremo un semplice database di nome test.db, contenente una sola tabella, motti, a sua volta avente un unico campo di tipo TEXT, Motto. Una volta salvato sul proprio pc, il file test.db potrà essere trascinato nella directory Assets, come precedentemente mostrato: in questo modo, esso verrà incluso tra i files binari compilati con l'applicazione, e potremo fare in modo di copiarlo sul dispositivo che eseguirà l'app, se le condizioni operative lo richiedono (tipicamente, se esso ancora non è presente, oppure se la sua struttura è variata).

SQLite-net

Scriviamo ora una classe per il controllo del database, ovvero per l'esecuzione della copia appena menzionata, e delle istruzioni che vorremo poi impiegare nello sviluppo della nostra app.
Ci affideremo ad una libreria open source e decisamente leggera, ovvero SQLite-net. È scaricabile come pacchetto NuGet, e da GitHub all'indirizzo https://github.com/praeclarum/sqlite-net.

La classe di gestione database

Creiamo quindi una nuova classe, che chiameremo SQLiteORM.cs. Segue l'intero listato della classe, successivamente al quale scenderemo maggiormente nel dettaglio della sua struttura.

using Android.App;
using SQLite;
using System.Collections.Generic;
using System.IO;
 
namespace AndroidTest01
{
    public class  SQLiteORM
    {
        [Table("Motti")]
        public class  Motti
        {
            public string  Motto { get; set; }
        }
 
        public string  PercorsoDb { get; set; }
 
        public SQLiteORM(string dbName)
        {
            string dbPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.ToString(), dbName);
            if (!File.Exists(dbPath))
            {
                using (BinaryReader br = new BinaryReader(Application.Context.Assets.Open(dbName)))
                {
                    using (BinaryWriter bw = new BinaryWriter(new FileStream(dbPath, FileMode.Create)))
                    {
                        byte[] buffer = new  byte[2048];
                        int len = 0;
                        while ((len = br.Read(buffer, 0, buffer.Length)) > 0)
                        {
                            bw.Write(buffer, 0, len);
                        }
                    }
                }
            }
 
            PercorsoDb = dbPath;
        }
 
        public List<string> GetMotto()
        {
            List<string> motti = new  List<string>();
 
            using(var db = new  SQLiteConnection(this.PercorsoDb))
            {
                foreach (var s in db.Table<Motti>())
                    motti.Add(s.Motto);
            }
 
            return motti;
        }
 
        public void  InsertMotto(string  motto)
        {
            using (var db = new SQLiteConnection(this.PercorsoDb))
            {
                db.Insert(new Motti { Motto = motto });
            }
        }
 
        public void  Svuota()
        {
            using (var db = new SQLiteConnection(this.PercorsoDb))
            {
                db.DeleteAll<Motti>();
            }
        }
 
    }
}

In essa, viene anzitutto definita una sottoclasse, che indica il nome e la struttura della tabella con la quale interagire:

[Table("Motti")]
public class  Motti
{
    public string  Motto { get; set; }
}

        
L'annotazione [Table("Motti")] indica quale sia il nome fisico della tabella su database, indipendentemente dal nome di classe che assegneremo. Per questioni di praticità, li manteremmo identici.
Si noti come le routine GetMotto(), InsertMotto() e Svuota presentino fondamentalmente la stessa struttura: ciascuna di esse viene introdotta dall'inizializzazione di un oggetto di tipo SQLiteConnection, che realizza la connessione al database specificato, per poi eseguire le funzionalità specifiche della routine.

Ad esempio, nel caso di GetMotto(), ovvero della routine che utilizzeremo per popolare la vista, vediamo che - dopo la realizzazione della connessione - viene eseguito un ciclo su tutti gli elementi contenuti nella Table<Motti>, e come ciascuno di essi venga aggiunto ad una List<string> che costituirà il tipo di ritorno della routine. Ancora, nel caso di InsertMotto(), che accetta come parametro obbligatorio una stringa, dopo l'instaurarsi della connessione venga richiamata la funzione Insert, passandole un oggetto Motti la cui proprietà Motto sia la stringa passata al metodo. Dopo l'esecuzione dei vari comandi, per effetto della keyword using, la connessione viene scaricata: in altri termini, essa è mantenuta per il tempo necessario all'esecuzione dei comandi.

Focalizziamoci ora sul costruttore

public SQLiteORM(string dbName)
{
    string dbPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.ToString(), dbName);
    if (!File.Exists(dbPath))
    {
        using (BinaryReader br = new BinaryReader(Application.Context.Assets.Open(dbName)))
        {
            using (BinaryWriter bw = new BinaryWriter(new FileStream(dbPath, FileMode.Create)))
            {
                byte[] buffer = new  byte[2048];
                int len = 0;
                while ((len = br.Read(buffer, 0, buffer.Length)) > 0)
                {
                    bw.Write(buffer, 0, len);
                }
            }
        }
    }
 
    PercorsoDb = dbPath;
}

Dal momento che abbiamo il database nella directory Assets, e vogliamo copiarlo sul dispositivo, verifichiamo anzitutto che esso non sia già presente. Supponiamo qui di voler salvare la base dati sul supporto esterno, tipicamente una SD Card. Verifichiamo allora che il nostro database non sia presente in detta locazione:

string dbPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.ToString(), dbName);
if (!File.Exists(dbPath))
{
    // TO DO
}

Android.OS.Environment.ExternalStorageDirectory è una proprietà mediante la quale poter leggere il percorso fisico della storage card. Creiamo quindi un path dato da tale proprietà e dal nostro nome database, e verifichiamone quindi l'esistenza. Se esso non esiste, procediamo, tramite classi BinaryReader e BinaryWriter, a leggere il nostro file di database ed effettuarne la copia. Si noti che per poter eseguire tale operazione, l'app dovrà disporre degli opportuni permessi. Vediamo quindi come impostarli.

Settaggio permessi di I/O su storage card

Il sistema operativo Android necessita che, per le app che fanno uso di caratteristiche particolari, queste dichiarino il proprio intento attraverso un file di manifesto. In esso vanno specificate tutte quelle risorse e comportamenti che la propria app abbisogna per il corretto funzionamento. Nel nostro caso, se eseguissimo l'app senza specificare l'utilizzo in lettura e scrittura dello storage esterno, l'applicazione stessa subirebbe un crash al tentativo di accedere tale risorsa, in quanto l'OS non le avrebbe concesso i permessi per farlo.

Indicare tali permessi è semplice. Andiamo nelle proprietà del progetto, e alla voce Android Manifest indichiamo le specificità del programma. Nel nostro caso, i flag da apporre saranno sulle voci READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE

Layout di lista

Anche se pare abbiamo ora tutti gli elementi per completare l'app, in realtà un paragrafo a parte va dedicato al controllo ListView, che utilizzeremo per mostrare tutte le frasi memorizzate nel database. Come una qualsiasi lista, essa è deputata a contenere elementi; tuttavia, a differenza per esempio di una ListView di ambito WinForms, non è possibile accedere direttamente ad una proprietà Items, e valorizzare quindi gli elementi facendo diretto riferimento ad una collezione.

Anche dal punto di vista del layout, è necessario definire come ciascun elemento debba essere rappresentato, ed in questo senso siamo più vicini ad una lista di tipo WPF, obbligati però a customizzarne il DataTemplate. Detto in altri termini, per utilizzare correttamente una ListView Android (che è soltanto un contenitore), dovremo anzitutto definire un layout per gli elementi figli, e - tramite un adattore - passare alla lista la collezione di oggetti da esporre, unitamente al modello grafico con il quale essi verranno esposti.

Il caso qui presentato è il più semplice possibile (una sola stringa per elemento di lista), ma se tale approccio può sembrare eccessivo, esso risulta invece decisamente versatile quando si tratta di comporre elaborazioni grafiche anche molto complesse, in cui ogni oggetto di lista debba presentare più dati nelle forme più diverse.

Definiamo quindi un nuovo layout AXML. Lo chiameremo lvItem.axml, ed in esso specificheremo un semplice oggetto di tipo TextView (equivalente di una Label in ambito WinForms). Il codice XML del nostro layout sarà pertanto:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:p1="http://schemas.android.com/apk/res/android"
    p1:text="Medium Text"
    p1:textAppearance="?android:attr/textAppearanceMedium"
    p1:layout_width="match_parent"
    p1:layout_height="match_parent"
    p1:id="@+id/textView1" />

    
La ListView possiede una proprietà di nome Adapter. Di conseguenza, quando utilizzeremo la routine GetMotto(), definita nella classe SQLiteORM.cs, potremo comporre un adattatore con cui inviare alla lista stessa la collezione dati e la risorsa costituita dal layout appena creato, in modo da permetterle la visualizzazione. 

Vediamo ora il codice completo di MainActivity.cs, in modo da analizzare le operazioni su database e gli eventi che verranno assegnati a ciascun controllo.

Sorgente completo di MainActivity.cs

using System;
using Android.App;
using Android.Views;
using Android.Widget;
using Android.OS;
 
namespace AndroidTest01
{
    [Activity(Label = "AndroidTest01", MainLauncher = true, Icon = "@drawable/icon")]
    public class  MainActivity : Activity
    {
        SQLiteORM o = null;
        EditText  t = null;
        ListView lv = null;
 
        protected override  void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);
 
            Button b1 = FindViewById<Button>(Resource.Id.button1);
            Button b2 = FindViewById<Button>(Resource.Id.button2);
 
            lv = FindViewById<ListView>(Resource.Id.listView1);
            t  = FindViewById<EditText>(Resource.Id.editText1);
 
            o = new  SQLiteORM("test.db");
            RefreshAdapter();
 
            b1.Click += B1_Click;
            b2.Click += B2_Click;
 
            t.KeyPress += (object sender, View.KeyEventArgs e) => {
                e.Handled = false;
                if (e.Event.Action == KeyEventActions.Down && e.KeyCode == Keycode.Enter)
                {
                    B1_Click(sender, e);
                    e.Handled = true;
                }
            };
        }
 
        private void  RefreshAdapter()
        {
            lv.Adapter = new  ArrayAdapter<string>(lv.Context, Resource.Layout.lvItem, o.GetMotto().ToArray());
        }
 
        private void  B2_Click(object  sender, EventArgs e)
        {
            AlertDialog.Builder alert = new  AlertDialog.Builder(this);
            alert.SetTitle("Conferma eliminazione");
            alert.SetMessage("Verrà eliminato l'intero contenuto di tabella");
            alert.SetPositiveButton("Procedi", (senderAlert, args) => {
                o.Svuota();
                RefreshAdapter();
            });
 
            alert.SetNegativeButton("Annulla", (senderAlert, args) => {  });
 
            Dialog dialog = alert.Create();
            dialog.Show();
        }
 
        private void  B1_Click(object  sender, EventArgs e)
        {
            if (o != null) 
            {
                if (t.Text != "")
                {
                    o.InsertMotto(t.Text);
                    RefreshAdapter();
                    t.Text = "";
                } else
                {
                    Toast.MakeText(this, "È necessario inserire una stringa", ToastLength.Short).Show();
                }
                t.RequestFocus();                
            }
        }
    }
}

Rispetto al codice visto sopra, si noti la dichiarazione di una variabile o di tipo SQLiteORM, al cui costruttore passeremo il nome del nostro database, in modo da permettere all'app di verificarne l'esistenza o, alternativamente, di crearlo sullo storage esterno. Notiamo inoltre la presenza della routine RefreshAdapter: in essa viene assegnato alla proprietà Adapter della lista un oggetto ArrayAdapter castato su tipo stringa. Esso viene creato passando il contesto cui l'adattatore deve riferirsi, il layout da impiegare per la realizzazione grafica del singolo elemento, e la collezione di oggetti da esporre, che nel nostro caso sarà la lista di stringhe ottenuta dal metodo GetMotto().

Infine, le due routine assegnate all'evento Click dei bottoni a video, B1_Click e B2_Click, eseguiranno - rispettivamente - l'inserimento nel database della stringa digitata, oppure lo svuotamento della base dati, previa conferma dell'utente, realizzata tramite Dialog. In entrambi i casi, al termine dell'operazione, una chiamata a RefreshAdapter fa in modo che la lista venga ripopolata con i dati effettivi.

Demo

Segue un video in cui osservare una breve dimostrazione dell'utilizzo dell'app così creata (visibile all'URL: http://www.youtube.com/watch?v=rNAmeZD2Jzk)

View

Download 

Il codice sorgente del progetto presentato è liberamente scaricabile al seguente indirizzo: https://code.msdn.microsoft.com/Interazione-con-SQLite-per-5150e933