10. Aug 2021
AndroidHow to debug code in the Android Studio
Learning to debug software effectively is difficult. It takes patience and a lot of debugging. There must also be an abstract imagination and knowledge of the project structure. Well, everybody started somewhere. I will introduce you to a solid foundation on how to start debugging in an Android studio and what it is used for.
Debug (or deworm)
Debugging is a process of detecting a code error. The more we care about thorough code implementation, the less or not we have to go back to it. We spend much more time isolating the bug and debugging it than we do with the implementation. The golden rule of our mothers also applies here:
Measure twice, cut once! - random mom
We can't avoid debugging when developing Android applications. Even the best developers make mistakes that are not immediately apparent to the eye. They will be revealed later in testing or in future development. At first glance, it may not be immediately clear what the problem is, and then we reach for debuggers.
Android debugger
Android debugger is a tool in Android Studio, with which we can connect to the running process of the developed application or we can start it so that it is connected right from the start. If we want to use a physical device, we must enable debugging and install the correct application variant through the studio.
How to turn on the development interface on a physical device
- Open phone settings
- Navigate to About phone
- Tap Build number couple of times
- Approximately after 5 taps, you'll get a message "You are now a developer"
- Navigate back to phone settings
- Open System
- You should see an option Developer options
- Open and scroll down to section Debugging
- Turn on USB debugging
How to run debugger in Android studio
If we have already opened project in Android Studio, connect the device with a an USB cable to your computer. If you want to debug on an emulator, start the emulator. Most of the android devices will notify you, if you trust the connected computer. Choose allow because we need to read data trough USB.
Now we have a choice:
- We can run the app trough launcher or studio and then connect the debugger
- We cam run the app with connected debugger trough studio
After successful run, nothing special happens if we have not yet choose any breakpoints.
Debugging
In out example we have a ViewModel which calculates factorial of the given number n.
class DebugViewModel: ViewModel() {
private val _state: MutableStateFlow<Int> = MutableStateFlow(0)
val state: StateFlow<Int> get() = _state
fun calculateFactorialOf(n: Int) {
viewModelScope.launch {
var result = 1
repeat(n) { i ->
result = calculate(result, i)
}
_state.emit(result)
}
}
private fun calculate(res: Int, i: Int): Int = res * i
}
After running the app and executing function calculateFactorialOf we can see our result is always 0. At first glance it's not clear, why is this happening. Our code looks good. After clicking on a line next to the line number, we can choose a breakpoint
For starting the app with debugger connected click on the bug at top toolbar. For connecting the debugger to a running process click on the bug with an arrow at top toolbar.
Than we can choose which process we want to debug. In our case we are choosing our example.
After confirming you should see a tab called Debug at the bottom of the IDEA. If that does not happen, you can open it from status bar View → Tool Windows → Debug. This is our debugging interface in which we will controll the debug process step by step.
Now we need to reproduce the action which executed function calculateFactorialOf. In our case it a button. After tapping the button, IDEA opens the file with breakpoint, where it stopped, and focuses on the line with the breakpoint. As you have noticed, user interface on the device "froze". It is because our viewmodel code is running on the main UI thread. During debug, when a process stops on a breakpoint it stops the execution on the current thread until we continue or resume.
- Step over. After clicking it, process will continue to the next line.
- Step into. If there is a function at the current line, it will enter the function. If there is non or every function on the line was already executed, it will continue to the next line.
- Force step into. If its an external function, it may be configured to not be accessible by standard step into, this will force the debugger to step into the function.
- Step out. We can step out of the function to the line where we entered it. This is our program callstack and we can observe it under number 12.
- If we place the cursor on other line, we can let the process execute until it hits the selected line or it will stop on the way if there are other breakpoints.
- Evaluate expression. Its a separate context executor, where we can evaluate values of variables or execute small blocks of code to check their return value. For an example we can evaluate calculate(2, 2) and see what result would it return without changing the current process.
- Restart app.
- Continue execution until next breakpoint.
- Stop debug. This will detach debugger from the process.
- Open list of breakpoints.
- Ignore breakpoints. Debugger is still attached, but won't stop on any breakpoint.
- Callstack - Is a list of routines and subroutines. Thanks to it, we can identify from where we entered the current routine and where we return after finishing current work. We have white lines - these identify routines, which are defined in our project source code and yellow lines - which identifies routines, that are defined in external source code or dependencies. Try clicking on a white line, and it will take you to the file and line where it entered the routine with breakpoint
- Variables - Here we can see all allocated variables which are visible to our scope.
Identify the problem
Reason we have result 0 can be bad calculation, which is not visible on the first glance or we did not take in count a side effect during our calculation. Thats why we chose to put a breakpoint at the beginning of our calculation.
Now we can step over to the next line. Now we can see in the Variables, we have a new variable called result and its value is 1.
Now we are on a line with function repeat, which executes given lambda n times. In the lambda, we call a function, where we multiple our current result with the given index. Logically it makes sense, factorial of number 5 is f=5*4*3*2*1, index in repeat starts with the lowest number and if we multiply other way around, we get the same result because of commutativity.
Lets try to enter our lambda by clicking on the number of the line. Now in our Variables section we see actual index in our repeat function called i and its value. So our index does not start with 1 but with 0!. When we multiple by 0 we always get 0. We can now fix our code by adding result = calculate(result, i+1). Done, we have found and fixed our error with the help of debugger 👏.
Threads
As mentioned above, if a debugger reaches a breakpoint, it suspends current thread until we let it continue. We can have a breakpoint on other thread, which is still running on the background. What happens, if we reach a breakpoint on such thread? Out debugger will let us know, that we have a suspension on another thread. During debug, we can switch between threads we want to debug.
Quick example:
USING watchers
Often during debug we are observing more variables at once and observe they value how they change. Our example is very primitive and only for demonstration, but in reality, there are a lot of variables with not just primitive values but nested classes. Sometimes, our little bug can be one of those nested variables witch should be 1 but instead was 0 can lose hair of any developer.
Watcher can help us out here. Watcher is an variable observer. Our watchers are displayed on top of our Variables section. Their main goal is to make debugging easier and faster. Without them, we would need to click through nested classes each time.
In the previous example, we define a breakpoint inside lambda of repeat. We run the program, attach debugger and execute the function so we reach our breakpoint. Lets say we want to know the result of the calculation before it is executed. We click on the + sign and we define out function call same way as we have in the line of code and submit with enter.
Now if we continue our process and reach again the same breakpoint, we see the result of the calculation.
The same way we can add a watcher by right clicking on a variable and choosing Add to Watches.
List of breakpoint and their options
Debug can often be surgical. By that I mean an error can happen only in a specific condition and during debug we don't want to waste time by stopping and a breakpoint each time.
Opening the list of breakpoints, we can observe all breakpoints defined in our project. Lets say, our calculation does not work when we use number 5, all other cases work as expected. We can define a breakpoint condition when should the debugger suspend:
Or if we had a couple of breakpoints, we can create dependencies between them when should they activate. For example when we reach breakpoint at line 15, than activate breakpoint at line 17.
We can set the current breakpoint to not just suspend current thread, but all running threads. Or that it should not suspend at all, only to print something in the console. This is specially useful, if you need to activate a set of breakpoints but the activation point is not relevant to us.
There are a lot of combinations and all these options are for to make debugging faster.
Options of a breakpoint can be also viewed by right clicking on it in the line.