Corso programmazione Android Lezione 12: L’app BeReader Parte 1
Le altre lezioni del Corso di Programmazione Android sono reperibili a questo indirizzo
In questa prima parte vedremo di riprendere le redini della nostra app.
Infatti, pur attenendomi alle specifiche originali, sono andato molto avanti con lo sviluppo (credo di poter rilasciare a breve la versione 1.0), quindi urge un riepilogo.
Il codice sorgente mostrato in questa lezione è reperibile qui.
Fragment principali
Come da specifiche la nostra App è dotata di un Navigation Drawer che richiama le categorie principali:
- Home: mostra tutte le registrazioni in ordine alfabetico
- Libri: mostra tutte le registrazioni relative ai libri
- Fumetti: mostra tutte le registrazioni relative ai fumetti
- Dischi: mostra tutte le registrazioni relative ai dischi
- Prestiti: mostra tutte le registrazione nelle quali è stato registrato un prestito (sarà oggetto della lezione 14)
Tutte le registrazioni sono in ordine alfabetico, le categorie libri, fumetti e dischi permettono l’inserimento di una nuova registrazione.
Vediamo come è costruito il Layout (file layout/home_fragment.xml) di uno dei Fragment principali (come descritto nelle precedenti lezioni il Drawer è gestito grazie ai fragment)
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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="match_parent" android:paddingLeft="10dp" android:paddingRight="10dp" android:background="@drawable/paper_tile"> <ListView android:id="@+id/home_lv" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:layout_width="match_parent" android:layout_height="match_parent" android:divider="@android:color/transparent" android:dividerHeight="10dp"/> </android.support.design.widget.CoordinatorLayout>
La spazio che separa gli elementi è definito dal parametro android:dividerHeight=”10dp” all’interno della ListView.
Ripassiamo il layout di un singolo elemento (file layout/cursor_layout.xml)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="@drawable/border_layout"> <TextView android:id="@+id/raw1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAlignment="center" android:textStyle="bold" android:textSize="20sp" android:layout_gravity="center"/> <TextView android:id="@+id/raw2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAlignment="center" android:textSize="20sp" android:layout_gravity="center"/> </LinearLayout>
e la definizione di forma e sfondo (file drawable/border:layout.xml)
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <padding android:left="15dp" android:top="10dp" android:right="15dp" android:bottom="10dp"/> <stroke android:width="1dp" android:color="#D8FDFB"/> <corners android:radius="5dp"/> <solid android:color="#90FFFFFF"/> </shape>
Il codice Java è relativamente semplice e non dovrebbe aver subito modifiche (file LibriFragment.java):
package com.begeekmyfriend.bereader; import android.app.Fragment; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; import android.widget.SimpleCursorAdapter; /** * Created by fabrizio on 12/03/16. */ public class LibriFragment extends Fragment{ private ListView libriLV; DatabaseBeReader db; Cursor c, c_resume; SimpleCursorAdapter cur, cur_resume; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_libri, container, false); libriLV = (ListView) rootView.findViewById(R.id.libri_lv); db = new DatabaseBeReader(rootView.getContext()); db.open(); c = db.fetchByTipologia(getResources().getString(R.string.libri)); cur = new SimpleCursorAdapter( getActivity(), R.layout.cursor_layout, c, new String[] {DatabaseBeReader.BeReaderMetaData.TITOLO, DatabaseBeReader.BeReaderMetaData.AUTORE}, new int[]{R.id.raw1 , R.id.raw2}, 0); libriLV.setAdapter(cur); libriLV.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { db.open(); Intent intent = new Intent(getActivity(), VisualizzaLibri.class); //apro una nuova activity String pkg = getActivity().getPackageName(); //Passo come parametro alla nuova Activity l'id della registrazione intent.putExtra(pkg+".ID", c.getString(c.getColumnIndex(DatabaseBeReader.BeReaderMetaData.ID))); startActivity(intent); } }); db.close(); FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id.fb_add_libri); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(getActivity(), AggiungiLibri.class); startActivity(intent); } }); return rootView; } @Override public void onResume() { super.onResume(); db.open(); c = db.fetchByTipologia(getResources().getString(R.string.libri)); //c_resume = db.fetchByTipologia(getResources().getString(R.string.libri)); cur_resume = new SimpleCursorAdapter( getActivity(), R.layout.cursor_layout, c, new String[] {DatabaseBeReader.BeReaderMetaData.TITOLO, DatabaseBeReader.BeReaderMetaData.AUTORE}, new int[]{R.id.raw1 , R.id.raw2}, 0); libriLV.setAdapter(cur_resume); db.close(); } }
Il Database
Inizialmente avevo pensato di utilizzare una sola tabella per tutta l’applicazione, poi mi sono scontrato con alcuni problemi tecnici. Il più difficile da risolvere (e che mi ha portato poi a staccare il db) è relativo al DatePicker (un componente che vedremo nella prossima lezione) che non può restituire il valore null, quindi ci trovavamo a inserire un valore non coerente con la realtà (prestito = no, prestato_chi = null e prestato_quando = data odierna). Il problema si poteva risolvere in più modi, ho scelto quello che apre più possibilità, una tabella separata per i prestiti.
Se qualcuno ha già installato l’app dai sorgenti allegati alle precedenti lezioni è necessario disinstallarla e reinstallarla, essendo variati i campi del db (oppure cancellare i dati, ma disinstallare è più sicuro).
Vediamo alcuni spezzoni della nuova versione della classe DatabaseBeReader (file DatabaseBeReader.java)
Definizione di campi e creazione delle tabelle
static class BeReaderMetaData { static final String BEREADER_TABLE = "bereader_table"; static final String ID = "_id"; static final String TIPOLOGIA = "tipologia"; static final String FORMATO = "formato"; static final String TITOLO = "titolo"; static final String AUTORE = "autore"; static final String GENERE = "genere"; static final String POSIZIONE = "posizione"; } private static final String BEREADER_TABLE_CREATE = "CREATE TABLE IF NOT EXISTS " + BeReaderMetaData.BEREADER_TABLE + " (" + BeReaderMetaData.ID+ " integer primary key autoincrement, " + BeReaderMetaData.TIPOLOGIA+ " text not null, " + BeReaderMetaData.FORMATO+ " text not null, " + BeReaderMetaData.TITOLO+ " text not null, " + BeReaderMetaData.AUTORE+ " text," + BeReaderMetaData.GENERE+ " text," + BeReaderMetaData.POSIZIONE+ " text" //," //+ BeReaderMetaData.PRESTATO+ " text not null," //+ BeReaderMetaData.PRESTATO_CHI+ " text," //+ BeReaderMetaData.PRESTATO_QUANDO + " text" +");"; static class BeReaderPresMetaData { static final String BEREADER_PRES_TABLE = "bereader_pres_table"; static final String ID = "_id"; static final String ID_BER = "id_ber"; static final String PRESTATO_CHI = "prestato_chi"; static final String PRESTATO_QUANDO = "prestato_quando"; static final String PRESTATO_FINE = "prestato_fine"; } private static final String BEREADER_PRES_TABLE_CREATE = "CREATE TABLE IF NOT EXISTS " + BeReaderPresMetaData.BEREADER_PRES_TABLE + " (" + BeReaderPresMetaData.ID+ " integer primary key autoincrement, " + BeReaderPresMetaData.ID_BER+ " integer, " + BeReaderPresMetaData.PRESTATO_CHI+ " text," + BeReaderPresMetaData.PRESTATO_QUANDO + " text," + BeReaderPresMetaData.PRESTATO_FINE + " text" +");";
Come si vede abbastanza chiaramente ora abbiamo una tabella bereader_table contente i campi:
- id
- tipologia
- formato
- titolo
- autore
- genere
- posizione
e una nuova tabella bereader_pres_table contenente i campi:
- id
- id_ber (l’id della registrazione alla quale è associato il prestito, quindi della tabella precedente)
- prestato_chi
- prestato_quando
- prestato_fine
Il campo prestato_fine al momento non è gestito l’ho messo per avere un campo da poter riutilizzare.
Inserimento delle registrazioni
public void inserisci(String tipologia, String formato, String titolo,String autore, String genere, String posizione)//,String prestato, String prestato_chi, String prestato_quando) { ContentValues cv = new ContentValues(); cv.put(BeReaderMetaData.TIPOLOGIA, tipologia); cv.put(BeReaderMetaData.FORMATO, formato); cv.put(BeReaderMetaData.TITOLO, titolo); cv.put(BeReaderMetaData.AUTORE, autore); cv.put(BeReaderMetaData.GENERE, genere); cv.put(BeReaderMetaData.POSIZIONE, posizione); //cv.put(BeReaderMetaData.PRESTATO, prestato); //cv.put(BeReaderMetaData.PRESTATO_CHI, prestato_chi); //cv.put(BeReaderMetaData.PRESTATO_QUANDO, prestato_quando); mDb.insert(BeReaderMetaData.BEREADER_TABLE, null, cv); } public void inserisci_pres(String id_ber, String prestato_chi, String prestato_quando,String prestato_fine) { ContentValues cv = new ContentValues(); cv.put(BeReaderPresMetaData.ID_BER, id_ber); cv.put(BeReaderPresMetaData.PRESTATO_CHI, prestato_chi); cv.put(BeReaderPresMetaData.PRESTATO_QUANDO, prestato_quando); cv.put(BeReaderPresMetaData.PRESTATO_FINE, prestato_quando); mDb.insert(BeReaderPresMetaData.BEREADER_PRES_TABLE, null, cv); }
Ci serve un metodo per ognuna delle tabelle
Query
public Cursor fetch() //Ordinato per titolo { return mDb.query(BeReaderMetaData.BEREADER_TABLE, null, null, null, null, null, BeReaderMetaData.TITOLO); } public Cursor fetchByTipologia(String tipologia) //Ordinato per titolo { return mDb.query(BeReaderMetaData.BEREADER_TABLE, null, BeReaderMetaData.TIPOLOGIA+"='"+tipologia+"'", null, null, null, BeReaderMetaData.TITOLO); } public Cursor fetchById(String _id) { return mDb.query(BeReaderMetaData.BEREADER_TABLE, null, BeReaderMetaData.ID+"='"+_id+"'", null, null, null, null); } public Cursor fetchPresById(String _id) { return mDb.query(BeReaderPresMetaData.BEREADER_PRES_TABLE, null, BeReaderPresMetaData.ID_BER+"='"+_id+"'", null, null, null, null); } public Cursor fetchByPres(String prestito) //Ordinato per titolo { return mDb.query(BeReaderPresMetaData.BEREADER_PRES_TABLE, null, null, null, null, null, null); //BeReaderPresMetaData.PRESTATO+"='"+prestito+"'", null, null, null, null); } public void editById(String id, String campo, String nuovo_valore) { String editQuery = "UPDATE "+ BeReaderMetaData.BEREADER_TABLE+" SET "+campo+" = '"+nuovo_valore+"' WHERE "+ BeReaderMetaData.ID+"= '"+id+"';"; mDb.execSQL(editQuery); }
Sulle query ho un po abbondato (non credo neanche di usarli tutti), passando al metodo mDd.query un campo come argomento fisso l’ordinamento, passando invece il terzo argomento possiamo filtrare la query.
Cancellazione
public void cancellaById(String id) { String deleteQuery = "DELETE FROM "+BeReaderMetaData.BEREADER_TABLE+" WHERE "+BeReaderMetaData.ID+"='" + id + "';"; String deleteQuery2 = "DELETE FROM "+BeReaderPresMetaData.BEREADER_PRES_TABLE+" WHERE "+BeReaderPresMetaData.ID_BER+"='" + id + "';"; mDb.execSQL(deleteQuery); mDb.execSQL(deleteQuery2); } public void cancellaPresById(String id) { String deleteQuery = "DELETE FROM "+BeReaderPresMetaData.BEREADER_PRES_TABLE+" WHERE "+BeReaderPresMetaData.ID+"='" + id + "';"; mDb.execSQL(deleteQuery); }
Anche qui serve un metodo per ogni tabella
SQLiteOpenHelper
private class DbHelper extends SQLiteOpenHelper { public DbHelper (Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); } @Override public void onCreate(SQLiteDatabase _db) { _db.execSQL(BEREADER_TABLE_CREATE); _db.execSQL(BEREADER_PRES_TABLE_CREATE); } @Override public void onUpgrade(SQLiteDatabase _db, int oldVersion, int newVersion) { //TODO: Implement this method } }
Anche in questa classe innestata dobbiamo aggiungere la creazione per entrambe le tabelle.
Aggiunta registrazioni
Questa classe ha subito solo un taglio (dovuto all’eliminazione di alcuni campi dalla tabella del Db) quindi ripassiamola (file AggiungiLibri.java)
package com.begeekmyfriend.bereader; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.ArrayAdapter; import android.widget.DatePicker; import android.widget.EditText; import android.widget.ImageButton; import android.widget.Spinner; import android.widget.Toast; public class AggiungiLibri extends Activity { //Defnisco istanze degli oggetti EditText et_titolo, et_autore, et_genere, et_posizione; Spinner spinner_formato; ImageButton button_salva; ArrayAdapter<CharSequence> aa_formato; DatabaseBeReader db; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_aggiungi_libri); et_titolo = (EditText) findViewById(R.id.et_titolo); //Associo id all'oggetto et_autore = (EditText) findViewById(R.id.et_autore); spinner_formato = (Spinner) findViewById(R.id.spinner_formato); //Creo un array adapter utilizzando l'array di stringhe aa_formato = ArrayAdapter.createFromResource(this, R.array.ar_formato_libri, android.R.layout.simple_spinner_item); //Associo un layout di sistema aa_formato.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner_formato.setAdapter(aa_formato); //Associo l'adapter allo spinner et_genere = (EditText) findViewById(R.id.et_genere); et_posizione = (EditText) findViewById(R.id.et_posizione); button_salva = (ImageButton) findViewById(R.id.ib_salva); button_salva.setOnClickListener(new View.OnClickListener() { //Classe anonima per il clicj dell'image button @Override public void onClick(View v) { salva(); //Richiamo metodo per il salvataggio } }); } public void salva() { db = new DatabaseBeReader(this); //Denisco istanza dell'oggetto db e richiamo il costruttore db.open(); //Apro il db if (et_titolo.getText().toString().equals("")) { //Controllo che il titolo sia valorizzato Toast toast = Toast.makeText(getApplicationContext(), getResources().getString(R.string.tit_no_val), Toast.LENGTH_SHORT); //Mosto un messaggio toast.show(); } else { //OK è valorizzato quindi inserisco //Inserisco nel db il contenuto dei campi db.inserisci(getResources().getString(R.string.libri), spinner_formato.getSelectedItem().toString(), et_titolo.getText().toString(), et_autore.getText().toString(), et_genere.getText().toString(), et_posizione.getText().toString()); db.close(); //Chiudo il db finish(); //chiudo il dialog } } }
Come sempre definiamo tutti i campi e intercettiamo il tap sul bottone richiamando il metodo salva(), il quale si occupa di richiamare la funzione di inserimento definita nella classe DatabaseBeReader.
Le classi di aggiunta libri, fumetti e dischi sono praticamente identiche, cambia solo la tipologia nel metodo salva.
Il layout è piuttosto semplice eccolo (file layout/activity_aggiungi_libri.xml):
<?xml version="1.0" encoding="utf-8"?> <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" android:fitsSystemWindows="true" tools:context=".AggiungiLibri"> <include layout="@layout/content_aggiungi" /> </RelativeLayout>
Per pulizia l’abbiamo diviso in due ecco il resto (file layout/content_aggiungi.xml):
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".AggiungiLibri" tools:showIn="@layout/activity_aggiungi_libri"> <ScrollView android:id="@+id/sv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="50dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/tv1_libri" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/titolo"/> <EditText android:id="@+id/et_titolo" android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/autore_ins"/> <EditText android:id="@+id/et_autore" android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/formato"/> <Spinner android:id="@+id/spinner_formato" android:layout_width="match_parent" android:layout_height="wrap_content" android:spinnerMode="dropdown" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/genere"/> <EditText android:id="@+id/et_genere" android:layout_width="match_parent" android:layout_height="wrap_content"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/posizione"/> <EditText android:id="@+id/et_posizione" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout> </ScrollView> <ImageButton android:id="@+id/ib_salva" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" android:text="@string/salva" android:src="@mipmap/ic_action_save"/> </RelativeLayout>
L’activity è stata trasformata in un Dialog grazie alla definizione del tema nel file AndroidManifest.xml vediamo la porzione di codice relativa:
<activity android:name=".AggiungiLibri" android:label="@string/title_activity_aggiungi_libri" android:theme="@style/Theme.AppCompat.Dialog" />
Nella prossima lezione continueremo con l’analisi.
Originariamente pubblicato su Be Geek My Friend