1. Welcome
Introduction
The GuessTheWord app that you worked on in the previous three codelabs implements the LiveData
observer pattern to observe ViewModel
data. The views in the UI controller observe the LiveData
in the ViewModel
and update the data to be displayed.
When passing LiveData
between the components, sometimes you might want to map or transform the data. Your code might need to perform calculations, display only a subset of the data, or change the rendition of the data. For example, for the word
LiveData
, you could create a transformation that returns the number of letters in the word rather than the word itself.
You can transform LiveData
using the helper methods in the Transformations
class:
In this codelab, you add a countdown timer in the app. You learn how to use Transformations.map()
on the LiveData
to transform elapsed time into a format to display on the screen.
What you should already know
- How to create basic Android apps in Kotlin
- How to use
ViewModel
objects in your app - How to store data using
LiveData
in aViewModel
- How to add
LiveData
observer methods to observe the changes in the data - How to use data binding with
ViewModel
andLiveData
What you'll learn
- How to use
Transformations
withLiveData
What you'll do
- Add a timer to end the game.
- Use
Transformations.map()
to transform oneLiveData
into another.
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.)
In this codelab, you improve the GuessTheWord app by adding a one-minute countdown timer that appears above the score. The timer ends the game when the countdown reaches 0
.
You also use a transformation to format the elapsed time LiveData
object into a timer string LiveData
object. The transformed LiveData
is the data binding source for the timer's text view.
3. Task: Get started
In this task, you locate and run your starter code for this codelab. You can use the GuessTheWord app that you built in previous codelab as your starter code, or you can download a starter app.
- (Optional) If you're not using your code from the previous codelab, download the starter code for this codelab. Unzip the code, and open the project in Android Studio.
- Run the app and play the game.
- 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 ends the game.
- Cycle through all the words, and notice that the app navigates automatically to the score screen.
4. Task: Add a timer
In this task, you add a countdown timer to the app. Instead of the game ending when the word list is empty, the game ends when the timer finishes. Android provides a utility class called CountDownTimer
that you use to implement the timer.
Add the logic for the timer in the GameViewModel
so that the timer does not get destroyed during configuration changes. The fragment contains the code to update the timer text view as the timer ticks.
Implement the following steps in the GameViewModel
class:
- Create a
companion
object to hold the timer constants.
companion object {
// Time when the game is over
private const val DONE = 0L
// Countdown time interval
private const val ONE_SECOND = 1000L
// Total time for the game
private const val COUNTDOWN_TIME = 60000L
}
- To store the countdown time of the timer, add a
MutableLiveData
member variable called_currentTime
and a backing property,currentTime
.
// Countdown time
private val _currentTime = MutableLiveData<Long>()
val currentTime: LiveData<Long>
get() = _currentTime
- Add a
private
member variable calledtimer
of the typeCountDownTimer
. You resolve the initialization error in the next step.
private val timer: CountDownTimer
- Inside the
init
block, initialize and start the timer. Pass in the total time,COUNTDOWN_TIME
. For the time interval, useONE_SECOND
. Override the callback methodsonTick()
andonFinish()
and start the timer.
// 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) {
}
override fun onFinish() {
}
}
timer.start()
- Implement the
onTick()
callback method, which is called on every interval or on every tick. Update the_currentTime
, using the passed-in parametermillisUntilFinished
. ThemillisUntilFinished
is the amount of time until the timer is finished in milliseconds. ConvertmillisUntilFinished
to seconds and assign it to_currentTime
.
override fun onTick(millisUntilFinished: Long)
{
_currentTime.value = millisUntilFinished/ONE_SECOND
}
- The
onFinish()
callback method is called when the timer is finished. ImplementonFinish()
to update the_currentTime
and trigger the game finish event.
override fun onFinish() {
_currentTime.value = DONE
onGameFinish()
}
- Update the
nextWord()
method to reset the word list when the list is empty, instead of finishing the game.
private fun nextWord() {
// Shuffle the word list, if the list is empty
if (wordList.isEmpty()) {
resetList()
} else {
// Remove a word from the list
_word.value = wordList.removeAt(0)
}
}
- Inside the
onCleared()
method, cancel the timer to avoid memory leaks. You can remove the log statement, because it's no longer needed. TheonCleared()
method is called before theViewModel
is destroyed.
override fun onCleared() {
super.onCleared()
// Cancel the timer
timer.cancel()
}
- Run your app and play the game. Wait 60 seconds, and the game finishes automatically. However, the timer text is not displayed on the screen. You fix that next.
5. Task: Add transformation for the LiveData
The Transformations.map()
method provides a way to perform data manipulations on the source LiveData
and return a result LiveData
object. These transformations aren't calculated unless an observer is observing the returned LiveData
object.
This method takes the source LiveData
and a function as parameters. The function manipulates the source LiveData
.
In this task, you format the elapsed time LiveData
object into a new string LiveData
object in "MM:SS
" format. You also display the formatted elapsed time on the screen.
The game_fragment.xml
layout file already includes the timer text view. So far, the text view has had no text to display, so the timer text has not been visible.
- In the
GameViewModel
class, after instantiating thecurrentTime
, create a newLiveData
object namedcurrentTimeString
. This object is for the formatted string version of thecurrentTime
. - Use
Transformations.map()
to definecurrentTimeString
. Pass in thecurrentTime
and a lambda function to format the time. You can implement the lambda function using theDateUtils.formatElapsedTime()
utility method, which takes along
number of seconds and formats it to "MM:SS
" string format.
// The String version of the current time
val currentTimeString = Transformations.map(currentTime) { time ->
DateUtils.formatElapsedTime(time)
}
- In the
game_fragment.xml
file, in the timer text view, bind thetext
attribute to thecurrentTimeString
of thegameViewModel
.
<TextView
android:id="@+id/timer_text"
...
android:text="@{gameViewModel.currentTimeString}"
... />
- Run your app and play the game. The timer text updates once a second. Notice that the game does not finish when you have cycled through all the words. The game now finishes when the timer is up.
6. Solution code
Congratulations! You successfully added a timer to the app that ends the game automatically. You also learned how to use Transformations.map()
to convert one LiveData
object to another.
Android Studio project: GuessTheWord
7. Coding challenge
Challenge: Create a hint about the word, and display the hint in a text view above the timer. This hint can say how many characters the word has, and can reveal one of the letters at a random position.
Hints: Use Transformations.map()
on the current word
LiveData
object. Add an extra TextView
to display the word hint.
- In
GameViewModel
class, add aval
to transform the current word into the hint.
// The Hint for the current word
val wordHint = Transformations.map(word) { word ->
val randomPosition = (1..word.length).random()
"Current word has " + word.length + " letters" +
"\nThe letter at position " + randomPosition + " is " +
word.get(randomPosition - 1).toUpperCase()
}
- In
game_fragment.xml
, add a new text view above the timer text view to display the hint. Bind the text attribute to thewordHint
that you added above.
android:text="@{gameViewModel.wordHint}"
8. Summary
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.
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
TextView
declares a<data>
variable for theViewModel
.
<data> <variable name="MyViewModel" type="com.example.android.something.MyViewModel" /> </data>
- In the layout file, set the
text
attribute of theTextView
to the binding of thenewResult
of theViewModel
. For example:
android:text="@{SomeViewModel.newResult}"
Formatting dates
- The
DateUtils.formatElapsedTime()
utility method takes along
number of milliseconds and formats the number to use aMM:SS
string format.
9. Learn more
Udacity course:
Android developer documentation:
10. 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
In which class should you add the data-formatting logic that uses the Transformations.map()
method to convert LiveData
to a different value or format?
ViewModel
Fragment
Activity
MainActivity
Question 2
The Transformations.map()
method provides an easy way to perform data manipulations on the LiveData
and returns __________ .
- A
ViewModel
object - A
LiveData
object - A formatted
String
- A
RoomDatabase
object
Question 3
What are the parameters for the Transformations.map()
method?
- A source
LiveData
and a function to be applied to theLiveData
- Only a source
LiveData
- No parameters
ViewModel
and a function to be applied
Question 4
The lambda function passed into the Transformations.map()
method is executed in which thread?
- Main thread
- Background thread
- UI thread
- In a coroutine
11. Next codelab
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.