Android Kotlin Fundamentals 05.1: ViewModel e ViewModelFactory

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.

Schermata del titolo

Schermata di gioco

Schermata del punteggio

Introduzione

In questo codelab, imparerai a conoscere uno dei componenti dell'architettura Android, ViewModel:

  • Utilizzi la classe ViewModel per archiviare e gestire i dati correlati all'interfaccia utente in modo consapevole del ciclo di vita. La classe ViewModel consente ai dati di sopravvivere alle modifiche alla configurazione del dispositivo, come rotazioni dello schermo e modifiche alla disponibilità della tastiera.
  • Utilizzi la classe ViewModelFactory per creare un'istanza e restituire l'oggetto ViewModel che sopravvive alle modifiche alla configurazione.

Cosa devi già sapere

  • Come creare app per Android di base in Kotlin.
  • Come utilizzare il grafico di navigazione per implementare la navigazione nella tua app.
  • Come aggiungere codice per spostarsi tra le destinazioni dell'app e trasferire dati tra le destinazioni di navigazione.
  • Come funzionano i cicli di vita di attività e frammenti.
  • Come aggiungere informazioni di logging a un'app e leggere i log utilizzando Logcat in Android Studio.

Obiettivi didattici

  • Come utilizzare l'architettura delle app Android consigliata.
  • Come utilizzare le classi Lifecycle, ViewModel e ViewModelFactory nella tua app.
  • Come conservare i dati dell'interfaccia utente durante le modifiche alla configurazione del dispositivo.
  • Che cos'è il pattern di progettazione del metodo di fabbrica e come utilizzarlo.
  • Come creare un oggetto ViewModel utilizzando l'interfaccia ViewModelProvider.Factory.

In questo lab proverai a:

  • Aggiungi un ViewModel all'app per salvare i dati dell'app in modo che sopravvivano alle modifiche alla configurazione.
  • Utilizza ViewModelFactory e il pattern di progettazione del metodo di fabbrica per creare un'istanza di un oggetto ViewModel con parametri del costruttore.

Nei codelab della lezione 5, sviluppi l'app IndovinaParola, partendo dal codice iniziale. IndovinaParola è un gioco in stile Sarabanda per due giocatori, in cui i giocatori collaborano per ottenere il punteggio più alto possibile.

Il primo giocatore guarda le parole nell'app e le mima a turno, assicurandosi di non mostrarle al secondo giocatore. Il secondo giocatore cerca di indovinare la parola.

Per giocare, il primo giocatore apre l'app sul dispositivo e vede una parola, ad esempio "chitarra", come mostrato nello screenshot di seguito.

Il primo giocatore recita la parola, facendo attenzione a non dirla.

  • Quando il secondo giocatore indovina la parola correttamente, il primo giocatore preme il pulsante Indovinato, che aumenta il conteggio di uno e mostra la parola successiva.
  • Se il secondo giocatore non riesce a indovinare la parola, il primo giocatore preme il pulsante Salta, che diminuisce il conteggio di uno e passa alla parola successiva.
  • Per terminare la partita, premi il pulsante Termina partita. Questa funzionalità non è presente nel codice iniziale del primo codelab della serie.

In questa attività scaricherai ed eseguirai l'app iniziale ed esaminerai il codice.

Passaggio 1: inizia

  1. Scarica il codice iniziale di GuessTheWord e apri il progetto in Android Studio.
  2. Esegui l'app su un dispositivo Android o su un emulatore.
  3. Tocca i pulsanti. Tieni presente che il pulsante Salta mostra la parola successiva e diminuisce il punteggio di uno, mentre il pulsante Capito mostra la parola successiva e aumenta il punteggio di uno. Il pulsante Termina partita non è implementato, quindi non succede nulla quando lo tocchi.

Passaggio 2: esegui una revisione del codice

  1. In Android Studio, esplora il codice per capire come funziona l'app.
  2. Assicurati di esaminare i file descritti di seguito, che sono particolarmente importanti.

MainActivity.kt

Questo file contiene solo codice predefinito generato dal modello.

res/layout/main_activity.xml

Questo file contiene il layout principale dell'app. Il NavHostFragment ospita gli altri fragment mentre l'utente naviga nell'app.

Frammenti di UI

Il codice iniziale ha tre frammenti in tre pacchetti diversi nel pacchetto com.example.android.guesstheword.screens:

  • title/TitleFragment per la schermata del titolo
  • game/GameFragment per la schermata di gioco
  • score/ScoreFragment per la schermata del punteggio

screens/title/TitleFragment.kt

Il frammento del titolo è la prima schermata visualizzata all'avvio dell'app. Un gestore di clic è impostato sul pulsante Gioca per passare alla schermata di gioco.

screens/game/GameFragment.kt

Questo è il frammento principale, in cui si svolge la maggior parte dell'azione del gioco:

  • Le variabili sono definite per la parola e il punteggio attuali.
  • wordList definito all'interno del metodo resetList() è un elenco di parole di esempio da utilizzare nel gioco.
  • Il metodo onSkip() è il gestore dei clic per il pulsante Salta. Diminuisce il punteggio di 1, quindi visualizza la parola successiva utilizzando il metodo nextWord().
  • Il metodo onCorrect() è il gestore dei clic per il pulsante Ho capito. Questo metodo viene implementato in modo simile al metodo onSkip(). L'unica differenza è che questo metodo aggiunge 1 al punteggio anziché sottrarlo.

screens/score/ScoreFragment.kt

ScoreFragment è l'ultima schermata del gioco e mostra il punteggio finale del giocatore. In questo codelab, aggiungi l'implementazione per visualizzare questa schermata e mostrare il punteggio finale.

res/navigation/main_navigation.xml

Il grafico di navigazione mostra come sono collegati i fragment tramite la navigazione:

  • Dal frammento del titolo, l'utente può passare al frammento del gioco.
  • Dal frammento della partita, l'utente può passare al frammento del punteggio.
  • Dal frammento del punteggio, l'utente può tornare al frammento della partita.

In questa attività, troverai problemi con l'app iniziale GuessTheWord.

  1. Esegui il codice iniziale e gioca per qualche parola, toccando Salta o Ok dopo ogni parola.
  2. La schermata di gioco ora mostra una parola e il punteggio attuale. Modifica l'orientamento dello schermo ruotando il dispositivo o l'emulatore. Tieni presente che il punteggio attuale viene perso.
  3. Continua a giocare con altre parole. Quando viene visualizzata la schermata di gioco con un punteggio, chiudi e riapri l'app. Nota che il gioco riprende dall'inizio, perché lo stato dell'app non viene salvato.
  4. Gioca per qualche parola, poi tocca il pulsante Termina partita. Noterai che non succede nulla.

Problemi nell'app:

  • L'app iniziale non salva e ripristina lo stato dell'app durante le modifiche alla configurazione, ad esempio quando l'orientamento del dispositivo cambia o quando l'app si chiude e si riavvia.
    Puoi risolvere questo problema utilizzando il callback onSaveInstanceState(). Tuttavia, l'utilizzo del metodo onSaveInstanceState() richiede di scrivere codice aggiuntivo per salvare lo stato in un bundle e per implementare la logica per recuperare lo stato. Inoltre, la quantità di dati che possono essere archiviati è minima.
  • Quando l'utente tocca il pulsante Fine partita, la schermata di gioco non viene reindirizzata alla schermata del punteggio.

Puoi risolvere questi problemi utilizzando i componenti dell'architettura dell'app che imparerai a conoscere in questo codelab.

Architettura dell'app

L'architettura dell'app è un modo per progettare le classi delle tue app e le relazioni tra loro, in modo che il codice sia organizzato, funzioni bene in scenari particolari e sia facile da usare. In questo insieme di quattro codelab, i miglioramenti apportati all'app GuessTheWord seguono le linee guida dell'architettura delle app per Android e utilizzi i componenti dell'architettura Android. L'architettura dell'app per Android è simile al pattern architetturale MVVM (model-view-viewmodel).

L'app GuessTheWord segue il principio di progettazione della separazione delle responsabilità ed è suddivisa in classi, ognuna delle quali si occupa di un problema separato. In questo primo codelab della lezione, le classi con cui lavori sono un controller UI, un ViewModel e un ViewModelFactory.

Controller UI

Un controller UI è una classe basata sulla UI come Activity o Fragment. Un controller UI deve contenere solo la logica che gestisce le interazioni con la UI e il sistema operativo, ad esempio la visualizzazione delle visualizzazioni e l'acquisizione dell'input utente. Non inserire la logica decisionale, ad esempio quella che determina il testo da visualizzare, nel controller UI.

Nel codice iniziale di IndovinaParola, i controller UI sono i tre frammenti: GameFragment, ScoreFragment, e TitleFragment. Seguendo il principio di progettazione della "separazione delle responsabilità", l'GameFragment è responsabile solo del disegno degli elementi di gioco sullo schermo e di sapere quando l'utente tocca i pulsanti, e nient'altro. Quando l'utente tocca un pulsante, queste informazioni vengono trasmesse a GameViewModel.

ViewModel

Un ViewModel contiene i dati da visualizzare in un fragment o in un'attività associata all'ViewModel. Un ViewModel può eseguire calcoli e trasformazioni semplici sui dati per prepararli alla visualizzazione da parte del controller UI. In questa architettura, ViewModel prende le decisioni.

GameViewModel contiene dati come il valore del punteggio, l'elenco delle parole e la parola corrente, perché questi sono i dati da visualizzare sullo schermo. Il GameViewModel contiene anche la logica di business per eseguire calcoli semplici per decidere qual è lo stato attuale dei dati.

ViewModelFactory

Un ViewModelFactory crea istanze di oggetti ViewModel, con o senza parametri del costruttore.

Nei codelab successivi, scoprirai altri componenti dell'architettura Android correlati ai controller UI e a ViewModel.

La classe ViewModel è progettata per archiviare e gestire i dati relativi all'interfaccia utente. In questa app, ogni ViewModel è associato a un frammento.

In questa attività, aggiungi il primo ViewModel alla tua app, ovvero il GameViewModel per GameFragment. Scopri anche cosa significa che ViewModel è consapevole del ciclo di vita.

Passaggio 1: aggiungi la classe GameViewModel

  1. Apri il file build.gradle(module:app). All'interno del blocco dependencies, aggiungi la dipendenza Gradle per ViewModel.

    Se utilizzi l'ultima versione della libreria, la compilazione dell'app di soluzione dovrebbe avvenire come previsto. In caso contrario, prova a risolvere il problema o ripristina la versione mostrata di seguito.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. Nella cartella del pacchetto screens/game/, crea una nuova classe Kotlin denominata GameViewModel.
  2. Fai in modo che la classe GameViewModel estenda la classe astratta ViewModel.
  3. Per aiutarti a comprendere meglio in che modo ViewModel è consapevole del ciclo di vita, aggiungi un blocco init con un'istruzione log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Passaggio 2: esegui l'override di onCleared() e aggiungi la registrazione

ViewModel viene eliminato quando il fragment associato viene scollegato o quando l'attività è terminata. Poco prima che ViewModel venga distrutto, viene chiamata la richiamata onCleared() per pulire le risorse.

  1. Nella classe GameViewModel, esegui l'override del metodo onCleared().
  2. Aggiungi un'istruzione di log all'interno di onCleared() per monitorare il ciclo di vita di GameViewModel.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Passaggio 3: associa GameViewModel al fragment di gioco

Un ViewModel deve essere associato a un controller UI. Per associarli, crei un riferimento a ViewModel all'interno del controller UI.

In questo passaggio, crei un riferimento di GameViewModel all'interno del controller UI corrispondente, ovvero GameFragment.

  1. Nella classe GameFragment, aggiungi un campo di tipo GameViewModel al livello superiore come variabile di classe.
private lateinit var viewModel: GameViewModel

Passaggio 4: inizializza il ViewModel

Durante le modifiche alla configurazione, come le rotazioni dello schermo, i controller UI come i fragment vengono ricreati. Tuttavia, ViewModel istanze sopravvivono. Se crei l'istanza ViewModel utilizzando la classe ViewModel, viene creato un nuovo oggetto ogni volta che il fragment viene ricreato. Crea invece l'istanza ViewModel utilizzando un ViewModelProvider.

Come funziona ViewModelProvider:

  • ViewModelProvider restituisce un ViewModel esistente, se presente, o ne crea uno nuovo se non esiste già.
  • ViewModelProvider crea un'istanza ViewModel in associazione all'ambito specificato (un'attività o un fragment).
  • ViewModel creato viene conservato finché l'ambito è attivo. Ad esempio, se l'ambito è un frammento, ViewModel viene mantenuto fino al distacco del frammento.

Inizializza ViewModel utilizzando il metodo ViewModelProviders.of() per creare un ViewModelProvider:

  1. Nella classe GameFragment, inizializza la variabile viewModel. Inserisci questo codice all'interno di onCreateView(), dopo la definizione della variabile di binding. Utilizza il metodo ViewModelProviders.of() e trasmetti il contesto GameFragment associato e la classe GameViewModel.
  2. Sopra l'inizializzazione dell'oggetto ViewModel, aggiungi un'istruzione di log per registrare la chiamata al metodo ViewModelProviders.of().
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. Esegui l'app. In Android Studio, apri il riquadro Logcat e filtra in base a Game. Tocca il pulsante Riproduci sul dispositivo o sull'emulatore. Si apre la schermata di gioco.

    Come mostrato in Logcat, il metodo onCreateView() di GameFragment chiama il metodo ViewModelProviders.of() per creare GameViewModel. Le istruzioni di logging che hai aggiunto a GameFragment e GameViewModel vengono visualizzate in Logcat.

  1. Attiva l'impostazione di rotazione automatica sul dispositivo o sull'emulatore e cambia l'orientamento dello schermo alcune volte. GameFragment viene distrutto e ricreato ogni volta, quindi ViewModelProviders.of() viene chiamato ogni volta. Tuttavia, GameViewModel viene creato una sola volta e non viene ricreato o eliminato per ogni chiamata.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. Esci dal gioco o dal frammento di gioco. GameFragment è distrutto. Viene eliminato anche l'GameViewModel associato e viene chiamato il callback onCleared().
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

ViewModel sopravvive alle modifiche alla configurazione, quindi è un buon posto per i dati che devono sopravvivere a queste modifiche:

  • Inserisci i dati da visualizzare sullo schermo e il codice per elaborarli in ViewModel.
  • ViewModel non deve mai contenere riferimenti a frammenti, attività o visualizzazioni, perché attività, frammenti e visualizzazioni non sopravvivono alle modifiche alla configurazione.

A titolo di confronto, ecco come vengono gestiti i dati dell'interfaccia utente GameFragment nell'app iniziale prima di aggiungere ViewModel e dopo aver aggiunto ViewModel:

  • Prima di aggiungere ViewModel:
    quando l'app subisce una modifica della configurazione, ad esempio una rotazione dello schermo, il frammento di gioco viene eliminato e ricreato. I dati vengono persi.
  • Dopo aver aggiunto ViewModel e spostato i dati dell'interfaccia utente del frammento di gioco in ViewModel:
    tutti i dati necessari per la visualizzazione del frammento sono ora ViewModel. Quando l'app subisce una modifica alla configurazione, ViewModel rimane e i dati vengono conservati.

In questa attività, sposterai i dati dell'UI dell'app nella classe GameViewModel, insieme ai metodi per elaborare i dati. In questo modo, i dati vengono conservati durante le modifiche alla configurazione.

Passaggio 1: sposta i campi di dati e l'elaborazione dei dati nel ViewModel

Sposta i seguenti campi e metodi di dati da GameFragment a GameViewModel:

  1. Sposta i campi di dati word, score e wordList. Assicurati che word e score non siano private.

    Non spostare la variabile di binding, GameFragmentBinding, perché contiene riferimenti alle visualizzazioni. Questa variabile viene utilizzata per gonfiare il layout, configurare i listener di clic e visualizzare i dati sullo schermo, responsabilità del fragment.
  2. Sposta i metodi resetList() e nextWord(). Questi metodi decidono quale parola mostrare sullo schermo.
  3. Dall'interno del metodo onCreateView(), sposta le chiamate di metodo a resetList() e nextWord() nel blocco init di GameViewModel.

    Questi metodi devono trovarsi nel blocco init, perché devi reimpostare l'elenco di parole quando viene creato ViewModel, non ogni volta che viene creato il fragmento. Puoi eliminare l'istruzione di log nel blocco init di GameFragment.

I gestori dei clic onSkip() e onCorrect() in GameFragment contengono il codice per l'elaborazione dei dati e l'aggiornamento della UI. Il codice per aggiornare la UI deve rimanere nel fragment, ma il codice per l'elaborazione dei dati deve essere spostato in ViewModel.

Per ora, inserisci i metodi identici in entrambi i punti:

  1. Copia i metodi onSkip() e onCorrect() da GameFragment a GameViewModel.
  2. In GameViewModel, assicurati che i metodi onSkip() e onCorrect() non siano private, perché farai riferimento a questi metodi dal fragment.

Di seguito è riportato il codice per la classe GameViewModel dopo il refactoring:

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

Di seguito è riportato il codice per la classe GameFragment, dopo il refactoring:

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


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

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

Passaggio 2: aggiorna i riferimenti ai gestori di clic e ai campi di dati in GameFragment

  1. In GameFragment, aggiorna i metodi onSkip() e onCorrect(). Rimuovi il codice per aggiornare il punteggio e chiama invece i metodi onSkip() e onCorrect() corrispondenti su viewModel.
  2. Poiché hai spostato il metodo nextWord() in ViewModel, il frammento di gioco non può più accedervi.

    In GameFragment, nei metodi onSkip() e onCorrect(), sostituisci la chiamata a nextWord() con updateScoreText() e updateWordText(). Questi metodi mostrano i dati sullo schermo.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. In GameFragment, aggiorna le variabili score e word in modo da utilizzare le variabili GameViewModel, perché ora si trovano in GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. In GameViewModel, all'interno del metodo nextWord(), rimuovi le chiamate ai metodi updateWordText() e updateScoreText(). Questi metodi vengono ora chiamati da GameFragment.
  2. Crea l'app e assicurati che non siano presenti errori. Se si verificano errori, pulisci e ricompila il progetto.
  3. Esegui l'app e gioca con alcune parole. Mentre ti trovi nella schermata di gioco, ruota il dispositivo. Tieni presente che il punteggio attuale e la parola attuale vengono mantenuti dopo la modifica dell'orientamento.

Ottimo lavoro. Ora tutti i dati dell'app vengono archiviati in un ViewModel, quindi vengono conservati durante le modifiche alla configurazione.

In questa attività, implementa il listener di clic per il pulsante Termina partita.

  1. In GameFragment, aggiungi un metodo chiamato onEndGame(). Il metodo onEndGame() viene chiamato quando l'utente tocca il pulsante Termina partita.
private fun onEndGame() {
   }
  1. In GameFragment, all'interno del metodo onCreateView(), individua il codice che imposta i listener di clic per i pulsanti Ho capito e Salta. Appena sotto queste due righe, imposta un listener dei clic per il pulsante Fine partita. Utilizza la variabile di binding binding. All'interno del listener di clic, chiama il metodo onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. In GameFragment, aggiungi un metodo chiamato gameFinished() per navigare nell'app fino alla schermata del punteggio. Trasferisci il punteggio come argomento utilizzando Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. Nel metodo onEndGame(), chiama il metodo gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. Esegui l'app, gioca e scorri alcune parole. Tocca il pulsante Termina partita . Nota che l'app passa alla schermata del punteggio, ma il punteggio finale non viene visualizzato. Lo correggerai nell'attività successiva.

Quando l'utente termina la partita, ScoreFragment non mostra il punteggio. Vuoi che un ViewModel contenga il punteggio da visualizzare sul ScoreFragment. Passerai il valore del punteggio durante l'inizializzazione di ViewModel utilizzando il pattern del metodo di fabbrica.

Il pattern del metodo di fabbrica è un pattern di progettazione creazionale che utilizza metodi di fabbrica per creare oggetti. Un metodo di fabbrica è un metodo che restituisce un'istanza della stessa classe.

In questa attività, creerai un ViewModel con un costruttore parametrizzato per il frammento del punteggio e un metodo di fabbrica per creare un'istanza di ViewModel.

  1. Nel pacchetto score, crea una nuova classe Kotlin denominata ScoreViewModel. Questo corso sarà il ViewModel per il frammento del punteggio.
  2. Estendi la classe ScoreViewModel da ViewModel. Aggiungi un parametro del costruttore per il punteggio finale. Aggiungi un blocco init con un'istruzione di log.
  3. Nella classe ScoreViewModel, aggiungi una variabile denominata score per salvare il punteggio finale.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. Nel pacchetto score, crea un'altra classe Kotlin chiamata ScoreViewModelFactory. Questa classe sarà responsabile dell'istanza dell'oggetto ScoreViewModel.
  2. Estendi il corso ScoreViewModelFactory a partire da ViewModelProvider.Factory. Aggiungi un parametro del costruttore per il punteggio finale.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. In ScoreViewModelFactory, Android Studio mostra un errore relativo a un membro astratto non implementato. Per risolvere l'errore, esegui l'override del metodo create(). Nel metodo create(), restituisci l'oggetto ScoreViewModel appena creato.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. In ScoreFragment, crea variabili di classe per ScoreViewModel e ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. In ScoreFragment, all'interno di onCreateView(), dopo aver inizializzato la variabile binding, inizializza viewModelFactory. Utilizza ScoreViewModelFactory. Passa il punteggio finale dal bundle di argomenti come parametro del costruttore a ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. In onCreateView(, dopo aver inizializzato viewModelFactory, inizializza l'oggetto viewModel. Chiama il metodo ViewModelProviders.of(), passa il contesto del frammento di punteggio associato e viewModelFactory. In questo modo viene creato l'oggetto ScoreViewModel utilizzando il metodo factory definito nella classe viewModelFactory.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. Nel metodo onCreateView(), dopo aver inizializzato viewModel, imposta il testo della visualizzazione scoreText sul punteggio finale definito in ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Esegui l'app e gioca. Scorri alcune o tutte le parole e tocca Termina partita. Nota che il frammento del punteggio ora mostra il punteggio finale.

  1. (Facoltativo) Controlla i log di ScoreViewModel in Logcat filtrando in base a ScoreViewModel. Il valore del punteggio deve essere visualizzato.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

In questa attività hai implementato ScoreFragment per utilizzare ViewModel. Hai anche imparato a creare un costruttore parametrizzato per un ViewModel utilizzando l'interfaccia ViewModelFactory.

Complimenti! Hai modificato l'architettura della tua app per utilizzare uno dei componenti dell'architettura Android, ViewModel. Hai risolto il problema del ciclo di vita dell'app e ora i dati del gioco sopravvivono alle modifiche alla configurazione. Hai anche imparato a creare un costruttore parametrizzato per creare un ViewModel utilizzando l'interfaccia ViewModelFactory.

Progetto Android Studio: GuessTheWord

  • Le linee guida per l'architettura delle app per Android consigliano di separare le classi con responsabilità diverse.
  • Un controller UI è una classe basata sulla UI come Activity o Fragment. I controller dell'interfaccia utente devono contenere solo la logica che gestisce le interazioni con l'interfaccia utente e il sistema operativo; non devono contenere dati da visualizzare nell'interfaccia utente. Inserisci questi dati in un ViewModel.
  • La classe ViewModel archivia e gestisce i dati relativi all'interfaccia utente. La classe ViewModel consente ai dati di sopravvivere alle modifiche alla configurazione, ad esempio le rotazioni dello schermo.
  • ViewModel è uno dei componenti dell'architettura Android consigliati.
  • ViewModelProvider.Factory è un'interfaccia che puoi utilizzare per creare un oggetto ViewModel.

La tabella seguente confronta i controller UI con le istanze ViewModel che contengono i dati:

Controller UI

ViewModel

Un esempio di controller UI è ScoreFragment che hai creato in questo codelab.

Un esempio di ViewModel è il ScoreViewModel che hai creato in questo codelab.

Non contiene dati da visualizzare nella UI.

Contiene i dati che il controller UI visualizza nella UI.

Contiene il codice per la visualizzazione dei dati e il codice degli eventi utente, ad esempio i listener di clic.

Contiene il codice per l'elaborazione dei dati.

Distrutti e ricreati a ogni modifica della configurazione.

Viene eliminato solo quando il controller UI associato viene rimosso definitivamente. Per un'attività, quando l'attività termina o, per un fragment, quando il fragment viene scollegato.

Contiene le visualizzazioni.

Non deve mai contenere riferimenti ad attività, frammenti o visualizzazioni, perché non sopravvivono alle modifiche alla configurazione, a differenza di ViewModel.

Contiene un riferimento al ViewModel associato.

Non contiene alcun riferimento al controller UI associato.

Corso Udacity:

Documentazione per sviluppatori Android:

Altro:

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

Per evitare di perdere dati durante una modifica della configurazione del dispositivo, in quale classe devi salvare i dati dell'app?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Domanda 2

Un ViewModel non deve mai contenere riferimenti a frammenti, attività o visualizzazioni. Vero o falso?

  • Vero
  • Falso

Domanda 3

Quando viene distrutto un ViewModel?

  • Quando il controller UI associato viene eliminato e ricreato durante una modifica dell'orientamento del dispositivo.
  • In un cambio di orientamento.
  • Al termine del controller UI associato (se si tratta di un'attività) o quando viene scollegato (se si tratta di un frammento).
  • Quando l'utente preme il pulsante Indietro.

Domanda 4

A cosa serve l'interfaccia ViewModelFactory?

  • Creazione dell'istanza di un oggetto ViewModel.
  • Conservazione dei dati durante le modifiche all'orientamento.
  • Aggiornamento dei dati visualizzati sullo schermo.
  • Ricevere notifiche quando i dati dell'app vengono modificati.

Inizia la lezione successiva: 5.2: LiveData e osservatori LiveData

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