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.
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 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.
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:
After successful run, nothing special happens if we have not yet choose any breakpoints.
In out example we have a ViewModel which calculates factorial of the given number n.
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.
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 👏.
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.
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.
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.