Android Kotlin Fundamentals 06.2: Coroutine e Room

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

Una delle priorità principali per creare un'esperienza utente impeccabile per la tua app è assicurarsi che l'UI sia sempre reattiva e funzioni senza problemi. Un modo per migliorare le prestazioni della UI è spostare in background le attività a esecuzione prolungata, come le operazioni sui database.

In questo codelab, implementerai la parte rivolta agli utenti dell'app TrackMySleepQuality, utilizzando le coroutine Kotlin per eseguire operazioni sul database al di fuori del thread principale.

Cosa devi già sapere

Devi avere familiarità con:

  • Creazione di un'interfaccia utente (UI) di base utilizzando un'attività, frammenti, visualizzazioni e gestori di clic.
  • Spostarsi tra i fragment e utilizzare safeArgs per passare dati semplici tra i fragment.
  • Visualizza modelli, fabbriche di modelli, trasformazioni e LiveData.
  • Come creare un database Room, creare un DAO e definire le entità.
  • È utile se hai familiarità con i concetti di threading e multiprocessing.

Obiettivi didattici

  • Come funzionano i thread in Android.
  • Come utilizzare le coroutine Kotlin per spostare le operazioni del database dal thread principale.
  • Come visualizzare i dati formattati in un TextView.

In questo lab proverai a:

  • Estendi l'app TrackMySleepQuality per raccogliere, archiviare e visualizzare i dati nel database e dal database.
  • Utilizza le coroutine per eseguire operazioni di database a lunga esecuzione in background.
  • Utilizza LiveData per attivare la navigazione e la visualizzazione di una snackbar.
  • Usa LiveData per attivare e disattivare i pulsanti.

In questo codelab, creerai il view model, le coroutine e le sezioni di visualizzazione dei dati 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

In questa attività, utilizzerai un TextView per visualizzare i dati di monitoraggio del sonno formattati. (Questa non è l'interfaccia finale. In un altro codelab imparerai un modo migliore.)

Puoi continuare con l'app TrackMySleepQuality che hai creato nel codelab precedente o scaricare l'app iniziale per questo codelab.

Passaggio 1: scarica ed esegui l'app iniziale

  1. Scarica l'app TrackMySleepQuality-Coroutines-Starter da GitHub.
  2. Crea ed esegui l'app. L'app mostra l'interfaccia utente per il fragment SleepTrackerFragment, ma nessun dato. I pulsanti non rispondono ai tocchi.

Passaggio 2: esamina il codice

Il codice iniziale di questo codelab è lo stesso del codice della soluzione del codelab 6.1 Crea un database Room.

  1. Apri res/layout/activity_main.xml. Questo layout contiene il fragmento nav_host_fragment. Nota anche il tag <merge>.

    Il tag merge può essere utilizzato per eliminare i layout ridondanti durante l'inclusione dei layout ed è consigliabile utilizzarlo. Un esempio di layout ridondante è ConstraintLayout > LinearLayout > TextView, in cui il sistema potrebbe essere in grado di eliminare LinearLayout. Questo tipo di ottimizzazione può semplificare la gerarchia delle visualizzazioni e migliorare il rendimento dell'app.
  2. Nella cartella navigation, apri navigation.xml. Puoi visualizzare due frammenti e le azioni di navigazione che li collegano.
  3. Nella cartella layout, fai doppio clic sul frammento del monitoraggio del sonno per visualizzarne il layout XML. Tieni presente quanto segue:
  • I dati del layout sono racchiusi in un elemento <layout> per consentire l'associazione di dati.
  • ConstraintLayout e le altre visualizzazioni sono disposte all'interno dell'elemento <layout>.
  • Il file ha un tag segnaposto <data>.

L'app iniziale fornisce anche dimensioni, colori e stile per la UI. L'app contiene un database Room, un DAO e un'entità SleepNight. Se non hai completato il codelab precedente, assicurati di esplorare questi aspetti del codice in autonomia.

Ora che hai un database e una UI, devi raccogliere i dati, aggiungerli al database e visualizzarli. Tutto questo lavoro viene svolto nel modello di visualizzazione. Il modello di visualizzazione del tracker del sonno gestirà i clic sui pulsanti, interagirà con il database tramite il DAO e fornirà i dati alla UI tramite LiveData. Tutte le operazioni del database dovranno essere eseguite lontano dal thread UI principale e lo farai utilizzando le coroutine.

Passaggio 1: aggiungi SleepTrackerViewModel

  1. Nel pacchetto sleeptracker, apri SleepTrackerViewModel.kt.
  2. Esamina la classe SleepTrackerViewModel, fornita nell'app iniziale e mostrata anche di seguito. Tieni presente che la classe estende AndroidViewModel(). Questa classe è uguale a ViewModel, ma accetta il contesto dell'applicazione come parametro e lo rende disponibile come proprietà. Ti servirà in un secondo momento.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

Passaggio 2: aggiungi SleepTrackerViewModelFactory

  1. Nel pacchetto sleeptracker, apri SleepTrackerViewModelFactory.kt.
  2. Esamina il codice fornito per la fabbrica, mostrato di seguito:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Tieni presente quanto segue:

  • Il SleepTrackerViewModelFactory fornito accetta lo stesso argomento di ViewModel ed estende ViewModelProvider.Factory.
  • All'interno della fabbrica, il codice esegue l'override di create(), che accetta qualsiasi tipo di classe come argomento e restituisce un ViewModel.
  • Nel corpo di create(), il codice controlla che sia disponibile una classe SleepTrackerViewModel e, in caso affermativo, ne restituisce un'istanza. In caso contrario, il codice genera un'eccezione.

Passaggio 3: aggiorna SleepTrackerFragment

  1. In SleepTrackerFragment, ottieni un riferimento al contesto dell'applicazione. Inserisci il riferimento in onCreateView(), sotto binding. Per passare al provider di factory del view model, devi fare riferimento all'app a cui è collegato questo fragment.

    La funzione Kotlin requireNotNull genera un'eccezione IllegalArgumentException se il valore è null.
val application = requireNotNull(this.activity).application
  1. Devi fare riferimento all'origine dati tramite un riferimento al DAO. In onCreateView(), prima di return, definisci un dataSource. Per ottenere un riferimento al DAO del database, utilizza SleepDatabase.getInstance(application).sleepDatabaseDao.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. In onCreateView(), prima di return, crea un'istanza di viewModelFactory. Devi superare il test dataSource e il test application.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. Ora che hai una factory, ottieni un riferimento a SleepTrackerViewModel. Il parametro SleepTrackerViewModel::class.java fa riferimento alla classe Java di runtime di questo oggetto.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. Il codice finale dovrebbe avere questo aspetto:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

Ecco il metodo onCreateView() finora:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

Passaggio 4: aggiungi l'associazione di dati per il modello di visualizzazione

Con il ViewModel di base in posizione, devi completare la configurazione del data binding nel SleepTrackerFragment per connettere il ViewModel all'interfaccia utente.


Nel file di layout fragment_sleep_tracker.xml:

  1. All'interno del blocco <data>, crea un <variable> che faccia riferimento alla classe SleepTrackerViewModel.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

In SleepTrackerFragment:

  1. Imposta l'attività corrente come proprietario del ciclo di vita del binding. Aggiungi questo codice all'interno del metodo onCreateView(), prima dell'istruzione return:
binding.setLifecycleOwner(this)
  1. Assegna la variabile di binding sleepTrackerViewModel a sleepTrackerViewModel. Inserisci questo codice all'interno di onCreateView(), sotto il codice che crea SleepTrackerViewModel:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. Probabilmente visualizzerai un errore perché devi ricreare l'oggetto di binding. Pulisci e ricompila il progetto per eliminare l'errore.
  2. Infine, come sempre, assicurati che la build e l'esecuzione del codice avvengano senza errori.

In Kotlin, le coroutine sono il modo per gestire le attività a lunga esecuzione in modo elegante ed efficiente. Le coroutine Kotlin ti consentono di convertire il codice basato su callback in codice sequenziale. Il codice scritto in sequenza è in genere più facile da leggere e può persino utilizzare funzionalità del linguaggio come le eccezioni. Alla fine, le coroutine e i callback fanno la stessa cosa: attendono che un risultato sia disponibile da un'attività a esecuzione prolungata e continuano l'esecuzione.

Le coroutine hanno le seguenti proprietà:

  • Le coroutine sono asincrone e non bloccanti.
  • Le coroutine utilizzano le funzioni suspend per rendere sequenziale il codice asincrono.

Le coroutine sono asincrone.

Una coroutine viene eseguita indipendentemente dai passaggi di esecuzione principali del programma. Potrebbe essere in parallelo o su un processore separato. Potrebbe anche succedere che, mentre il resto dell'app è in attesa di input, tu esegua un po' di elaborazione. Uno degli aspetti importanti di async è che non puoi aspettarti che il risultato sia disponibile finché non lo attendi esplicitamente.

Ad esempio, supponiamo che tu abbia una domanda che richiede una ricerca e chiedi a un collega di trovare la risposta. Vanno avanti e ci lavorano, come se stessero lavorando "in modo asincrono" e "su un thread separato". Puoi continuare a svolgere altre attività che non dipendono dalla risposta, finché il tuo collega non torna e ti comunica la risposta.

Le coroutine non sono bloccanti.

Non bloccante significa che una coroutine non blocca il thread principale o dell'interfaccia utente. In questo modo, con le coroutine, gli utenti hanno sempre l'esperienza più fluida possibile, perché l'interazione con la UI ha sempre la priorità.

Le coroutine utilizzano le funzioni di sospensione per rendere sequenziale il codice asincrono.

La parola chiave suspend è il modo in cui Kotlin contrassegna una funzione o un tipo di funzione come disponibile per le coroutine. Quando una coroutine chiama una funzione contrassegnata con suspend, anziché bloccarsi fino a quando la funzione non restituisce un valore come una normale chiamata di funzione, la coroutine sospende l'esecuzione fino a quando il risultato non è pronto. La coroutine riprende quindi da dove era stata interrotta, con il risultato.

Mentre la coroutine è sospesa e in attesa di un risultato, sblocca il thread su cui è in esecuzione. In questo modo, possono essere eseguite altre funzioni o coroutine.

La parola chiave suspend non specifica il thread su cui viene eseguito il codice. Una funzione di sospensione può essere eseguita su un thread in background o sul thread principale.

Per utilizzare le coroutine in Kotlin, sono necessarie tre cose:

  • Un lavoro
  • Un dispatcher
  • Un ambito

Job: in sostanza, un job è qualsiasi cosa possa essere annullata. Ogni coroutine ha un job e puoi utilizzare il job per annullare la coroutine. I job possono essere organizzati in gerarchie principale-secondario. L'annullamento di un job padre annulla immediatamente tutti i job secondari, il che è molto più comodo dell'annullamento manuale di ogni coroutine.

Dispatcher : il dispatcher invia le coroutine da eseguire su vari thread. Ad esempio, Dispatcher.Main esegue le attività sul thread principale, mentre Dispatcher.IO scarica le attività di I/O di blocco in un pool condiviso di thread.

Ambito : l'ambito di una coroutine definisce il contesto in cui viene eseguita. Un ambito combina informazioni sul job e sul dispatcher di una coroutine. Gli ambiti tengono traccia delle coroutine. Quando avvii una coroutine, questa si trova "in un ambito", il che significa che hai indicato quale ambito terrà traccia della coroutine.

Vuoi che l'utente possa interagire con i dati sul sonno nei seguenti modi:

  • Quando l'utente tocca il pulsante Inizia, l'app crea una nuova notte di sonno e la memorizza nel database.
  • Quando l'utente tocca il pulsante Interrompi, l'app aggiorna la notte con un orario di fine.
  • Quando l'utente tocca il pulsante Cancella, l'app cancella i dati nel database.

Queste operazioni sul database possono richiedere molto tempo, quindi devono essere eseguite su un thread separato.

Passaggio 1: configura le coroutine per le operazioni del database

Quando viene toccato il pulsante Avvia nell'app Monitoraggio del sonno, vuoi chiamare una funzione in SleepTrackerViewModel per creare una nuova istanza di SleepNight e memorizzarla nel database.

Se tocchi uno dei pulsanti, viene attivata un'operazione di database, ad esempio la creazione o l'aggiornamento di un SleepNight. Per questo e altri motivi, utilizzi le coroutine per implementare i gestori di clic per i pulsanti dell'app.

  1. Apri il file build.gradle a livello di app e trova le dipendenze per le coroutine. Per utilizzare le coroutine, hai bisogno di queste dipendenze, che sono state aggiunte per te.

    $coroutine_version è definito nel file build.gradle del progetto come coroutine_version = '1.0.0'.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. Apri il file SleepTrackerViewModel.
  2. Nel corpo della classe, definisci viewModelJob e assegnagli un'istanza di Job. Questo viewModelJob ti consente di annullare tutte le coroutine avviate da questo modello di visualizzazione quando il modello di visualizzazione non viene più utilizzato e viene eliminato. In questo modo, non avrai coroutine che non hanno un punto di ritorno.
private var viewModelJob = Job()
  1. Alla fine del corpo della classe, esegui l'override di onCleared() e annulla tutte le coroutine. Quando ViewModel viene eliminato, viene chiamato onCleared().
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Subito sotto la definizione di viewModelJob, definisci un uiScope per le coroutine. L'ambito determina il thread su cui verrà eseguita la coroutine e deve anche conoscere il job. Per ottenere un ambito, richiedi un'istanza di CoroutineScope e trasmetti un dispatcher e un job.

L'utilizzo di Dispatchers.Main significa che le coroutine avviate in uiScope verranno eseguite sul thread principale. Questo è ragionevole per molte coroutine avviate da un ViewModel, perché dopo che queste coroutine eseguono un'elaborazione, comportano un aggiornamento della UI.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. Sotto la definizione di uiScope, definisci una variabile chiamata tonight per contenere la notte corrente. Imposta la variabile su MutableLiveData, perché devi poter osservare i dati e modificarli.
private var tonight = MutableLiveData<SleepNight?>()
  1. Per inizializzare la variabile tonight il prima possibile, crea un blocco init sotto la definizione di tonight e chiama initializeTonight(). Definisci initializeTonight() nel passaggio successivo.
init {
   initializeTonight()
}
  1. Sotto il blocco init, implementa initializeTonight(). In uiScope, avvia una coroutine. All'interno, ottieni il valore di tonight dal database chiamando getTonightFromDatabase() e assegna il valore a tonight.value. Definisci getTonightFromDatabase() nel passaggio successivo.
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. Implementa getTonightFromDatabase(). Definisci questa funzione come private suspend che restituisce un valore SleepNight nullable, se non è presente alcun SleepNight avviato. In questo modo, si verifica un errore perché la funzione deve restituire qualcosa.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. All'interno del corpo della funzione di getTonightFromDatabase(), restituisci il risultato di una coroutine eseguita nel contesto Dispatchers.IO. Utilizza il dispatcher I/O, perché il recupero dei dati dal database è un'operazione di I/O e non ha nulla a che fare con la UI.
  return withContext(Dispatchers.IO) {}
  1. All'interno del blocco return, lascia che la coroutine recuperi la notte più recente dal database. Se gli orari di inizio e di fine non coincidono, il che significa che la notte è già terminata, restituisci null. In caso contrario, restituisci la notte.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

La funzione di sospensione getTonightFromDatabase() completata dovrebbe avere questo aspetto. Non dovrebbero essere presenti altri errori.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

Passaggio 2: aggiungi il gestore di clic per il pulsante Avvia

Ora puoi implementare onStartTracking(), il gestore dei clic per il pulsante Avvia. Devi creare un nuovo SleepNight, inserirlo nel database e assegnarlo a tonight. La struttura di onStartTracking() sarà molto simile a quella di initializeTonight().

  1. Inizia con la definizione della funzione per onStartTracking(). Puoi inserire i gestori dei clic sopra onCleared() nel file SleepTrackerViewModel.
fun onStartTracking() {}
  1. All'interno di onStartTracking(), avvia una coroutine in uiScope, perché hai bisogno di questo risultato per continuare e aggiornare la UI.
uiScope.launch {}
  1. All'interno dell'avvio della coroutine, crea un nuovo SleepNight, che acquisisce l'ora corrente come ora di inizio.
        val newNight = SleepNight()
  1. Sempre all'interno dell'avvio della coroutine, chiama insert() per inserire newNight nel database. Visualizzerai un errore perché non hai ancora definito questa funzione di sospensione insert(). (Questa non è la funzione DAO con lo stesso nome.)
       insert(newNight)
  1. Inoltre, all'interno dell'avvio della coroutine, aggiorna tonight.
       tonight.value = getTonightFromDatabase()
  1. Sotto onStartTracking(), definisci insert() come funzione private suspend che accetta SleepNight come argomento.
private suspend fun insert(night: SleepNight) {}
  1. Per il corpo di insert(), avvia una coroutine nel contesto I/O e inserisci la notte nel database chiamando insert() dal DAO.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. Nel file di layout fragment_sleep_tracker.xml, aggiungi il gestore dei clic per onStartTracking() a start_button utilizzando la magia del data binding che hai configurato in precedenza. La notazione della funzione @{() -> crea una funzione lambda che non accetta argomenti e chiama il gestore dei clic in sleepTrackerViewModel.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. Crea ed esegui la tua app. Tocca il pulsante Avvia. Questa azione crea dati, ma non puoi ancora vedere nulla. Lo correggerai nel passaggio successivo.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

Passaggio 3: visualizza i dati

In SleepTrackerViewModel, la variabile nights fa riferimento a LiveData perché getAllNights() nel DAO restituisce LiveData.

Si tratta di una funzionalità di Room che, ogni volta che i dati nel database cambiano, il LiveData nights viene aggiornato per mostrare i dati più recenti. Non devi mai impostare esplicitamente LiveData o aggiornarlo. Room aggiorna i dati in modo che corrispondano al database.

Tuttavia, se visualizzi nights in una visualizzazione di testo, verrà mostrato il riferimento all'oggetto. Per visualizzare i contenuti dell'oggetto, trasforma i dati in una stringa formattata. Utilizza una mappa Transformation eseguita ogni volta che nights riceve nuovi dati dal database.

  1. Apri il file Util.kt e rimuovi il commento dal codice per la definizione di formatNights() e dalle istruzioni import associate. Per rimuovere il commento dal codice in Android Studio, seleziona tutto il codice contrassegnato con // e premi Cmd+/ o Control+/.
  2. Tieni presente che formatNights() restituisce un tipo Spanned, ovvero una stringa formattata in HTML.
  3. Apri strings.xml. Nota l'utilizzo di CDATA per formattare le risorse stringa per la visualizzazione dei dati sul sonno.
  4. Apri SleepTrackerViewModel. Nella classe SleepTrackerViewModel, sotto la definizione di uiScope, definisci una variabile denominata nights. Recupera tutte le notti dal database e assegnale alla variabile nights.
private val nights = database.getAllNights()
  1. Subito sotto la definizione di nights, aggiungi il codice per trasformare nights in un nightsString. Utilizza la funzione formatNights() di Util.kt.

    Passa nights nella funzione map() della classe Transformations. Per accedere alle risorse stringa, definisci la funzione di mappatura come chiamata di formatNights(). Fornisci nights e un oggetto Resources.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. Apri il file di layout fragment_sleep_tracker.xml. In TextView, nella proprietà android:text, ora puoi sostituire la stringa della risorsa con un riferimento a nightsString.
"@{sleepTrackerViewModel.nightsString}"
  1. Ricompila il codice ed esegui l'app. Ora dovrebbero essere visualizzati tutti i dati sul sonno con gli orari di inizio.
  2. Tocca il pulsante Inizia altre volte per visualizzare altri dati.

Nel passaggio successivo, attiverai la funzionalità del pulsante Stop.

Passaggio 4: aggiungi il gestore dei clic per il pulsante Stop

Utilizzando lo stesso pattern del passaggio precedente, implementa il gestore dei clic per il pulsante Interrompi in SleepTrackerViewModel.

  1. Aggiungi onStopTracking() a ViewModel. Avvia una coroutine in uiScope. Se l'ora di fine non è ancora stata impostata, imposta endTimeMilli sull'ora di sistema attuale e chiama update() con i dati notturni.

    In Kotlin, la sintassi return@label specifica la funzione da cui viene restituita questa istruzione, tra diverse funzioni nidificate.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. Implementa update() utilizzando lo stesso pattern che hai utilizzato per implementare insert().
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. Per connettere il gestore dei clic all'interfaccia utente, apri il file di layout fragment_sleep_tracker.xml e aggiungi il gestore dei clic a stop_button.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. Crea ed esegui la tua app.
  2. Tocca Avvia, quindi tocca Interrompi. Vedi l'ora di inizio, l'ora di fine, la qualità del sonno senza valore e il tempo di sonno.

Passaggio 5: aggiungi il gestore dei clic per il pulsante Cancella

  1. Allo stesso modo, implementa onClear() e clear().
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. Per connettere il gestore dei clic alla UI, apri fragment_sleep_tracker.xml e aggiungi il gestore dei clic a clear_button.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. Crea ed esegui la tua app.
  2. Tocca Cancella per eliminare tutti i dati. Quindi tocca Avvia e Interrompi per creare nuovi dati.

Progetto Android Studio: TrackMySleepQualityCoroutines

  • Utilizza ViewModel, ViewModelFactory e il data binding per configurare l'architettura dell'interfaccia utente per l'app.
  • Per garantire il corretto funzionamento della UI, utilizza le coroutine per le attività a lunga esecuzione, come tutte le operazioni del database.
  • Le coroutine sono asincrone e non bloccanti. Utilizzano le funzioni suspend per rendere sequenziale il codice asincrono.
  • Quando una coroutine chiama una funzione contrassegnata con suspend, anziché bloccarsi fino a quando la funzione non restituisce un valore come una normale chiamata di funzione, sospende l'esecuzione fino a quando il risultato non è pronto. Poi riprende da dove era stato interrotto con il risultato.
  • La differenza tra blocco e sospensione è che se un thread viene bloccato, non viene eseguito altro lavoro. Se il thread viene sospeso, vengono eseguite altre operazioni finché il risultato non è disponibile.

Per avviare una coroutine, hai bisogno di un job, un dispatcher e uno scope:

  • In sostanza, un job è qualsiasi cosa possa essere annullata. Ogni coroutine ha un job e puoi utilizzare un job per annullare una coroutine.
  • Il dispatcher invia le coroutine da eseguire su vari thread. Dispatcher.Main esegue le attività sul thread principale, mentre Dispartcher.IO serve per delegare le attività di I/O di blocco a un pool condiviso di thread.
  • L'ambito combina informazioni, tra cui un lavoro e un dispatcher, per definire il contesto in cui viene eseguita la coroutine. Gli ambiti tengono traccia delle coroutine.

Per implementare i gestori di clic che attivano le operazioni del database, segui questo pattern:

  1. Avvia una coroutine che viene eseguita sul thread principale o dell'interfaccia utente, perché il risultato influisce sull'interfaccia utente.
  2. Chiama una funzione di sospensione per eseguire il lavoro a esecuzione prolungata, in modo da non bloccare il thread UI durante l'attesa del risultato.
  3. Il lavoro di lunga durata non ha nulla a che fare con la UI, quindi passa al contesto I/O. In questo modo, il lavoro può essere eseguito in un pool di thread ottimizzato e riservato a questo tipo di operazioni.
  4. Quindi chiama la funzione del database per eseguire l'operazione.

Utilizza una mappa Transformations per creare una stringa da un oggetto LiveData ogni volta che l'oggetto cambia.

Corso Udacity:

Documentazione per sviluppatori Android:

Altri documenti e articoli:

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

Quali dei seguenti sono vantaggi delle coroutine:

  • Non sono bloccanti
  • Vengono eseguiti in modo asincrono.
  • Possono essere eseguiti su un thread diverso da quello principale.
  • Rendono sempre più veloce l'esecuzione delle app.
  • Può utilizzare le eccezioni.
  • Possono essere scritti e letti come codice lineare.

Domanda 2

Che cos'è una funzione di sospensione?

  • Una funzione ordinaria annotata con la parola chiave suspend.
  • Una funzione che può essere chiamata all'interno delle coroutine.
  • Mentre è in esecuzione una funzione di sospensione, il thread chiamante viene sospeso.
  • Le funzioni di sospensione devono essere sempre eseguite in background.

Domanda 3

Qual è la differenza tra bloccare e sospendere un thread? Contrassegna tutte le risposte vere.

  • Quando l'esecuzione è bloccata, non è possibile eseguire altre operazioni sul thread bloccato.
  • Quando l'esecuzione viene sospesa, il thread può svolgere altre attività in attesa del completamento del lavoro delegato.
  • La sospensione è più efficiente, perché i thread potrebbero non essere in attesa e non fare nulla.
  • Che sia bloccata o sospesa, l'esecuzione è ancora in attesa del risultato della coroutine prima di continuare.

Inizia la lezione successiva: 6.3 Utilizzare LiveData per controllare gli stati dei pulsanti

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