Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 79 di 97
  • livello avanzato
Indice lezioni

Architettura unica con Room, ViewModel e LiveData

Utilizzare ViewModel, LiveData e la libreria Room per realizzare un'architettura software completa, adatta allo sviluppo di app moderne per Android.
Utilizzare ViewModel, LiveData e la libreria Room per realizzare un'architettura software completa, adatta allo sviluppo di app moderne per Android.
Link copiato negli appunti

Parlando di Android Architecture Components, abbiamo trattato separatamente ViewModel e LiveData (per offrire uno strato di dati a supporto unicamente dell'interfaccia utente), e Room (per agire su database SQLite con un'interfaccia di alto livello).

In questa lezione li utilizzeremo insieme in una architettura unica, moderna e stabile, per sviluppare applicazioni Android.

Architettura per applicazioni

L'architettura proposta tra le nuove best practises del mondo Android vede la strutturazione di un'applicazione
su quattro livelli:

  • la UI (user interface, l'interfaccia utente), contenente Activity, Fragment ma
    anche Adapter e qualsiasi classe collegata alla fruizione dell'app;
  • ViewModel, ovvero lo strato destinato a fornire dati all'interfaccia utente e a ricevere da questa direttive sulle operazioni di modifica da attuare. Viene rappresentato da una classe che deriva da ViewModel, ed i metodi che restituiscono dati (ad esempio, provenienti da query) devono offrire come tipo di ritorno dei LiveData;
  • Repository, ovvero lo store complessivo dei dati. Da qui il ViewModel attingerà qualsiasi informazione per l'interfaccia utente, indipendentemente dal fatto che venga prelevata da database, rete, file o altro: il Repository astrarrà l'accesso alle fonti. Si tratta di una normale classe Java che non verrà marcata da annotazioni nè deriverà da alcuna interfaccia/superclasse del framework;
  • accesso al database: sotto questa espressione intendiamo un sistema di più classi organizzate secondo i
    dettami di Room. Il programmatore farà uso di SQLite senza invocarne mai le funzionalità di base. In questo settore, avremo entity (marcate con l'annotazione @Entity), una classe RoomDatabase (marcata con @Database) e uno o più DAO (marcati con @Dao). Si noti che il Repository dialogarà direttamente solo con il Dao, ed invocherà la classe RoomDatabase esclusivamente per ottenere un riferimento al Dao.

L'esempio con cui metteremo alla prova questa architettura sarà quello della lezione introduttiva su Room, ampliato secondo queste nuove osservazioni. Per rendere più chiari i livelli di accesso tra le componenti, abbiamo scelto di racchiudere tutte le classi relative al terzo e quarto strato (Repository e Database) in un package a sè stante. In esso solo la classe Repository e le entity avranno visibilità public, le altre saranno limitate al package in quanto destinate solo ad un uso interno.

Figura 1. Struttura dell'esempio (click per ingrandire)

Struttura dell'esempio

L'esempio

L'esempio tratta un database destinato a salvare note testuali. Le operazioni consentite su tale archivio saranno: lettura di tutte le note; cancellazione di tutti i dati; eliminazione di una singola nota; inserimento di una nuova. Di seguito analizziamo i punti salienti di ogni parte.

La classe MainActivity conserva un riferimento al ViewModel tra le sue variabili d'istanza:

private NoteViewModel viewModel;

Tale ViewModel viene inizializzato nel metodo onCreate(), luogo in cui si inizializza l'Adapter e si sottopone a "osservazione" il LiveData che ne costituisce l'insieme dei dati:

viewModel= ViewModelProviders.of(this).get(NoteViewModel.class);
viewModel.getAllNotes().observe(this, new Observer<List<Note>>() {
@Override
public void onChanged(@Nullable final List<Note> notes) {
adapter.clear();
adapter.addAll(notes);
}
});

Il ViewModel svolge per lo più due ruoli: quello suo principale (supporto ai dati della UI nonostante cambi di configurazione e transizioni di stato dell'app) e quello più architetturale, costituendo uno snodo tra la UI ed il Repository ai fini di una maggiore testabilità e manutenibilità:

public class NoteViewModel extends AndroidViewModel {
private NoteRepository noteRepository;
private LiveData<List<Note>> note;
public NoteViewModel(Application application) {
super(application);
noteRepository=new NoteRepository(application);
note=noteRepository.getAllNotes();
}
public LiveData<List<Note>> getAllNotes() {
return note;
}
public void deleteNote(Note n) {
noteRepository.deleteNote(n);
}
public void insertNote(Note currentNote) {
noteRepository.insertNote(currentNote);
}
public void deleteAll() {
noteRepository.deleteAll();
}
}

Un aspetto fondamentale da notare è che nel costruttore del ViewModel passiamo un oggetto Application dal quale potrà essere ricavato un Context: è bene, infatti, tenere a mente la regola "mai legare direttamente il ViewModel ad un Context", come, ad esempio, l'Activity. In caso di distruzione dell'Activity, infatti, il ViewModel rimarrebbe in vita ma con un riferimento al Context non più valido.

Il Repository in questo caso non è di grandi dimensioni, in quanto fa solo da legame tra il ViewModel ed il Dao, ma possiamo immaginarlo in casi più complessi in cui sotto di esso si annidano diversi Dao e recuperi di informazioni in varie modalità: in casi simili, si esalterebbe il suo ruolo di punto unico di accesso al sottosistema dei dati dell'app. Nell'esempio, lo corrediamo di un costruttore che riceve l'oggetto Application (da cui estrarrà un Context valido per l'inizializzazione del database) ed inizierà a preparare i LiveData che verranno richiesti per la UI:

public class NoteRepository {
private NoteDao noteDao;
private LiveData<List<Note>> note;
public NoteRepository(Application application)
{
noteDao=NoteRoomDatabase.getInMemoryDatabase(application.getApplicationContext())
.noteModel();
note=noteDao.loadAllNotes();
}
...
...
}

La differenza tra il Repository ed i Dao consiste nel diverso rapporto che questi hanno con i dati. I Dao effettuano un'interazione diretta con le fonti dati, ma più semplice; il Repository invece non accede alle fonti ma applica alle operazioni CRUD logiche di fusione tra risultati di sorgenti diverse, organizzando sessioni di modifica più complesse o prendendosi carico delle modalità di esecuzione come l'avvio di thread secondari per rendere asincrone operazioni onerose.

I Dao di Room consistono, come abbiamo visto, in interfacce dove viene applicato poco codice e ci si affida molto all'azione più velata delle annotazioni:

@Dao
interface NoteDao {
@Insert(onConflict = REPLACE)
void insertNote(Note note);
@Query("select * from Note")
LiveData<List<Note>> loadAllNotes();
@Query("DELETE FROM Note")
void deleteAll();
@Delete
void deleteNote(Note n);
}

Si noti che la query restituisce un risultato incapsulato in LiveData il quale da qui arriverà, attraverso i vari
passaggi, nell'interfaccia utente dove verrà sottoposto a osservazione, come detto. Da ricordare che restituendo un LiveData
non avremo possibilità di modificarne il contenuto, operazione che può essere svolta solo con MutableLiveData.

Istanze del Dao saranno recuperate direttamente dalle classi RoomDatabase. Qui a titolo di esempio si offre la possibilità di
richiedere un database in memoria o persistente:

@Database(entities = {Note.class}, version = 1)
abstract class NoteRoomDatabase extends RoomDatabase {
private static NoteRoomDatabase INSTANCE;
abstract NoteDao noteModel();
// database in memoria
static NoteRoomDatabase getInMemoryDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.inMemoryDatabaseBuilder(context.getApplicationContext(), NoteRoomDatabase.class)
.allowMainThreadQueries()
.build();
}
return INSTANCE;
}
// database persistente
static NoteRoomDatabase getDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(context.getApplicationContext(), NoteRoomDatabase.class, "note_db")
.allowMainThreadQueries()
.build();
}
return INSTANCE;
}
}

Come ultima nota, si faccia caso che nella creazione dei database abbiamo invocato il metodo allowMainThreadQueries(). Di norma, Room impedisce esecuzione di operazioni sui dati sul thread principale per evitare rallentamenti dell'interfaccia utente. Con l'applicazione di questo metodo, non si avranno limiti ma tale deroga non andrebbe mai applicata in casi reali dove si lavora con quantità considerevoli di dati. Qualora rimuovessimo l'invocazione a allowMainThreadQueries(), sin dall'operazione di inserimento otterremmo un'eccezione. La soluzione consiste nell'attivazione di un thread secondario, possibilmente sfruttando le comodità offerte dalla classe AsyncTask. Ad esempio, l'inserimento nel Repository diventerebbe così:

public void insertNote(Note currentNote) {
new AsyncTask<Note, Void, Void>()
{
@Override
protected Void doInBackground(Note... notes) {
noteDao.insertNote(notes[0]);
return null;
}
}.execute(currentNote);
}

Ti consigliamo anche