– A guide to debugging applications with Android Studio and common sense
As we all know, the world is on the other end because of a pandemic. This has forced the majority of us to work from home.
As a student, you most likely have multiple courses during a semester, and some of these courses may or may not include assignments where you need to write code. There are often some challenges that you will face when taking a course that requires you to not only learn about new concepts and methodologies but also require you to learn a new programming language on top of all that.
This is a challenge and most people will run into some kind of problems while trying to solve problems using unfamiliar frameworks, tools and programming languages.
This guide aims to help you be better at swatting bugs yourself and at asking for help in the most efficient way for both you and the person helping you.
2. The checklist
In order to make it easier for the reviewer to help you with the problem, try to narrow it down to the smallest possible size. This could include creating a whole new project and trying to reproduce the problem with a minimum amount of code.
- Clean up –Delete unused code and comments.
- Identification –What kind of bug is it? NullPointerException, IndexOutOfBoundsException.
- Investigation –Where in the code does it occur?
- Isolation –Create the smallest possible implementation that reproduces the error.
The first thing you need to do when your code is misbehaving is to identify what kind of error it is and where it occurs. It is important to understand that an error is not just an error – errors can be divided into different categories:
- Syntax errors
- Dependency errors
- Runtime exceptions
- Logical errors
The first place to look is in the ‘build’ and/or ‘Logcat’ tabs in Android Studio.
Syntax errors are by far the easiest errors to identify and fix in your code. They will be in the ‘build’ tab since they are simply a question of reading the language specification. When using some libraries in Android, the framework will auto-generate (a lot) of code for you behind the scenes, and if something goes wrong here, it may result in an
'error: cannot find symbol' or
'error: ';' expected'.
Dependency errors occur either when something is missing or the wrong version of a library is included. Package managers are both a blessing and a curse. They open up a world of functionality ready to use right off the shelves, but you’ll have to navigate carefully when including dependencies into your codebase.
Android uses Gradle and various repositories such as Maven to distribute libraries and this can sometimes result in developers getting their hands on an outdated or wrong version of a library. Android Support Library was used up until Android 9.0 where it was replaced by Android x, which is a part of Jetpack, so be sure that you are using the right versions of the libraries (AndroidX/Jetpack).
Runtime errors are the type of errors that may occur when you are trying to run your application. To see what’s going on under the hood during runtime, we will look at the ‘Logcat’ tab in Android Studio.
There are two types of types exceptions in Java: Checked and unchecked – a checked exception will result in a compiler error and has to be taken care of with a try/catch clause. Checked exceptions denote error scenarios which are not controlled by your code. This could be trying to fetch data from the network, connecting to a database or reading a file from local storage. Although these can be ignored, it is considered bad practice to do so.
Unchecked exceptions are not checked by the compiler, and they occur when the program is running. If an exception of any is not handled, it will propagate to the JVM which will usually terminate the program. Runtime errors are the type of errors that may occur when you are trying to run your application, some of these will result in an unchecked exception of a subtype of
java.lang.RuntimeException). To see what’s going on under the hood during runtime, we will look at the ‘Logcat’ tab in Android Studio.
java.lang.IllegalStateException– Signals that a method has been invoked at an illegal or inappropriate time. In other words, the Java environment or Java application is not in an appropriate state for the requested operation.
java.lang.NullPointerException– Attempt to invoke virtual method ‘void dk.au.orbitlab.testdemo.data.ListItemRepository.setSelectedDataSource(int)’ on a null object reference.
java.lang.IndexOutOfBoundsException– Thrown to indicate that an index of some sort (such as to an array, to a string, or to a vector) is out of range.
java.lang.NumberFormatException– Thrown to indicate that the application has attempted to convert a string to one of the numeric types, but that the string does not have the appropriate format.
android.content.res.Resources.NotFoundException– This exception is thrown by the resource APIs when a requested resource can not be found.
Logical errors are typically the category of errors that consume the biggest amount of time when developing software. This is because they cause our programs to execute unintentionally and do not cause our programs to terminate abnormally or crash.
Some examples of logical errors:
- The data in a RecyclerView does not update after returning for a “details” view where changes to the data were made.
- A text field does not load the correct image into an ImageView.
- A RecyclerView is not showing any data after fetching it from an endpoint on the web.
This category of errors is typically easy to spot when executing the program but can be hard to pinpoint in the source code, especially if you are not familiar with the framework and/or tools you are using to develop the application.
Here you can read about investigating the code:
- Stack traces
- Step-by-step debugging
Printing is a quick-and-dirty way to figure out what is going on in your code. It can be used to get information about variables as the code is executing. Most browsers and IDEs have consoles that display log messages. Remember to remove all your log messages before deploying your application in production – there is no reason to give all users the possibility to see what is going on under the hood.
Stack traces can give an overview of where the error has occurred. The ability to read and understand what is going on is one common way to find out where things have gone wrong. Stack traces typically provide you with the exact file or class and line number of where the error occurred. It is used extensively when debugging runtime errors, so pay close attention to the console next time your code crashes.
Step-by-step debugging allows us to step through our code line by line. It gives us a snapshot of the program state and allows us to advance to the next execution step.
During step-by-step debugging we have some options when we are stepping through the code:
- Step over – Steps over the highlighted line, even if it contains method calls.
- Step into – Steps into a method to see what’s going on. This is often used when a method returns an unexpected result.
- Step out – Steps out of a method and takes you to the caller method.
- Resume – Resumes the program and it will execute until it encounters a breakpoint or terminates.
Caveat – When it comes to stepping through code that is running asynchronous, extra measures have to be taken. See the references at the bottom of this page on how to handle this.
Sometimes, an error can be hard to pinpoint because the program can have many moving parts or even worse it only happens intermittently. Not only is it frustrating to try to figure out what is going on, but it can also be hard to get help because the error only occurs when the reviewer is not looking
This is where the last tool in the toolbox has to be put into action, and that is trying to isolate the problem to a minimal number of lines of code. This exercise helps you to not only think about what your code is during as you are reviewing your own work, but also produces something that can be shown to other programmers and provide them with a stripped-down version of the error, without having to scan through your whole codebase and get overwhelmed by the sheer amount of code you have written.
Generating a really small minimal test case will not always be possible, but trying to is good discipline. It may help you learn what you need to solve the problem on your own.
3. Minimize the chance of introducing bugs
The larger the codebase the higher the chances are that there will be bugs in the code. One of the things we see often is where multiple attempts to solve a specific problem have been made, but no effort to “clean up” the code before moving to the possible solution has been made. This results in a lot of “old” code that in a worst-case scenario has an impact on the execution of the rest of the code.
3.1 Keep things nice and tidy
When the number of moving parts increases, the chance of failure increases as well. This is true for any mechanical system, but also for code.
Be sure to remove unused code from your classes and cleanup after attempts. I recommend complete deletion of the stubs, so they do not take up precious space on your screen when you are scrolling through your files.
3.2 Coding styles
When we are trying to find errors in code, how it was written will often have an impact on our ability to quickly get an idea of how things are working and where potential pitfalls might be.
While all the implementations below are technically correct (the best type of correct), they all have their pros and cons.
Think about which coding style you find most understandable: It is just one, or could it be a combination of two or more?
Code written by a CS 101 student
Code written at a hackathon
Code written at a startup
Code written at a large company
Code written by a math Ph.D.
3.3 Treat warnings as errors
When we are writing code, especially if we are in a hurry, we tend to ignore any warnings the compiler throws at us in the heat of the battle of getting everything up and running before the deadline.
But we are doing ourselves a disservice by doing so. Some warnings can mean the difference between being able to run the code in the future and others are (mostly) harmless nudges to get us to check for null or some other hint that will improve code quality.
So pay attention when you see them! Some are easy to fix, others require quite some work and others can be ignored. Being able to assess the severity and what implications that might occur are just as valuable as being able to the code itself.
4. Reporting bugs
Sometimes you run into a problem that you simply cannot solve yourself within a given time frame. When asking for help, the way you ask will often have a great impact on the answers you get. Especially when you are posting your question on StackOverflow or some mailing list.
Be sure to include the following (from how to ask questions the smart way):
- Describe the symptoms of your problem or bug carefully and clearly.
- Describe the research you did to try and understand the problem before you asked the question.
- Describe the diagnostic steps you took to try and pin down the problem yourself before you asked the question.
- If at all possible, provide a way to reproduce the problem in a controlled environment.
Do the best you can to anticipate the questions a reviewer will ask, and answer them in advance in your request for help.
If you are using email, send it in plaintext and attach relevant source code. Screenshots of chunks of source code or stack traces generally do not help the reviewer to understand what is going on.
Github and Bitbucket are much more than just places where you can upload your code and share it with other developers. Each platform also has the possibility to create pull requests where other people can review and comment on the changes.
By making small incremental changes to your code on isolated branches, it becomes much easier to go back in time to see where you added the code that is misbehaving.
When you are working with Git, there are different workflows that can be applied to the process of creating, reviewing and deploying code. One of the most popular flows is the Feature Branch Workflow.
- Clone master
- Create a new branch
- Update, add, commit, and push changes
- Push feature branch to remote
- Create a pull request
- Resolve feedback
- Merge pull request
So how can it be used when debugging code? It is simple. You provide access for the reviewer to your repository. The reviewer clones your repository and by doing so they get access to everything they need to reproduce the problem on their computer. After looking at your code, they can add comments tied to specific points in your code or create a new branch with their changes and create a pull request.
By using these tools everything is written down and formalized in pull request or comments tied to individual changesets. This makes it possible to go back in time to review changes and find information that might have been forgotten or written down somewhere far away from the code.
5. Reading list
- How to ask questions the smart way
- How to report bugs effectively
- The six most common species of code
- Android Developers – Debug your app
- Android Developers – Exception
- IntelliJ IDEA – Debugging
- IntelliJ IDEA – Tutorial: Detect concurrency issues
- Github – Understanding the GitHub workflow
- Atlassian Git Tutorial – Git feature branch workflow