4.
Debugging
Written by Darryl Bayliss
In the previous two chapters, you developed TimeFighter into a full-fledged app. In this chapter, you’ll focus on debugging it.
All apps have bugs. Some are subtle, such as glitches within the UI, while others are obvious, such as outright crashes. As a developer, it’s your job to keep your app bug-free.
Android Studio provides developers with some tools to help track down and fix bugs. In this chapter, you’ll learn how to:
- Debug your app using Android Studio’s debug tools.
- Add landscape support to TimeFighter.
Getting started
If you’ve been following along, open your project in Android Studio and keep using it for this chapter. If not, don’t worry. Locate the projects folder for this chapter and open the TimeFighter app inside the starter folder.
The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.
You might not have noticed, but TimeFighter has a bug. Start the app in the emulator or on your device. Push TAP ME a few times, and then change the orientation of the device to landscape.
Note: For devices running Android Pie and above. You may need to enable auto-rotate on your device or emulator if the screen doesn’t rotate automatically.
To do this, swipe the notification drawer down to reveal the quick settings and ensure the auto-rotate button is colored green to signify it’s enabled.
Notice anything strange? TimeFighter resets the game when you rotate the device. Whoops! To understand why this happens, you need to begin analyzing the code.
Add some logging
The first debugging approach is to add logging to your app. With logging, you can find out what’s happening at certain points within your code. You can even log and check the values of your variables at runtime.
In MainActivity.kt, add the following property to the top of the existing properties:
private val TAG = MainActivity::class.java.simpleName
Then, add the following line below the call to setContentView
in onCreate()
:
Log.d(TAG, "onCreate called. Score is: $score")
Going through the code you added:
-
You assign the name of your class to
TAG
. The convention on Android is to use the class name in log messages. This makes it easier to see which class the message is coming from. -
You
Log
a message when the Activity is created. Your app informs you whenonCreate()
is called and the current value inscore
. Injecting$score
into the message is an example of string interpolation in Kotlin. At runtime, Kotlin looks forscore
and replaces it in the log message.
Run the app again. After it’s loaded, go to Android Studio. At the bottom of the window there’s a button labeled Logcat. Click that button, and Android Studio displays a console-like window at the bottom:
With Logcat, you can see everything your emulator or device is doing via log messages, including messages coming from outside of your app. For now, you can ignore most messages and filter down to only the ones you’ve added yourself.
In the Logcat window, there’s a search bar with a magnifying glass. The text you enter here filters the log messages so that you’ll only see log messages that match that text.
In the Logcat search bar, type the name of your Activity — MainActivity — and watch as the filter gets applied.
Excellent, you can now see the log messages you added earlier. The score is currently 0 because you haven’t yet started the game.
Try to reproduce the bug by rotating the screen as you play the game.
That’s strange! Why is the score reset to 0? You’ll work that out in the next section.
Note: You’ll only scratch the surface of Logcat in this chapter. For more information about Logcat and everything it can do, read the Android developer documentation: https://developer.android.com/studio/command-line/logcat.html.
Orientation changes
From the Timefighter log messages, you can establish that score
is reset to 0 whenever you rotate the device. But why? The reason for this relates to how Android handles device orientation changes.
When Android detects a change in orientation, it does three things:
- Attempts to save any properties for the Activity specified by the developer.
- Destroys the Activity.
- Recreates the Activity for the new orientation by calling
onCreate()
, which resets any properties specified by the developer.
But it’s more than just orientation changes. Android performs these steps any time there’s a change to the configuration of a device. A configuration change can happen for many reasons, including changes to the orientation or the selected device language.
In fact, your Activity can get destroyed and recreated several times while the user is using the app, so it’s incredibly important that you develop your app so it can recover from these changes.
Back in MainActivity.kt, add the following companion object at the bottom of the class:
// 1
companion object {
private const val SCORE_KEY = "SCORE_KEY"
private const val TIME_LEFT_KEY = "TIME_LEFT_KEY"
}
Next, add the following methods below onCreate()
:
// 2
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SCORE_KEY, score)
outState.putInt(TIME_LEFT_KEY, timeLeft)
countDownTimer.cancel()
Log.d(TAG, "onSaveInstanceState: Saving Score: $score & Time Left: $timeLeft")
}
// 3
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy called.")
}
Here’s what’s happening:
-
You create a companion object containing two string constants,
SCORE_KEY
andTIME_LEFT_KEY
. These track the variables you want to save when the orientation changes. You’ll use these constants as keys into a dictionary of saved properties. -
You override
onSaveInstanceState
and insert the values ofscore
andtimeLeft
into the passed-inBundle
.onSaveInstanceState
is called before a configuration change happens, giving you a chance to save anything important. ABundle
is a hashmap Android uses to pass values across different screens. You also cancel the game timer and add a log to track when the method is called. -
You override
onDestroy()
, a method used by the Activity to clean itself up when it is being destroyed. Activities are destroyed when Android needs to reclaim memory or it’s explictly destroyed by a developer. You callsuper
so your Activity can perform any essential cleanup, and you add a final log to track whenonDestroy()
is called.
Run your app again, then play the game for a few seconds. Change the orientation, and then look at the Logcat output:
The Activity is still resetting the score back to 0. However, the log statement in onSaveInstanceState()
is informing you that the score and the amount of time left are saved. You’ll learn how to verify this is happening in the next part.
Breakpoints
Logging is an effective way of understanding what your app is doing, but it can be tedious to write a log message, recompile, rerun your app and attempt to reproduce the bug. But don’t worry, there’s another way!
Android Studio provides breakpoints. With breakpoints, you can pause the execution of your app to inspect its current state.
In MainActivity.kt, scroll to onSaveInstanceState()
and find the log line at the bottom of the method. Click on the grey border (also known as the gutter) to the left of the line.
This adds a red dot to the gutter to indicate where the breakpoint will trigger. Next, click the Debug button at the top of the window, it looks like a green bug.
The app loads in the same way it did when using the run button, except this time, it attaches the debugger.
Once the app reloads, rotate the screen. Android Studio changes windows and highlights the breakpoint.
Your app is paused at the line that has the breakpoint. In this case, it’s the log message you added earlier where you save the game variables to a Bundle.
When Android Studio hits a breakpoint, it gives you the opportunity to inspect your app’s state at that exact moment in time. You can see this information in the Debug window below your code.
Move to the debugger view and click the arrow next to this = {MainActivity}
.
The number postfixing your MainActivity is likely different since this number indicates where your Activity is allocated in memory.
You might recognize some of the values as your own. However, there are also other values that may be unfamiliar to you. These are values specific to an Activity and give you an appreciation of how much work the Activity
class does behind the scenes.
Also, when Android Studio hits a breakpoint, it inlines some debugging information within your code, which makes it even easier to inspect things.
Time to put this knowledge to use. Close this
in the debugger, expand outState
, and then expand mMap
.
Looking through the items in mMap
, you may notice some familar looking numbers. Compare those numbers with the values of score
and timeLeft
— they should match.
This informs you that those values are now safely stored in the Bundle. In the next section, you’ll see how to restore those numbers when the device orientation changes.
Restarting the game
So far, you’ve only used onCreate()
to set up your Activity. You want to make sure the game doesn’t reset when onCreate()
is called, to do that you need to use the savedInstanceState
object passed into the method as a parameter.
Inside onCreate()
, replace the call to resetGame()
with the following:
if (savedInstanceState != null) {
score = savedInstanceState.getInt(SCORE_KEY)
timeLeft = savedInstanceState.getInt(TIME_LEFT_KEY)
restoreGame()
} else {
resetGame()
}
Here, you check to see if savedInstanceState
contains a value. If it does, you attempt to get the values of score
and timeLeft
from the Bundle that you passed in earlier from onSaveInstanceState
.
You then assign those values to the properties and restore the game. If, however, savedInstanceState
does not contain a value, you reset the game.
Next, implement the following method below resetGame()
:
private fun restoreGame() {
val restoredScore = getString(R.string.your_score, score)
gameScoreTextView.text = restoredScore
val restoredTime = getString(R.string.time_left, timeLeft)
timeLeftTextView.text = restoredTime
countDownTimer = object : CountDownTimer((timeLeft * 1000).toLong(), countDownInterval) {
override fun onTick(millisUntilFinished: Long) {
timeLeft = millisUntilFinished.toInt() / 1000
val timeLeftString = getString(R.string.time_left, timeLeft)
timeLeftTextView.text = timeLeftString
}
override fun onFinish() {
endGame()
}
}
countDownTimer.start()
gameStarted = true
}
restoreGame()
sets up the TextViews and countDownTimer
properties using the values inserted into the Bundle before the change in orientation.
Run the app and play the game for a few seconds. Then, rotate the device to see what happens:
Woohoo! The score and time remaining stayed the same — bug fixed.
Where to go from here?
You only scratched the surface of debugging in Android Studio. Finding and fixing bugs is an important part of software development, so it’s essential that you get comfortable with the tools.
Android Studio contains many debugging tools that are beyond the scope of this chapter. To find out more, read the Android developer documentation: https://developer.android.com/studio/debug/index.html.
Note: Sometimes you aren’t able to fix bugs due to factors beyond your control. There may be bugs in a third-party library you’re using, or maybe even within Android itself. If you find yourself in this situation, inform the developers who maintain that code via their bug reporting channels.
For now, you’re armed with enough tools and techniques to debug potential problems in your own apps. In the next chapter, you’ll finish up TimeFighter so that it looks and feels more in place in the Android ecosystem.