Android Kotlin Fundamentals 06.3: Use LiveData to control button states

Questo codelab fa parte del corso Android Kotlin Fundamentals. Per ottenere il massimo valore da questo corso, ti consigliamo di seguire le codelab in sequenza. Tutti i codelab del corso sono elencati nella pagina di destinazione dei codelab Android Kotlin Fundamentals.

Introduzione

Questo codelab riepiloga come utilizzare ViewModel e i fragment insieme per implementare la navigazione. Ricorda che l'obiettivo è inserire la logica di quando navigare in ViewModel, ma definire i percorsi nei fragment e nel file di navigazione. Per raggiungere questo obiettivo, utilizzi view model, fragment, LiveData e osservatori.

Il codelab si conclude mostrando un modo intelligente per monitorare gli stati dei pulsanti con un codice minimo, in modo che ogni pulsante sia abilitato e cliccabile solo quando è opportuno che l'utente lo tocchi.

Cosa devi già sapere

Devi avere familiarità con:

  • Creazione di un'interfaccia utente (UI) di base utilizzando un'attività, frammenti e visualizzazioni.
  • Spostarsi tra i fragment e utilizzare safeArgs per passare i dati tra i fragment.
  • Visualizza modelli, fabbriche di modelli, trasformazioni e LiveData e i relativi osservatori.
  • Come creare un database Room, creare un oggetto di accesso ai dati (DAO) e definire le entità.
  • Come utilizzare le coroutine per le interazioni con il database e altre attività di lunga durata.

Obiettivi didattici

  • Come aggiornare un record esistente sulla qualità del sonno nel database.
  • Come utilizzare LiveData per monitorare gli stati dei pulsanti.
  • Come visualizzare una snackbar in risposta a un evento.

In questo lab proverai a:

  • Estendi l'app TrackMySleepQuality per raccogliere una valutazione della qualità, aggiungerla al database e visualizzare il risultato.
  • Utilizza LiveData per attivare la visualizzazione di una snackbar.
  • Usa LiveData per attivare e disattivare i pulsanti.

In questo codelab, creerai la registrazione della qualità del sonno e l'interfaccia utente finale dell'app TrackMySleepQuality.

L'app ha due schermate, rappresentate da frammenti, come mostrato nella figura seguente.

La prima schermata, mostrata a sinistra, ha pulsanti per avviare e interrompere il monitoraggio. La schermata mostra tutti i dati sul sonno dell'utente. Il pulsante Cancella elimina definitivamente tutti i dati raccolti dall'app per l'utente.

La seconda schermata, mostrata a destra, serve per selezionare una valutazione della qualità del sonno. Nell'app, la valutazione è rappresentata numericamente. A scopo di sviluppo, l'app mostra sia le icone delle facce sia i relativi equivalenti numerici.

Il flusso dell'utente è il seguente:

  • L'utente apre l'app e visualizza la schermata di monitoraggio del sonno.
  • L'utente tocca il pulsante Avvia. Viene registrata l'ora di inizio e visualizzata. Il pulsante Avvia è disattivato e il pulsante Interrompi è attivato.
  • L'utente tocca il pulsante Interrompi. Viene registrata l'ora di fine e si apre la schermata della qualità del sonno.
  • L'utente seleziona un'icona della qualità del sonno. La schermata si chiude e la schermata di monitoraggio mostra l'ora di fine del sonno e la qualità del sonno. Il pulsante Interrompi è disattivato e il pulsante Avvia è attivato. L'app è pronta per un'altra notte.
  • Il pulsante Cancella è attivo ogni volta che sono presenti dati nel database. Quando l'utente tocca il pulsante Cancella, tutti i suoi dati vengono cancellati senza possibilità di recupero. Non viene visualizzato alcun messaggio di conferma.

Questa app utilizza un'architettura semplificata, come mostrato di seguito nel contesto dell'architettura completa. L'app utilizza solo i seguenti componenti:

  • Controller UI
  • Visualizza modello e LiveData
  • Un database delle stanze

Questo codelab presuppone che tu sappia come implementare la navigazione utilizzando i fragment e il file di navigazione. Per risparmiarti del lavoro, gran parte di questo codice viene fornita.

Passaggio 1: esamina il codice

  1. Per iniziare, continua con il tuo codice dalla fine dell'ultimo codelab o scarica il codice iniziale.
  2. Nel codice iniziale, esamina SleepQualityFragment. Questa classe gonfia il layout, recupera l'applicazione e restituisce binding.root.
  3. Apri navigation.xml nell'editor di progettazione. Vedi che esiste un percorso di navigazione da SleepTrackerFragment a SleepQualityFragment e viceversa da SleepQualityFragment a SleepTrackerFragment.



  4. Ispeziona il codice di navigation.xml. In particolare, cerca <argument> denominato sleepNightKey.

    Quando l'utente passa da SleepTrackerFragment a SleepQualityFragment,, l'app trasmette un sleepNightKey a SleepQualityFragment per la notte da aggiornare.

Passaggio 2: aggiungi la navigazione per il monitoraggio della qualità del sonno

Il grafico di navigazione include già i percorsi da SleepTrackerFragment a SleepQualityFragment e viceversa. Tuttavia, i gestori dei clic che implementano la navigazione da un frammento all'altro non sono ancora codificati. Aggiungi questo codice ora in ViewModel.

Nel gestore dei clic, imposti un LiveData che cambia quando vuoi che l'app si sposti in una destinazione diversa. Il frammento rispetta questo LiveData. Quando i dati cambiano, il fragment passa alla destinazione e comunica al modello di visualizzazione che l'operazione è terminata, il che reimposta la variabile di stato.

  1. Apri SleepTrackerViewModel. Devi aggiungere la navigazione in modo che quando l'utente tocca il pulsante Stop, l'app passi a SleepQualityFragment per raccogliere una valutazione della qualità.
  2. In SleepTrackerViewModel, crea un LiveData che cambia quando vuoi che l'app passi a SleepQualityFragment. Utilizza l'incapsulamento per esporre solo una versione ottenibile di LiveData a ViewModel.

    Puoi inserire questo codice ovunque nel livello superiore del corpo della classe.
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. Aggiungi una funzione doneNavigating() che reimposta la variabile che attiva la navigazione.
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. Nel gestore dei clic per il pulsante Arresta, onStopTracking(), attiva la navigazione fino a SleepQualityFragment. Imposta la variabile _navigateToSleepQuality alla fine della funzione come ultima cosa all'interno del blocco launch{}. Tieni presente che questa variabile è impostata su night. Quando questa variabile ha un valore, l'app passa alla SleepQualityFragment, trasmettendo la notte.
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment deve osservare _navigateToSleepQuality in modo che l'app sappia quando navigare. In SleepTrackerFragment, in onCreateView(), aggiungi un osservatore per navigateToSleepQuality(). Tieni presente che l'importazione è ambigua e devi importare androidx.lifecycle.Observer.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. All'interno del blocco dell'osservatore, vai avanti e passa l'ID della notte corrente, poi chiama doneNavigating(). Se l'importazione è ambigua, importa androidx.navigation.fragment.findNavController.
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. Crea ed esegui l'app. Tocca Avvia, quindi tocca Arresta, che ti porta alla schermata SleepQualityFragment. Per tornare indietro, usa il pulsante Indietro del sistema.

In questa attività, registri la qualità del sonno e torni al fragment del monitoraggio del sonno. Il display dovrebbe aggiornarsi automaticamente per mostrare all'utente il valore aggiornato. Devi creare un ViewModel e un ViewModelFactory e aggiornare SleepQualityFragment.

Passaggio 1: crea un ViewModel e un ViewModelFactory

  1. Nel pacchetto sleepquality, crea o apri SleepQualityViewModel.kt.
  2. Crea una classe SleepQualityViewModel che accetta un sleepNightKey e un database come argomenti. Come hai fatto per SleepTrackerViewModel, devi inserire database di fabbrica. Devi anche passare sleepNightKey dalla navigazione.
class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. All'interno della classe SleepQualityViewModel, definisci Job e uiScope ed esegui l'override di onCleared().
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Per tornare a SleepTrackerFragment utilizzando lo stesso pattern di prima, dichiara _navigateToSleepTracker. Implementa navigateToSleepTracker e doneNavigating().
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

val navigateToSleepTracker: LiveData<Boolean?>
   get() = _navigateToSleepTracker

fun doneNavigating() {
   _navigateToSleepTracker.value = null
}
  1. Crea un gestore di clic, onSetSleepQuality(), per tutte le immagini della qualità del sonno da utilizzare.

    Utilizza lo stesso pattern di coroutine del codelab precedente:
  • Avvia una coroutine in uiScope e passa al dispatcher I/O.
  • Ottieni tonight utilizzando sleepNightKey.
  • Imposta la qualità del sonno.
  • Aggiorna il database.
  • Attiva la navigazione.

Tieni presente che l'esempio di codice riportato di seguito esegue tutte le operazioni nel gestore dei clic, anziché estrarre l'operazione del database in un contesto diverso.

fun onSetSleepQuality(quality: Int) {
        uiScope.launch {
            // IO is a thread pool for running operations that access the disk, such as
            // our Room database.
            withContext(Dispatchers.IO) {
                val tonight = database.get(sleepNightKey) ?: return@withContext
                tonight.sleepQuality = quality
                database.update(tonight)
            }

            // Setting this state variable to true will alert the observer and trigger navigation.
            _navigateToSleepTracker.value = true
        }
    }
  1. Nel pacchetto sleepquality, crea o apri SleepQualityViewModelFactory.kt e aggiungi la classe SleepQualityViewModelFactory, come mostrato di seguito. Questa classe utilizza una versione dello stesso codice boilerplate che hai visto in precedenza. Controlla il codice prima di procedere.
class SleepQualityViewModelFactory(
       private val sleepNightKey: Long,
       private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
           return SleepQualityViewModel(sleepNightKey, dataSource) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Passaggio 2: aggiorna SleepQualityFragment

  1. Apri SleepQualityFragment.kt.
  2. In onCreateView(), dopo aver ottenuto application, devi ottenere arguments fornito con la navigazione. Questi argomenti si trovano in SleepQualityFragmentArgs. Devi estrarli dal bundle.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. Poi, prendi la linea dataSource.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Crea una factory, passando dataSource e sleepNightKey.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. Ottieni un riferimento per ViewModel.
val sleepQualityViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. Aggiungi ViewModel all'oggetto di binding. Se visualizzi un errore relativo all'oggetto di binding, ignoralo per il momento.
binding.sleepQualityViewModel = sleepQualityViewModel
  1. Aggiungi l'osservatore. Quando richiesto, importa androidx.lifecycle.Observer.
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
   if (it == true) { // Observed state is true.
       this.findNavController().navigate(
               SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
       sleepQualityViewModel.doneNavigating()
   }
})

Passaggio 3: aggiorna il file di layout ed esegui l'app

  1. Apri il file di layout fragment_sleep_quality.xml. Nel blocco <data>, aggiungi una variabile per SleepQualityViewModel.
 <data>
       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. Per ciascuna delle sei immagini sulla qualità del sonno, aggiungi un gestore di clic come quello riportato di seguito. Abbina la valutazione della qualità all'immagine.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
  1. Pulisci e ricompila il progetto. In questo modo dovrebbero essere risolti eventuali errori relativi all'oggetto di binding. In caso contrario, svuota la cache (File > Invalidate Caches / Restart) e ricompila l'app.

Complimenti! Hai appena creato un'app di database Room completa utilizzando le coroutine.

Ora la tua app funziona alla perfezione. L'utente può toccare Avvia e Interrompi tutte le volte che vuole. Quando l'utente tocca Interrompi, può inserire una qualità del sonno. Quando l'utente tocca Cancella, tutti i dati vengono cancellati in modo invisibile in background. Tuttavia, tutti i pulsanti sono sempre attivi e selezionabili, il che non danneggia l'app, ma consente agli utenti di creare notti di sonno incomplete.

In quest'ultima attività, imparerai a utilizzare le mappe di trasformazione per gestire la visibilità dei pulsanti in modo che gli utenti possano fare solo la scelta giusta. Puoi utilizzare un metodo simile per visualizzare un messaggio amichevole dopo che tutti i dati sono stati cancellati.

Passaggio 1: aggiorna gli stati dei pulsanti

L'idea è di impostare lo stato del pulsante in modo che all'inizio sia attivato solo il pulsante Avvia, ovvero sia cliccabile.

Dopo che l'utente tocca Avvia, il pulsante Interrompi viene attivato e Avvia no. Il pulsante Cancella è attivo solo se nel database sono presenti dati.

  1. Apri il file di layout fragment_sleep_tracker.xml.
  2. Aggiungi la proprietà android:enabled a ogni pulsante. La proprietà android:enabled è un valore booleano che indica se il pulsante è abilitato o meno. Un pulsante attivo può essere toccato, mentre un pulsante disattivato non può. Assegna alla proprietà il valore di una variabile di stato che definirai tra poco.

start_button:

android:enabled="@{sleepTrackerViewModel.startButtonVisible}"

stop_button:

android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"

clear_button:

android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
  1. Apri SleepTrackerViewModel e crea tre variabili corrispondenti. Assegna a ogni variabile una trasformazione che la testa.
  • Il pulsante Avvia deve essere attivato quando tonight è null.
  • Il pulsante Interrompi deve essere attivato quando tonight non è null.
  • Il pulsante Cancella deve essere attivato solo se nights e quindi il database contengono notti di sonno.
val startButtonVisible = Transformations.map(tonight) {
   it == null
}
val stopButtonVisible = Transformations.map(tonight) {
   it != null
}
val clearButtonVisible = Transformations.map(nights) {
   it?.isNotEmpty()
}
  1. Esegui l'app e prova i pulsanti.

Passaggio 2: utilizza uno snackbar per notificare l'utente

Dopo che l'utente ha cancellato il database, mostragli una conferma utilizzando il widget Snackbar. Uno snackbar fornisce un breve feedback su un'operazione tramite un messaggio nella parte inferiore dello schermo. Una snackbar scompare dopo un timeout, dopo un'interazione dell'utente in un altro punto dello schermo o dopo che l'utente la fa scorrere fuori dallo schermo.

La visualizzazione della snackbar è un'attività della UI e deve avvenire nel fragment. La decisione di mostrare la snackbar viene presa in ViewModel. Per configurare e attivare una snackbar quando i dati vengono cancellati, puoi utilizzare la stessa tecnica di attivazione della navigazione.

  1. In SleepTrackerViewModel, crea l'evento incapsulato.
private var _showSnackbarEvent = MutableLiveData<Boolean>()

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. Poi implementa doneShowingSnackbar().
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. In SleepTrackerFragment, in onCreateView(), aggiungi un osservatore:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. All'interno del blocco dell'observer, mostra la snackbar e reimposta immediatamente l'evento.
   if (it == true) { // Observed state is true.
       Snackbar.make(
               activity!!.findViewById(android.R.id.content),
               getString(R.string.cleared_message),
               Snackbar.LENGTH_SHORT // How long to display the message.
       ).show()
       sleepTrackerViewModel.doneShowingSnackbar()
   }
  1. In SleepTrackerViewModel, attiva l'evento nel metodo onClear(). Per farlo, imposta il valore dell'evento su true all'interno del blocco launch:
_showSnackbarEvent.value = true
  1. Crea ed esegui la tua app.

Progetto Android Studio: TrackMySleepQualityFinal

L'implementazione del monitoraggio della qualità del sonno in questa app è come suonare un brano musicale familiare in una nuova tonalità. Sebbene i dettagli cambino, il pattern di base di ciò che hai fatto nei codelab precedenti di questa lezione rimane invariato. Essere consapevoli di questi pattern rende la programmazione molto più veloce, perché puoi riutilizzare il codice delle app esistenti. Ecco alcuni dei pattern utilizzati finora in questo corso:

  • Crea un ViewModel e un ViewModelFactory e configura un'origine dati.
  • Attiva la navigazione. Per separare le problematiche, inserisci il gestore dei clic nel modello di visualizzazione e la navigazione nel fragmento.
  • Utilizza l'incapsulamento con LiveData per monitorare e rispondere alle modifiche dello stato.
  • Utilizza le trasformazioni con LiveData.
  • Crea un database singleton.
  • Configura le coroutine per le operazioni del database.

Attivazione della navigazione

Definisci i possibili percorsi di navigazione tra i frammenti in un file di navigazione. Esistono diversi modi per attivare la navigazione da un frammento all'altro. tra cui:

  • Definisci i gestori di onClick per attivare la navigazione verso un fragment di destinazione.
  • In alternativa, per attivare la navigazione da un frammento all'altro:
  • Definisci un valore LiveData da registrare se deve verificarsi la navigazione.
  • Collega un osservatore a questo valore LiveData.
  • Il codice modifica quindi questo valore ogni volta che la navigazione deve essere attivata o è completata.

Impostazione dell'attributo android:enabled

  • L'attributo android:enabled è definito in TextView ed ereditato da tutte le sottoclassi, inclusa Button.
  • L'attributo android:enabled determina se un View è abilitato o meno. Il significato di "attivato" varia in base alla sottoclasse. Ad esempio, un EditText non abilitato impedisce all'utente di modificare il testo contenuto, mentre un Button non abilitato impedisce all'utente di toccare il pulsante.
  • L'attributo enabled non è uguale all'attributo visibility.
  • Puoi utilizzare le mappe di trasformazione per impostare il valore dell'attributo enabled dei pulsanti in base allo stato di un altro oggetto o variabile.

Altri punti trattati in questo codelab:

  • Per attivare le notifiche per l'utente, puoi utilizzare la stessa tecnica che utilizzi per attivare la navigazione.
  • Puoi utilizzare un Snackbar per inviare una notifica all'utente.

Corso Udacity:

Documentazione per sviluppatori Android:

Questa sezione elenca i possibili compiti a casa per gli studenti che seguono questo codelab nell'ambito di un corso guidato da un insegnante. Spetta all'insegnante:

  • Assegna i compiti, se richiesto.
  • Comunica agli studenti come inviare i compiti.
  • Valuta i compiti a casa.

Gli insegnanti possono utilizzare questi suggerimenti nella misura che ritengono opportuna e sono liberi di assegnare qualsiasi altro compito a casa che ritengono appropriato.

Se stai seguendo questo codelab in autonomia, sentiti libero di utilizzare questi compiti per casa per mettere alla prova le tue conoscenze.

Rispondi a queste domande

Domanda 1

Un modo per consentire all'app di attivare la navigazione da un fragment all'altro è utilizzare un valore LiveData per indicare se attivare o meno la navigazione.

Quali sono i passaggi per utilizzare un valore LiveData, chiamato gotoBlueFragment, per attivare la navigazione dal frammento rosso a quello blu? Seleziona tutte le risposte pertinenti:

  • In ViewModel, definisci il valore LiveData gotoBlueFragment.
  • In RedFragment, osserva il valore gotoBlueFragment. Implementa il codice observe{} per passare a BlueFragment quando opportuno, quindi reimposta il valore di gotoBlueFragment per indicare che la navigazione è completata.
  • Assicurati che il codice imposti la variabile gotoBlueFragment sul valore che attiva la navigazione ogni volta che l'app deve passare da RedFragment a BlueFragment.
  • Assicurati che il codice definisca un gestore onClick per View su cui l'utente fa clic per passare a BlueFragment, dove il gestore onClick osserva il valore goToBlueFragment.

Domanda 2

Puoi modificare l'attivazione (cliccabile) o meno di un Button utilizzando LiveData. Come faresti in modo che la tua app modifichi il pulsante UpdateNumber in modo che:

  • Il pulsante è attivo se myNumber ha un valore maggiore di 5.
  • Il pulsante non è abilitato se myNumber è uguale o inferiore a 5.

Supponiamo che il layout che contiene il pulsante UpdateNumber includa la variabile <data> per NumbersViewModel, come mostrato qui:

<data>
   <variable
       name="NumbersViewModel"
       type="com.example.android.numbersapp.NumbersViewModel" />
</data>

Supponiamo che l'ID del pulsante nel file di layout sia il seguente:

android:id="@+id/update_number_button"

Cos'altro devi fare? Seleziona tutte le opzioni pertinenti.

  • Nella classe NumbersViewModel, definisci una variabile LiveData, myNumber, che rappresenta il numero. Definisci anche una variabile il cui valore viene impostato chiamando Transform.map() sulla variabile myNumber, che restituisce un valore booleano che indica se il numero è maggiore di 5.

    Nello specifico, in ViewModel, aggiungi il seguente codice:
val myNumber: LiveData<Int>

val enableUpdateNumberButton = Transformations.map(myNumber) {
   myNumber > 5
}
  • Nel layout XML, imposta l'attributo android:enabled di update_number_button button su NumberViewModel.enableUpdateNumbersButton.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
  • Nel Fragment che utilizza la classe NumbersViewModel, aggiungi un osservatore all'attributo enabled del pulsante.

    Nello specifico, in Fragment, aggiungi il seguente codice:
// Observer for the enabled attribute
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
   myNumber > 5
})
  • Nel file di layout, imposta l'attributo android:enabled di update_number_button button su "Observable".

Inizia la lezione successiva: 7.1 RecyclerView fundamentals

Per i link ad altri codelab di questo corso, consulta la pagina di destinazione dei codelab di Android Kotlin Fundamentals.