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 setcurrentWord
to 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
, theViewModel
andLiveData
.
The first class is the UI Controller:
-
UI Controller
is the word that I’m using to describe what activities and fragments are. -
UI Controller
is responsible for any user interface related tasks like displaying views and capturing user input. -
By design,
UI Controllers
execute 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 Controller
that get notified first. -
You should limit
UI Controller
to only user interface related tasks and take any sort of decision-making power out ofUI Controller
so whileUI Controller
is 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 Controller
is 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
ViewModel
is to hold the specific data needed to display the fragment or activity it’s associated with. -
Also,
ViewModels
may do simple calculations and transformations on that data so that it’s ready to be displayed by theUI Controller
. -
The
ViewModel
class 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
ViewModel
will 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
-
ViewModel
is 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
ViewModel
and have the fragment reference theViewModel
. -
ViewModel
survives 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
ViewModel
all of the data is just right there for you. -
Unlike the
onSavedInstanceState
bundle theViewModel
has 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.gradle
file, add thelifecycle-extensions
dependency. You can find the most current version of the dependency here.
- Create the
GameViewModel
class, extendingViewModel
: Create a new file calledGameViewModel.kt
in thejava/com.example.android.guesstheword/game
package. Then in this file, create a classGameViewModel
that extendsViewModel
:
-
Add
init
block and overrideonCleared
. Add log statements to both:Make an
init
block that prints out a log saying “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
.
-
Create and initialize a
GameViewModel
, usingViewModelProvider
. Add a log statement:Back in GameFragment use
lateinit
to create a field forGameViewModel
calledviewModel
.
Then in onCreateView
, request the current GameViewModel
using the ViewModelProvider
class:
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.
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 Controller
only 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
Fragment
draws 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
ViewModel
never 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
, andwordList
variables to theGameViewModel
:In the
GameFragment
find theword
,score
, andwordList
variables, and move them toGameViewModel
. Make sure you delete the versions of these variables inGameFragment
.
Remember, do not move the binding! This is because the binding contains references to views.
-
Move methods
resetList
,nextWord
,onSkip
, andonCorrect
to theGameViewModel
:In
GameFragment
, find each of the methods:resetList
,nextWord
,onSkip
andonCorrect
. Move them toGameViewModel
.Remove the private modifier
onSkip
andonCorrect
methods so they can be called from theGameFragment
.
-
Move the initialization methods to the
GameViewModel
:Initialization in the
GameFragment
involved callingresetList
andnextWord
. Now that they are both in theGameViewModel
, call them in theGameViewModel
when it is created.
-
Update the
onClickListeners
to refer to call methods in theViewModel
, and then update the UI:Now that
onSkip
andonCorrect
have been moved to theGameViewModel
, theOnClickListeners
in theGameFragment
, refer to method that aren’t there. Update theOnClickListeners
to call the methods in theGameViewModel
. Then in theOnClickListeners
, update the score and word texts so that they have the newest data.
-
Update the methods to get
word
andscore
from theviewModel
:In the
GameViewModel
, remove the private modifier fromword
andscore
.In the
GameFragment
, updategameFinished
,updateWordText
andupdateScoreText
to get the data from thegameViewModel
.
-
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 togameFinished
innextWord
. 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
ViewModel
you protect yourself from having to worry about lifecycle problems and bugs.
Another aspect of this design is:
-
The code is very modular.
UI Controller
knows about theViewModel
but theViewModel
doesn’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
-
ViewModel
contains 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
ViewModel
you 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.
-
LiveData
holds data this means that it wraps around some data. For example, we’ll have live data wrap around the current score. -
LiveData
is 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
word
andscore
inMutableLiveData
:Since
MutableLiveData
is generic we need to specify the type.
- Initialize
score.value
to 0 becauseMutableLiveData
is nullable.
-
Change references to
score
andword
toscore.value
andword.value
and 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.
-
Set up the observation relationship for the
score
andword
LiveDatas:Move over to
GameFragment
. UI controllers are where you’ll set up the observation relationship. Get theLiveData
from yourviewModel
and call theobserve
method. Make sure to pass inthis
and then an observer lambda. Move the code to update the scoreTextView
and the wordTextView
to your Observers. Here’s the code to set up an observation relationship for the score. This should be inGameFragment.onCreate
:
And now you can remove updateWordText()
and updateScoreText()
any references to them completely.
-
Add a null safety check when passing
currentScore
in thegameFinished
method:viewModel.score.value
can possibly be null, so add a null safety check in thegameFinished
method:
Lifecycle-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 theViewModel
should 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 theLiveData
so it can’t be completely private. -
Being able to write and read from
LiveData
is where the distinction betweenMutableLiveData
and just plain oldLiveData
comes.MutableLiveData
is aLiveData
that can be mutated or modified, whereasLiveData
on the other hand isLiveData
that you could read but you cannot call set value on. -
Inside the
ViewModel
we want theLiveData
to beMutableLiveData
, but outside theViewModel
we want theMutableLiveData
to 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
word
andscore
: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
LiveData
and not have an underscore in front of its name. -
Make a backing property for the external version that returns the internal
MutableLiveData
as 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 (_score
and_word
). Now you can take your public versions of the variables (score
andword
) and overrideget
to return the backing properties.
By 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
score
andword
:In
GameViewModel
, update the code so that you use the mutable versions,_score
and_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
LiveData
calledeventGameFinish
in theGameViewModel
that holds a boolean and will represent game end Event:
Also, initialize its value to false.
-
Make the function
onGameFinishComplete
which makes the value ofeventGameFinish
false:This function simply sets the value of
_eventGameFinish
to 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.
-
Set
eventGameFinish
to true, to signify that the game is over:In the
GameViewModel
, find the condition where thewordList
is empty. If it’s empty, the game is over, so signal that by setting the value ofeventGameFinish
to true.
-
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
eventGameFinished
is 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
.
-
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:
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
LiveData
for the current time calledcurrentTime
:Use the same method as you did earlier to encapsulate
LiveData
forscore
andword
. The type ofcurrentTime
should be of type Long.
- Create a
timer
field of type CountDownTimer in theGameViewModel
: You don’t need to worry about initializing it yet. Just declare it and ignore the error for now.
-
Copy over the
CountDownTimer
code and then updatecurrentTime
andeventGameFinish
appropriately as the timer ticks and finishes:In the
init
ofGameViewModel
, copy theCountDownTimer
code below:
-
Update the logic in the
nextWord
function 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
.
-
Cancel the timer in
onCleared
:To avoid memory leaks, you should always cancel a
CountDownTimer
if you no longer need it. To do that, you can call:
-
Update the UI:
You want the
timerText
on the screen to show the appropriate time. Figure out how to useLiveData
from theGameViewModel
to do this - remember, you’ve done something similar forscore
andword
. You’ll need to convert the Long for the timer into a String. You can use the DateUtils tool to do that.
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
ViewModel
that takes in a constructor parameter - Make a
ViewModel
Factory forViewModel
- Have factory construct
ViewModel
with constructor parameter - Add
ViewModel
Factory when usingViewModel
Providers
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
ScoreViewModel
class and have it take in an integer constructor parameter calledfinalScore
:Make sure to create the
ScoreViewModel
class file in the same package as theScoreFragment
.
-
Copy over ScoreViewModelFactory:
Create
ScoreViewModel
factory 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
ScoreViewModel
and return it. You’ll also need to deal with the generics, so the statement will be:
And the full code will be:
-
Create and construct a
ScoreViewModelFactory
:In
ScoreFragment
, createviewModelFactory
fromScoreViewModelFactory
.
- Create
ScoreViewModel
by usingViewModelProvider
as usual, except you’ll also pass in yourScoreViewModelFactory
:
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:
-
Add a
LiveData
for the score and the play again event:Create LiveData for
score
andeventPlayAgain
using the best practices for encapsulation and event handling that you’ve learned. Make sure to initializescore
’s value to thefinalScore
you pass into the view model.
-
Convert
ScoreFragment
to properly observe and useScoreViewModel
to update the UI:In
ScoreFragment
, add observers forscore
andeventPlayAgain
LiveData. Use them to update the UI.
In ScoreViewModel
, helper functions to work LiveData Events.
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
GameViewModel
data binding variable toGameFragment
layout:In the layout xml file for the
GameFragment
(game_fragment.xml
), create agameViewModel
variable inside the layout.
Then pass the GameViewModel
into the data binding:
In the GameFragment.onCreate
, pass in the view model to the GameFragmentBinding
.
-
In the
GameFragment
layout, use the view model variable and data binding to handle clicking:In XML, you can define an
onClick
attribute for buttons. Using data binding, you can define a data binding expression which is a Listener binding. Essentially this means that you define theOnClickListener
in the XML. You also have your view model variable available via data binding. So to create anonClick
attribute that will callonSkip
in the view model, you can use:
Now you can (and should) remove the OnClickListener
setup from the GameFragment
. Everything should work just as before.
-
Add a
ScoreViewModel
data binding variable toScoreFragment
layout:In
score_fragment.xml
, repeat the process you used forgame_fragment.xml
in Step 1. -
In the
ScoreFragment
layout, use the view model variable and data binding to handle clicking. -
Pass the
ScoreViewModel
into the data binding and removeOnClickListener
setup 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.setLifecycleOwner
to 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.setLifecycleOwner
tothis
— which refers toGameFragment
. This looks like:
-
For
score_text
andword_text
use theLiveData
fromGameViewModel
to set the text attribute:Open up the
game_fragment
layout. You can use the wordLiveData
to set the text for theword_text
andscore_text
TextViews. For example, forword_text
:
You can also use text formatting.
-
Remove the score and word observers:
Note that we’ll fix the
currentTime
observation 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
LiveData
fromScoreViewModel
to set thescore_text
text attribute:This is very similar to what you just did with
word_text
andscore_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.setLifecycleOwner
and remove the score observer.Repeat steps 2 and 3 in the
ScoreFragment
code. 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
GameViewModel
create a newLiveData
calledcurrentTimeString
and Use Transformation.map to takecurrentTime
to a String output fromcurrentTimeString
:This will store the String version of
currentTime
.What you want is to use
DateUtils
to convert thecurrentTime
number output into a String. Then we want to emit that from thecurrentTimeString
LiveData
.
-
Set
timer_text
to the value ofcurrentTimeString
:In the
game_fragment.xml
, find thetimer_text
view and set the text attribute to the value ofcurrentTimeString
(not currentTime!) in a binding expression.
-
Delete the observer for
currentTime
from 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.xml
file, above theapplication
tag, add the following tag:
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
MainActivity
tag:
-
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:
Put these in the GameViewModel
, above the class.
-
Make an enum called
BuzzType
inGameViewModel
.This enum will represent the different types of buzzing that can occur:
- Create a properly encapsulated LiveData for a buzz event - its’ type should be
BuzzType
.
- Set the value of buzz event to the correct
BuzzType
when 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.
- Add a function
onBuzzComplete
for telling the view model when the buzz event has completed.
-
Copy over the
buzz
method.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
:
- Created an observer for the buzz event which calls the buzz method with the correct pattern. Remember to call
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
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 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
LiveData
is an observable data holder class that is lifecycle-aware, one of the Android Architecture Components.- You can use
LiveData
to enable your UI to update automatically when the data updates. LiveData
is observable, which means that an observer like an activity or an fragment can be notified when the data held by theLiveData
object changes.LiveData
holds data; it is a wrapper that can be used with any data.LiveData
is lifecycle-aware, meaning that it only updates observers that are in an active lifecycle state such asSTARTED
orRESUMED
.
To add LiveData
- Change the type of the data variables in
ViewModel
toLiveData
orMutableLiveData
.
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 theLiveData
variable.
To encapsulate LiveData
- The
LiveData
inside theViewModel
should be editable. Outside theViewModel
, theLiveData
should 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
, useprivate
MutableLiveData
inside theViewModel
and return aLiveData
backing property outside theViewModel
.
Observable LiveData
LiveData
follows an observer pattern. The “observable” is theLiveData
object, and the observers are the methods in the UI controllers, like fragments. Whenever the data wrapped insideLiveData
changes, the observer methods in the UI controllers are notified.- To make the
LiveData
observable, attach an observer object to theLiveData
reference in the observers (such as activities and fragments) using theobserve()
method. - This
LiveData
observer pattern can be used to communicate from theViewModel
to the UI controllers.
Data binding with ViewModel and LiveData
- The Data Binding Library works seamlessly with Android Architecture Components like
ViewModel
andLiveData
. - 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
ViewModel
with a layout by using data binding. ViewModel
objects hold the UI data. By passingViewModel
objects into the data binding, you can automate some of the communication between the views and theViewModel
objects.
How to associate a ViewModel
with a layout:
- In the layout file, add a data-binding variable of the type
ViewModel
.
- In the
GameFragment
file, pass theGameViewModel
into the data binding.
Listener 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.
Adding LiveData to data binding
LiveData
objects 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
LiveData
object in theViewModel
. When theLiveData
in theViewModel
changes, the views in the layout can be automatically updated, without the observer methods in the UI controllers.
- To make the
LiveData
data binding work, set the current activity (the UI controller) as the lifecycle owner of thebinding
variable in the UI controller.
String formatting with data binding
- Using data binding, you can format a string resource with placeholders like
%s
for strings and%d
for integers. - To update the
text
attribute of the view, pass in theLiveData
object as an argument to the formatting string.
LiveData transformations
Transforming LiveData
- Sometimes you want to transform the results of
LiveData
. For example, you might want to format aDate
string 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 theTransformations
class. - The
Transformations.map()
method provides an easy way to perform data manipulations on theLiveData
and return anotherLiveData
object. The recommended practice is to put data-formatting logic that uses theTransformations
class in theViewModel
along with the UI data.
Displaying the result of a transformation in a TextView
- Make sure the source data is defined as
LiveData
in theViewModel
. - Define a variable, for example
newResult
. UseTransformation.map()
to perform the transformation and return the result to the variable.
- Make sure the layout file that contains the
TextView
declares a<data>
variable for theViewModel
.
- In the layout file, set the
text
attribute of theTextView
to the binding of thenewResult
of theViewModel
. For example:
Formatting dates
- The
DateUtils.formatElapsedTime()
utility method takes along
number of milliseconds and formats the number to use aMM:SS
string format.