Exceptions and Stack Traces

Expand the Logcat tool window so that you can see what has happened. If you scroll up and down in Logcat, you should eventually find an expanse of red, as shown in Figure 5.2. This is a standard AndroidRuntime exception report.

If you do not see much in Logcat and cannot find the exception, you may need to select the No Filters option in the filter dropdown. On the other hand, if you see too much in Logcat, you can adjust the Log Level to Error, which will show only the most severe log messages. You can also search for the text fatal exception, which will bring you straight to the exception that caused the app to crash.

Figure 5.2  Exception and stack trace in Logcat

Exception and stack trace in Logcat

The report tells you the top-level exception and its stack trace, then the exception that caused that exception and its stack trace, and so on until it finds an exception with no cause.

It may seem strange to see a java.lang exception in the stack trace, since you are writing Kotlin code. When building for Android, Kotlin code is compiled to the same kind of low-level bytecode to which Java code is compiled. During that process, many Kotlin exceptions are mapped to java.lang exception classes under the hood through type-aliasing. kotlin.RuntimeException is the superclass of kotlin.UninitializedPropertyAccessException, and it is aliased to java.lang.RuntimeException when running on Android.

In most of the code you will write, that last exception with no cause is the interesting one. Here, the exception without a cause is a kotlin.UninitializedPropertyAccessException. The line just below this exception is the first line in its stack trace. This line tells you the class and function where the exception occurred as well as what file and line number the exception occurred on. Click the blue link, and Android Studio will take you to that line in your source code.

The line to which you are taken is the first use of the questionTextView variable, inside updateQuestion(). The name UninitializedPropertyAccessException gives you a hint to the problem: This variable was not initialized.

Uncomment the line initializing questionTextView to fix the bug.

Listing 5.2  Uncommenting a crucial line (MainActivity.kt)

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    trueButton = findViewById(R.id.true_button)
    falseButton = findViewById(R.id.false_button)
    nextButton = findViewById(R.id.next_button)
    // questionTextView = findViewById(R.id.question_text_view)
    ...
}

When you encounter runtime exceptions, remember to look for the last exception in Logcat and the first line in its stack trace that refers to code that you have written. That is where the problem occurred, and it is the best place to start looking for answers.

If a crash occurs while a device is not plugged in, all is not lost. The device will store the latest lines written to the log. The length and expiration of the stored log depends on the device, but you can usually count on retrieving log results within 10 minutes. Just plug in the device and select it in the Devices view. Logcat will fill itself with the stored log.

Diagnosing misbehaviors

Problems with your apps will not always be crashes. In some cases, they will be misbehaviors. For example, suppose that every time you pressed the NEXT button, nothing happened. That would be a noncrashing, misbehaving bug.

In MainActivity.kt, make a change to the nextButton listener to comment out the code that increments the question index.

Listing 5.3  Forgetting a critical line of code (MainActivity.kt)

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    nextButton.setOnClickListener {
        // quizViewModel.moveToNext()
        updateQuestion()
    }
    ...
}

Run GeoQuiz and press the NEXT button. You should see no effect.

This bug is trickier than the last bug. It is not throwing an exception, so fixing the bug is not a simple matter of making the exception go away. On top of that, this misbehavior could be caused in two different ways: The index might not be changed, or updateQuestion() might not be called.

You know what caused this bug, because you just introduced it intentionally. But if this type of bug popped up on its own and you had no idea what was causing the problem, you would need to track down the culprit. In the next few sections, you will see two ways to do this: diagnostic logging of a stack trace and using the debugger to set a breakpoint.

Logging stack traces

In MainActivity, add a log statement to updateQuestion().

Listing 5.4  Exception for fun and profit (MainActivity.kt)

private fun updateQuestion() {
    Log.d(TAG, "Updating question text", Exception())
    val questionTextResId = quizViewModel.currentQuestionText
    questionTextView.setText(questionTextResId)
}

The Log.d(String, String, Throwable) version of Log.d logs the entire stack trace, like the UninitializedPropertyAccessException you saw earlier. The stack trace will tell you where the call to updateQuestion() was made.

The exception that you pass to Log.d(String, String, Throwable) does not have to be a thrown exception that you caught. You can create a brand new Exception and pass it to the function without ever throwing it, and you will get a report of where the exception was created.

Run GeoQuiz, press the NEXT button, and then check the output in Logcat (Figure 5.3).

Figure 5.3  The results

The results

The top line in the stack trace is the line where you logged out the Exception. A few lines after that you can see where updateQuestion() was called from within your onClick(View) implementation. Click the link on this line, and you will be taken to where you commented out the line to increment your question index. But do not get rid of the bug; you are going to use the debugger to find it again in a moment.

Logging out stack traces is a powerful tool, but it is also a verbose one. Leave a bunch of these hanging around, and soon Logcat will be an unmanageable mess. Also, a competitor might steal your ideas by reading your stack traces to understand what your code is doing.

On the other hand, sometimes a stack trace showing what your code does is exactly what you need. If you are seeking help with a problem at stackoverflow.com or forums.bignerdranch.com, it often helps to include a stack trace. You can copy and paste lines directly from Logcat.

Before continuing, delete the log statement in MainActivity.kt.

Listing 5.5  Farewell, old friend (MainActivity.kt)

private fun updateQuestion() {
    Log.d(TAG, "Updating question text", Exception())
    val questionTextResId = quizViewModel.currentQuestionText
    questionTextView.setText(questionTextResId)
}

Setting breakpoints

Now you will use the debugger that comes with Android Studio to track down the same bug. You will set a breakpoint on updateQuestion() to see whether it was called. A breakpoint pauses execution before the line executes and allows you to examine line by line what happens next.

In MainActivity.kt, return to the updateQuestion() function. Next to the first line of this function, click the gray gutter area in the lefthand margin. You should now see a red circle in the left gutter like the one shown in Figure 5.4. This is a breakpoint. You can also toggle a breakpoint on and off by placing the cursor on the desired line and pressing Command-F8 (Ctrl-F8).

Figure 5.4  A breakpoint

A breakpoint

To engage the debugger and trigger your breakpoint, you need to debug your app instead of running it. To debug your app, click the Debug 'app' button (Figure 5.5). You can also navigate to RunDebug 'app' in the menu bar. Your device will report that it is waiting for the debugger to attach, and then it will proceed normally.

Figure 5.5  Debug app buttons

Debug app buttons

In some circumstances, you may want to debug a running app without relaunching it. You can attach the debugger to a running application by clicking the Attach Debugger to Android Process button shown in Figure 5.5 or by navigating to RunAttach to process.... Choose your app’s process on the dialog that appears and click OK, and the debugger will attach. Note that breakpoints are only active when the debugger is attached, so any breakpoints that are hit before you attach the debugger will be ignored.

You want to debug GeoQuiz from the start of your code, which is why you used the Debug 'app' option. Shortly after your app is up and running with the debugger attached, it will pause. Firing up GeoQuiz called MainActivity.onCreate(Bundle?), which called updateQuestion(), which hit your breakpoint. (If you had attached the debugger to the process after it had started, then the app likely would not pause, because MainActivity.onCreate(Bundle?) would have executed before you could attach the debugger.)

In Figure 5.6, you can see that MainActivity.kt is now open in the editor tool window and that the line with the breakpoint where execution has paused is highlighted. The debug tool window at the bottom of the screen is now visible. It contains the Frames and Variables views. (If the debug tool window did not open automatically, you can open it by clicking Debug at the bottom of the Android Studio window.)

Figure 5.6  Stop right there!

Stop right there!

You can use the arrow buttons at the top of the debug tool window (Figure 5.7) to step through your program. You can use the Evaluate Expression button to execute simple Kotlin statements on demand during debugging, which is a powerful tool.

Figure 5.7  Debug tool window controls

Debug tool window controls

You can see from the stack trace on the left that updateQuestion() has been called from inside onCreate(Bundle?). But you are interested in investigating the NEXT button’s behavior, so click the Resume Program button to continue execution. Then press the NEXT button in GeoQuiz to see if your breakpoint is hit and execution is stopped. (It should be.)

Now that you are stopped at an interesting point of execution, you can take a look around. The Variables view allows you to examine the values of the objects in your program. At the top, you should see the value this (the MainActivity instance itself).

Expand the this variable to see all the variables declared in MainActivity, in MainActivity’s superclass (Activity), in Activity’s superclass, in its super-superclass, and so on. For now, focus on the variables that you created.

You are only interested in one value: quizViewModel.currentIndex. Scroll down in the variables view until you see quizViewModel. Expand quizViewModel and look for currentIndex (Figure 5.8).

Figure 5.8  Inspecting variable values at runtime

Inspecting variable values at runtime

You would expect currentIndex to have a value of 1. You pressed the NEXT button, which should have resulted in currentIndex being incremented from 0 to 1. However, as shown in Figure 5.8, currentIndex still has a value of 0.

Check the code in the editor tool window. The code in MainActivity.updateQuestion() simply updates the question text based the contents of QuizViewModel. There is no problem there. So where does the bug originate?

To continue your investigation, you need to step out of this function to determine what code was executed just before MainActivity.updateQuestion(). To do this, click the Step Out button.

Check the editor tool window. It has now jumped you over to your nextButton’s OnClickListener, right after updateQuestion() was called. Pretty nifty.

As you already knew, the problematic behavior is a result of quizViewModel.moveToNext() never being called (because you commented it out). You will want to fix this implementation – but before you make any changes to code, you should stop debugging your app. If you edit your code while debugging, the code running with the debugger attached will be out of date compared to what is in the editor tool window, so the debugger can show misleading information compared to the updated code.

You can stop debugging in two ways: You can stop the program, or you can simply disconnect the debugger. To stop the program, click the Stop button shown in Figure 5.7.

Now return your OnClickListener to its former glory.

Listing 5.6  Returning to normalcy (MainActivity.kt)

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    nextButton.setOnClickListener {
        // quizViewModel.moveToNext()
        updateQuestion()
    }
    ...
}

You have tried out two ways of tracking down a misbehaving line of code: stack trace logging and setting a breakpoint in the debugger. Which is better? Each has its uses, and one or the other will probably end up being your favorite.

Logging out stack traces has the advantage that you can see stack traces from multiple places in one log. The downside is that to learn something new you have to add new log statements, rebuild, deploy, and navigate through your app to see what happened.

The debugger is more convenient. If you run your app with the debugger attached (or attach the debugger to the application’s process after it has started), then you can set a breakpoint while the application is running and poke around to get information about multiple issues.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.227.69