Android Kotlin Fundamentals 05.1: ViewModel e ViewModelFactory

Questo codelab fa parte del corso Android Kotlin Fundamentals. Otterrai il massimo valore da questo corso se lavori in sequenza nei codelab. Tutti i codelab del corso sono elencati nella pagina di destinazione di Android Kotlin Fundamentals.

Schermata del titolo

Schermata di gioco

Schermata punteggio

Introduzione

In questo codelab, scopri uno dei componenti dell'architettura Android, ViewModel:

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

Informazioni importanti

  • Come creare app Android di base in Kotlin.
  • Come utilizzare il grafico di navigazione per implementare la navigazione nella tua app.
  • Come aggiungere codice per navigare tra le destinazioni della tua app e passare 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 usando Logcat in Android Studio.

Obiettivi didattici

  • Come utilizzare l'architettura delle app Android consigliata.
  • Come utilizzare i corsi Lifecycle, ViewModel e ViewModelFactory nell'app.
  • Come conservare i dati dell'interfaccia utente attraverso le modifiche alla configurazione del dispositivo.
  • Cos'è il modello 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 elemento ViewModel all'app per salvare i dati di quest'ultima, 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 costruttore.

Nei codelab della Lezione 5, sviluppi l'app GuessTheWord, iniziando con il codice di avvio. GuessTheWord è un gioco in stile charade per giocatori, in cui i giocatori collaborano per raggiungere il punteggio più alto possibile.

Il primo giocatore controlla le parole nell'app e le reagisce a turno, assicurandoti di non mostrare la parola al secondo giocatore. Il secondo giocatore prova a 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 pronuncia la parola, facendo attenzione a non pronunciare la parola stessa.

  • Quando il secondo giocatore indovina la parola correttamente, il primo pulsante preme il pulsante OK, 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 Ignora: in questo modo il numero viene ridotto di uno e passa alla parola successiva.
  • Per terminare il gioco, premi il pulsante Termina gioco. Questa funzionalità non è presente nel codice di avvio del primo codelab della serie.

In questa attività dovrai scaricare ed eseguire l'app iniziale ed esaminare il codice.

Passaggio 1: inizia

  1. Scarica il codice di avvio 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 Ignora mostra la parola successiva e riduce il punteggio di uno, mentre il pulsante OK mostra la parola successiva e aumenta il punteggio di uno. Il pulsante Termina gioco non è implementato, quindi non accade nulla quando lo tocchi.

Passaggio 2: esegui una procedura dettagliata del codice

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

MainActivity.kt

Questo file contiene solo un codice predefinito generato dal modello.

res/layout/main_activity.xml

Questo file contiene il layout principale dell'app. L'NavHostFragment ospita gli altri frammenti durante la navigazione dell'utente all'interno dell'app.

Frammenti dell'interfaccia utente

Il codice di avvio contiene 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 del gioco
  • score/ScoreFragment per la schermata del punteggio

screen/title/TitleFragment.kt

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

screens/game/GameFragment.kt

Si tratta del frammento principale in cui avviene la maggior parte del gioco:

  • Sono definite le variabili per la parola corrente e il punteggio corrente.
  • Il metodo wordList definito nel metodo resetList() è un elenco di parole di esempio da utilizzare nel gioco.
  • Il metodo onSkip() è il gestore dei clic per il pulsante Ignora. Diminuisce il punteggio di 1, quindi mostra la parola successiva utilizzando il metodo nextWord().
  • Il metodo onCorrect() è il gestore dei clic per il pulsante OK. Questo metodo viene implementato in modo simile al metodo onSkip(). L'unica differenza è che questo metodo aggiunge 1 al punteggio anziché sottrarre.

screens/score/ScoreFragment.kt

ScoreFragment è la schermata finale 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 la modalità di connessione dei frammenti tramite navigazione:

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

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

  1. Esegui il codice iniziale e gioca con qualche parola, toccando Salta o OK dopo ogni parola.
  2. Ora sullo schermo del gioco vengono visualizzati una parola e il punteggio corrente. Modifica l'orientamento dello schermo facendo ruotare il dispositivo o l'emulatore. Nota che il punteggio attuale è perso.
  3. Esegui il gioco con qualche altra parola. Quando la schermata del gioco viene visualizzata con un punteggio, chiudi e riapri l'app. Tieni presente che il gioco si riavvia dall'inizio perché lo stato dell'app non è stato salvato.
  4. Gioca con poche parole, poi tocca il pulsante Termina gioco. Tieni presente che non accade 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 spegne e si riavvia.
    Puoi risolvere il problema utilizzando il callback onSaveInstanceState(). Tuttavia, per usare il metodo onSaveInstanceState() è necessario scrivere un codice aggiuntivo per salvare lo stato in un pacchetto e implementare la logica per recuperarlo. Inoltre, la quantità di dati che è possibile archiviare è minima.
  • La schermata del gioco non viene visualizzata quando l'utente tocca il pulsante Termina gioco.

Puoi risolvere questi problemi utilizzando i componenti dell'architettura dell'app illustrati in questo codelab.

Architettura dell'app

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

L'app GuessTheWord segue il principio di progettazione delle separazioni dei problemi ed è suddivisa in classi, ognuna delle quali affronta una questione separata. In questo primo codelab della lezione, i corsi con cui lavori sono un controller dell'interfaccia utente, un ViewModel e un ViewModelFactory.

Controller dell'interfaccia utente

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

Nel codice di avvio di GuessTheWord, i controller dell'interfaccia utente sono i tre frammenti: GameFragment, ScoreFragment, e TitleFragment. In linea con la"separazione dei problemi"secondo il principio di progettazione, GameFragment è responsabile solo di disegnare elementi del 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 al GameViewModel.

Visualizzazione modello

Un elemento ViewModel contiene i dati da visualizzare in un frammento o in un'attività associata a ViewModel. Un ViewModel può eseguire semplici calcoli e trasformazioni sui dati per preparare quelli da mostrare con il controller dell'interfaccia utente. In questa architettura, ViewModel esegue il processo decisionale.

La 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. La GameViewModel contiene anche la logica di business per eseguire semplici calcoli per decidere lo stato corrente dei dati.

VisualizzaFabbrica modelli

Un elemento ViewModelFactory crea un'istanza di ViewModel oggetti, con o senza parametri di 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 i primi ViewModel all'app, ovvero GameViewModel per GameFragment. Scoprirai anche cosa significa che ViewModel è sensibile al 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, l'app della soluzione dovrebbe essere compilata come previsto. Se il problema persiste, 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 aiutare gli utenti a comprendere meglio come ViewModel è sensibile al ciclo di vita, aggiungi un blocco init con un'istruzione log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Passaggio 2: sostituisci onCleared() e aggiungi il logging

Il ViewModel viene eliminato quando il frammento associato viene scollegato o quando l'attività è terminata. Subito prima dell'eliminazione di ViewModel, viene richiamato il callback onCleared() per la pulizia delle risorse.

  1. Nel corso GameViewModel, sostituisci il 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 frammento di gioco

È necessario associare un ViewModel a un controller dell'interfaccia utente. Per associare le due parti, crea un riferimento all'elemento ViewModel all'interno del controller dell'interfaccia utente.

In questo passaggio, creerai un riferimento per GameViewModel nel controller dell'interfaccia utente corrispondente, che è GameFragment.

  1. Nel corso GameFragment, aggiungi come campo una variabile di tipo GameViewModel al livello superiore.
private lateinit var viewModel: GameViewModel

Passaggio 4: inizializza il modello di modelli

Durante le modifiche alla configurazione, come la rotazione dello schermo, vengono ricreati i controller dell'interfaccia utente, ad esempio i frammenti. Tuttavia, esistono ancora ViewModel istanze. Se crei l'istanza ViewModel utilizzando la classe ViewModel, viene creato un nuovo oggetto ogni volta che viene ricreato il frammento. Crea invece l'istanza ViewModel utilizzando un elemento ViewModelProvider.

Come funziona ViewModelProvider:

  • ViewModelProvider restituisce un ViewModel esistente se ne esiste uno o ne crea uno nuovo se non esiste già.
  • ViewModelProvider crea un'istanza ViewModel in associazione con l'ambito specificato (un'attività o un frammento).
  • Il ViewModel creato viene conservato finché l'ambito è attivo. Ad esempio, se l'ambito è un frammento, il valore di ViewModel viene conservato fino a quando il frammento non viene scollegato.

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

  1. Nella classe GameFragment, inizializza la variabile viewModel. Inserisci questo codice in onCreateView(), dopo la definizione della variabile di associazione. Utilizzare il metodo ViewModelProviders.of() e passare nel contesto GameFragment associato e alla classe GameViewModel.
  2. Sopra l'inizializzazione dell'oggetto ViewModel, aggiungi un'istruzione di log per registrare la chiamata del 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 Game. Tocca il pulsante Play sul tuo dispositivo o emulatore. Si apre la schermata del gioco.

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

  1. Attiva l'impostazione di rotazione automatica sul tuo dispositivo o emulatore e modifica alcune volte l'orientamento dello schermo. GameFragment viene eliminato e ricreato ogni volta, quindi ViewModelProviders.of() viene richiamato ogni volta. Tuttavia, l'elemento 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 oppure esci dal frammento di gioco. GameFragment è stato eliminato. Anche il GameViewModel associato viene eliminato e viene richiamato 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 è in grado di sopravvivere ai cambiamenti di configurazione, quindi è un buon posto per i dati che devono sopravvivere ai cambiamenti di configurazione:

  • Inserisci i dati da visualizzare sullo schermo e il codice per elaborare i dati nella ViewModel.
  • L'elemento ViewModel non deve mai contenere riferimenti a frammenti, attività o viste, in quanto attività, frammenti e viste non sopravvivono alle modifiche alla configurazione.

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

  • Prima di aggiungere ViewModel:
    quando l'app esegue una modifica alla configurazione, ad esempio una rotazione dello schermo, il frammento di gioco viene eliminato e ricreato. I dati andranno persi.
  • Dopo aver aggiunto ViewModel e spostato i dati dell'interfaccia utente del frammento di gioco nella ViewModel:
    Tutti i dati che il frammento deve visualizzare sono ora ViewModel. Quando l'app passa attraverso una modifica alla configurazione, ViewModel continua a essere valido e i dati vengono conservati.

In questa attività, sposti i dati dell'interfaccia utente 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 in Viewmodel

Sposta i seguenti campi di dati e metodi 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 associazione, GameFragmentBinding, perché contiene riferimenti alle viste. Questa variabile viene utilizzata per gonfiare il layout, impostare i listener di clic e visualizzare i dati sullo schermo, ovvero le responsabilità del frammento.
  2. Sposta i metodi resetList() e nextWord(). Questi metodi decidono quale parola mostrare sullo schermo.
  3. Dall'interno del metodo onCreateView(), sposta le chiamate metodo a resetList() e nextWord() al blocco init di GameViewModel.

    Questi metodi devono essere inseriti nel blocco init, perché dovresti reimpostare l'elenco di parole quando viene creato l'elemento ViewModel, non ogni volta che viene creato il frammento. Puoi eliminare l'istruzione di log nel blocco init di GameFragment.

I gestori dei clic di onSkip() e onCorrect() in GameFragment contengono codice per l'elaborazione dei dati e l'aggiornamento dell'interfaccia utente. Il codice per aggiornare l'interfaccia utente deve rimanere nel frammento, 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 in GameViewModel.
  2. In GameViewModel, assicurati che i metodi onSkip() e onCorrect() non siano private, poiché farai riferimento a questi metodi dal frammento.

Ecco 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!")
   }
}

Ecco 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 dei 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 che utilizzino le variabili GameViewModel, perché si trovano ora 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 ci siano errori. Se sono presenti errori, pulisci e ricrea il progetto.
  3. Esegui l'app e utilizza il gioco con alcune parole. Quando sei nella schermata del gioco, ruota il dispositivo. Tieni presente che il punteggio corrente e la parola corrente vengono conservati dopo la modifica dell'orientamento.

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

In questa attività, implementi il listener di clic per il pulsante Termina gioco.

  1. In GameFragment, aggiungi un metodo chiamato onEndGame(). Il metodo onEndGame() viene richiamato quando l'utente tocca il pulsante Termina gioco.
private fun onEndGame() {
   }
  1. In GameFragment, all'interno del metodo onCreateView(), individua il codice che imposta i listener di clic per i pulsanti OK e Ignora. Appena sotto queste due righe, imposta un listener di clic per il pulsante Termina gioco. Utilizza la variabile di associazione binding. All'interno del listener di clic, chiama il metodo onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. In GameFragment, aggiungi un metodo chiamato gameFinished() per andare all'app nella schermata del punteggio. Trasmetti il punteggio come argomento, utilizzando argomenti sicuri.
/**
* 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, usa il gioco e scorri alcune parole. Tocca il pulsante Termina gioco. Nota che l'app accede alla schermata del punteggio, ma non viene visualizzato il punteggio finale. Devi risolvere il problema nella prossima attività.

Quando l'utente termina il gioco, ScoreFragment non mostra il punteggio. Vuoi che un valore ViewModel venga mantenuto affinché il punteggio venga mostrato da ScoreFragment. Passerai il valore del punteggio durante l'inizializzazione di ViewModel utilizzando il pattern del metodo di fabbrica.

Il pattern di fabbrica è un motivo di progettazione creativa 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. Sotto il pacchetto score, crea una nuova classe Kotlin denominata ScoreViewModel. Questa classe sarà la ViewModel per il frammento del punteggio.
  2. Estendi la classe ScoreViewModel da ViewModel. Aggiungi un parametro costruttore per il punteggio finale. Aggiungi un blocco init con un'istruzione di log.
  3. Nel corso 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. Sotto il pacchetto score, crea un'altra classe Kotlin denominata ScoreViewModelFactory. Questa classe sarà responsabile dell'istanza dell'oggetto ScoreViewModel.
  2. Estendi la classe ScoreViewModelFactory da ViewModelProvider.Factory. Aggiungi un parametro 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 il problema, sostituisci il metodo create(). Nel metodo create(), restituisci l'oggetto ScoreViewModel appena costruito.
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 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 la viewModelFactory. Usa l'ScoreViewModelFactory. Passa il punteggio finale dall'argomento gruppo come parametro costruttore alla 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. L'oggetto ScoreViewModel verrà creato utilizzando il metodo della fabbrica 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 nella metrica ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Esegui l'app e gioca. Scorri alcune o tutte le parole e tocca Termina gioco. Nota che ora il frammento del punteggio mostra il punteggio finale.

  1. (Facoltativo) Controlla i log ScoreViewModel in Logcat filtrando per ScoreViewModel. Dovrebbe essere visualizzato il valore del punteggio.
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 con parametri per un elemento ViewModel utilizzando l'interfaccia di ViewModelFactory.

Complimenti! Hai modificato l'architettura della tua app in modo da utilizzare uno dei componenti dell'architettura Android, ViewModel. Hai risolto il problema del ciclo di vita dell'app e ora i dati del gioco sono ancora validi. Hai anche imparato a creare un costruttore con parametri per la creazione di un ViewModel, utilizzando l'interfaccia di ViewModelFactory.

Progetto Android Studio: GuessTheWord

  • Le linee guida relative all'architettura delle app per Android consiglia di separare i corsi che hanno responsabilità diverse.
  • Un controller UI è una classe basata su 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. Inserisci i dati in un elemento ViewModel.
  • Il corso ViewModel archivia e gestisce i dati relativi all'interfaccia utente. La classe ViewModel consente ai dati di sopravvivere a modifiche della configurazione come le rotazioni schermo.
  • ViewModel è uno dei componenti dell'architettura Android consigliati.
  • ViewModelProvider.Factory è un'interfaccia che puoi utilizzare per creare un oggetto ViewModel.

La tabella riportata di seguito confronta i controller UI con le istanze ViewModel che contengono i relativi dati:

Controller UI

Visualizza modello

Un controller di interfaccia utente è un esempio di ScoreFragment creato in questo codelab.

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

Non contiene dati da visualizzare nell'interfaccia utente.

Contiene dati che il controller dell'interfaccia utente mostra nell'interfaccia.

Contiene codice per visualizzare i dati e codice evento utente, come i listener di clic.

Contiene codice per l'elaborazione dei dati.

Eliminato e ricreato durante ogni modifica alla configurazione.

Eliminata solo quando il controller dell'interfaccia utente associato scompare definitivamente, per un'attività, quando termina l'attività o per un frammento, quando il frammento viene scollegato.

Contiene visualizzazioni.

Non devono mai contenere riferimenti ad attività, frammenti o viste, in quanto non sopravvivono alle modifiche alla configurazione, al contrario di ViewModel.

Contiene un riferimento all'elemento ViewModel associato.

Non contiene alcun riferimento al controller dell'interfaccia utente associato.

Corso Udacity:

Documentazione per gli sviluppatori Android:

Altro:

In questa sezione sono elencati i possibili compiti per gli studenti che lavorano attraverso questo codelab nell'ambito di un corso tenuto da un insegnante. Spetta all'insegnante fare quanto segue:

  • Assegna i compiti, se necessario.
  • Comunica agli studenti come inviare compiti.
  • Valuta i compiti.

Gli insegnanti possono utilizzare i suggerimenti solo quanto e come vogliono e dovrebbero assegnare i compiti che ritengono appropriati.

Se stai lavorando da solo a questo codelab, puoi utilizzare questi compiti per mettere alla prova le tue conoscenze.

Rispondi a queste domande

Domanda 1

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

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Domanda 2

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

  • Vero
  • falso.

Domanda 3

Quando viene eliminata una ViewModel?

  • Quando il controller dell'interfaccia utente associato viene eliminato e ricreato durante una modifica dell'orientamento del dispositivo.
  • Con una variazione di orientamento.
  • Una volta terminato il controller dell'interfaccia utente associato (se è un'attività) o scollegato (se è un frammento).
  • Quando l'utente preme il pulsante Indietro.

Domanda 4

A cosa serve l'interfaccia di ViewModelFactory?

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

Inizia la lezione successiva: 5.2: LiveData and LiveDataObservers

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