1. Welcome
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. TheViewModel
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 theViewModel
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
, andViewModelFactory
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 interfaceViewModelProvider.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 aViewModel
object with constructor parameters.
2. App overview
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.)
3. Task: Explore the starter code
In this task, you download and run the starter app and examine the code.
Step 1: Get started
- Download the GuessTheWord - Starter code and open the project in Android Studio.
- Run the app on an Android-powered device, or on an emulator.
- 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
- In Android Studio, explore the code to get a feel for how the app works.
- 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 screengame/GameFragment
for the game screenscore/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 theresetList()
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 thenextWord()
method. - The
onCorrect()
method is the click handler for the Got It button. This method is implemented similarly to theonSkip()
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.
4. Task: Find problems in the starter app
In this task, you find issues with the GuessTheWord starter app.
- Run the starter code and play the game through a few words, tapping either Skip or Got It after each word.
- 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.
- 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.
- 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 theonSaveInstanceState()
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
.
5. Task: Create the GameViewModel
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
- Open the
build.gradle(module:app)
file. Inside thedependencies
block, add the Gradle dependency for theViewModel
.
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-viewmodel-ktx:2.2.0'
- In the package
screens/game/
folder, create a new Kotlin class calledGameViewModel
. - Make the
GameViewModel
class extend the abstract classViewModel
. - To help you better understand how the
ViewModel
is lifecycle-aware, add aninit
block with alog
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.
- In the
GameViewModel
class, override theonCleared()
method. - Add a log statement inside
onCleared()
to track theGameViewModel
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
.
- In the
GameFragment
class, add a field of the typeGameViewModel
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 existingViewModel
if one exists, or it creates a new one if it does not already exist.ViewModelProvider
creates aViewModel
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, theViewModel
is retained until the fragment is detached.
Initialize the ViewModel
, using the ViewModelProvider.get()
method to create a ViewModelProvider
:
- In the
GameFragment
class, initialize theviewModel
variable. Put this code insideonCreateView()
, after the definition of the binding variable. Use theViewModelProvider.get()
method, and pass in the associatedGameFragment
context and theGameViewModel
class. - Above the initialization of the
ViewModel
object, add a log statement to log theViewModelProvider.get()
method call.
Log.i("GameFragment", "Called ViewModelProvider.get")
viewModel = ViewModelProvider(this).get(GameViewModel::class.java)
- 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 ViewModelProvider.get()
method to create the GameViewModel
. The logging statements that you added to the GameFragment
and the GameViewModel
show up in the Logcat.
I/GameFragment: Called ViewModelProvider.get I/GameViewModel: GameViewModel created!
- 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, soViewModelProvider.get()
is called each time. But theGameViewModel
is created only once, and it is not re-created or destroyed for each call.
I/GameFragment: Called ViewModelProvider.get I/GameViewModel: GameViewModel created! I/GameFragment: Called ViewModelProvider.get I/GameFragment: Called ViewModelProvider.get I/GameFragment: Called ViewModelProvider.get
- Exit the game or navigate out of the game fragment. The
GameFragment
is destroyed. The associatedGameViewModel
is also destroyed, and the callbackonCleared()
is called.
I/GameFragment: Called ViewModelProvider.get I/GameViewModel: GameViewModel created! I/GameFragment: Called ViewModelProvider.get I/GameFragment: Called ViewModelProvider.get I/GameFragment: Called ViewModelProvider.get I/GameViewModel: GameViewModel destroyed!
6. Task: Populate the GameViewModel
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 theViewModel
: All the data that the fragment needs to display is now theViewModel
. When the app goes through a configuration change, theViewModel
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
:
- Move the
word
,score
, andwordList
data fields. Make sureword
andscore
are notprivate
.
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 GameViewModel
.
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:
- Copy the
onSkip()
andonCorrect()
methods from theGameFragment
to theGameViewModel
. - In the
GameViewModel
, make sure theonSkip()
andonCorrect()
methods are notprivate
, 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() {
score--
nextWord()
}
fun onCorrect() {
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 ViewModelProvider.get")
viewModel = ViewModelProvider(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() {
score--
nextWord()
}
private fun onCorrect() {
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
- In
GameFragment
, update theonSkip()
andonCorrect()
methods. Remove the code to update the score and instead call the correspondingonSkip()
andonCorrect()
methods onviewModel
. - Because you moved the
nextWord()
method to theViewModel
, 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()
}
- In the
GameFragment
, update thescore
andword
variables to use theGameViewModel
variables, because these variables are now in theGameViewModel
.
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
- In the
GameViewModel
, inside thenextWord()
method, remove the calls to theupdateWordText()
andupdateScoreText()
methods. These methods are now being called from theGameFragment
. - Build the app and make sure there are no errors. If you have errors, clean and rebuild the project.
- 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.
7. Task: Implement click listener for the End Game button
In this task, you implement the click listener for the End Game button.
- In
GameFragment
, add a method calledonEndGame()
. TheonEndGame()
method will be called when the user taps the End Game button.
private fun onEndGame() {
}
- In
GameFragment
, inside theonCreateView()
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 theonEndGame()
method.
binding.endGameButton.setOnClickListener { onEndGame() }
- In
GameFragment
, add a method calledgameFinished()
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)
}
- In the
onEndGame()
method, call thegameFinished()
method.
private fun onEndGame() {
gameFinished()
}
- 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.
8. Task: Use a ViewModelFactory
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
.
- Under the
score
package, create a new Kotlin class calledScoreViewModel
. This class will be theViewModel
for the score fragment. - Extend the
ScoreViewModel
class fromViewModel.
Add a constructor parameter for the final score. Add aninit
block with a log statement. - In the
ScoreViewModel
class, add a variable calledscore
to save the final score.
class ScoreViewModel(finalScore: Int) : ViewModel() {
// The final score
var score = finalScore
init {
Log.i("ScoreViewModel", "Final score is $finalScore")
}
}
- Under the
score
package, create another Kotlin class calledScoreViewModelFactory
. This class will be responsible for instantiating theScoreViewModel
object. - Extend the
ScoreViewModelFactory
class fromViewModelProvider.Factory
. Add a constructor parameter for the final score.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
- In
ScoreViewModelFactory
, Android Studio shows an error about an unimplemented abstract member. To resolve the error, override thecreate()
method. In thecreate()
method, return the newly constructedScoreViewModel
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")
}
- In
ScoreFragment
, create class variables forScoreViewModel
andScoreViewModelFactory
.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
- In
ScoreFragment
, insideonCreateView()
, after initializing thebinding
variable, initialize theviewModelFactory
. Use theScoreViewModelFactory
. Pass in the final score from the argument bundle, as a constructor parameter to theScoreViewModelFactory()
.
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(requireArguments()).score)
- In
onCreateView(
), after initializingviewModelFactory
, initialize theviewModel
object. Call theViewModelProvider.get()
method, pass in the associated score fragment context andviewModelFactory
. This will create theScoreViewModel
object using the factory method defined in theviewModelFactory
class.
viewModel = ViewModelProvider(this, viewModelFactory)
.get(ScoreViewModel::class.java)
- In
onCreateView()
method, after initializing theviewModel
, set the text of thescoreText
view to the final score defined in theScoreViewModel
.
binding.scoreText.text = viewModel.score.toString()
- 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.
- Optional: Check the
ScoreViewModel
logs in the Logcat by filtering onScoreViewModel
. 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.
9. Solution code
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
10. Summary
- The Android app architecture guidelines recommend separating classes that have different responsibilities.
- A UI controller is UI-based class like
Activity
orFragment
. 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 aViewModel
. - The
ViewModel
class stores and manages UI-related data. TheViewModel
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 aViewModel
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 | An example of a |
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 |
Contains a reference to the associated | Doesn't contain any reference to the associated UI controller. |
11. Learn more
Udacity course:
Android developer documentation:
- ViewModel Overview
- Handling Lifecycles with Lifecycle-Aware Components
- Guide to app architecture
ViewModelProvider
ViewModelProvider.Factory
Other:
- MVVM (model-view-viewmodel) architectural pattern.
- Separation of concerns (SoC) design principle
- Factory method pattern
12. Homework
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.
13. Next codelab
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.