Advanced debugging in Android (part 1)

Furkan Yurdakul
Appcent
Published in
8 min readMar 24, 2024

--

In this article we’ll be talking about how to debug apps in Android using Android Studio. This article is the part 1 of the debugging topic and will include explanations on what the debugging process is and how to start doing so.

What is debugging?

Debugging is, the essential requirement (the backbone if you will) to determine the state resolve bugs that occur on almost all cases. It becomes possible to do this convenient procedure by using what is called a “Debugger”.

In Android, a debugger is a sub-process inside Android Studio that connects to the device’s internals and allows the code execution to stop at specific points. At the stopped moment, the debugger shows all the variables, current stack trace and the current thread the debugger has stopped on, which provides useful information to determine if something has gone wrong, and eventually fix it.

How to use the debugger in Android Studio?

It is fairly simple to start an application and attaching the debugger at the same time. In these examples, we are going to use an API 30 (Android 11 version) emulator, but it is also possible to do the debugging on a real device that has its developer options enabled. The screenshots and directions are going to be taken from a Macbook, and the version of Android Studio is going to be Android Studio Hedgehog | 2023.1.1 and a new Jetpack Compose project will be used, which is the default project generation tool for Android nowadays.

To initiate where to stop the code from executing, first we need to add what is called a breakpoint. A breakpoint is a tool for IDE (a.k.a Android Studio) to know where to stop executing. For the code to stop there, it needs to hit the exact line, otherwise the debugging is going to be useless. So, make sure the bug actually occurs on that point, or go step-by-step to determine that the bug actually occurs on that line in the end. It can be added by clicking the line number you want to stop the code at. Clicking the breakpoint again removes it. The breakpoints are shown with a red dot on the line number and the line is also covered with a tint of red.

Here is the button:

The debugging button

Using this button will start the application by attaching the debugger to the process, allowing breakpoints to hit.

There are a couple of shortcuts you can use to perform instructions after a breakpoint is hit:

  • F9: Resume the program -> This completely resumes the program until another breakpoint is hit.
  • F8: Step over -> Executes one line, skipping entering function calls if there are any.
  • F7: Step In -> Executes one line and enters the function if there is a function call on that line.
  • Shift + F8: Step out -> If pressed, this function causes the debugger to step out from the function, resuming from the caller a.k.a the line that called the function.

All breakpoints are going to be hit as long as there are breakpoints to hit in between the code.

Here is an example of what happens when a debugger is hit:

A session with a breakpoint hit for debugging

You can see 4 variables. 3 of them belong in Jetpack Compose framework:

  • changed is an internal variable describing a state in compose.
  • composer is the responsible for composing a.k.a drawing and updating the screen, or the local function.
  • RecompositionState is the internal state for this composition, a.k.a function.

The variable we’re looking for is the text variable, which is currently shown as “Android”. This is fairly simple, but it will also show data classes and structures that have their toString method overridden in the debugger. Let’s see what that means.

We will create 2 classes:

class Class1 {
var text = "Android"
}

data class Class2(
val text: String = "Android"
)

And use it in the code like this:

val class1Variable = Class1()
val class2Variable = Class2()
Surface( // we will hit the breakpoint here
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting(class1Variable.text)
}

Here is the result:

Notice the differences between class1Variable and class2Variable , one of them does not show the contents of the class. Instead, it shows the class name and memory address of the variable itself, which is probably not we want. That’s because in every class in Kotlin, there is a toString() method that the debugger also uses to display the variable information. In a data class, the method returns so that the class name will be displayed first, and the information about the variable names and values in between.

Let’s override the toString() method in Class1:

class Class1 {
var text = "Android"

override fun toString(): String {
return "Hello!"
}
}

And here is the result:

As we can see, the class1Variable is now shown as “Hello” in the debugger. The debugger basically takes the value returned from toString() and displays it there, and that is how you can customize the output of a class.

Can we query complex expressions and their results?

Yes, we can. In the screenshot above, there is a textbox with a hint text: “Evaluate Expression or add a watch”. In that textbox, you can query a variable that is not captured or shown, or evaluate expressions. For example, if we write “class1Variable” in there and hit enter, this is what we will see:

An evaluation of class1Variable after hitting enter

The result is shown in the result variable. Since we have overridden Class1’s toString() method and simply returned “Hello!”, the debugger now shows “Hello” as its result. We can also access in its public or private parameters too. Let’s change the Class1 structure:

class Class1 {
var text = "Android"
private var otherText = "OtherText"

override fun toString(): String {
return "Hello!"
}
}

We have now defined a private var otherText = “OtherText” and going to show its value in debugger. When we write class1Variable.text in the evaluate expression textbox, this is what we see:

Accessing the `text` fields

Notice the otherText_field . This is a special property provided from the debugger, by exposing the otherText variable because it is defined as private. When we query the information, this is what happens:

The result of `otherText`

And now we have the result defined as "OtherText" which is the value of the property.

Can we change a variable while debugging?

That is also possible, and it is probably the easiest way to trigger a bug, where you expect a state but cannot get it even once. Let’s try with the text variable from Class1 again.

Let’s check the result of the text variable before changing:

The value is currently "Android" . Now, in the textbox, we’ll write class1Variable.text = “NewAndroid” and hit enter. Let’s see what happens:

It currently shows the result as “undefined” but the value is changed already. To verify, we can query the variable again:

As we can see, the variable is now changed. When the program continues now, the class1Variable.text will have the “NewAndroid” string as its value.

Before showing the result, for reference, the inside of the Greeting function created by default Android Studio project creator looks like this:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}

Let’s verify it by hitting F9 (which is the key to resume the program) by looking at a screenshot from emulator:

Screenshot from emulator

This is the result. So when the text is displayed, it prepends the “Hello ” text before displaying the parameter itself. That means our parameter value change through “Evaluate Expression” textbox was successful!

Here is what would’ve happened if we didn’t do that:

The text remained as “Android”. So, this is basically a live-edit without the need of changing the source code.

We can also evaluate a variable by checking its value. For example:

In its original state, if the check the classVariable.text value which is originally “Android” by giving it an expression (a.k.a something that we write in if conditions for example), now the result is true. If we compared to something else, the result would be false. This action does not change the variable itself.

Conclusion

To be honest, Android Studio or the IntelliJ based editors in general probably provides the strongest debuggers, and I am in love with them. Those were some examples on how to display, edit and even check variables which can be very useful to understand bugs, and even trigger them by forcing a change in the variable. It is also possible to change a value inside an array, list, set, map, whatever you can think of!

However, that’s not all and there are more features and some slight setbacks of using a debugger. Stay tuned for the 2nd part, and I hope this article was helpful to you for understanding debugging a bit further.

Happy debugging!

--

--