Lo scopo dei componenti architettura è fornire linee guida 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 di 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 di codice, errori grammaticali, parole non chiare e così via) via email con questo codelab, segnala il problema tramite il link Segnala un errore nell'angolo in basso a sinistra del codelab.
Prerequisiti
Avere familiarità con Kotlin, i concetti di progettazione orientati agli oggetti e i concetti fondamentali dello sviluppo di Android, in particolare:
RecyclerView
e adattatori- Database SQLite e linguaggio di query SQLite
- Coroutine di base (se non conosci le coroutine, puoi seguire la sezione Utilizzare Kotlin Coroutines nella tua app Android.)
È inoltre utile avere familiarità con i pattern architetturali del software che separano i dati dall'interfaccia utente, ad esempio MVP o MVC. Questo codelab implementa l'architettura definita nella Guida all'architettura delle app.
Questo codelab è incentrato sui componenti di architettura Android. Puoi copiare e incollare facilmente i concetti fuori tema e il codice.
Se non conosci Kotlin, qui trovi una versione di questo codelab nel linguaggio di programmazione Java.
Attività previste
In questo codelab, imparerai a progettare e creare un'app utilizzando la stanza dei componenti dell'architettura, i modelli di vista e LiveData e a creare un'app che:
- Implementa la nostra architettura consigliata utilizzando i componenti dell'architettura Android.
- Funziona con un database per recuperare e salvare i dati e precompila il database con alcune parole.
- Visualizza tutte le parole in una
RecyclerView
inMainActivity
. - Apre una seconda attività quando l'utente tocca il pulsante +. Quando l'utente inserisce una parola, questa viene aggiunta al database e all'elenco.
L'app è essenziale, ma sufficientemente complessa da poter essere utilizzata come modello su cui creare. Ecco un'anteprima:
Che cosa ti serve
- Android Studio 3.0 o versioni successive e informazioni sull'utilizzo. Assicurati che Android Studio sia aggiornato, nonché il tuo SDK e Gradle.
- Un dispositivo o 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 prevedono molti passaggi. L'aspetto più importante è creare un modello mentale di cosa sta accadendo, comprendendo in che modo i componenti si integrano tra loro e come passano i dati. Mentre lavori in questo codelab, non copiare e incolla il codice, ma prova a iniziare a sviluppare la comprensione interna.
Quali sono i componenti dell'architettura consigliati?
Per introdurre la terminologia, ecco una breve introduzione ai componenti di architettura e al modo in cui interagiscono. Tieni presente che questo codelab è incentrato su un sottoinsieme dei componenti, ovvero LiveData, ViewModel e Room. Ogni componente è spiegato più dettagliatamente quando lo utilizzi.
Questo diagramma mostra una forma di base dell'architettura:
Entità: classe annotata che descrive una tabella di database quando si lavora con Stanza.
Database SQLite:spazio di archiviazione del dispositivo. La libreria di persistenza della stanza virtuale crea e gestisce questo database per te.
DAO: oggetto di accesso ai dati. Una mappatura delle query SQL alle funzioni. Quando utilizzi un DAO, chiami i metodi e la camera viene occupata dal resto.
Database delle camere: semplifica il lavoro del database e funge da punto di accesso al database SQLite sottostante (nascosta SQLiteOpenHelper)
). Il database delle stanze virtuali utilizza il DAO per inviare query al database SQLite.
Repository: una classe creata da te utilizzata principalmente per gestire più origini dati.
ViewModel: funge da centro di comunicazione tra il repository (dati) e l'interfaccia utente. Non è più necessario preoccuparsi dell'origine dei dati. Le istanze Viewmodel sopravvivono alla creazione di attività/frammenti.
LiveData: una classe del titolare dei dati che può essere osservata. Conserva/memorizza sempre nella cache l'ultima versione dei dati e avvisa gli osservatori quando i dati vengono modificati. LiveData
è consapevole del ciclo di vita. I componenti dell'interfaccia utente semplicemente osservano i dati pertinenti e non interrompono o riprendono l'osservazione. LiveData gestisce tutto questo automaticamente perché è a conoscenza dei cambiamenti pertinenti nello stato del ciclo di vita durante l'osservazione.
Panoramica dell'architettura di RoomWordSample
Il seguente diagramma mostra tutte le parti dell'app. Ciascuno dei riquadri che lo contengono (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 un nome all'app RoomWordSample e fai clic su Fine.
Successivamente, devi aggiungere le librerie dei componenti ai tuoi file Gradle.
- In Android Studio, fai clic sulla scheda Progetti ed espandi la cartella Script di Gradle.
Apri build.gradle
(Modulo: app).
- Applica il plug-in per l'annotazione
kapt
di Kotlin aggiungendolo dopo gli altri plug-in definiti nella parte superiore del filebuild.gradle
(Modulo: app).
apply plugin: 'kotlin-kapt'
- Aggiungi il blocco
packagingOptions
all'interno del bloccoandroid
per escludere il modulo Funzioni atomiche dal pacchetto e impedire gli 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 di questa app sono parole e sarà necessaria una semplice tabella in cui inserire questi valori:
La stanza virtuale consente di creare tabelle tramite un'entità. Iniziamo subito.
- Crea un nuovo file di classe Kotlin denominato
Word
contenente la classe datiWord
.
Questa classe descrive l'entità (che rappresenta la tabella SQLite) per le tue parole. Ogni proprietà della classe rappresenta una colonna nella tabella. Infine, la stanza virtuale utilizzerà queste proprietà per creare la tabella e creare un'istanza di oggetti dalle righe del database.
Ecco il codice:
data class Word(val word: String)
Per rendere la classe Word
significativa per un database di una stanza, devi annotarla. Le annotazioni identificano in che modo ciascuna parte di questa classe è correlata a una voce nel database. La stanza virtuale utilizza queste informazioni per generare il codice.
Se digiti personalmente le annotazioni, invece di incollarle, Android Studio importerà automaticamente le classi di annotazioni.
- Aggiorna la classe
Word
con le annotazioni come mostrato in questo codice:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
Vediamo cosa fanno le annotazioni:
@Entity(tableName =
"word_table"
)
Ogni classe@Entity
rappresenta una tabella SQLite. Annota la dichiarazione del corso per indicare che si tratta di un'entità. Puoi specificare il nome della tabella se vuoi che sia diverso dal nome della classe. Denomina la tabella "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. Tale colonna assegna alla colonna la parola "parola".- Ogni proprietà archiviata nel database deve avere visibilità pubblica, che è l'impostazione predefinita di Kotlin.
Puoi trovare un elenco completo delle annotazioni nel riepilogo del pacchetto camera.
Che cos'è il DAO?
Nell'oggetto di accesso dati (DAO), puoi specificare le query SQL e associarle alle chiamate di metodo. Il compilatore controlla SQL e genera query da annotazioni di convenienza per le query comuni, come @Insert
. La stanza virtuale utilizza il DAO per creare un'API pulita per il codice.
Il DAO deve essere un'interfaccia o una classe astratta.
Per impostazione predefinita, tutte le query devono essere eseguite in un thread separato.
La stanza supporta la funzionalità coroutine, che consente di annotare le query con il modificatore suspend
e poi di essere chiamato da una coroutina o da un'altra funzione di sospensione.
Implementa il DAO
Scriviamo un DAO che fornisca query per:
- Ordine di tutte le parole in ordine alfabetico
- Inserimento di una parola
- Eliminazione di tutte le parole
- Crea un nuovo file di corso Kotlin denominato
WordDao
. - Copia e incolla il seguente codice in
WordDao
e correggi le importazioni se necessario per renderlo compilato.
@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()
}
Esaminiamole:
WordDao
è un'interfaccia; i DAO devono essere interfacce o classi astratte.- L'annotazione
@Dao
lo identifica come classe DAO per la stanza virtuale. 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 SQL. Esistono anche le annotazioni@Delete
e@Update
per eliminare e aggiornare le righe, ma non le usi in questa app. onConflict = OnConflictStrategy.IGNORE
: la strategia on conflitto selezionata ignora una nuova parola se è esattamente la stessa di una parola già presente nell'elenco. Per saperne di più sulle strategie in conflitto disponibili, consulta la documentazione.suspend fun deleteAll()
: dichiara una funzione di sospensione per eliminare tutte le parole.- Non è disponibile alcuna annotazione relativa alla comodità dell'eliminazione di più entità, quindi è presente un'annotazione generica
@Query
. @Query
("DELETE FROM word_table")
:@Query
richiede che tu fornisca 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 far restituire unList
diWords
.@Query(
"SELECT * from word_table ORDER BY word ASC"
)
: query che restituisce un elenco di parole ordinate in ordine crescente.
Quando i dati cambiano di solito è necessario eseguire una determinata azione, ad esempio visualizzare i dati aggiornati nell'interfaccia utente. Ciò significa che devi osservare i dati per poter reagire quando cambiano.
A seconda della modalità di archiviazione dei dati, può risultare complicato. Osservare le modifiche apportate ai dati in più componenti della tua app può creare percorsi di dipendenza espliciti e rigidi tra i componenti. Ciò rende difficile eseguire test e debug.
LiveData
, una classe della libreria del ciclo di vita per l'osservazione dei dati, risolve questo problema. Utilizza un valore restituito di tipo LiveData
nella descrizione del metodo e la stanza virtuale genera tutto il codice necessario per aggiornare LiveData
quando il database viene aggiornato.
In WordDao
, modifica la firma del metodo getAlphabetizedWords()
in modo che venga restituito il carattere List<Word>
con 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 delle stanze?
- La stanza virtuale è un livello database sopra un database SQLite.
- La stanza virtuale si occupa delle attività ordinarie che hai utilizzato per la gestione con una
SQLiteOpenHelper
. - La stanza virtuale utilizza il DAO per inviare query al database.
- Per impostazione predefinita, per evitare scarse prestazioni dell'interfaccia utente, la stanza virtuale non consente di emettere query nel thread principale. Quando le query stanza virtuale restituiscono
LiveData
, le query vengono eseguite automaticamente in modo asincrono in un thread di sfondo. - La stanza virtuale fornisce controlli in tempo reale delle istruzioni SQLite.
Implementare il database della stanza virtuale
La classe del database Room deve essere astratta ed estendere RoomDatabase
. Di solito hai bisogno di una sola istanza di un database delle stanze per l'intera app.
Facciamone uno.
- Crea un file del corso Kotlin denominato
WordRoomDatabase
e aggiungi il codice seguente:
// 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 del database per la camera deve essere
abstract
ed estendereRoomDatabase
- Annoti la classe come database della stanza virtuale con
@Database
e 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 dei database non rientrano nell'ambito di questo codelab, quindi impostiamoexportSchema
su false per evitare avvisi di build. In un'app reale, dovresti prendere in considerazione l'impostazione di una directory per l'utilizzo della stanza virtuale per esportare lo schema, in modo da poter controllare lo schema corrente nel sistema di controllo della versione. - Il database espone i DAO attraverso un metodo astratto "getter" per ogni @Dao.
- Abbiamo definito un Singleton,
WordRoomDatabase,
per evitare che più istanze del database vengano aperte contemporaneamente. getDatabase
restituisce il singleton. Verrà creato il database la prima volta che viene eseguito l'accesso, utilizzando il generatore di database di Room per creare un oggettoRoomDatabase
nel contesto dell'applicazione dalla classeWordRoomDatabase
e il nome"word_database"
.
Cos'è un repository?
Una classe di repository astraggono l'accesso a più origini dati. Il repository non fa parte delle librerie di componenti dell'architettura, ma è una best practice consigliata per la separazione e l'architettura del codice. 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 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 del corso Kotlin denominato WordRepository
e incolla il seguente codice al suo interno:
// 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)
}
}
Concetti principali:
- Il DAO viene passato nel costruttore del repository anziché nell'intero database. Questo perché ha bisogno solo di accedere al DAO, poiché DAO contiene tutti i metodi di lettura/scrittura per il database. Non è necessario esporre l'intero database al repository.
- L'elenco di parole è una proprietà pubblica. È stato inizializzato recuperando l'elenco di parole di
LiveData
dalla stanza virtuale. Possiamo farlo a causa di come abbiamo definito il metodogetAlphabetizedWords
per restituireLiveData
nel passaggio "La classe LiveData". La stanza virtuale esegue tutte le query in un thread separato. Successivamente,LiveData
riceverà una notifica all'osservatore sul thread principale quando i dati sono stati modificati. - Il modificatore
suspend
indica al compilatore che deve essere chiamato da una coroutine o da un'altra funzione di sospensione.
Che cos'è un Viewmodel?
Il ruolo di ViewModel
è fornire dati all'interfaccia utente e sopravvivere alle modifiche alla configurazione. Un ViewModel
funge da centro di comunicazione tra il repository e l'interfaccia utente. Puoi anche utilizzare un ViewModel
per condividere dati tra i frammenti. Viewmodel fa parte della libreria del ciclo di vita.
Per una guida introduttiva a questo argomento, consulta il post del blog Viewmodels: A Simple Example o ViewModel Overview
.
Perché usare un modello di modello?
Un ViewModel
conserva i dati dell'interfaccia utente della tua app in un modo sensibile al ciclo di vita che sopravviva alle modifiche alla configurazione. Separare i dati dell'interfaccia utente dell'app dalle classi Activity
e Fragment
ti consente di seguire meglio il singolo principio di responsabilità: le tue attività e i tuoi frammenti sono responsabili del rendering dei dati sullo schermo, mentre il tuo ViewModel
può occuparsi della conservazione e dell'elaborazione di 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 inserire un osservatore nei dati (anziché eseguire sondaggi per le modifiche) e aggiornare l'interfaccia utente
solo quando i dati cambiano effettivamente. - Repository e UI sono completamente separati da
ViewModel
. - Non ci sono chiamate al database da
ViewModel
(gestite in tutto il repository), rendendo il codice più testabile.
viewModelScope
In Kotlin, tutte le coroutine si trovano all'interno di una CoroutineScope
. Un ambito controlla la durata delle coroutine attraverso il proprio lavoro. Quando annulli il job di un ambito, vengono annullate tutte le coroutine avviate in tale ambito.
La libreria di AndroidX lifecycle-viewmodel-ktx
aggiunge una viewModelScope
come funzione di estensione della classe ViewModel
, consentendoti di lavorare con gli ambiti.
Per scoprire di più su come lavorare con le coroutine nel ViewView, consulta il passaggio 5 del codelab Utilizzo di Kotlin Coroutines nella tua app Android o il post del blog di Easy Coroutines in Android: viewmodelScope.
Implementare il modello del modello di vista
Crea un file del corso 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 quanto segue:
- È stata creata una classe denominata
WordViewModel
che riceveApplication
come parametro ed estendeAndroidViewModel
. - Aggiunta una variabile membro privato per contenere un riferimento al repository.
- Una variabile membro pubblica
LiveData
è stata aggiunta alla cache dell'elenco di parole. - Creato blocco
init
che riceve un riferimento aWordDao
daWordRoomDatabase
. - Nel blocco
init
, è stato creatoWordRepository
in base aWordRoomDatabase
. - Nel blocco
init
, ha inizializzato il valore di LiveData diallWords
utilizzando il repository. - Creato il metodo
insert()
di un wrapper che chiama il metodoinsert()
di Repository. In questo modo, l'implementazione diinsert()
viene incapsulata dall'interfaccia utente. Non vogliamo bloccare l'inserimento del thread principale, quindi stiamo avviando una nuova coroutine e chiamiamo l'inserimento del repository, che è una funzione di sospensione. Come accennato in precedenza, i modelli di vista hanno un ambito coroutine basato sul ciclo di vita chiamatoviewModelScope
, che utilizziamo qui.
Successivamente, devi aggiungere il layout XML dell'elenco e degli elementi.
Questo codelab presuppone che tu abbia familiarità con la creazione di layout in XML, quindi ti forniamo soltanto il codice.
Per impostare il materiale dell'applicazione, imposta l'elemento principale AppTheme
su Theme.MaterialComponents.Light.DarkActionBar
. Aggiungi uno stile per gli elementi dell'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>
Aggiungi 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 un RecyclerView
e aggiungi un pulsante di azione mobile (FAB). Ora il layout dovrebbe 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 tuo FAB deve corrispondere all'azione disponibile, pertanto vogliamo sostituire l'icona con un simbolo '+'.
Innanzitutto, dobbiamo aggiungere un nuovo asset vettoriali:
- Seleziona File > New > Vector Asset.
- Fai clic sull'icona del robot Android nel campo Clip Art: .
- Cerca "add" e seleziona l'asset '+'. Fai clic su OK.
- Successivamente, fai clic su Avanti.
- Verifica il percorso dell'icona come
main > drawable
e fai clic su Fine per aggiungere l'asset. - Sempre nel
layout/activity_main.xml
, aggiorna il FAB in modo da includere il nuovo disegno:
<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"/>
Stai per visualizzare i dati in un elemento RecyclerView
, che è un po' più utile della semplice trasmissione 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 nella cache i dati. Nell'attività successiva aggiungi il codice che aggiorna automaticamente i dati.
Crea un file del corso 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 esistono dati nel database. Puoi aggiungere i dati in due modi: aggiungendo alcuni dati all'apertura del database e utilizzando un Activity
per l'aggiunta di parole.
Per eliminare tutti i contenuti e ricompilare il database ogni volta che l'app viene avviata, crea una RoomDatabase.Callback
e sostituisci onOpen()
. Poiché non puoi eseguire operazioni del database delle stanze virtuali nel thread dell'interfaccia utente, onOpen()
avvia una coroutine sull'ordine di invio dell'ordine di inserzione.
Per lanciare una coroutine abbiamo bisogno di un CoroutineScope
. Aggiorna il metodo getDatabase
della classe WordRoomDatabase
per ottenere anche un ambito coroutine come parametro:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
Aggiorna l'inizializzazionere 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 un CoroutineScope
come parametro costruttore. Quindi, eseguiamo l'override del metodo onOpen
per completare il database.
Ecco il codice per la creazione del 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 build 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 le seguenti 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 per le dimensioni:
- Fai clic sul modulo app nella finestra Progetto.
- Seleziona File > Nuovo > file di risorse Android
- Da Qualificatori disponibili, seleziona Dimensione
- Imposta il nome file: dimens
Aggiungi le seguenti risorse di dimensioni in values/dimens.xml
:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
Crea un nuovo Activity
vuoto con il modello Attività vuoto:
- Seleziona File > Nuova > attività > attività vuota
- Inserisci
NewWordActivity
per il nome dell'attività. - Verifica che la nuova attività sia stata aggiunta al file manifest Android.
<activity android:name=".NewWordActivity"></activity>
Aggiorna il file activity_new_word.xml
nella cartella di 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>
Aggiornare il codice per l'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 collegare l'interfaccia utente al database salvando le nuove parole inserite dall'utente e visualizzando i contenuti correnti del database di parole in RecyclerView
.
Per visualizzare i contenuti correnti del database, aggiungi un osservatore che osserva 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: WordViewModel
Utilizza ViewModelProvider
per associare ViewModel
al tuo Activity
.
Quando Activity
inizierà per la prima volta, ViewModelProviders
creerà il ViewModel
. Quando l'attività viene eliminata, ad esempio a causa di una modifica alla configurazione, l'elemento ViewModel
persiste. Quando l'attività viene ricreata, ViewModelProviders
restituisce l'elemento ViewModel
esistente. Per saperne di più, vedi ViewModel
.
In onCreate()
sotto il blocco di codice RecyclerView
, ricevi una ViewModel
dalla ViewModelProvider
:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
Sempre in onCreate()
, aggiungi un osservatore per la proprietà LiveData
di Word da WordViewModel
.
Il metodo onChanged()
(il metodo predefinito per la nostra Lambda) si attiva 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 il NewWordActivity
quando tocchi il FAB e, quando torniamo nel MainActivity
, inserire la nuova parola nel database o mostrare un Toast
. A questo scopo, iniziamo definendo un codice di richiesta:
private val newWordActivityRequestCode = 1
In MainActivity
, aggiungi il codice onActivityResult()
per NewWordActivity
.
Se l'attività restituisce RESULT_OK
, inserisci la parola restituita nel database richiamando 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,
avvia NewWordActivity
quando l'utente tocca il FAB. In onCreate
MainActivity
, 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 completato avrà il seguente 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 avvia la tua app. Quando aggiungi una parola al database in NewWordActivity
, l'interfaccia utente si aggiorna automaticamente.
Ora che hai creato un'app funzionante, ricapitoliamo ciò che hai creato. Ecco di nuovo la struttura dell'app:
I componenti dell'app sono:
MainActivity
: visualizza le parole in un elenco utilizzandoRecyclerView
eWordListAdapter
. InMainActivity
, è presente unObserver
che osserva le parole LiveData dal database e viene notificato quando cambiano.NewWordActivity:
aggiunge una nuova parola all'elenco.WordViewModel
: fornisce metodi di accesso al livello dati e restituisce LiveData in modo che MainActivity possa configurare la relazione con l'osservatore.*LiveData<List<Word>>
: consente gli aggiornamenti automatici nei componenti dell'interfaccia utente. NelMainActivity
, c'è unObserver
che osserva le parole LiveData dal database e viene notificato quando cambiano.Repository:
gestisce una o più origini dati.Repository
espone metodi per consentire a ViewModel di interagire con il fornitore di dati sottostante. In questa app, il backend è un database per le stanze.Room
: è un wrapper attorno a un implementazione di un database SQLite. La stanza lavora per te, un tempo, in passato.- DAO: mappa le chiamate di metodo alle query del database, in modo che quando il repository chiama un metodo come
getAlphabetizedWords()
, la stanza virtuale può eseguireSELECT * from word_table ORDER BY word ASC
. Word
: è la classe dell'entità che contiene una singola parola.
* Views
, Activities
(e Fragments
) interagiscono solo con i dati tramite ViewModel
. Pertanto, non importa da dove provengono i dati.
Flusso di dati per aggiornamenti automatici dell'interfaccia utente (interfaccia utente reattiva)
L'aggiornamento automatico è possibile perché stiamo utilizzando LiveData. Nel MainActivity
, c'è un Observer
che osserva le parole LiveData dal database e viene notificato quando cambiano. Quando c'è una modifica, viene eseguito il metodo onChange()
dell'osservatore e viene aggiornato mWords
in WordListAdapter
.
Puoi osservare i dati perché sono LiveData
. E ciò che viene osservato è LiveData<List<Word>>
che viene restituito dalla proprietà WordViewModel
allWords
.
WordViewModel
nasconde tutto il backend dal livello dell'interfaccia utente. Fornisce i metodi per accedere al livello dati e restituisce LiveData
in modo che MainActivity
possa configurare la relazione con l'osservatore. Views
, Activities
(e Fragments
) interagiscono solo con i dati attraverso ViewModel
. Pertanto, non importa da dove provengono i dati.
In questo caso, i dati provengono da un elemento Repository
. ViewModel
non ha bisogno di sapere con cosa interagisce il repository. Deve solo sapere come interagire con Repository
, attraverso i metodi esposti all'Repository
.
Il repository gestisce una o più origini dati. Nell'app WordListSample
, il backend è un database per le stanze. La camera è un wrapper attorno a un implementazione di un database SQLite. La stanza lavora per te, un tempo, in passato. Ad esempio, la stanza virtuale esegue tutte le operazioni che eseguivi con un corso SQLiteOpenHelper
.
Il metodo DAO mappa le chiamate del database alle query in modo che, quando il repository chiama un metodo come getAllWords()
, la stanza virtuale può eseguire SELECT * from word_table ORDER BY word ASC
.
Poiché il risultato restituito dalla query viene rilevato LiveData
, ogni volta che i dati in Room cambiano, viene eseguito il metodo onChanged()
dell'interfaccia di Observer
e l'interfaccia utente viene aggiornata.
[Facoltativo] Scarica il codice della soluzione
Se non lo hai già fatto, dai un'occhiata al codice della soluzione per il codelab. Puoi consultare il repository di GitHub o scaricare il codice qui:
Apri il file ZIP scaricato. Decomprimi la cartella principale android-room-with-a-view-kotlin
, che contiene l'app completa.