When building Android apps, your app is bound to crash from time to time or exhibit strange unexpected behavior. You know you have experienced a runtime exception when you see this in your emulator or device:
Don't worry though! This is totally normal and there's a specific set of steps you can take to solve these. Refer to our guide below and/or these debugging slides for more a detailed look at debugging crashes and investigating unexpected problems with your app.
As an Android developer, you'll need to cultivate a "debugging mindset" as well as build up defensive programming practices that make writing error-prone code less likely. In addition, you'll often find yourself in the role of a coding investigator in order to understand where and why an app is crashing or not working as expected. A few key principles about debugging are captured below:
The following high-level principles should be applied when faced with an unexpected app behavior during investigation:
Armed with this mindset and the above principles, let's take a look at how to debug and investigate issues that arise within our apps.
When you see your app crash and close, the basic steps for diagnosing and resolving this are outlined below:
This process nearly always starts with an unexpected app crash as we'll see below.
Suppose we were building a simple movie trailer app called Flixster that lets the users browse new movies and watch trailers from Youtube. Imagine we ran the app, and we wanted to play the trailer and we saw this crash instead:
First off though when you see the crash dialog, don't press OK on that dialog until after you've already went through these steps below to identify the stacktrace details.
First, within Android Studio, be sure to setup your Android Monitor to filter for "Errors" only to reduce noise:
This will set you up to see only serious issues as they come up.
Note: See this blog post for improving the coloring of errors or logs in LogCat
and for other related tools.
Now let's go into Android Studio and select open up the "Android Monitor". Expand the monitor so you can read the log messages easily.
Caused by
all the way at the bottom of the stack trace block. The "original cause" towards the bottom of the block is the important part of the error, ignore most of the rest of the stack trace above that.Caused by
line as well as the line that has the blue link with the name of your activity i.e VideoActivity.java:13
. Copy them onto your clipboard and paste them both into a separate text file for easy review.In this case the bottom-most "Caused by" line and the adjacent blue file link copied together looks like:
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method
'java.lang.String android.content.Intent.getStringExtra(java.lang.String)'
on a null object reference
at com.codepath.flixster.VideoActivity.<init>(VideoActivity.java:13)
Note that the top part of the stack trace block above that line noting FATAL EXCEPTION
and the first portion with the java.lang.RuntimeException
are much more generic errors and are less useful than that final bottom-most "Caused by" exception captured above which points to the real culprit.
Next, we need to identify the actual issue that this stack trace is warning us about. To do this, we need to identify the following elements of the problem:
Consider the exception we copied above:
This exception needs to be translated to identifying the following elements:
Element | Identified culprit |
---|---|
Filename | VideoActivity.java |
Line Number | Line 13 |
Exception Type | java.lang.NullPointerException |
Exception Message | Attempt to invoke virtual method 'java.lang.String android.content.Intent.getStringExtra(java.lang.String)' on a null object reference |
Next, we need to open up the offending file and line number. In this case, the VideoActivity.java
to line 13 which looks like this:
public class VideoActivity extends YouTubeBaseActivity{
// -----> LINE 13 is immediately below <--------
String videoKey = getIntent().getStringExtra("video_key");
// ...
protected void onCreate(Bundle savedInstanceState) {
// ....
}
}
Therefore, we know the crash is happening on line 13:
String videoKey = getIntent().getStringExtra("video_key");
This is a great first step to now solving the problem because we know exactly where the exception is being triggered.
Next, we need to use the exception type and message to diagnose the problem at that line. The type in this case is java.lang.NullPointerException
and the message was Attempt to invoke virtual method 'java.lang.String android.content.Intent.getStringExtra(java.lang.String)' on a null object reference
.
The type is the most important part because there are a limited number of types. If the type is java.lang.NullPointerException
then we know that some object is null when it shouldn't be. In other words, we are calling a method on an object or passing an object that is null (has no value). This usually means we forgot to set the value or the value is being set incorrectly.
The message above gives you the specifics that the method getStringExtra
is being called on a null object. This tell us that getIntent()
is actually null since this is the object getStringExtra
is being called on. That might seem strange, why is the getIntent()
null here? How do we fix this?
Often we won't know what is going wrong even after we've diagnosed the issue. We know that "getIntent()
is null and shouldn't be". But we don't know why or how to fix.
At this stage, we need to google cleverly for the solution. Any problem you have, stackoverflow probably has the answer. We need to identify a search query that is likely to find us answers. The recipe is generally a query like android [exception type] [partial exception message]
. The type in this case is java.lang.NullPointerException
and the message was Attempt to invoke virtual method 'java.lang.String android.content.Intent.getStringExtra(java.lang.String)' on a null object reference
.
We might start by googling: android java.lang.NullPointerException android.content.Intent.getStringExtra(java.lang.String)
. These results get returned.
You generally want to look for "stackoverflow" links but in this case, the first result is this other forum.
Scroll to the bottom and you will find this message by the original author:
I realize my mistake now. I was calling the getIntent() method outside of the onCreate method. As soon as I moved the code calling getIntent() inside the onCreate method it's all working fine.
Let's give this solution a try! He seems to suggest the issue is that getIntent()
is being called outside of the onCreate
block and we need to move it inside that method.
Based on the advice given on stackoverflow or other sites from Google, we can then apply a potential fix. In this case, the VideoActivity.java
can then be changed to:
public class VideoActivity extends YouTubeBaseActivity{
// ...move the call into the onCreate block
String videoKey; // declare here
protected void onCreate(Bundle savedInstanceState) {
// ....
// set the value inside the block
videoKey = getIntent().getStringExtra("video_key");
}
}
Now, we can run the app and see if things work as expected!
Let's re-run the app and try this out again:
Great! The exception seems to have been fixed!
Sometimes the first fix we try after googling doesn't work. Or it makes things worse. Sometimes the solutions are wrong. Sometimes we have to try different queries. Here's a few guidelines:
Don't get discouraged! Look through a handful of sites and you are bound to find the solution in 80% of cases as long as you have the right query. Try multiple variations of your query until you find the right solution.
In certain cases, we need to investigate the problem further. The methods for investigating why something is broken are outlined below.
In addition to finding and googling errors, often additional methods have to be applied to figure out what is going wrong. This usually becomes helpful when something isn't as we expected in the app and we need to figure out why. The three most common investigation techniques in Android are:
Both methods are intended for us to be able to determine why a value isn't as we expect. Let's take a look at how to use each.
Let's start with the same app from above and suppose we are trying to get the app so that when the image at the top of the movie details page is clicked, then the movie trailer begins playing.
public class InfoActivity extends YouTubeBaseActivity {
private ImageView backdropTrailer;
private String videoKey;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_info);
// ...
backdropTrailer = (ImageView) findViewById(R.id.ivPoster);
// Trigger video when image is clicked
backdropTrailer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Launch youtube video with key if not null
if (videoKey != null) {
Intent i = new Intent(InfoActivity.this, VideoActivity.class);
i.putExtra("videoKey", videoKey);
startActivity(i);
}
}
});
}
}
Unfortunately when testing, we see that the trailer does not come up as expected when we run the app and click the top image:
Why doesn't the video launch when you click on the image as expected? Let's investigate.
The video activity isn't launching when the user presses the image but let's see if we can narrow down the problem. First, let's add a toast message to make sure we can begin to understand the issue. Toasts are messages that popup within the app. For example:
Toast.makeText(this, "Message saved as draft.", Toast.LENGTH_SHORT).show();
would produce:
In InfoActivity.java
, we will add the following toast message inside the onClick
listener method:
public class InfoActivity extends YouTubeBaseActivity {
// ...
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_info);
// ...
backdropTrailer = (ImageView) findViewById(R.id.ivPoster);
backdropTrailer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Launch youtube video with key if not null
if (videoKey != null) {
Intent i = new Intent(InfoActivity.this, VideoActivity.class);
i.putExtra("videoKey", videoKey);
startActivity(i);
} else {
// ----> ADD A TOAST HERE. This means video key is null
Toast.makeText(InfoActivity.this, "The key is null!", Toast.LENGTH_SHORT).show();
}
}
});
}
}
With the toast added, running this again, we see the problem is confirmed:
The problem is that the youtube video key is null where as it should have a value. We haven't fixed the problem, but we at least know what the initial issue is.
Using a toast message we can easily notify ourselves when something goes wrong in our app. We don't even have to check the logs. Be sure to add toasts whenever there are failure cases for networking, intents, persistence, etc so you can easily spot when things go wrong.
Next, let's fix the problem the toast clarified for us. We now know the problem is that the youtube video key is null where as it should have a value. Let's take a look at the method that fetches the video key for us:
public void fetchMovies(int videoId) {
// URL should be: https://api.themoviedb.org/3/movie/246655/videos?api_key=KEY
String url = "https://api.themoviedb.org/3/movie" + movie_id + "/videos?api_key=" + KEY;
client.get(url, new JsonHttpResponseHandler(){
@Override
public void onSuccess(int statusCode, Headers headers, JSON response) {
JSONArray movieJsonResults = null;
try {
movieJsonResults = response.getJSONArray("results");
JSONObject result = movieJsonResults.getJSONObject(0);
// Get the key from the JSON
videoKey = result.getString("key");
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
This method is somehow not fetching the key that we'd expect. Let's start by logging inside the onSuccess
method to see if we are getting inside there as we expect.
The Android logger is pretty easy to use. The log options are as follows:
Level | Method |
---|---|
Verbose | Log.v() |
Debug | Log.d() |
Info | Log.i() |
Warn | Log.w() |
Error | Log.e() |
They are sorted by relevance with Log.i()
being the least important one. The first parameter of these methods is the category string (can be any label we choose) and the second is the actual message to display.
We can use the logger by adding two lines to the code: one at the top before the network call and one inside onSuccess
to see if they both display:
Log.e("VIDEOS", "HELLO"); // <------------ LOGGING
client.get(url, new JsonHttpResponseHandler(){
@Override
public void onSuccess(int statusCode, Headers headers, JSON response) {
JSONArray movieJsonResults = null;
try {
// LOG the entire JSON response string below
Log.e("VIDEOS", response.toString()); // <------------ LOGGING
movieJsonResults = response.getJSONArray("results");
JSONObject result = movieJsonResults.getJSONObject(0);
videoKey = result.getString("key");
} catch (JSONException e) {
e.printStackTrace();
}
}
});
When running the app, the first log does show up as expected but the one inside onSuccess
does not show up at all in the Android monitor:
Notice that we see HELLO
but no other line logs. This means that we now know that the onSuccess method is never called. This means our network request sent out is failing for some reason. We are one step closer to fixing this issue.
In order to investigate why the network request sent out is failing, we are going to bring out the most powerful debugging tool we have which is the breakpointing engine in Android Studio which allows us to stop the app and investigate our environment thoroughly.
First, we have to decide at which points we want to stop the app and investigate. This is done by setting breakpoints. Let's set two breakpoints to investigate the network request:
Now, we need to run the app using the "debugger" rather than the normal run command:
Once the debugger connects, we can click on the movie to trigger the code to run. Once the code hits the spot with a breakpoint, the entire code pauses and let's us inspect everything:
Here we were able to inspect the URL and compare the URL against the expected value. Our actual URL was "https://api.themoviedb.org/3/movie246655/videos?api_key=KEY" while the expected URL was "https://api.themoviedb.org/3/movie/246655/videos?api_key=KEY". Extremely subtle difference. Can you spot it?
Then we can hit "resume" () to continue until the next breakpoint or stop debugging () to end the session.
Breakpoints are incredibly powerful and worthy of additional investigation. To learn more about breakpoints, check out this official Android Guide on debugging and this third-party breakpoints guide.
Now that we know the issue is the URL being incorrectly formed, we can fix that in the original code in InfoActivity.java
:
public void fetchMovies(int videoId) {
// ADD trailing slash to the URL to fix the issue
String url = "https://api.themoviedb.org/3/movie/" + // <----- add trailing slash
movie_id + "/videos?api_key=" + KEY;
client.get(url, new JsonHttpResponseHandler(){
// ...same as before...
});
}
and remove any old log statements we don't need. Now, we can try running the app again:
Great! The video now plays exactly as expected!
In this section, we looked at three important ways to investigate your application:
With these tools and the power of Google, you should be well-equipped to debug any errors or issues that come up as you are developing your apps. Good luck!