Debug with breakpoints

1. Before you begin

Up until this point, most beginning developers will probably be aware of debugging with Log statements. If you've completed Unit 1, you'll also be aware of how to read stack traces and research error messages. While these are both powerful debugging tools, modern IDEs provide more functionality to make your debugging process more efficient.

In this lesson, you'll learn about Android Studio's integrated debugger, how to pause execution of an app, and how to execute single lines of code at a time to pinpoint the exact source of the bug. You'll also learn how to use a feature called Watches, and track specific variables instead of having to add specific log statements.

Prerequisites

  • You know how to navigate a project in Android Studio.
  • Familiarity with logging in Kotlin.

What you'll learn

  • How to attach the debugger to your running app.
  • Use breakpoints to pause a running app and inspect the code one line at a time.
  • Add conditional expressions to breakpoints to save time debugging.
  • Add variables to the Watches pane to aid with debugging.

What you'll need

  • A computer with Android studio installed.

2. Create a new project

Rather than debugging a large and complex app, we're going to start with a blank project and introduce some buggy code to demonstrate the debugging tools in Android Studio.

Start by creating a new Android Studio project.

  1. On the Select a Project Template screen, choose Blank Activity.

a949156bcfbf8a56.png

  1. Name the app Debugging, make sure the language is set to Kotlin, and everything else is unchanged.

9863157e10628a87.png

  1. You'll be greeted with a new Android Studio project, showing a file called MainActivity.kt.

e3ab4a557c50b9b0.png

Introduce a bug

Remember the division by zero example from the debugging lesson in Unit 1? On the final iteration of the loop, when the app attempts to perform division by zero, the app crashes with a java.langArithmeticException, because division by zero is impossible. That bug was found and fixed by examining the stack trace, and this assumption was verified using log statements.

As you're already familiar with that example, it will be used to demonstrate how breakpoints can be used. Breakpoints step through code one line at a time without the need to add log statements and re-running your app first.

  1. Open up MainActivity.kt and replace the code with the following:
package com.example.myapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

public val TAG = "MainActivity"

class MainActivity : AppCompatActivity() {
  
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
       division()
   }

   fun division() {
       val numerator = 60
       var denominator = 4
       repeat(5) {
           Log.v(TAG, "${numerator / denominator}")
           denominator--
       }
   }

}
  1. Execute the app. Bbserve that the app crashes as expected.

9468226e5f4d5729.png

3. Debug with breakpoints

When you learned about logging, you learned to strategically place logs to help identify bugs, and verify that they've been fixed. However, when faced with bugs that you didn't introduce, it's not always clear where to place log statements, or which variables to print out. Often, you can only find this information at runtime.

fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

This is where breakpoints come in! Even if you have a vague idea of what's causing the bug based on information in the stack trace, you can add a breakpoint serving as a stop sign for a specific line of code. Once a breakpoint is reached, it will pause execution, allowing you to use other debugging tools at runtime to get an up-close look at what's happening, and what's gone wrong.

Attach the debugger

Behind the scenes, Android Studio uses a tool called Android Debug Bridge, also known abbreviated as ADB. This is a command-line tool that's integrated into Android Studio, and provides debugging capabilities, such as breakpoints, to your running apps. A tool for debugging is often called a debugger.

To use or attach the debugger to an app, you can't simply run the app with Run > Run like before. Instead, you run the app with Run > Debug ‘app'.

21d706a854ebe710.png

Add breakpoints to your project

Perform the following steps to see breakpoints in action:

  1. Add a breakpoint by clicking the gutter next to the line number you want to pause at. A dot will appear next to the line number, and the line will be highlighted.

6b6c2cd97bdc08ba.png

  1. Run your app with the debugger attached using Run > Debug ‘app' or the f6a141c7f2a4e444.png icon in the toolbar. When the app launches, you should see a screen like this:

3bd9cbe69d5a0d0e.png

Once the app has launched, you'll see the breakpoint highlighted when it's activated.

a4860e59534f216a.png

At the bottom of the screen where you previously viewed the Logcat window, a new Debug tab has opened up.

ce37d2791db7302.png

On the left side is a list of functions, which are the same as the list that appeared in the stack trace. On the right side is a pane where you can check the values of individual variables in the current function (i.e. division()). At the top, there are also buttons that help you navigate your program while it's paused. The button you'll use most often is Step Over, which executes the single highlighted line of code.

a6c07c89e81abdc5.png

Perform the following steps to debug the code:

  1. After the breakpoint was reached, line 19 (declaring the numerator variable) is now highlighted but hasn't run yet. Use the Step Over 1d02d8134802ee64.png button to execute line 19. Now line 20 will be highlighted.

aa8f7063c836af0d.png

  1. Set a breakpoint at line 22. This is where the division occurred and is the line that the stack trace reported the exception.

85758e1d4e69f9eb.png

  1. Use the Resume Program 8119afebc5492126.png button at the left of the Debug window to go to the next breakpoint. Run the rest of the division() function.

433d1c2a610b7945.png

  1. Notice that execution stops at line 17 before executing it.

2a16075287a03058.png

  1. The values of each variable numerator and denominator are shown next to their declarations. The values of the variables can be seen in the debug window in the Variables tab.

ebac20924bafbea5.png

  1. Press the Resume Program button on the left of the debug window four more times. Each time the loop pauses and observes the values of numerator and denominator. On the last iteration, numerator should be 60, and denominator should be 0. And you can't divide 60 by 0!

246dd310b7fb54fe.png

Now you know the exact line of code that causes the bug, and know the exact reason. Like before, you can fix the bug by changing the number of times to repeat the code from 5 to 4.

fun division() {
    val numerator = 60
    var denominator = 4
    repeat(4) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

4. Set conditions for breakpoints

In the previous section, you had to step through each iteration of the loop until the denominator was zero. In more complicated apps, this can be tedious when you have less information about the bug. However, if you have an assumption, such as the app only crashing when the denominator is zero, you can modify your breakpoint so that it will only be reached when this assumption is met, rather than having to step through each iteration of the loop.

  1. If necessary, re-introduce the bug by changing 4 to 5 in the repeat loop.
repeat(4) {
    ...
}
  1. Place a new breakpoint on the line with the repeat statement.

2da539348a57af97.png

  1. Right click on the red breakpoint icon. A menu will appear with a few options, such as whether or not the breakpoint is enabled. A disabled breakpoint still exists, but won't be triggered at runtime. You also have an option to add a Kotlin expression that if it evaluates to true, the breakpoint will be triggered. For example, if you used the expression denominator > 3, the breakpoint would only be triggered on the first iteration of the loop. To only trigger the breakpoint when your app is potentially going to divide by zero, set the expression to denominator == 0. The options for the breakpoint should look like the following:

890b999988aef59b.png

  1. Run your app using Run > Debug ‘app' and observe that the breakpoint is reached.

482ec7c2a23040ef.png

You can see that the denominator is already 0. The breakpoint was only triggered when the condition was met, saving you time and effort to step through the code.

9f4e29b5a91fdb12.png

  1. Like before, you see that the bug is caused by the loop executing one too many times, where the denominator was set to 0.

Add watches

If you want to monitor a specific value while debugging, you don't need to search in the Variables tab to find it. You can add something called Watches to monitor specific variables. These variables will be visible in the debug pane. When execution is paused and that variable is in scope, it will be visible in the Watches pane. This makes your debugging more efficient when working with larger projects. You'll be able to keep track of all the relevant variables in one place.

  1. In the debug view, to the right of the variables pane there should be another empty pane called Watches. Click the plus d6fb21f8b9c67448.png button in the top left corner. You may see a menu option that says New Watch.

990d62a98ec9ba5b.png

  1. Type the name of the variable, denominator, in the provided field and click enter.
  2. Re-run your app with Run > debug ‘app', and observe that when the breakpoint is hit, you'll see the value of the denominator in the Watches pane.

5. Congratulations

In summary:

  • You can set breakpoints to pause the execution of your app.
  • When execution is paused, you can "step over" to execute only a single line of code.
  • You can set conditional statements to only trigger breakpoints based on a Kotlin expression.
  • Watches allow you to group variables of interest when debugging.

Learn more