Lo scopo di Architecture Components è fornire indicazioni sull'architettura delle app, con librerie per attività comuni come la gestione del ciclo di vita e la persistenza dei dati. I componenti dell'architettura ti aiutano a strutturare l'app in modo robusto, testabile e gestibile con meno codice boilerplate. Le librerie dei componenti dell'architettura fanno parte di Android Jetpack.
Questa è la versione Kotlin del codelab. La versione nel linguaggio di programmazione Java è disponibile qui.
Se riscontri problemi (bug del codice, errori grammaticali, formulazione poco chiara e così via) mentre segui questo codelab, segnalali tramite il link Segnala un errore nell'angolo in basso a sinistra del codelab.
Prerequisiti
Devi avere familiarità con Kotlin, i concetti di progettazione orientata agli oggetti e i fondamenti dello sviluppo per Android, in particolare:
RecyclerViewe adattatori- Database SQLite e linguaggio di query SQLite
- Coroutine di base (se non hai familiarità con le coroutine, puoi consultare l'articolo Utilizzare le coroutine Kotlin nell'app per Android).
Inoltre, è utile conoscere i pattern architetturali del software che separano i dati dall'interfaccia utente, come MVP o MVC. Questo codelab implementa l'architettura definita nella Guida all'architettura dell'app.
Questo codelab è incentrato sui componenti dell'architettura Android. Concetti e codice non pertinenti sono forniti solo per operazioni di copia e incolla.
Se non hai familiarità con Kotlin, una versione di questo codelab è disponibile nel linguaggio di programmazione Java qui.
Attività previste
In questo codelab imparerai a progettare e creare un'app utilizzando i componenti dell'architettura Room, ViewModel e LiveData e a creare un'app che esegue le seguenti operazioni:
- Implementa la nostra architettura consigliata utilizzando i componenti dell'architettura Android.
- Funziona con un database per ottenere e salvare i dati e precompila il database con alcune parole.
- Mostra tutte le parole in un
RecyclerViewinMainActivity. - Apre una seconda attività quando l'utente tocca il pulsante +. Quando l'utente inserisce una parola, la aggiunge al database e all'elenco.
L'app è semplice, ma sufficientemente complessa da poter essere utilizzata come modello su cui basarsi. Ecco un'anteprima:
|
|
|
Che cosa ti serve
- Android Studio 3.0 o versioni successive e conoscenza del suo utilizzo. Assicurati che Android Studio, l'SDK e Gradle siano aggiornati.
- Un dispositivo o un emulatore Android.
Questo codelab fornisce tutto il codice necessario per creare l'app completa.
L'utilizzo dei componenti dell'architettura e l'implementazione dell'architettura consigliata richiedono molti passaggi. La cosa più importante è creare un modello mentale di ciò che sta succedendo, capire come si incastrano i pezzi e come fluiscono i dati. Mentre segui questo codelab, non limitarti a copiare e incollare il codice, ma cerca di sviluppare una comprensione più approfondita.
Quali sono i componenti dell'architettura consigliati?
Per introdurre la terminologia, ecco una breve introduzione ai componenti dell'architettura e al loro funzionamento congiunto. Tieni presente che questo codelab si concentra su un sottoinsieme dei componenti, ovvero LiveData, ViewModel e Room. Ogni componente viene spiegato man mano che lo utilizzi.
Questo diagramma mostra una forma base dell'architettura:

Entità: classe annotata che descrive una tabella di database quando utilizzi Room.
Database SQLite: spazio di archiviazione sul dispositivo. La libreria di persistenza Room crea e gestisce questo database per te.
DAO: Data Access Object. Una mappatura delle query SQL alle funzioni. Quando utilizzi un DAO, chiami i metodi e Room si occupa del resto.
Database Room: semplifica il lavoro con i database e funge da punto di accesso al database SQLite sottostante (nasconde SQLiteOpenHelper). Il database Room utilizza il DAO per inviare query al database SQLite.
Repository:una classe che crei e che viene utilizzata principalmente per gestire più origini dati.
ViewModel: funge da centro di comunicazione tra il repository (dati) e la UI. L'interfaccia utente non deve più preoccuparsi dell'origine dei dati. Le istanze ViewModel sopravvivono alla ricreazione di Activity/Fragment.
LiveData: una classe di contenitore di dati che può essere osservata. Contiene/memorizza sempre nella cache l'ultima versione dei dati e invia una notifica ai suoi osservatori quando i dati cambiano. LiveData è consapevole del ciclo di vita. I componenti dell'interfaccia utente osservano solo i dati pertinenti e non interrompono o riprendono l'osservazione. LiveData gestisce automaticamente tutto questo perché è a conoscenza delle modifiche dello stato del ciclo di vita pertinenti durante l'osservazione.
Panoramica dell'architettura di RoomWordSample
Il seguente diagramma mostra tutti i componenti dell'app. Ogni riquadro contenitore (ad eccezione del database SQLite) rappresenta una classe che creerai.

- Apri Android Studio e fai clic su Avvia un nuovo progetto Android Studio.
- Nella finestra Crea nuovo progetto, scegli Attività vuota e fai clic su Avanti.
- Nella schermata successiva, assegna all'app il nome RoomWordSample e fai clic su Fine.

Successivamente, dovrai aggiungere le librerie dei componenti ai file Gradle.
- In Android Studio, fai clic sulla scheda Progetti ed espandi la cartella Gradle Scripts.
Apri build.gradle (Modulo: app).
- Applica il plug-in Kotlin Annotation Processor
kaptaggiungendolo dopo gli altri plug-in definiti nella parte superiore del filebuild.gradle(Modulo: app).
apply plugin: 'kotlin-kapt'- Aggiungi il blocco
packagingOptionsall'interno del bloccoandroidper escludere il modulo delle funzioni atomiche dal pacchetto ed evitare avvisi.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}- Aggiungi il seguente codice alla fine del blocco
dependencies.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"- Nel file
build.gradle(Project: RoomWordsSample), aggiungi i numeri di versione alla fine del file, come indicato nel codice riportato di seguito.
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}I dati per questa app sono parole e avrai bisogno di una semplice tabella per contenere questi valori:

Room ti consente di creare tabelle tramite un'entità. Facciamolo subito.
- Crea un nuovo file di classe Kotlin denominato
Wordcontenente la classe di datiWord.
Questa classe descriverà l'entità (che rappresenta la tabella SQLite) per le tue parole. Ogni proprietà della classe rappresenta una colonna della tabella. Room utilizzerà queste proprietà per creare la tabella e istanziare gli oggetti dalle righe del database.
Ecco il codice:
data class Word(val word: String)Per rendere significativa la classe Word per un database Room, devi annotarla. Le annotazioni identificano la relazione tra ogni parte di questa classe e una voce nel database. Room utilizza queste informazioni per generare il codice.
Se digiti le annotazioni manualmente (anziché incollarle), Android Studio importerà automaticamente le classi di annotazione.
- Aggiorna la classe
Wordcon le annotazioni mostrate in questo codice:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)Vediamo cosa fanno queste annotazioni:
@Entity(tableName ="word_table")
Ogni classe@Entityrappresenta una tabella SQLite. Annota la dichiarazione della classe per indicare che si tratta di un'entità. Puoi specificare il nome della tabella se vuoi che sia diverso dal nome della classe. In questo modo la tabella viene denominata "word_table".@PrimaryKey
Ogni entità deve avere una chiave primaria. Per semplicità, ogni parola funge da chiave primaria.@ColumnInfo(name ="word")
Specifica il nome della colonna nella tabella se vuoi che sia diverso dal nome della variabile membro. In questo modo, la colonna viene denominata "word".- Ogni proprietà archiviata nel database deve avere visibilità pubblica, che è l'impostazione predefinita di Kotlin.
Puoi trovare un elenco completo delle annotazioni nel Riferimento al riepilogo del pacchetto della stanza.
Che cos'è la DAO?
Nell'DAO, specifica le query SQL e le associa alle chiamate di metodi. Il compilatore controlla l'SQL e genera query dalle annotazioni di convenienza per le query comuni, ad esempio @Insert. Room utilizza il DAO per creare un'API pulita per il tuo codice.
Il DAO deve essere un'interfaccia o una classe astratta.
Per impostazione predefinita, tutte le query devono essere eseguite su un thread separato.
Room supporta le coroutine, consentendo di annotare le query con il modificatore suspend e di chiamarle da una coroutine o da un'altra funzione di sospensione.
Implementare l'oggetto di accesso ai dati
Scriviamo un DAO che fornisca query per:
- Ottenere tutte le parole in ordine alfabetico
- Inserire una parola
- Eliminazione di tutte le parole
- Crea un nuovo file di classe Kotlin denominato
WordDao. - Copia e incolla il seguente codice in
WordDaoe correggi le importazioni in base alle necessità per compilarlo.
@Dao
interface WordDao {
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
Vediamo come fare:
WordDaoè un'interfaccia; i DAO devono essere interfacce o classi astratte.- L'annotazione
@Daolo identifica come classe DAO per Room. suspend fun insert(word: Word): dichiara una funzione di sospensione per inserire una parola.- L'annotazione
@Insertè un'annotazione speciale del metodo DAO in cui non devi fornire alcun SQL. Esistono anche annotazioni@Deletee@Updateper l'eliminazione e l'aggiornamento delle righe, ma non le utilizzi in questa app. onConflict = OnConflictStrategy.IGNORE: la strategia onConflict selezionata ignora una nuova parola se è esattamente uguale a una già presente nell'elenco. Per saperne di più sulle strategie di conflitto disponibili, consulta la documentazione.suspend fun deleteAll(): dichiara una funzione di sospensione per eliminare tutte le parole.- Non esiste un'annotazione di convenienza per l'eliminazione di più entità, quindi viene annotata con il generico
@Query. @Query("DELETE FROM word_table"):@Queryrichiede di fornire una query SQL come parametro stringa all'annotazione, consentendo query di lettura complesse e altre operazioni.fun getAlphabetizedWords(): List<Word>: un metodo per ottenere tutte le parole e restituire unListdiWords.@Query("SELECT * from word_table ORDER BY word ASC"): query che restituisce un elenco di parole ordinate in ordine crescente.
Quando i dati cambiano, in genere vuoi intraprendere un'azione, ad esempio visualizzare i dati aggiornati nell'interfaccia utente. Ciò significa che devi osservare i dati in modo da poter reagire quando cambiano.
A seconda di come vengono archiviati i dati, questa operazione può essere complicata. L'osservazione delle modifiche ai dati in più componenti dell'app può creare percorsi di dipendenza espliciti e rigidi tra i componenti. Ciò rende difficile il test e il debug, tra le altre cose.
LiveData, una classe lifecycle library per l'osservazione dei dati, risolve questo problema. Utilizza un valore restituito di tipo LiveData nella descrizione del metodo e Room genera tutto il codice necessario per aggiornare LiveData quando il database viene aggiornato.
In WordDao, modifica la firma del metodo getAlphabetizedWords() in modo che List<Word> restituito sia racchiuso tra LiveData.
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>Più avanti in questo codelab, monitorerai le modifiche ai dati tramite un Observer in MainActivity.
Che cos'è un database Room?
- Room è un livello di database sopra un database SQLite.
- Room si occupa delle attività banali che prima gestivi con un
SQLiteOpenHelper. - Room utilizza DAO per inviare query al database.
- Per impostazione predefinita, per evitare prestazioni scadenti dell'interfaccia utente, Room non consente di eseguire query sul thread principale. Quando le query Room restituiscono
LiveData, vengono eseguite automaticamente in modo asincrono su un thread in background. - Room fornisce controlli in fase di compilazione delle istruzioni SQLite.
Implementare il database Room
La classe del database Room deve essere astratta ed estendere RoomDatabase. In genere, è necessaria una sola istanza di un database Room per l'intera app.
Creiamone uno ora.
- Crea un file di classe Kotlin denominato
WordRoomDatabasee aggiungi questo codice:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}Esaminiamo il codice:
- La classe di database per Room deve essere
abstracted estendereRoomDatabase - Annoti la classe in modo che sia un database Room con
@Databasee utilizzi i parametri di annotazione per dichiarare le entità che appartengono al database e impostare il numero di versione. Ogni entità corrisponde a una tabella che verrà creata nel database. Le migrazioni del database non rientrano nell'ambito di questo codelab, quindi abbiamo impostatoexportSchemasu false per evitare un avviso di build. In un'app reale, devi prendere in considerazione l'impostazione di una directory che Room utilizzerà per esportare lo schema, in modo da poterlo controllare nel sistema di controllo della versione. - Il database espone i DAO tramite un metodo "getter" astratto per ogni @Dao.
- Abbiamo definito un singleton,
WordRoomDatabase,, per evitare che vengano aperte più istanze del database contemporaneamente. getDatabaserestituisce il singleton. La prima volta che viene eseguito l'accesso, viene creato il database utilizzando il builder di database di Room per creare un oggettoRoomDatabasenel contesto dell'applicazione dalla classeWordRoomDatabasee viene assegnato il nome"word_database".
Che cos'è un repository?
Una classe di repository astrae l'accesso a più origini dati. Il repository non fa parte delle librerie dei componenti dell'architettura, ma è una best practice suggerita per la separazione del codice e dell'architettura. Una classe Repository fornisce un'API pulita per l'accesso ai dati al resto dell'applicazione.

Perché utilizzare un repository?
Un repository gestisce le query e ti consente di utilizzare più backend. Nell'esempio più comune, il repository implementa la logica per decidere se recuperare i dati da una rete o utilizzare i risultati memorizzati nella cache in un database locale.
Implementazione del repository
Crea un file di classe Kotlin denominato WordRepository e incolla il seguente codice:
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {
// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}I principali concetti chiave:
- L'oggetto DAO viene passato al costruttore del repository anziché all'intero database. Questo perché ha bisogno solo dell'accesso al DAO, in quanto contiene tutti i metodi di lettura/scrittura per il database. Non è necessario esporre l'intero database al repository.
- L'elenco di parole è di proprietà pubblica. Viene inizializzato ottenendo l'elenco di parole
LiveDatada Room. Possiamo farlo grazie al modo in cui abbiamo definito il metodogetAlphabetizedWordsper restituireLiveDatanel passaggio "La classe LiveData". Room esegue tutte le query su un thread separato. L'LiveDataosservato invierà una notifica all'osservatore nel thread principale quando i dati saranno cambiati. - Il modificatore
suspendindica al compilatore che questa funzione deve essere chiamata da una coroutine o da un'altra funzione di sospensione.
Che cos'è un ViewModel?
Il ruolo di ViewModel è fornire dati alla UI e sopravvivere alle modifiche alla configurazione. Un ViewModel funge da centro di comunicazione tra il repository e la UI. Puoi anche utilizzare un ViewModel per condividere i dati tra i fragment. ViewModel fa parte della libreria del ciclo di vita.

Per una guida introduttiva a questo argomento, consulta ViewModel Overview o il post del blog ViewModels: A Simple Example.
Perché utilizzare un ViewModel?
Un ViewModel contiene i dati dell'interfaccia utente della tua app in modo consapevole del ciclo di vita che sopravvive alle modifiche alla configurazione. Separando i dati dell'interfaccia utente dell'app dalle classi Activity e Fragment, puoi seguire meglio il principio di responsabilità singola: le attività e i fragment sono responsabili del disegno dei dati sullo schermo, mentre ViewModel può occuparsi di contenere ed elaborare tutti i dati necessari per l'interfaccia utente.
In ViewModel, utilizza LiveData per i dati modificabili che l'interfaccia utente utilizzerà o visualizzerà. L'utilizzo di LiveData offre diversi vantaggi:
- Puoi impostare un osservatore sui dati (anziché eseguire il polling per rilevare le modifiche) e aggiornare l'interfaccia utente solo quando i dati cambiano effettivamente.
- Il repository e la UI sono completamente separati da
ViewModel. - Non ci sono chiamate al database da
ViewModel(tutto viene gestito nel repository), il che rende il codice più testabile.
viewModelScope
In Kotlin, tutte le coroutine vengono eseguite all'interno di un CoroutineScope. Un ambito controlla la durata delle coroutine tramite il relativo job. Quando annulli il job di un ambito, vengono annullate tutte le coroutine avviate in quell'ambito.
La libreria AndroidX lifecycle-viewmodel-ktx aggiunge un viewModelScope come funzione di estensione della classe ViewModel, consentendoti di lavorare con gli ambiti.
Per scoprire di più su come utilizzare le coroutine in ViewModel, consulta il passaggio 5 del codelab Utilizzo delle coroutine Kotlin nell'app Android o il post del blog Easy Coroutines in Android: viewModelScope.
Implementare il ViewModel
Crea un file di classe Kotlin per WordViewModel e aggiungi questo codice:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}Ecco cosa abbiamo fatto:
- È stata creata una classe denominata
WordViewModelche riceveApplicationcome parametro ed estendeAndroidViewModel. - È stata aggiunta una variabile membro privata per contenere un riferimento al repository.
- È stata aggiunta una variabile membro pubblica
LiveDataper memorizzare nella cache l'elenco delle parole. - È stato creato un blocco
initche fa riferimento aWordDaodaWordRoomDatabase. - Nel blocco
init, è stato creatoWordRepositoryin base aWordRoomDatabase. - Nel blocco
init, inizializzaallWordsLiveData utilizzando il repository. - È stato creato un metodo wrapper
insert()che chiama il metodoinsert()del repository. In questo modo, l'implementazione diinsert()è incapsulata dalla UI. Non vogliamo che l'inserimento blocchi il thread principale, quindi avviamo una nuova coroutine e chiamiamo l'inserimento del repository, che è una funzione di sospensione. Come accennato, i ViewModel hanno un ambito di coroutine basato sul loro ciclo di vita chiamatoviewModelScope, che utilizziamo qui.
Successivamente, devi aggiungere il layout XML per l'elenco e gli elementi.
Questo codelab presuppone che tu abbia familiarità con la creazione di layout in XML, quindi ti forniamo solo il codice.
Rendi il tema dell'applicazione materiale impostando il genitore AppTheme su Theme.MaterialComponents.Light.DarkActionBar. Aggiungi uno stile per gli elementi di elenco in values/styles.xml:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>Aggiungere un layout layout/recyclerview_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>In layout/activity_main.xml, sostituisci TextView con RecyclerView e aggiungi un pulsante di azione sovrapposto (FAB). Il layout dovrebbe ora avere il seguente aspetto:
<androidx.constraintlayout.widget.ConstraintLayout
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"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
tools:listitem="@layout/recyclerview_item"
android:padding="@dimen/big_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"/>
</androidx.constraintlayout.widget.ConstraintLayout>L'aspetto del pulsante Azione rapida deve corrispondere all'azione disponibile, quindi sostituiremo l'icona con un simbolo "+".
Innanzitutto, dobbiamo aggiungere un nuovo asset vettoriale:
- Seleziona File > Nuovo > Risorsa vettoriale.
- Fai clic sull'icona del robot Android nel campo Clip Art: .

- Cerca "aggiungi" e seleziona l'asset "+". Fai clic su Ok
. - Dopodiché, fai clic su Avanti.

- Conferma il percorso dell'icona come
main > drawablee fai clic su Fine per aggiungere l'asset.
- Sempre in
layout/activity_main.xml, aggiorna il FAB in modo che includa il nuovo elemento disegnabile:
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"
android:src="@drawable/ic_add_black_24dp"/>Visualizzerai i dati in un RecyclerView, che è un po' più bello che inserirli semplicemente in un TextView. Questo codelab presuppone che tu sappia come funzionano RecyclerView, RecyclerView.LayoutManager, RecyclerView.ViewHolder e RecyclerView.Adapter.
Tieni presente che la variabile words nell'adattatore memorizza i dati nella cache. Nell'attività successiva, aggiungi il codice che aggiorna automaticamente i dati.
Crea un file di classe Kotlin per WordListAdapter che estende RecyclerView.Adapter. Ecco il codice:
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}Aggiungi RecyclerView nel metodo onCreate() di MainActivity.
Nel metodo onCreate() dopo setContentView:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)Esegui l'app per assicurarti che tutto funzioni. Non sono presenti elementi perché non hai ancora collegato i dati.

Non ci sono dati nel database. Aggiungerai i dati in due modi: aggiungendo alcuni dati all'apertura del database e aggiungendo un Activity per aggiungere parole.
Per eliminare tutti i contenuti e ripopolare il database ogni volta che viene avviata l'app, crea un RoomDatabase.Callback e sostituisci onOpen(). Poiché non puoi eseguire operazioni sul database Room nel thread UI, onOpen() avvia una coroutine sul dispatcher I/O.
Per avviare una coroutine abbiamo bisogno di un CoroutineScope. Aggiorna il metodo getDatabase della classe WordRoomDatabase per ottenere anche un ambito di coroutine come parametro:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}Aggiorna l'inizializzatore di recupero del database nel blocco init di WordViewModel per passare anche l'ambito:
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()In WordRoomDatabase, creiamo un'implementazione personalizzata di RoomDatabase.Callback(), che riceve anche CoroutineScope come parametro del costruttore. Poi, eseguiamo l'override del metodo onOpen per popolare il database.
Ecco il codice per creare il callback all'interno della classe WordRoomDatabase:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
}
}Infine, aggiungi il callback alla sequenza di creazione del database subito prima di chiamare .build() su Room.databaseBuilder():
.addCallback(WordDatabaseCallback(scope))Ecco come dovrebbe apparire il codice finale:
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
word = Word("TODO!")
wordDao.insert(word)
}
}
}
}
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.addCallback(WordDatabaseCallback(scope))
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
Aggiungi queste risorse stringa in values/strings.xml:
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>Aggiungi questa risorsa di colore in value/colors.xml:
<color name="buttonLabel">#FFFFFF</color>Crea un nuovo file di risorse delle dimensioni:
- Fai clic sul modulo dell'app nella finestra Progetto.
- Seleziona File > Nuovo > File di risorse Android.
- In Qualificatori disponibili, seleziona Dimensione .
- Imposta il nome del file: dimens

Aggiungi queste risorse delle dimensioni in values/dimens.xml:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>Crea un nuovo progetto Android Activity vuoto con il modello Empty Activity:
- Seleziona File > Nuovo > Attività > Attività vuota.
- Inserisci
NewWordActivityper il nome dell'attività. - Verifica che la nuova attività sia stata aggiunta al file manifest di Android.
<activity android:name=".NewWordActivity"></activity>Aggiorna il file activity_new_word.xml nella cartella del layout con il seguente codice:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_height"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:layout_margin="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:layout_margin="@dimen/big_padding"
android:textColor="@color/buttonLabel" />
</LinearLayout>Aggiorna il codice dell'attività:
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
Il passaggio finale consiste nel connettere la UI al database salvando le nuove parole inserite dall'utente e visualizzando i contenuti attuali del database di parole in RecyclerView.
Per visualizzare i contenuti attuali del database, aggiungi un osservatore che osservi LiveData in ViewModel.
Ogni volta che i dati cambiano, viene richiamato il callback onChanged(), che chiama il metodo setWords() dell'adattatore per aggiornare i dati memorizzati nella cache dell'adattatore e aggiornare l'elenco visualizzato.
In MainActivity, crea una variabile membro per ViewModel:
private lateinit var wordViewModel: WordViewModelUtilizza ViewModelProvider per associare il tuo ViewModel al tuo Activity.
Quando Activity viene avviato per la prima volta, ViewModelProviders crea ViewModel. Quando l'attività viene eliminata, ad esempio a causa di una modifica alla configurazione, ViewModel persiste. Quando l'attività viene ricreata, ViewModelProviders restituisce ViewModel esistente. Per ulteriori informazioni, vedi ViewModel.
In onCreate() sotto il blocco di codice RecyclerView, ottieni un ViewModel da ViewModelProvider:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)In onCreate(), aggiungi anche un osservatore per la proprietà allWords LiveData da WordViewModel.
Il metodo onChanged() (il metodo predefinito per la nostra Lambda) viene attivato quando i dati osservati cambiano e l'attività è in primo piano:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})Vogliamo aprire NewWordActivity quando tocchiamo il pulsante Azione rapida e, una volta tornati in MainActivity, inserire la nuova parola nel database o mostrare un Toast. Per raggiungere questo obiettivo, iniziamo definendo un codice di richiesta:
private val newWordActivityRequestCode = 1In MainActivity, aggiungi il codice onActivityResult() per NewWordActivity.
Se l'attività restituisce RESULT_OK, inserisci la parola restituita nel database chiamando il metodo insert() di WordViewModel:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}In MainActivity,start NewWordActivity quando l'utente tocca il pulsante di azione rapida. In MainActivity onCreate, trova il FAB e aggiungi un onClickListener con questo codice:
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}Il codice finale dovrebbe avere questo aspetto:
class MainActivity : AppCompatActivity() {
private const val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}Ora esegui l'app. Quando aggiungi una parola al database in NewWordActivity, l'interfaccia utente viene aggiornata automaticamente.
Ora che hai un'app funzionante, riepiloghiamo ciò che hai creato. Ecco di nuovo la struttura dell'app:

I componenti dell'app sono:
MainActivity: mostra le parole in un elenco utilizzando unRecyclerViewe ilWordListAdapter. InMainActivity, è presente unObserverche osserva le parole LiveData del database e riceve una notifica quando cambiano.NewWordActivity:aggiunge una nuova parola all'elenco.WordViewModel: fornisce metodi per accedere al livello dati e restituisce LiveData in modo che MainActivity possa impostare la relazione di osservazione.*LiveData<List<Word>>: consente gli aggiornamenti automatici nei componenti dell'interfaccia utente. InMainActivity, è presente unObserverche osserva le parole LiveData del database e riceve una notifica quando cambiano.Repository:gestisce una o più origini dati.Repositoryespone i metodi per consentire alla ViewModel di interagire con il provider di dati sottostante. In questa app, il backend è un database Room.Room: è un wrapper intorno a un database SQLite e lo implementa. Room fa molto lavoro per te che prima dovevi fare da solo.- DAO: mappa le chiamate ai metodi alle query del database, in modo che quando il repository chiama un metodo come
getAlphabetizedWords(), Room possa eseguireSELECT * from word_table ORDER BY word ASC. Word: è la classe di entità che contiene una sola parola.
* Views e Activities (e Fragments) interagiscono con i dati solo tramite ViewModel. Pertanto, la provenienza dei dati non è importante.
Flusso di dati per gli aggiornamenti automatici della UI (UI reattiva)
L'aggiornamento automatico è possibile perché utilizziamo LiveData. In MainActivity, è presente un Observer che osserva le parole LiveData del database e riceve una notifica quando cambiano. Quando si verifica una modifica, viene eseguito il metodo onChange() dell'osservatore e viene aggiornato mWords in WordListAdapter.
I dati possono essere osservati perché sono LiveData. Ciò che viene osservato è LiveData<List<Word>> restituito dalla proprietà WordViewModel allWords.
WordViewModel nasconde tutto ciò che riguarda il backend dal livello UI. Fornisce metodi per accedere al data layer e restituisce LiveData in modo che MainActivity possa configurare la relazione di osservazione. Views e Activities (e Fragments) interagiscono con i dati solo tramite ViewModel. Pertanto, la provenienza dei dati non è importante.
In questo caso, i dati provengono da un Repository. ViewModel non ha bisogno di sapere con cosa interagisce il repository. Deve solo sapere come interagire con Repository, ovvero tramite i metodi esposti da Repository.
Il repository gestisce una o più origini dati. Nell'app WordListSample, il backend è un database Room. Room è un wrapper che implementa un database SQLite. Room fa molto lavoro per te che prima dovevi fare da solo. Ad esempio, Room fa tutto ciò che facevi con un corso SQLiteOpenHelper.
Il metodo DAO mappa le chiamate ai metodi alle query del database, in modo che quando il repository chiama un metodo come getAllWords(), Room possa eseguire SELECT * from word_table ORDER BY word ASC.
Poiché il risultato restituito dalla query è osservato LiveData, ogni volta che i dati in Room cambiano, viene eseguito il metodo onChanged() dell'interfaccia Observer e l'interfaccia utente viene aggiornata.
[Facoltativo] Scarica il codice della soluzione
Se non l'hai ancora fatto, puoi dare un'occhiata al codice della soluzione per il codelab. Puoi consultare il repository GitHub o scaricare il codice qui:
Decomprimi il file ZIP scaricato. Verrà estratta una cartella principale, android-room-with-a-view-kotlin, che contiene l'app completa.


