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 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 Show only selected application or No Filters option in the filter dropdown. On the other hand, if you see too much in Logcat, you can adjust the log level from Verbose 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 Java code is compiled to. During that process, many Kotlin exceptions are mapped to java.lang exception classes 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, the last exception in the Logcat report – the one 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 you are taken to is the first use of the binding variable, inside onCreate(Bundle?). The name UninitializedPropertyAccessException gives you a hint to the problem: This variable was not initialized.

Uncomment the line initializing binding to fix the bug.

Listing 5.2  Uncommenting a crucial line (MainActivity.kt)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d(TAG, "onCreate(Bundle?) called")
    // binding = ActivityMainBinding.inflate(layoutInflater)
    ...
}

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 nothing happened any time you pressed the NEXT button. That would be a noncrashing, misbehaving bug.

In QuizViewModel.kt, comment out the code in the moveToNext() function that increments the current question index.

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

fun moveToNext() {
    // currentIndex = (currentIndex + 1) % questionBank.size
}

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 code to update the UI 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 QuizViewModel, add a log statement to moveToNext().

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

fun moveToNext() {
    Log.d(TAG, "Updating question text", Exception())
    // currentIndex = (currentIndex + 1) % questionBank.size
}

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 moveToNext() 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. On the next line, you can see where moveToNext() was called from within your MainActivity. 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.

Setting breakpoints

Now you will use the debugger that comes with Android Studio to track down the same bug. You will set a breakpoint in moveToNext() 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 QuizViewModel.kt, return to the moveToNext() function. Next to the first line of this function, click the gray gutter area in the lefthand margin. You should see a red circle in the gutter like the one shown in Figure 5.4. This is a breakpoint.

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.

Click the NEXT button. In Figure 5.6, you can see that QuizViewModel.kt is now open in the editor 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 the Debug tool window bar 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

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 QuizViewModel instance itself).

Expand the this variable to see all the variables declared in QuizViewModel and in QuizViewModel’s superclass (ViewModel). For now, focus on the variables that you created.

You are only interested in one value – currentIndex – but it is not here. That is because currentIndex is a computed property. Note that you also do not see currentQuestionAnswer or currentQuestionText. But questionBank is there. Expand it and look at each one of its Questions (Figure 5.8).

Figure 5.8  Inspecting variable values at runtime

Inspecting variable values at runtime

Even though you do not see currentIndex, you can still access it. Click the Evaluate Expression button within the debug tool window. In the Expression: text field, enter currentIndex and press the Evaluate button (Figure 5.9).

Figure 5.9  Evaluating the current index

Evaluating the current index

The debugger will evaluate and print the current value of currentIndex. You pressed the NEXT button, which should have resulted in currentIndex being incremented from 0 to 1. So you would expect currentIndex to have a value of 1. However, as shown in Figure 5.9, currentIndex still has a value of 0.

Close the Evaluate window. As you already knew, the problematic behavior results from the code within 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 QuizViewModel to its former glory. You are done with the log message (and the TAG constant), so delete them to keep your file tidy. Also, remove the breakpoint you set by clicking it in the gutter.

Listing 5.5  Returning to normalcy (QuizViewModel.kt)

const val CURRENT_INDEX_KEY = "CURRENT_INDEX_KEY"
private const val TAG = "QuizViewModel"

class QuizViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    ...
    fun moveToNext() {
        Log.d(TAG, "Updating question text", Exception())
        // currentIndex = (currentIndex + 1) % questionBank.size
    }
}

You have tried 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 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
3.137.164.24