Lesson 05: App Architecture (UI Layer)
With Architecture Components you’ll have the power to design even the most complicated app ideas. Combine ViewModels with LiveData to build this super fun “Guess it” game.
- Many Android teams will follow an application architecture, which is a great upon set of design rules.
- Architecture provides the basic bones and structure of your app.
- A well-thought-out architecture can make your code maintainable for years and help your team collaborate.
Tour of the App
MainActivity- This class does very little. It’s just a container for the NavHostFragment. The fragments that go in the NavHostFragment do most of the heavy lifting. This is similar to what we did in lesson 3res/navigation/main_navigation.xml- This is the navigation graph for this app. The navigation flow goes from the TitleFragment to the GameFragment to the ScoreFragment. From the ScoreFragment, you can play the game again by going back to the GameFragment. Note that the action from the GameFragment to the ScoreFragment has a Pop To attribute that removes the game from the backstack. This makes it so that you can never press the back button from the ScoreFragment to go back to a finished game. Instead, you’ll go to the title screen.TitleFragment- This is a simple fragment showing the title screen. Has a single button that takes you to the GameFragment.GameFragment- This fragment contains all the logic for the GuessIt game itself. It contains:word- A variable for the current word to guess.score- A variable for the current score.wordList- A variable for a mutable list of all the words you need to guess. This list is created and immediately shuffled usingresetList()so that you get a new order of words every time.resetList()- Method that creates and shuffles the list of words.onSkip()/onCorrect()- Methods for when you press the Skip/Got It buttons. They modify the score and then go to the next word in yourworldList.nextWord()- A method for moving to the next word to guess. If there are still words in your mutable list of words, remove the current word, and then setcurrentWordto whatever is next in the list. This will finish the game if there are no more words to guess.gameFinished()- A method that is called to finish the game. This passes your current score to the ScoreFragment using SafeArgs.
ScoreFragment- This fragment gets the score passed in from the argument bundle and displays it. There’s also a button for playing again, that takes you back to the GameFragment.
What is Architecture
-
A bloated class that does all sorts of different functions is a bad idea.
-
Application architecture is a way of designing your app’s classes and the relationship between them, such that the code base is more organized, performative particular scenarios, and easier to work.
-
There’s no one right way to architect an application much like there’s no empirically right way to architect a house.
-
Architecture is heavily dependent on your circumstance needs and tastes.
-
There are many different architectural styles for Android apps, each with different strengths and weaknesses. Some of these styles may even overlap. They fit different application needs team sizes team dynamics and more.
-
For this lesson, you’re gonna be learning about a single, multi-purpose architectural pattern that we’re gonna build on for the rest of this course. It’s loosely based on an architecture called
MVVM, which is model view viewmodel. The reason I’m choosing this style is because it’s officially endorsed by Google and it leverages the lifecycle classes.
Separation of Concerns
Divide your code into classes, each with separate, well-defined responsibilities.
Our App Architecture
-
Architecture gives you guidelines to figure out which classes should have what responsibilities in your app.
-
We’re going to be working with three different classes: the
UI Controller, theViewModelandLiveData.
The first class is the UI Controller:
-
UI Controlleris the word that I’m using to describe what activities and fragments are. -
UI Controlleris responsible for any user interface related tasks like displaying views and capturing user input. -
By design,
UI Controllersexecute all of the draw commands that put our views on the screen. -
Also when an operating system event happens like when a user presses a button it’s the
UI Controllerthat get notified first. -
You should limit
UI Controllerto only user interface related tasks and take any sort of decision-making power out ofUI Controllerso whileUI Controlleris responsible for drawing a text to you to the screen. It is not responsible for the calculations or processing that decides what actual text to draw. -
While
UI Controlleris what knows that a button has been pressed it’s going to immediately pass that information along to the second class in our architecture theViewModel.
ViewModel will do the actual decision-making.
-
The purpose of the
ViewModelis to hold the specific data needed to display the fragment or activity it’s associated with. -
Also,
ViewModelsmay do simple calculations and transformations on that data so that it’s ready to be displayed by theUI Controller. -
The
ViewModelclass will contain instances of a third-classLiveData.
LiveData classes are crucial for communicating information for the ViewModel to UI Controller that it should update and redraw the screen.
In our case, the UI Controllers will be our three fragments. Let’s take game fragment as an example:
-
Game fragment will be responsible for drawing the game elements to the screen and knowing when the user presses the buttons nothing more.
-
When the buttons are pressed game, fragment will pass that information to the game
ViewModel. -
The game
ViewModelwill hold data like the score value, the list of words, and the current word to be displayed because that’s the data that is needed to know what to display on the screen. It’ll also be in charge of simple calculations to decide the current state of the data. For example what the current word is in the list of words and what the current score should be.
ViewModel
-
ViewModelis an abstract class that you will extend and then implement. It holds your apps UI data and survives configuration changes. -
Instead of having the UI data the fragment move it to your
ViewModeland have the fragment reference theViewModel. -
ViewModelsurvives configuration changes so while the fragment is destroyed and then remade all of that juicy data that you need to display in the fragment like the score the current word and so on remain in theViewModel. -
If you reconnect your recreated fragment to the same
ViewModelall of the data is just right there for you. -
Unlike the
onSavedInstanceStatebundle theViewModelhas no restrictions on size so you can store lots of data in here without worrying
Adding A ViewModel
-
Add lifecycle-extensions gradle dependency:
In the Module: app
build.gradlefile, add thelifecycle-extensionsdependency. You can find the most current version of the dependency here.
// Lifecycles
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'- Create the
GameViewModelclass, extendingViewModel: Create a new file calledGameViewModel.ktin thejava/com.example.android.guesstheword/gamepackage. Then in this file, create a classGameViewModelthat extendsViewModel:
class GameViewModel : ViewModel()-
Add
initblock and overrideonCleared. Add log statements to both:Make an
initblock that prints out a log saying “GameViewModel created!”.
init {
Log.i("GameViewModel", "GameViewModel created!")
}Then override onCleared so you can track the lifetime of this ViewModel. You can use the keyboard shortcut Ctrl + O to do the override. Then add the log statement saying “GameViewModel destroyed!” to onCleared.
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}-
Create and initialize a
GameViewModel, usingViewModelProvider. Add a log statement:Back in GameFragment use
lateinitto create a field forGameViewModelcalledviewModel.
private lateinit var viewModel: GameViewModelThen in onCreateView, request the current GameViewModel using the ViewModelProvider class:
// Get the viewmodel
Log.i("GameFragment", "Called ViewModelProvider")
viewModel = ViewModelProvider(this).get(GameViewModel::class.java)Note 1: Sharing UI-related data between destinations with ViewModel : You can save ViewModel state between fragments with navigation by initializing it scoped to a navigation graph.
val viewModel: MyViewModel by navGraphViewModels(R.id.my_graph)Note 2: :: Usage in Kotlin:
- The keyword
::is used for Reflection in Kotlin:
- Class Reference
val myClass = MyClass::class - Function Reference
list::isEmpty() - Property Reference
::someVal.isInitialized - Constructor Reference
::MyClass
What should be moved to the ViewModel?
UI Controlleronly displays and gets user/OS events. It doesn’t make decisions.- The ViewModel is a stable place to store the data to display in the associated
UI controller. - The
Fragmentdraws the data on screen and captures input events. It should not decide what to display on screen or process what happens during an input event. - The
ViewModelnever contains references to activities, fragments, or views.
So, in our app, The score and word field, wordList field, and resetList and nextWord methods should be moved to the ViewModel.
-
Move the
word,score, andwordListvariables to theGameViewModel:In the
GameFragmentfind theword,score, andwordListvariables, and move them toGameViewModel. Make sure you delete the versions of these variables inGameFragment.
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>Remember, do not move the binding! This is because the binding contains references to views.
-
Move methods
resetList,nextWord,onSkip, andonCorrectto theGameViewModel:In
GameFragment, find each of the methods:resetList,nextWord,onSkipandonCorrect. Move them toGameViewModel.Remove the private modifier
onSkipandonCorrectmethods so they can be called from theGameFragment.
/** Methods for buttons presses **/
fun onSkip() {
score--
nextWord()
}
fun onCorrect() {
score++
nextWord()
}-
Move the initialization methods to the
GameViewModel:Initialization in the
GameFragmentinvolved callingresetListandnextWord. Now that they are both in theGameViewModel, call them in theGameViewModelwhen it is created.
init {
resetList()
nextWord()
}-
Update the
onClickListenersto refer to call methods in theViewModel, and then update the UI:Now that
onSkipandonCorrecthave been moved to theGameViewModel, theOnClickListenersin theGameFragment, refer to method that aren’t there. Update theOnClickListenersto call the methods in theGameViewModel. Then in theOnClickListeners, update the score and word texts so that they have the newest data.
binding.correctButton.setOnClickListener {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}
binding.skipButton.setOnClickListener {
viewModel.onSkip()
updateScoreText()
updateWordText()
}-
Update the methods to get
wordandscorefrom theviewModel:In the
GameViewModel, remove the private modifier fromwordandscore.In the
GameFragment, updategameFinished,updateWordTextandupdateScoreTextto get the data from thegameViewModel.
/**
* Called when the game is finished
*/
private fun gameFinished() {
val action = GameFragmentDirections.actionGameToScore(viewModel.score)
findNavController(this).navigate(action)
}/** Methods for updating the UI **/
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}-
Do final cleanup in the
GameViewModel:Once you’ve copied over the variables and methods, remove any code that refers to the
GameFragment. In theGameViewModel, comment out the reference togameFinishedinnextWord. You’ll deal with this later. You can also clean up the log statements from the prior step.
The Benefits of a Good Architecture
The separation of concerns design principle helps us in two ways:
- The code is more organized, manageable, and debuggable.
- By moving code to the
ViewModelyou protect yourself from having to worry about lifecycle problems and bugs.
Another aspect of this design is:
-
The code is very modular.
UI Controllerknows about theViewModelbut theViewModeldoesn’t know about theUI Controller. One of the goals of this architecture is to keep the number of references between classes small. This modularity allows us to swap out a single class for a different implementation without needing to update a bunch of other classes in your app. For example, you could redesign the UI and you wouldn’t necessarily need to make changes to theViewModel -
ViewModelcontains no references to activities fragments or views.This happens to be helpful for testing. Testing on Android differentiates between tests that can be run without emulating the android framework which is in the test folder and tests that are more heavyweight that require emulating the android framework. these tests can be found in the Android test folder.
By designing your app so that these Android classes aren’t referenced in the
ViewModelyou can run pure lightweight unit tests that don’t depend on the android framework code and therefore run faster and are easier to write.
The Power and Limits of the ViewModel
By adding the ViewModel you’ve actually fixed the rotation issue. The ViewModel does preserve data with the configuration changes, but the data is still lost after the app is shut down by the OS.
The proper way to preserve data even if the app is terminated usually involves using a combination of onSavedInstance and data persistence.
The second issue is that we have related functions that should be combined in one file, but since we are separating the logic and UI controller it’s okay to keep it like this.
Both of these issues boil down to the fact that there’s no way to communicate back to the fragment from the view. And since you should not have references to the fragment in your view we need another thing to deal with a situation like this.
LiveData
We need a way to communicate for the ViewModel back to the UI controller without having the ViewModel store references to any views activities or fragments.
For example, we need the ViewModel to communicate when data like the score has changed so that the game fragment knows to actually redraw the score.
What would be really great is if we could change the data in the ViewModel and then just have the screen magically know when to update itself. Fortunately, live data can do this for us.
LiveData is an observable data holder class that is lifecycle-aware.
-
LiveDataholds data this means that it wraps around some data. For example, we’ll have live data wrap around the current score. -
LiveDatais also observable.
The Observer pattern is where you have an object called a Subject. The subject keeps track of a list of other objects known as Observers. Observers watch or observe the subject when the status of the subject changes. it notifies the observers by calling a method in the observer.
In LiveData’s case, the subject is the live data object and the observers are the UI controllers. The state change is whenever the data wrapped inside of live data changes.
By setting up this observer relationship you could have the ViewModel communicate data changes back to the UI Controller.
The LiveData object should be val as it will always stay the same. Although the
data is stored within it might change.
Adding LiveData to GameViewModel
-
Wrap
wordandscoreinMutableLiveData:Since
MutableLiveDatais generic we need to specify the type.
val word = MutableLiveData<String>()
val score = MutableLiveData<Int>()- Initialize
score.valueto 0 becauseMutableLiveDatais nullable.
init {
resetList()
nextWord()
// Initialize score.value to 0
score.value = 0
}-
Change references to
scoreandwordtoscore.valueandword.valueand add the required null safety checks:Check the
onSkip()andonCorrect()methods and change references. Add null safety checks, then call the minus and plus functions, respectively.
fun onSkip() {
// score--
score.value = (score.value)?.minus(1)
nextWord()
}
fun onCorrect() {
// score++
score.value = (score.value)?.plus(1)
nextWord()
}-
Set up the observation relationship for the
scoreandwordLiveDatas:Move over to
GameFragment. UI controllers are where you’ll set up the observation relationship. Get theLiveDatafrom yourviewModeland call theobservemethod. Make sure to pass inthisand then an observer lambda. Move the code to update the scoreTextViewand the wordTextViewto your Observers. Here’s the code to set up an observation relationship for the score. This should be inGameFragment.onCreate:
// Setup the LiveData observation relationship by getting the LiveData from your
// ViewModel and calling observe. Make sure to pass in *this* and then an Observer lambda
/** Setting up LiveData observation relationship **/
viewModel.word.observe(viewLifecycleOwner, { newWord ->
binding.wordText.text = newWord
})
viewModel.score.observe(viewLifecycleOwner, { newScore ->
binding.scoreText.text = newScore.toString()
})And now you can remove updateWordText() and updateScoreText() any references to them completely.
-
Add a null safety check when passing
currentScorein thegameFinishedmethod:viewModel.score.valuecan possibly be null, so add a null safety check in thegameFinishedmethod:
val currentScore = viewModel.score.value ?: 0Lifecycle-Awareness for LiveData
-
LiveData is aware of its associated Ul controller lifecycle state.
-
Ul controller off-screen, no updates. (Only updates when it’s on the foreground)
-
Ul controller back on-screen, get current data. (Immediately send the freshest data when it comes back to foreground)
-
New Ul controller observes, get current data. (Immediately send the freshest data when a new Ul controller observes it)
-
Ul controller destroyed, it cleans up connection after itself.
Adding LiveData Encapsulation to GameViewModel
-
Encapsulation is the notion of restricting the direct access to objects fields. This way you could expose a public set of methods that modify the private internal fields and you can control the exact ways outside classes can and can’t manipulate these internal fields.
-
You should try to restrict edit access to your
LiveData. In this case, only theViewModelshould be updating the score and word fields. But for this observation code to work, it’s important that you could still get some access to theLiveDataso it can’t be completely private. -
Being able to write and read from
LiveDatais where the distinction betweenMutableLiveDataand just plain oldLiveDatacomes.MutableLiveDatais aLiveDatathat can be mutated or modified, whereasLiveDataon the other hand isLiveDatathat you could read but you cannot call set value on. -
Inside the
ViewModelwe want theLiveDatato beMutableLiveData, but outside theViewModelwe want theMutableLiveDatato only be exposed asLiveData. To do that we can use a concept in Kotlin known as a backing property. -
A backing property allows you to return something for a getter other than the exact object.
-
Make internal and external versions of
wordandscore:Open up
GameViewModel. The internal version should be aMutableLiveData, has an underscore in front of its name, and be private. The underscore is our convention for marking the variable as the internal version of a variable.The external version should be a
LiveDataand not have an underscore in front of its name. -
Make a backing property for the external version that returns the internal
MutableLiveDataas aLiveData:Kotlin automatically makes getters and setters for your fields. If you want to override the getter for
score, you can do so using a backing property. You’ve actually already defined your backing properties (_scoreand_word). Now you can take your public versions of the variables (scoreandword) and overridegetto return the backing properties.
// Make an internal and external version of the word and score
// The internal version should be a MutableLiveData, have an underscore in front of its' name and be private
// The external version should be a LiveData
// The current word
private val _word = MutableLiveData<String>()
val word: LiveData<String>
// Make a backing property for the external version that
// returns the internal MutableLiveData as a LiveData
get() = _word
// The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _scoreBy making the return type LiveData rather than MutableLiveData, you’ve exposed only score and word as LiveData.
-
In the view model, use the internal, mutable versions of
scoreandword:In
GameViewModel, update the code so that you use the mutable versions,_scoreand_word, throughout the view model.
Event vs. State
LiveData keeps track of data State, like button color and score value. But Navigating to another screen is an example of an Event.
An Event happens once and it’s done until it’s triggered again. For example:
- A notification
- A sound playing when a button is pressed
- Navigating to a different screen
LiveData And Events
- Make a properly encapsulated
LiveDatacalledeventGameFinishin theGameViewModelthat holds a boolean and will represent game end Event:
// Event which triggers the end of the game
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
get() = _eventGameFinishAlso, initialize its value to false.
_eventGameFinish.value = false-
Make the function
onGameFinishCompletewhich makes the value ofeventGameFinishfalse:This function simply sets the value of
_eventGameFinishto false. This is to signal that you’ve handled the game finish event and that you don’t need to handle it again. In this specific example, it’s a way to say you’ve done the navigation.
/** Methods for completed events **/
fun onGameFinishComplete() {
_eventGameFinish.value = false
}-
Set
eventGameFinishto true, to signify that the game is over:In the
GameViewModel, find the condition where thewordListis empty. If it’s empty, the game is over, so signal that by setting the value ofeventGameFinishto true.
if (wordList.isEmpty()) {
// Set eventGameFinish to true, to signify that the game is over
_eventGameFinish.value = true
} else {
_word.value = wordList.removeAt(0)
}-
Add an observer of
eventGameFinish:Back in
GameFragment, add an observer ofeventGameFinish.In the observer lambda you should:
-
Make sure that the boolean holding the current value of
eventGameFinishedis true. This means the game has finished. -
If the game has finished, call
gameFinished. -
Tell the view model that you’ve handled the game finished event by calling
onGameFinishComplete.
-
// Add an observer of eventGameFinish which, when eventGameFinish is true, calls gameFinished()
// Make sure to call onGameFinishCompete to tell your viewmodel that the game finish event was dealt with
// Sets up event listening to navigate the player when the game is finished
viewModel.eventGameFinish.observe(viewLifecycleOwner, {isFinished ->
if (isFinished) {
gameFinished()
viewModel.onGameFinishComplete()
}
})Adding a Timer
Where should the timer go? GameViewModel
We put the code for tracking and counting downtime in the fragment that would get destroyed whenever you rotated the phone. The logic for counting down a timer should go in the game view model. The only thing the fragment should worry about is updating this textview as the timer ticks.
-
Copy the provided companion object with the timer constants:
In
GameViewModel, copy the following companion object code. This companion object has constants for our timer:
companion object {
// These represent different important times
// This is when the game is over
const val DONE = 0L
// This is the number of milliseconds in a second
const val ONE_SECOND = 1000L
// This is the total time of the game
const val COUNTDOWN_TIME = 60000L
}Feel free to change the COUNTDOWN_TIME constant so that the game doesn’t last a whole minute. This can be helpful for running the app to check whether it’s working.
-
Create a properly encapsulated
LiveDatafor the current time calledcurrentTime:Use the same method as you did earlier to encapsulate
LiveDataforscoreandword. The type ofcurrentTimeshould be of type Long.
// The current time
private val _currentTime = MutableLiveData<Long>()
val currentTime: LiveData<Long>
get() = _currentTime- Create a
timerfield of type CountDownTimer in theGameViewModel: You don’t need to worry about initializing it yet. Just declare it and ignore the error for now.
private val timer: CountDownTimer-
Copy over the
CountDownTimercode and then updatecurrentTimeandeventGameFinishappropriately as the timer ticks and finishes:In the
initofGameViewModel, copy theCountDownTimercode below:
// Creates a timer which triggers the end of the game when it finishes
timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) {
override fun onTick(millisUntilFinished: Long) {
_currentTime.value = (millisUntilFinished / ONE_SECOND)
}
override fun onFinish() {
_currentTime.value = DONE
_eventGameFinish.value = true
}
}
timer.start()-
Update the logic in the
nextWordfunction so that it doesn’t end the game:The game should finish when the timer runs out not when there are no words left in the list. If there are no words in the list, you should add the words back to the list and re-shuffle the list.
You can do this using
resetList. Update the code so that it doesn’t end the game, but instead callsresetList.
private fun nextWord() {
if (wordList.isEmpty()) {
// Update this logic so that the game doesn't finish;
// Instead the list is reset and re-shuffled when you run out of words
resetList()
}
_word.value = wordList.removeAt(0)
}-
Cancel the timer in
onCleared:To avoid memory leaks, you should always cancel a
CountDownTimerif you no longer need it. To do that, you can call:
// Cancel the timer in onCleared
override fun onCleared() {
super.onCleared()
timer.cancel()
}-
Update the UI:
You want the
timerTexton the screen to show the appropriate time. Figure out how to useLiveDatafrom theGameViewModelto do this - remember, you’ve done something similar forscoreandword. You’ll need to convert the Long for the timer into a String. You can use the DateUtils tool to do that.
// Setup an observer relationship to update binding.timerText
// You can use DateUtils.formatElapsedTime to correctly format the long to a time string
viewModel.currentTime.observe(viewLifecycleOwner, { newTime ->
binding.timerText.text = DateUtils.formatElapsedTime(newTime)
})ViewModelFactory
A class that knows how to create ViewModels.
There’s one challenge with the score fragment and that is that you get the score data passed in from this arguments bundle, you want to display this immediately so this should probably be given to the ViewModel when it’s initialized.
There are two ways to do this. One is to make a setter for the score variable in the ViewModel and then to call it from onCreateView within the score fragment. The other is to add a constructor for the ViewModel.
ViewModel: Adding a constructor
- Create a
ViewModelthat takes in a constructor parameter - Make a
ViewModelFactory forViewModel - Have factory construct
ViewModelwith constructor parameter - Add
ViewModelFactory when usingViewModelProviders
Adding a ViewModelFactory
In this exercise, you’ll pass data into a ViewModel. You’ll create a view model factory that allows you to define a custom constructor for a ViewModel that gets called when you use ViewModelProvider.
-
Create the
ScoreViewModelclass and have it take in an integer constructor parameter calledfinalScore:Make sure to create the
ScoreViewModelclass file in the same package as theScoreFragment.
class ScoreViewModel(finalScore: Int) : ViewModel() {
}-
Copy over ScoreViewModelFactory:
Create
ScoreViewModelfactory in the same package as theScoreFragment. You can use this code later if you ever need to create a view model factory.Note that the constructor of your view model factory should take any parameters you want to pass into your
ScoreViewModel. In this case, it takes in the final score.In the overridden create method, construct and return an instance of
ScoreViewModel, passing infinalScore:The create method’s purpose is to create and return your view model. So you should construct a new
ScoreViewModeland return it. You’ll also need to deal with the generics, so the statement will be:
return ScoreViewModel(finalScore) as TAnd the full code will be:
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
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")
}
}-
Create and construct a
ScoreViewModelFactory:In
ScoreFragment, createviewModelFactoryfromScoreViewModelFactory.
// Get args using by navArgs property delegate
val scoreFragmentArgs by navArgs<ScoreFragmentArgs>()
// Create and construct a ScoreViewModelFactory
viewModelFactory = ScoreViewModelFactory(scoreFragmentArgs.score)- Create
ScoreViewModelby usingViewModelProvideras usual, except you’ll also pass in yourScoreViewModelFactory:
viewModel = ViewModelProvider(this, viewModelFactory)
.get(ScoreViewModel::class.java)By passing in the ViewModel factory, you’re telling ViewModelProvider to use this factory to create ScoreViewModel.
Note : Sharing UI-related data between destinations with ViewModel using ViewModelFactory:
private val winFragmentArgs by navArgs<WinFragmentArgs>()
private val viewModel: WinViewModel by navGraphViewModels(R.id.navigation) {
WinViewModelFactory(
winFragmentArgs.winnerPlayer
)
}-
Add a
LiveDatafor the score and the play again event:Create LiveData for
scoreandeventPlayAgainusing the best practices for encapsulation and event handling that you’ve learned. Make sure to initializescore’s value to thefinalScoreyou pass into the view model.
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
get() = _eventPlayAgain
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _score
init {
_score.value = finalScore
}-
Convert
ScoreFragmentto properly observe and useScoreViewModelto update the UI:In
ScoreFragment, add observers forscoreandeventPlayAgainLiveData. Use them to update the UI.
// Add observer for score
viewModel.score.observe(viewLifecycleOwner, { newScore ->
binding.scoreText.text = newScore.toString()
})// Navigates back to title when button is pressed
viewModel.eventPlayAgain.observe(viewLifecycleOwner, { playAgain ->
if (playAgain) {
findNavController().navigate(ScoreFragmentDirections.actionRestart())
viewModel.onPlayAgainComplete()
}
})In ScoreViewModel, helper functions to work LiveData Events.
fun onPlayAgain() {
_eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
_eventPlayAgain.value = false
}Adding ViewModel to Data Binding
In this exercise, you’re going to use data binding in the layout XML code to communicate directly with the ViewModel. In particular, we’re going to tell the ViewModel when various buttons are clicked!
-
Add a
GameViewModeldata binding variable toGameFragmentlayout:In the layout xml file for the
GameFragment(game_fragment.xml), create agameViewModelvariable inside the layout.
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>Then pass the GameViewModel into the data binding:
In the GameFragment.onCreate, pass in the view model to the GameFragmentBinding.
// Pass the GameViewModel into the data binding - then you can remove the
binding.gameViewModel = viewModel-
In the
GameFragmentlayout, use the view model variable and data binding to handle clicking:In XML, you can define an
onClickattribute for buttons. Using data binding, you can define a data binding expression which is a Listener binding. Essentially this means that you define theOnClickListenerin the XML. You also have your view model variable available via data binding. So to create anonClickattribute that will callonSkipin the view model, you can use:
android:onClick="@{() -> gameViewModel.onSkip()}"Now you can (and should) remove the OnClickListener setup from the GameFragment. Everything should work just as before.
-
Add a
ScoreViewModeldata binding variable toScoreFragmentlayout:In
score_fragment.xml, repeat the process you used forgame_fragment.xmlin Step 1. -
In the
ScoreFragmentlayout, use the view model variable and data binding to handle clicking. -
Pass the
ScoreViewModelinto the data binding and removeOnClickListenersetup forplayAgainButton.
Now instead of defining OnClickListener code in the fragment, you’re using data binding. Run your app and see how all the buttons still work.
Adding LiveData Data Binding
In this step, you’ll use LiveData to automagically update your layout via data binding. This will allow you to remove all of your observation lambdas for simple UI updates.
-
Call
binding.setLifecycleOwnerto make the data binding lifecycle aware:Open
GameFragment. To make your data binding lifecycle aware and to have it play nicely withLiveData, you need to setbinding.setLifecycleOwnertothis— which refers toGameFragment. This looks like:
// Specify the current activity as the lifecycle owner of the binding. This is used so that
// the binding can observe LiveData updates
binding.lifecycleOwner = this-
For
score_textandword_textuse theLiveDatafromGameViewModelto set the text attribute:Open up the
game_fragmentlayout. You can use the wordLiveDatato set the text for theword_textandscore_textTextViews. For example, forword_text:
android:text="@{gameViewModel.word}"You can also use text formatting.
android:text="@{@string/quote_format(gameViewModel.word)}"android:text="@{@string/score_format(gameViewModel.score)}"-
Remove the score and word observers:
Note that we’ll fix the
currentTimeobservation in the next exercise. Once you’ve removed the score and word observers, you should be able to run your app and see that the score and word text still updates. -
Use the
LiveDatafromScoreViewModelto set thescore_texttext attribute:This is very similar to what you just did with
word_textandscore_text. Repeat inscore_fragment.xml. Note that since score is an Int, you’ll need to useString.valueOf()in your binding expression to convert the Int to a String. -
Call
binding.setLifecycleOwnerand remove the score observer.Repeat steps 2 and 3 in the
ScoreFragmentcode. Run your app again to make sure it compiles and runs.
LiveData Map Transformation
One of the easiest ways to do simple data manipulations to LiveData such as changing an integer to a string is by using a method called Transformation.map().
The map function takes the output of one LiveData which I’ll call LiveData A and does some sort of conversion on it and then outputs the result to another LiveData which I’ll call LiveData B.
Observers can then observe LiveData B if they want. The conversion is defined in a function. So in our case LiveData A can output along representing how much time has passed. And then we could do a conversion function on it to format it as a string showing the elapsed time, and then that string would be output from LiveData B which in turn would update the game fragment.
In this step you’ll use a Tranformations.map to convert the current time into a formatted String.
-
In
GameViewModelcreate a newLiveDatacalledcurrentTimeStringand Use Transformation.map to takecurrentTimeto a String output fromcurrentTimeString:This will store the String version of
currentTime.What you want is to use
DateUtilsto convert thecurrentTimenumber output into a String. Then we want to emit that from thecurrentTimeStringLiveData.
// Create a new LiveData called currentTimeString.
// Use Transformation.map to take the number output from currentTime, and transform
// it into a String using DateUtils.
// The String version of the current time
val currentTimeString = Transformations.map(currentTime) { time ->
DateUtils.formatElapsedTime(time)
}-
Set
timer_textto the value ofcurrentTimeString:In the
game_fragment.xml, find thetimer_textview and set the text attribute to the value ofcurrentTimeString(not currentTime!) in a binding expression.
android:text="@{gameViewModel.currentTimeString}"-
Delete the observer for
currentTimefrom GameFragment:We don’t need it anymore! As always, run your code!
Optional Exercise: Adding the Buzzer
You’ll need some logic that determines when the phone should buzz. Where should that code go? ViewModel
Is this an Event or a State? Event
Let’s add a buzzer! Before you start, check the reference documentation on how to use Vibration on Android. This will mostly be a self-guided exercise, but we’ll give you a few steps to get started.
-
Add the Vibrate permission:
In the
AndroidManifest.xmlfile, above theapplicationtag, add the following tag:
<!-- Add the Vibrate permission -->
<uses-permission android:name="android.permission.VIBRATE" />This provides a permission that lets us vibrate the phone. We will describe Permissions in greater detail later in this course - suffice it to say that without this, our app cannot cause the phone to vibrate on its own.
-
You can also optionally lock the screen to landscape:
While you’re in the manifest, you can also optionally lock the phone to landscape mode. That is done by adding the following lines to the
MainActivitytag:
<activity
android:name=".MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="landscape">-
Copy over the different buzz pattern Long array constants:
Vibration is controlled by passing in an array representing the number of milliseconds each interval of buzzing and non-buzzing takes. So the array [0, 200, 100, 300] will wait 0 milliseconds, then buzz for 200ms, then wait 100ms, then buzz fo 300ms. Here are some example buzz patterns you can copy over:
private val CORRECT_BUZZ_PATTERN = longArrayOf(100, 100, 100, 100, 100, 100)
private val PANIC_BUZZ_PATTERN = longArrayOf(0, 200)
private val GAME_OVER_BUZZ_PATTERN = longArrayOf(0, 2000)
private val NO_BUZZ_PATTERN = longArrayOf(0)Put these in the GameViewModel, above the class.
-
Make an enum called
BuzzTypeinGameViewModel.This enum will represent the different types of buzzing that can occur:
enum class BuzzType(val pattern: LongArray) {
CORRECT(CORRECT_BUZZ_PATTERN),
GAME_OVER(GAME_OVER_BUZZ_PATTERN),
COUNTDOWN_PANIC(PANIC_BUZZ_PATTERN),
NO_BUZZ(NO_BUZZ_PATTERN)
}- Create a properly encapsulated LiveData for a buzz event - its’ type should be
BuzzType.
// Event that triggers the phone to buzz using different patterns, determined by BuzzType
private val _eventBuzz = MutableLiveData<BuzzType>()
val eventBuzz: LiveData<BuzzType>
get() = _eventBuzz- Set the value of buzz event to the correct
BuzzTypewhen the buzzer should fire. This should happen when the game is over when the user gets a correct answer, and on each tick when countdown buzzing starts.
override fun onTick(millisUntilFinished: Long) {
_currentTime.value = (millisUntilFinished / ONE_SECOND)
if (millisUntilFinished / ONE_SECOND <= COUNTDOWN_PANIC_SECONDS) {
// COUNTDOWN_PANIC buzz
_eventBuzz.value = BuzzType.COUNTDOWN_PANIC
}
}
override fun onFinish() {
_currentTime.value = DONE
// GAME_OVER buzz
_eventBuzz.value = BuzzType.GAME_OVER
_eventGameFinish.value = true
}- Add a function
onBuzzCompletefor telling the view model when the buzz event has completed.
fun onBuzzComplete() {
_eventBuzz.value = BuzzType.NO_BUZZ
}-
Copy over the
buzzmethod.Given a pattern, this method will actually perform the buzz. It uses the activity to get a system service, so you should put this in your
GameFragment:
private fun buzz(pattern: LongArray) {
val buzzer = activity?.getSystemService<Vibrator>()
buzzer?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
buzzer.vibrate(VibrationEffect.createWaveform(pattern, -1))
} else {
//deprecated in API 26
buzzer.vibrate(pattern, -1)
}
}
}- Created an observer for the buzz event which calls the buzz method with the correct pattern. Remember to call
onBuzzComplete!
// Buzzes when triggered with different buzz events
viewModel.eventBuzz.observe(viewLifecycleOwner, { buzzType ->
if (buzzType != GameViewModel.BuzzType.NO_BUZZ) {
buzz(buzzType.pattern)
viewModel.onBuzzComplete()
}
})Extra: How to force reload the layout?
You can force reload the layout by calling binding.invalidateAll().
Summary
- The Android app architecture guidelines recommend separating classes that have different responsibilities.
- A UI controller is UI-based class like
ActivityorFragment. 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
ViewModelclass stores and manages UI-related data. TheViewModelclass allows data to survive configuration changes such as screen rotations. ViewModelis one of the recommended Android Architecture Components.ViewModelProvider.Factoryis an interface you can use to create aViewModelobject.
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 lesson. |
| 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. |
LiveData
LiveDatais an observable data holder class that is lifecycle-aware, one of the Android Architecture Components.- You can use
LiveDatato enable your UI to update automatically when the data updates. LiveDatais observable, which means that an observer like an activity or an fragment can be notified when the data held by theLiveDataobject changes.LiveDataholds data; it is a wrapper that can be used with any data.LiveDatais lifecycle-aware, meaning that it only updates observers that are in an active lifecycle state such asSTARTEDorRESUMED.
To add LiveData
- Change the type of the data variables in
ViewModeltoLiveDataorMutableLiveData.
MutableLiveData is a LiveData object whose value can be changed. MutableLiveData is a generic class, so you need to specify the type of data that it holds.
- To change the value of the data held by the
LiveData, use thesetValue()method on theLiveDatavariable.
To encapsulate LiveData
- The
LiveDatainside theViewModelshould be editable. Outside theViewModel, theLiveDatashould be readable. This can be implemented using a Kotlin backing property. - A Kotlin backing property allows you to return something from a getter other than the exact object.
- To encapsulate the
LiveData, useprivateMutableLiveDatainside theViewModeland return aLiveDatabacking property outside theViewModel.
Observable LiveData
LiveDatafollows an observer pattern. The “observable” is theLiveDataobject, and the observers are the methods in the UI controllers, like fragments. Whenever the data wrapped insideLiveDatachanges, the observer methods in the UI controllers are notified.- To make the
LiveDataobservable, attach an observer object to theLiveDatareference in the observers (such as activities and fragments) using theobserve()method. - This
LiveDataobserver pattern can be used to communicate from theViewModelto the UI controllers.
Data binding with ViewModel and LiveData
- The Data Binding Library works seamlessly with Android Architecture Components like
ViewModelandLiveData. - The layouts in your app can bind to the data in the Architecture Components, which already help you manage the UI controller’s lifecycle and notify about changes in the data.
ViewModel data binding
- You can associate a
ViewModelwith a layout by using data binding. ViewModelobjects hold the UI data. By passingViewModelobjects into the data binding, you can automate some of the communication between the views and theViewModelobjects.
How to associate a ViewModel with a layout:
- In the layout file, add a data-binding variable of the type
ViewModel.
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>- In the
GameFragmentfile, pass theGameViewModelinto the data binding.
binding.gameViewModel = viewModelListener bindings
- Listener bindings are binding expressions in the layout that run when click events such as
onClick()are triggered. - Listener bindings are written as lambda expressions.
- Using listener bindings, you replace the click listeners in the UI controllers with listener bindings in the layout file.
- Data binding creates a listener and sets the listener on the view.
android:onClick="@{() -> gameViewModel.onSkip()}"Adding LiveData to data binding
LiveDataobjects can be used as a data-binding source to automatically notify the UI about changes in the data.- You can bind the view directly to the
LiveDataobject in theViewModel. When theLiveDatain theViewModelchanges, the views in the layout can be automatically updated, without the observer methods in the UI controllers.
android:text="@{gameViewModel.word}"- To make the
LiveDatadata binding work, set the current activity (the UI controller) as the lifecycle owner of thebindingvariable in the UI controller.
binding.lifecycleOwner = thisString formatting with data binding
- Using data binding, you can format a string resource with placeholders like
%sfor strings and%dfor integers. - To update the
textattribute of the view, pass in theLiveDataobject as an argument to the formatting string.
android:text="@{@string/quote_format(gameViewModel.word)}"LiveData transformations
Transforming LiveData
- Sometimes you want to transform the results of
LiveData. For example, you might want to format aDatestring as “hours:mins:seconds,” or return the number of items in a list rather than returning the list itself. To perform transformations onLiveData, use helper methods in theTransformationsclass. - The
Transformations.map()method provides an easy way to perform data manipulations on theLiveDataand return anotherLiveDataobject. The recommended practice is to put data-formatting logic that uses theTransformationsclass in theViewModelalong with the UI data.
Displaying the result of a transformation in a TextView
- Make sure the source data is defined as
LiveDatain theViewModel. - Define a variable, for example
newResult. UseTransformation.map()to perform the transformation and return the result to the variable.
val newResult = Transformations.map(someLiveData) { input ->
// Do some transformation on the input live data
// and return the new value
}- Make sure the layout file that contains the
TextViewdeclares a<data>variable for theViewModel.
<data>
<variable
name="MyViewModel"
type="com.example.android.something.MyViewModel" />
</data>- In the layout file, set the
textattribute of theTextViewto the binding of thenewResultof theViewModel. For example:
android:text="@{SomeViewModel.newResult}"Formatting dates
- The
DateUtils.formatElapsedTime()utility method takes alongnumber of milliseconds and formats the number to use aMM:SSstring format.