Chapters

Hide chapters

Android Apprentice

Fourth Edition · Android 11 · Kotlin 1.4 · Android Studio 4.1

Section II: Building a List App

Section 2: 7 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

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:

  1. Debug your app using Android Studio’s debug tools.
  2. 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.

Rotate the device, notice something different?
Rotate the device, notice something different?

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:

  1. 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.

  2. You Log a message when the Activity is created. Your app informs you when onCreate() is called and the current value in score. Injecting $score into the message is an example of string interpolation in Kotlin. At runtime, Kotlin looks for score 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:

Logcat shows all the logs happening on your device
Logcat shows all the logs happening on your device

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 established 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:

  1. Attempts to save any properties for the Activity specified by the developer.
  2. Destroys the Activity.
  3. 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:

  1. You create a companion object containing two string constants, SCORE_KEY and TIME_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.

  2. You override onSaveInstanceState and insert the values of score and timeLeft into the passed-in Bundle. onSaveInstanceState is called before a configuration change happens, giving you a chance to save anything important. A Bundle 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.

  3. 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 explicitly destroyed by a developer. You call super so your Activity can perform any essential cleanup, and you add a final log to track when onDestroy() is called.

Note: Companion objects are a quick way to add static properties to a class. These can then be referenced elsewhere in the class and behave similarly to constants. If you’re familiar with other languages that offer static properties, the companion object is analogous to that. Just like constants, there are often better ways to implement static values held in your logic, but you do see these in many examples and now you know what they are! Learn more about Companion objects on the Kotlin Language website.

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 gray 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, tap the button a couple times to start the game, then 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, other values 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 familiar-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()
}

You’ll get an error that the restoreGame() method doesn’t exist, but don’t worry, you’ll add that in a minute.

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.

Key Points

Well done for debugging your first Activity. There are always bugs to be found in an app, learning how to find them and fix them is invaluable. To recap, you learned:

  • How to set up logging for the app.

  • How to use a breakpoint in the app.

  • How to use the debugger in the app and inspect variables.

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 you must 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.