Android Kotlin Fundamentals 05.1: ViewModel and ViewModelFactory

This codelab is part of the Android Kotlin Fundamentals course. You'll get the most value out of this course if you work through the codelabs in sequence. All the course codelabs are listed on the Android Kotlin Fundamentals codelabs landing page.

Title screen

Game screen

Score screen

Introduction

In this codelab, you learn about one of the Android Architecture Components, ViewModel:

  • You use the ViewModel class to store and manage UI-related data in a lifecycle-conscious way. The ViewModel class allows data to survive device-configuration changes such as screen rotations and changes to keyboard availability.
  • You use the ViewModelFactory class to instantiate and return the ViewModel object that survives configuration changes.

What you should already know

  • How to create basic Android apps in Kotlin.
  • How to use the navigation graph to implement navigation in your app.
  • How to add code to navigate between your app's destinations and pass data between navigation destinations.
  • How the activity and fragment lifecycles work.
  • How to add logging information to an app and read logs using Logcat in Android Studio.

What you'll learn

  • How to use the recommended Android app architecture.
  • How to use the Lifecycle, ViewModel, and ViewModelFactory classes in your app.
  • How to retain UI data through device-configuration changes.
  • What the factory method design pattern is and how to use it.
  • How to create a ViewModel object using the interface ViewModelProvider.Factory.

What you'll do

  • Add a ViewModel to the app, to save app's data so the data survives configuration changes.
  • Use ViewModelFactory and the factory-method design pattern to instantiate a ViewModel object with constructor parameters.

In the Lesson 5 codelabs, you develop the GuessTheWord app, beginning with starter code. GuessTheWord is a two-player charades-style game, where the players collaborate to achieve the highest score possible.

The first player looks at the words in the app and acts each one out in turn, making sure not to show the word to the second player. The second player tries to guess the word.

To play the game, the first player opens the app on the device and sees a word, for example "guitar," as shown in the screenshot below.

The first player acts out the word, being careful not to actually say the word itself.

  • When the second player guesses the word correctly, the first player presses the Got It button, which increases the count by one and shows the next word.
  • If the second player can't guess the word, the first player presses the Skip button, which decreases the count by one and skips to the next word.
  • To end the game, press the End Game button. (This functionality isn't in the starter code for the first codelab in the series.)

In this task, you download and run the starter app and examine the code.

Step 1: Get started

  1. Download the GuessTheWord starter code and open the project in Android Studio.
  2. Run the app on an Android-powered device, or on an emulator.
  3. Tap the buttons. Notice that the Skip button displays the next word and decreases the score by one, and the Got It button shows the next word and increases the score by one. The End Game button is not implemented, so nothing happens when you tap it.

Step 2: Do a code walkthrough

  1. In Android Studio, explore the code to get a feel for how the app works.
  2. Make sure to look at the files described below, which are particularly important.

MainActivity.kt

This file contains only default, template-generated code.

res/layout/main_activity.xml

This file contains the app's main layout. The NavHostFragment hosts the other fragments as the user navigates through the app.

UI fragments

The starter code has three fragments in three different packages under the com.example.android.guesstheword.screens package:

  • title/TitleFragment for the title screen
  • game/GameFragment for the game screen
  • score/ScoreFragment for the score screen

screens/title/TitleFragment.kt

The title fragment is the first screen that is displayed when the app is launched. A click handler is set to the Play button, to navigate to the game screen.

screens/game/GameFragment.kt

This is the main fragment, where most of the game's action takes place:

  • Variables are defined for the current word and the current score.
  • The wordList defined inside the resetList() method is a sample list of words to be used in the game.
  • The onSkip() method is the click handler for the Skip button. It decreases the score by 1, then displays the next word using the nextWord() method.
  • The onCorrect() method is the click handler for the Got It button. This method is implemented similarly to the onSkip() method. The only difference is that this method adds 1 to the score instead of subtracting.

screens/score/ScoreFragment.kt

ScoreFragment is the final screen in the game, and it displays the player's final score. In this codelab, you add the implementation to display this screen and show the final score.

res/navigation/main_navigation.xml

The navigation graph shows how the fragments are connected through navigation:

  • From the title fragment, the user can navigate to the game fragment.
  • From the game fragment, the user can navigate to the score fragment.
  • From the score fragment, the user can navigate back to the game fragment.

In this task, you find issues with the GuessTheWord starter app.

  1. Run the starter code and play the game through a few words, tapping either Skip or Got It after each word.
  2. The game screen now shows a word and the current score. Change the screen orientation by rotating the device or emulator. Notice that the current score is lost.
  3. Run the game through a few more words. When the game screen is displayed with some score, close and re-open the app. Notice that the game restarts from the beginning, because the app state is not saved.
  4. Play the game through a few words, then tap the End Game button. Notice that nothing happens.

Issues in the app:

  • The starter app doesn't save and restore the app state during configuration changes, such as when the device orientation changes, or when the app shuts down and restarts.
    You could resolve this issue using the onSaveInstanceState() callback. However, using the onSaveInstanceState() method requires you to write extra code to save the state in a bundle, and to implement the logic to retrieve that state. Also, the amount of data that can be stored is minimal.
  • The game screen does not navigate to the score screen when the user taps the End Game button.

You can resolve these issues using the app architecture components that you learn about in this codelab.

App architecture

App architecture is a way of designing your apps' classes, and the relationships between them, such that the code is organized, performs well in particular scenarios, and is easy to work with. In this set of four codelabs, the improvements that you make to the GuessTheWord app follow the Android app architecture guidelines, and you use Android Architecture Components. The Android app architecture is similar to the MVVM (model-view-viewmodel) architectural pattern.

The GuessTheWord app follows the separation of concerns design principle and is divided into classes, with each class addressing a separate concern. In this first codelab of the lesson, the classes you work with are a UI controller, a ViewModel, and a ViewModelFactory.

UI controller

A UI controller is a UI-based class such as Activity or Fragment. A UI controller should only contain logic that handles UI and operating-system interactions such as displaying views and capturing user input. Don't put decision-making logic, such as logic that determines the text to display, into the UI controller.

In the GuessTheWord starter code, the UI controllers are the three fragments: GameFragment, ScoreFragment, and TitleFragment. Following the "separation of concerns" design principle, the GameFragment is only responsible for drawing game elements to the screen and knowing when the user taps the buttons, and nothing more. When the user taps a button, this information is passed to the GameViewModel.

ViewModel

A ViewModel holds data to be displayed in a fragment or activity associated with the ViewModel. A ViewModel can do simple calculations and transformations on data to prepare the data to be displayed by the UI controller. In this architecture, the ViewModel performs the decision-making.

The GameViewModel holds data like the score value, the list of words, and the current word, because this is the data to be displayed on the screen. The GameViewModel also contains the business logic to perform simple calculations to decide what the current state of the data is.

ViewModelFactory

A ViewModelFactory instantiates ViewModel objects, with or without constructor parameters.

In later codelabs, you learn about other Android Architecture Components that are related to UI controllers and ViewModel.

The ViewModel class is designed to store and manage the UI-related data. In this app, each ViewModel is associated with one fragment.

In this task, you add your first ViewModel to your app, the GameViewModel for the GameFragment. You also learn what it means that the ViewModel is lifecycle-aware.

Step 1: Add the GameViewModel class

  1. Open the build.gradle(module:app) file. Inside the dependencies block, add the Gradle dependency for the ViewModel.

    If you use the latest version of the library, the solution app should compile as expected. If it doesn't, try resolving the issue, or revert to the version shown below.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. In the package screens/game/ folder, create a new Kotlin class called GameViewModel.
  2. Make the GameViewModel class extend the abstract class ViewModel.
  3. To help you better understand how the ViewModel is lifecycle-aware, add an init block with a log statement.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Step 2: Override onCleared() and add logging

The ViewModel is destroyed when the associated fragment is detached, or when the activity is finished. Right before the ViewModel is destroyed, the onCleared() callback is called to clean up the resources.

  1. In the GameViewModel class, override the onCleared() method.
  2. Add a log statement inside onCleared() to track the GameViewModel lifecycle.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Step 3: Associate GameViewModel with the game fragment

A ViewModel needs to be associated with a UI controller. To associate the two, you create a reference to the ViewModel inside the UI controller.

In this step, you create a reference of the GameViewModel inside the corresponding UI controller, which is GameFragment.

  1. In the GameFragment class, add a field of the type GameViewModel at the top level as a class variable.
private lateinit var viewModel: GameViewModel

Step 4: Initialize the ViewModel

During configuration changes such as screen rotations, UI controllers such as fragments are re-created. However, ViewModel instances survive. If you create the ViewModel instance using the ViewModel class, a new object is created every time the fragment is re-created. Instead, create the ViewModel instance using a ViewModelProvider.

How ViewModelProvider works:

  • ViewModelProvider returns an existing ViewModel if one exists, or it creates a new one if it does not already exist.
  • ViewModelProvider creates a ViewModel instance in association with the given scope (an activity or a fragment).
  • The created ViewModel is retained as long as the scope is alive. For example, if the scope is a fragment, the ViewModel is retained until the fragment is detached.

Initialize the ViewModel, using the ViewModelProviders.of() method to create a ViewModelProvider:

  1. In the GameFragment class, initialize the viewModel variable. Put this code inside onCreateView(), after the definition of the binding variable. Use the ViewModelProviders.of() method, and pass in the associated GameFragment context and the GameViewModel class.
  2. Above the initialization of the ViewModel object, add a log statement to log the ViewModelProviders.of() method call.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. Run the app. In Android Studio, open the Logcat pane and filter on Game. Tap the Play button on your device or emulator. The game screen opens.

    As shown in the Logcat, the onCreateView() method of the GameFragment calls the ViewModelProviders.of() method to create the GameViewModel. The logging statements that you added to the GameFragment and the GameViewModel show up in the Logcat.

  1. Enable the auto-rotate setting on your device or emulator and change the screen orientation a few times. The GameFragment is destroyed and re-created each time, so ViewModelProviders.of() is called each time. But the GameViewModel is created only once, and it is not re-created or destroyed for each call.
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. Exit the game or navigate out of the game fragment. The GameFragment is destroyed. The associated GameViewModel is also destroyed, and the callback onCleared() is called.
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!

The ViewModel survives configuration changes, so it's a good place for data that needs to survive configuration changes:

  • Put data to be displayed on the screen, and code to process that data, in the ViewModel.
  • The ViewModel should never contain references to fragments, activities, or views, because activities, fragments, and views do not survive configuration changes.

For comparison, here's how the GameFragment UI data is handled in the starter app before you add ViewModel, and after you add ViewModel:

  • Before you add ViewModel:
    When the app goes through a configuration change such as a screen rotation, the game fragment is destroyed and re-created. The data is lost.
  • After you add ViewModel and move the game fragment's UI data into the ViewModel:
    All the data that the fragment needs to display is now the ViewModel. When the app goes through a configuration change, the ViewModel survives, and the data is retained.

In this task, you move the app's UI data into the GameViewModel class, along with the methods to process the data. You do this so the data is retained during configuration changes.

Step 1: Move data fields and data processing to the ViewModel

Move the following data fields and methods from the GameFragment to the GameViewModel:

  1. Move the word, score, and wordList data fields. Make sure word and score are not private.

    Do not move the binding variable, GameFragmentBinding, because it contains references to the views. This variable is used to inflate the layout, set up the click listeners, and display the data on the screen—responsibilities of the fragment.
  2. Move the resetList() and nextWord() methods. These methods decide what word to show on the screen.
  3. From inside the onCreateView() method, move the method calls to resetList() and nextWord() to the init block of the GameViewModel.

    These methods must be in the init block, because you should reset the word list when the ViewModel is created, not every time the fragment is created. You can delete the log statement in the init block of GameFragment.

The onSkip() and onCorrect() click handlers in the GameFragment contain code for processing the data and updating the UI. The code to update the UI should stay in the fragment, but the code for processing the data needs to be moved to the ViewModel.

For now, put the identical methods in both places:

  1. Copy the onSkip() and onCorrect() methods from the GameFragment to the GameViewModel.
  2. In the GameViewModel, make sure the onSkip() and onCorrect() methods are not private, because you will reference these methods from the fragment.

Here is the code for GameViewModel class, after 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!")
   }
}

Here is the code for the GameFragment class, after 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()
   }
}

Step 2: Update references to click handlers and data fields in GameFragment

  1. In GameFragment, update the onSkip() and onCorrect() methods. Remove the code to update the score and instead call the corresponding onSkip() and onCorrect() methods on viewModel.
  2. Because you moved the nextWord() method to the ViewModel, the game fragment can no longer access it.

    In GameFragment, in the onSkip() and onCorrect() methods, replace the call to nextWord() with updateScoreText() and updateWordText(). These methods display the data on the screen.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. In the GameFragment, update the score and word variables to use the GameViewModel variables, because these variables are now in the GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. In the GameViewModel, inside the nextWord() method, remove the calls to the updateWordText() and updateScoreText() methods. These methods are now being called from the GameFragment.
  2. Build the app and make sure there are no errors. If you have errors, clean and rebuild the project.
  3. Run the app and play the game through some words. While you are in the game screen, rotate the device. Notice that the current score and the current word are retained after the orientation change.

Great job! Now all your app's data is stored in a ViewModel, so it is retained during configuration changes.

In this task, you implement the click listener for the End Game button.

  1. In GameFragment, add a method called onEndGame(). The onEndGame() method will be called when the user taps the End Game button.
private fun onEndGame() {
   }
  1. In GameFragment, inside the onCreateView() method, locate the code that sets click listeners for the Got It and Skip buttons. Just beneath these two lines, set a click listener for the End Game button. Use the binding variable, binding. Inside the click listener, call the onEndGame() method.
binding.endGameButton.setOnClickListener { onEndGame() }
  1. In GameFragment, add a method called gameFinished() to navigate the app to the score screen. Pass in the score as an argument, using 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. In the onEndGame() method, call the gameFinished() method.
private fun onEndGame() {
   gameFinished()
}
  1. Run the app, play the game, and cycle through some words. Tap the End Game button. Notice that the app navigates to the score screen, but the final score is not displayed. You fix this in the next task.

When the user ends the game, the ScoreFragment does not show the score. You want a ViewModel to hold the score to be displayed by the ScoreFragment. You'll pass in the score value during the ViewModel initialization using the factory method pattern.

The factory method pattern is a creational design pattern that uses factory methods to create objects. A factory method is a method that returns an instance of the same class.

In this task, you create a ViewModel with a parameterized constructor for the score fragment and a factory method to instantiate the ViewModel.

  1. Under the score package, create a new Kotlin class called ScoreViewModel. This class will be the ViewModel for the score fragment.
  2. Extend the ScoreViewModel class from ViewModel. Add a constructor parameter for the final score. Add an init block with a log statement.
  3. In the ScoreViewModel class, add a variable called score to save the final score.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. Under the score package, create another Kotlin class called ScoreViewModelFactory. This class will be responsible for instantiating the ScoreViewModel object.
  2. Extend the ScoreViewModelFactory class from ViewModelProvider.Factory. Add a constructor parameter for the final score.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. In ScoreViewModelFactory, Android Studio shows an error about an unimplemented abstract member. To resolve the error, override the create() method. In the create() method, return the newly constructed ScoreViewModel object.
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, create class variables for ScoreViewModel and ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. In ScoreFragment, inside onCreateView(), after initializing the binding variable, initialize the viewModelFactory. Use the ScoreViewModelFactory. Pass in the final score from the argument bundle, as a constructor parameter to the ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. In onCreateView(), after initializing viewModelFactory, initialize the viewModel object. Call the ViewModelProviders.of() method, pass in the associated score fragment context and viewModelFactory. This will create the ScoreViewModel object using the factory method defined in the viewModelFactory class.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. In onCreateView() method, after initializing the viewModel, set the text of the scoreText view to the final score defined in the ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Run your app and play the game. Cycle through some or all the words and tap End Game. Notice that the score fragment now displays the final score.

  1. Optional: Check the ScoreViewModel logs in the Logcat by filtering on ScoreViewModel. The score value should be displayed.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

In this task, you implemented ScoreFragment to use ViewModel. You also learned how to create a parameterized constructor for a ViewModel using the ViewModelFactory interface.

Congratulations! You changed the architecture of your app to use one of the Android Architecture Components, ViewModel. You resolved the app's lifecycle issue, and now the game's data survives configuration changes. You also learned how to create a parameterized constructor for creating a ViewModel, using the ViewModelFactory interface.

Android Studio project: GuessTheWord

  • The Android app architecture guidelines recommend separating classes that have different responsibilities.
  • A UI controller is UI-based class like Activity or Fragment. UI controllers should only contain logic that handles UI and operating system interactions; they shouldn't contain data to be displayed in the UI. Put that data in a ViewModel.
  • The ViewModel class stores and manages UI-related data. The ViewModel class allows data to survive configuration changes such as screen rotations.
  • ViewModel is one of the recommended Android Architecture Components.
  • ViewModelProvider.Factory is an interface you can use to create a ViewModel object.

The table below compares UI controllers with the ViewModel instances that hold data for them:

UI controller

ViewModel

An example of a UI controller is the ScoreFragment that you created in this codelab.

An example of a ViewModel is the ScoreViewModel that you created in this codelab.

Doesn't contain any data to be displayed in the UI.

Contains data that the UI controller displays in the UI.

Contains code for displaying data, and user-event code such as click listeners.

Contains code for data processing.

Destroyed and re-created during every configuration change.

Destroyed only when the associated UI controller goes away permanently—for an activity, when the activity finishes, or for a fragment, when the fragment is detached.

Contains views.

Should never contain references to activities, fragments, or views, because they don't survive configuration changes, but the ViewModel does.

Contains a reference to the associated ViewModel.

Doesn't contain any reference to the associated UI controller.

Udacity course:

Android developer documentation:

Other:

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

To avoid losing data during a device-configuration change, you should save app data in which class?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Question 2

A ViewModel should never contain any references to fragments, activities, or views. True or false?

  • True
  • False

Question 3

When is a ViewModel destroyed?

  • When the associated UI controller is destroyed and recreated during a device-orientation change.
  • In an orientation change.
  • When the associated UI controller is finished (if it's an activity) or detached (if it's a fragment).
  • When the user presses the Back button.

Question 4

What is the ViewModelFactory interface for?

  • Instantiating a ViewModel object.
  • Retaining data during orientation changes.
  • Refreshing the data being displayed on the screen.
  • Receiving notifications when the app data is changed.

Start the next lesson: 5.2: LiveData and LiveData observers

For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.