Techbase Academy
Android · Lesson 18

Debugging with Android Studio

11 phasesBug Hunt: The Exam Marks CalculatorAndroid Studio
Phase 1 of 11
Lesson Introduction

Welcome back! In lesson 17 you built a Dice Roller that reacts to taps. Sooner or later every app you build will do something you did not expect: a wrong total, a button that does nothing, or the dreaded "App keeps stopping" dialog. Today you learn what professional developers do in that moment. They do not guess and they do not panic. They debug.

A bug is a gap between what you meant the program to do and what it actually does. Debugging is the detective work of closing that gap. Think of a mechanic whose danfo bus makes a strange noise. A bad mechanic replaces parts at random. A good one listens, opens the bonnet, tests one part at a time and finds the real cause. Android Studio gives you the stethoscope, the torch and the spanner.

By the end of this lesson you will be able to:

  • Write messages to Logcat with Log.d, Log.i, Log.w and Log.e, and explain the six log levels.
  • Filter Logcat by tag, level, package and message text so you can find one line among thousands.
  • Read a stack trace from the top and find the exact line in your own code that crashed.
  • Set a breakpoint, run your app in debug mode and pause it mid-execution.
  • Use the Debug tool window: Variables, Watches, the call stack (Frames), and the step buttons (step over, step into, step out, resume).
  • Use Evaluate Expression to test an idea on a paused app without editing code.
  • Set a conditional breakpoint that only stops when something interesting happens.
  • Follow a repeatable five-step method for hunting a bug.

How this fits the bigger picture: This lesson matches the official Use the debugger in Android Studio codelab, part of Unit 2, Pathway 2 of Google's Android Basics with Compose course. The official codelab walks through one sample app. We use our own app with two deliberate bugs, go further into Logcat and conditional breakpoints, and give you a method you can reuse for the rest of the course.

Why this lesson matters

Beginners often think good programmers write code without bugs. They do not. Good programmers are simply fast at finding bugs. A skilled debugger can fix in ten minutes what takes a guesser two hours, and the skills transfer to every later lesson: networking, databases, background work. Time spent here is repaid many times.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 11
Prerequisite Concepts

You should be comfortable with the following. Skim the lesson number if any item feels shaky.

1. A running Compose app

You need an Android Studio project with an Empty Activity and a working emulator or phone (lessons 3 to 5). If you can press Run and see your app, you are ready.

2. Functions, if and loops

We debug a function that loops over a list and an if that applies a discount. Both appear in lessons 8 and 9.

3. Data classes and lists

A data class is a small container for related values, such as a name and a price. A List holds several of them (lessons 11 and 14).

4. Basic state in Compose

You saw remember { mutableStateOf(...) } in lesson 17: a value that, when changed, makes the screen redraw. Lesson 19 explains it fully. Here we use it only to show a result on the screen.

5. The Android Studio layout

Android Studio has a Run tool window and a Logcat tool window along the bottom edge. If you cannot see them, use View > Tool Windows or click the icons on the bottom toolbar. Menu names move slightly between versions, so look for the nearest match.

✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 11
Conceptual Understanding: How Bugs Hide

Three kinds of bug

Kind What happens Example How you notice
Compile-time error The code is not valid Kotlin, so the app will not build. A missing bracket. Red underline and a Build error. Fix it before running.
Runtime crash The app builds but throws an exception while running. Reading item number 3 in a list of 3. The app closes. Logcat shows a red stack trace.
Logic bug The app runs and shows an answer, but the answer is wrong. A discount that takes off 90 percent instead of 10. Nothing warns you. Only you know the answer is wrong.

Logic bugs are the sneakiest, because the computer is happily doing exactly what you told it. Crashes at least tell you where they happened.

Your three debugging tools

text
              YOUR APP (running)
                    |
   +----------------+-----------------+
   |                |                 |
 LOGCAT        STACK TRACE       DEBUGGER
 "What did     "Where did it     "Freeze time and
  the app       crash, and        look inside the
  say?"         who called        app's memory."
                whom?"
  • Logcat is the app's diary. You write sentences into it (Log.d) and Android writes its own as well. It is cheap and works on any device, but you only see what you remembered to log.
  • Stack traces are the app's last words when it crashes. They list the chain of function calls that led to the crash.
  • The debugger pauses the app on a line you choose and lets you inspect every variable, then run the code one line at a time. It is the most powerful tool, but it takes a minute to set up.

The five-step debugging method

Write this down. It works for every bug in every language.

  1. Reproduce it. Find exact steps that trigger the bug every time. A bug you cannot reproduce cannot be fixed.
  2. Read the evidence. Read the error message and stack trace top to bottom before touching any code.
  3. Form one hypothesis. "I think the loop runs one time too many." One guess, written as a sentence.
  4. Test the hypothesis with a log line or a breakpoint. Do not change code yet. Confirm that your guess is true.
  5. Fix, then re-run the original steps to prove the bug is gone, and check that you broke nothing else.

Think about it: Why is step 4 (testing the guess) separate from step 5 (fixing)? Because fixing a wrong guess can hide the symptom while leaving the real cause alive, and then the bug returns later in a worse place.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: How Bugs Hide. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 11
The Practice App: Market Basket

We will debug a tiny app called Market Basket. It lists three things you bought at Bodija market, adds up the cost and gives a 10 percent discount when the total is above ₦10,000. It contains two deliberate bugs. Build it exactly as written first, then hunt the bugs with the tools above.

Step 1: Create the project

  1. Choose File > New > New Project and pick Empty Activity.
  2. Name it Market Basket, set the package name to com.techbase.basket, and keep the defaults for language (Kotlin) and build configuration.
  3. Wait for Gradle to finish syncing.

Step 2: The logic file

Right-click the com.techbase.basket package, choose New > Kotlin Class/File, pick File and name it Basket. Replace the content with this:

Basket.kt
package com.techbase.basket
import android.util.Log
private const val TAG = "BasketLogic"
data class BasketItem(val name: String, val price: Int, val quantity: Int)
/** Adds up price times quantity for every item. */
fun basketTotal(items: List<BasketItem>): Int {
 var total = 0
 for (i in 0..items.size) {
 val item = items[i]
 Log.d(TAG, "Adding ${item.name}: ${item.price} x ${item.quantity}")
 total += item.price * item.quantity
 }
 return total
}
/** Gives 10 percent off when the total is above 10,000 naira. */
fun applyDiscount(total: Int): Int {
 return if (total > 10_000) total / 10 else total
}

Do not fix anything yet. Both functions are wrong on purpose.

Step 3: The screen

Open MainActivity.kt and replace its content:

MainActivity.kt
package com.techbase.basket
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
private const val TAG = "BasketScreen"
class MainActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContent {
 MaterialTheme {
 Surface(modifier = Modifier.fillMaxSize()) {
 BasketScreen()
 }
 }
 }
 }
}
@Composable
fun BasketScreen(modifier: Modifier = Modifier) {
 val items = listOf(
 BasketItem("Rice", 4_500, 2),
 BasketItem("Beans", 1_500, 1),
 BasketItem("Garri", 800, 3)
 )
 var result by remember { mutableStateOf("Tap the button") }
 Column(
 modifier = modifier.padding(24.dp),
 horizontalAlignment = Alignment.CenterHorizontally,
 verticalArrangement = Arrangement.Center
 ) {
 items.forEach { item ->
 Text("${item.name}: ${item.quantity} x ₦${item.price}")
 }
 Button(
 onClick = {
 Log.d(TAG, "Calculate tapped")
 val total = basketTotal(items)
 val payable = applyDiscount(total)
 Log.i(TAG, "total=$total payable=$payable")
 result = "You pay ₦$payable"
 }
 ) {
 Text("Calculate")
 }
 Text(result, style = MaterialTheme.typography.headlineSmall)
 }
}

Block by block. BasketScreen keeps a list of three items and one piece of state, result, holding the sentence to show. When the button is tapped it logs a line, calls our two logic functions, logs the numbers, and updates result. The screen then redraws with the new text. The Log.d and Log.i calls are the beginning of your Logcat diary.

What should happen: 4,500 x 2 is 9,000, plus 1,500, plus 800 x 3 which is 2,400, so the total is ₦12,900. That is above 10,000, so a 10 percent discount of 1,290 leaves ₦11,610.

✏️ Your Task
Practise what you just learned about The Practice App: Market Basket. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 11
Logcat: Your App's Diary

Run the app (green triangle) and tap Calculate. Something goes wrong: the app closes. Good. A bug to hunt.

Opening Logcat

Click Logcat in the bottom tool bar (or View > Tool Windows > Logcat). Make sure the device drop-down at the top shows your emulator or phone. You will see a fast-moving river of text. Android itself logs hundreds of lines a minute, so the skill is not reading Logcat, it is filtering it.

Log levels

Every log line has a level that says how important it is. There are six, from least to most serious.

Level Function Letter Use it for
Verbose Log.v V Very chatty detail you will normally hide.
Debug Log.d D Developer notes while you are fixing something, such as values of variables.
Info Log.i I Normal milestones: "user signed in".
Warning Log.w W Something odd that the app survived.
Error Log.e E Something failed. Crashes appear here.
Assert Log.wtf A "What a terrible failure": a situation that should never happen. Rarely used.

Each call takes a tag (a short label, by convention the class name) and a message:

kotlin
import android.util.Log
private const val TAG = "BasketScreen"
fun demoLogging(total: Int) {
 Log.d(TAG, "total is $total") // debug note
 Log.i(TAG, "Calculation finished") // information
 Log.w(TAG, "Total is unusually large: $total") // warning
 Log.e(TAG, "Could not read item") // error
}

A call with an exception attached prints the stack trace as well: Log.e(TAG, "Failed", exception).

Tip: Logging runs on a real device, so never log passwords, tokens or personal data. Anyone who plugs in your phone with a cable can read Logcat. Remove noisy debug logs before you publish (lesson 49 covers release preparation).

Filtering Logcat

The Logcat search box at the top accepts plain words and key:value filters. The syntax below is the one used by recent Android Studio versions. Older versions have a different filter dialog, so if a key is not recognised, look for the filter drop-down next to the search box.

You type What you see
tag:BasketScreen Only lines whose tag is BasketScreen.
package:mine Only lines from your own app. Very useful.
level:error Errors and worse.
message:total Lines whose message contains the word total.
package:mine level:debug Your app, debug level and above. Several filters are combined with a space.
-tag:chatty A minus sign excludes lines with that tag.

Try it now. Type package:mine and look at your lines. You should see something similar to this, in the order the code ran:

D Calculate tapped
D Adding Rice: 4500 x 2
D Adding Beans: 1500 x 1
D Adding Garri: 800 x 3
E FATAL EXCEPTION: main

The last line is the clue. The logs prove the loop handled all three items, then something went wrong after Garri, which suggests it tried to go on to a fourth item that does not exist. We have a hypothesis already, without any debugger. That is the power of good logs.

Where Logcat is not enough

Logs only show what you remembered to log, and each new log needs a re-build and re-run. For a bug deep inside a calculation, you want to freeze the app and look around. That is the debugger.

✏️ Your Task
Practise what you just learned about Logcat: Your App's Diary. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 11
Reading a Stack Trace

When an exception is not handled, Android prints a stack trace in red. The trace for our crash will look similar to this. Exact wording and line numbers vary by Android version and by your code.

text
FATAL EXCEPTION: main
Process: com.techbase.basket, PID: 8123
java.lang.ArrayIndexOutOfBoundsException: length=3; index=3
    at java.util.Arrays$ArrayList.get(Arrays.java:3585)
    at com.techbase.basket.BasketKt.basketTotal(Basket.kt:13)
    at com.techbase.basket.MainActivityKt$BasketScreen$1$1.invoke(MainActivity.kt:62)
    at androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke(Clickable.kt:...)
    ...

How to read it

  1. The first line after FATAL EXCEPTION names the exception type and a message: ArrayIndexOutOfBoundsException: length=3; index=3. Translation: "You asked for item number 3, but the list has only 3 items (numbered 0, 1, 2)."
  2. Then come the frames, one per function call, newest first. Each is at package.Class.function(File.kt:line).
  3. The frames from Android or library code (java.util, androidx, android.) are plumbing. Skip down to the first frame that is in your own package, here com.techbase.basket.BasketKt.basketTotal(Basket.kt:13).
  4. Click the blue Basket.kt:13 link. Android Studio opens that file at that line. The crash happened at or just after that line.
text
read the trace like a family tree, from the crash upwards:

  Arrays$ArrayList.get      <- the library said "no such index"
        ^ called by
  basketTotal(Basket.kt:13) <- YOUR code asked for index 3   <-- start here
        ^ called by
  BasketScreen onClick      <- the button handler called basketTotal
        ^ called by
  Compose click handling    <- framework plumbing

Line 13 is val item = items[i]. Now look at the loop above it: for (i in 0..items.size). In Kotlin 0..3 includes 3. A list of three items has indexes 0, 1 and 2, so the loop made one visit too many. This is the famous off-by-one error.

The fix is to use until, which stops before the end value, or to loop over the items directly:

kotlin
for (i in 0 until items.size) { /* ... */ } // indexes 0, 1, 2
for (item in items) { /* ... */ } // simplest and safest

Apply the second form in Basket.kt:

kotlin
fun basketTotal(items: List<BasketItem>): Int {
 var total = 0
 for (item in items) {
 Log.d(TAG, "Adding ${item.name}: ${item.price} x ${item.quantity}")
 total += item.price * item.quantity
 }
 return total
}

Run again and tap Calculate. No crash. But the screen says You pay ₦1290, not ₦11,610. We have a second bug, a logic bug, and no stack trace to help. Time for the debugger.

✏️ Your Task
Practise what you just learned about Reading a Stack Trace. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 11
The Debugger: Freezing Time

Breakpoints

A breakpoint is a flag on a line of code that says: "when execution reaches here, pause the whole app before running this line."

  1. Open MainActivity.kt and find the line val total = basketTotal(items).
  2. Click once in the grey gutter (the narrow strip with line numbers) beside that line. A red dot appears. That is your breakpoint. Click it again to remove it.

Running in debug mode

A normal Run does not stop at breakpoints. You need Debug:

  • Click the bug icon in the toolbar (next to the green Run triangle), or use Run > Debug 'app'.
  • Android Studio installs the app in debug mode and opens the Debug tool window at the bottom.
  • Tap Calculate in the app. The app freezes, the breakpoint line turns blue, and the Debug window shows the app's internal state.

If the app does not stop, check that the red dot is solid (not a red circle with a line through it, which means it is muted) and that you pressed the bug icon, not the triangle. Debugging is slower than normal running and uses more memory, which matters on a modest laptop. Close other heavy programs if the emulator stalls.

Anatomy of the Debug window

text
+--------------------------------------------------------------+
| Debug: app    [Debugger] [Logcat]                            |
+--------------------------------------------------------------+
| TOOLBAR: Resume | Pause | Stop | Step Over | Step Into |     |
|          Step Out | Run to Cursor | Evaluate Expression      |
+----------------------+---------------------------------------+
| FRAMES (call stack)  | VARIABLES  and  WATCHES               |
|  > onClick           |  items = ArrayList (size = 3)         |
|    invoke            |  result = "Tap the button"            |
|    ...               |  total = (not yet assigned)           |
+----------------------+---------------------------------------+
  • Frames shows the same chain of calls as a stack trace, but live. Click a frame to see the variables as they were in that function.
  • Variables shows every value in scope. Click the small arrows to open objects, so you can look inside items and see each BasketItem.
  • Watches is a list of expressions you type in, such as items.size, that Studio re-calculates at every pause. Click the + in the Watches area.

The step buttons

When paused, you control time with these buttons. Shortcuts below are the defaults on Windows and Linux; on a Mac, hover over each button to see its shortcut, because they use the Command and Option keys.

Button Typical shortcut What it does
Resume Program F9 Run on until the next breakpoint (or until the app ends).
Step Over F8 Run this line completely, including any functions it calls, then pause on the next line.
Step Into F7 If this line calls a function you wrote, jump inside it and pause on its first line.
Step Out Shift + F8 Finish the current function and pause in whoever called it.
Run to Cursor Alt + F9 Run until the line where your text cursor sits.
Evaluate Expression Alt + F8 Open a mini calculator that can run any Kotlin expression on the paused app.

Step Over versus Step Into is the one to remember. Imagine reading a recipe that says "make the stew (see page 40)". Step over means "trust the stew recipe, move to the next line". Step into means "turn to page 40 and read it".

Hunting Bug 2

With the app paused on val total = basketTotal(items):

  1. Press Step Over (F8). The line runs and total appears in Variables. It should read 12900. That is right! So basketTotal is now correct.
  2. The next line is val payable = applyDiscount(total). Press Step Into (F7) this time. The debugger jumps into applyDiscount in Basket.kt, with total = 12900 visible.
  3. Press Step Over once. You are paused on the if line. Since total > 10_000 is true, the code takes the first branch and calculates total / 10.
  4. Look at the Variables or hover over total / 10. It is 1290. The code returns one tenth of the price, not the price minus one tenth. We have found it: the discount formula is wrong.

You never needed to guess. Each step shrank the search area: first the total (fine), then the discount (wrong), then the formula.

Evaluate Expression: test the fix before writing it

Still paused inside applyDiscount, open Evaluate Expression (Alt + F8, or the calculator icon). Type total - total / 10 and press Evaluate. The result is 11610, which is the number we expected. You tested the fix without changing a single line of code or restarting. You can evaluate almost any Kotlin expression, call functions, and inspect objects here.

Now apply the fix:

kotlin
fun applyDiscount(total: Int): Int {
 return if (total > 10_000) total - total / 10 else total
}

Press Stop (red square), then Run again and tap Calculate. The screen reads You pay ₦11610. Both bugs are fixed. Compare Logcat with package:mine:

D Calculate tapped
D Adding Rice: 4500 x 2
D Adding Beans: 1500 x 1
D Adding Garri: 800 x 3
I total=12900 payable=11610

✏️ Your Task
Practise what you just learned about The Debugger: Freezing Time. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 11
Advanced Breakpoints: Conditions, Logpoints and Exception Stops

The problem with simple breakpoints

Suppose the basket held 200 items and the bug only happens for the item called "Yam". A plain breakpoint inside the loop would pause 200 times. You would press Resume until your finger hurt. Android Studio offers three better answers.

Conditional breakpoints

Put a breakpoint on the total += item.price * item.quantity line in Basket.kt. Then right-click the red dot. A small pop-up appears with a Condition box. Type a Kotlin expression that returns true or false:

kotlin
item.name == "Garri"

Now the app pauses only when the condition is true. With our three items it stops once, on Garri, and you can see item.quantity is 3. Other useful conditions:

kotlin
item.price > 4000 // only expensive items
total > 9000 // only after the running total passes 9000

If you mistype the condition, Studio normally pauses anyway and complains, so check the Debug window for a message.

Logpoints (breakpoints that write instead of pause)

In the same right-click pop-up, tick Evaluate and log and type a message such as "item=" + item.name. The breakpoint no longer stops the app. It prints to the Debug console and the app runs at full speed. Logpoints let you add a log without editing code, rebuilding, or remembering to delete it later. Untick Suspend in the pop-up if the dialog offers it, so the app does not pause.

Exception breakpoints

Open Run > View Breakpoints (or Ctrl + Shift + F8). Use the + button to add a Java Exception Breakpoint and choose a type such as ArrayIndexOutOfBoundsException or the general IndexOutOfBoundsException. Now the debugger stops at the moment the exception is thrown, with all variables still alive, instead of after the app has already died. This is gold for crashes that are hard to trace.

Housekeeping

  • Mute Breakpoints (the red dot with a slash in the Debug toolbar) turns all stops off temporarily.
  • Breakpoints stay in the project until you remove them, so tidy up when you are done. A forgotten breakpoint in a loop makes the app look frozen.
  • Debug is slower than Run. Never judge the speed of your app while debugging.
✏️ Your Task
Practise what you just learned about Advanced Breakpoints: Conditions, Logpoints and Exception Stops. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 11
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Log, filter and find

Objective: Use Logcat as a diary and filter it.

Steps:

  1. Add a Log.d inside applyDiscount that records the incoming total and whether the discount was applied. Use tag "BasketLogic".
  2. Run the app, tap Calculate and use the Logcat search box to show only lines with tag:BasketLogic.
  3. Change the filter to also hide the Adding lines using a minus filter on their message text.
  4. Write down the exact Logcat text you needed.
kotlin
fun applyDiscount(total: Int): Int {
 val discounted = total > 10_000
 Log.d(TAG, "applyDiscount: total=$total discounted=$discounted")
 return if (discounted) total - total / 10 else total
}

Filter to your tag with tag:BasketLogic. To hide the Adding lines add -message:Adding, giving tag:BasketLogic -message:Adding. The only line left should be:

D applyDiscount: total=12900 discounted=true

Note that the Adding lines come from basketTotal, which also uses the tag BasketLogic, so the minus filter is what separates them.

🎯 Your Challenge

Exercise 2: A new bug, found by stepping

Add a fourth item BasketItem("Palm oil", 3_000, 0) and a rule that free items (quantity 0) must not appear in the total. First write basketTotal wrongly so it also adds one extra naira for each item (a "packing fee"). Then use a conditional breakpoint on item.quantity == 0 and Step Over to watch how total changes for that item. Explain in one sentence what the debugger showed you.

kotlin
fun basketTotal(items: List<BasketItem>): Int {
 var total = 0
 for (item in items) {
 total += item.price * item.quantity + 1 // wrong: fee added even for free items
 }
 return total
}

Put a breakpoint on the total += line, right-click it and set the condition item.quantity == 0. When it stops on Palm oil, Variables shows total = 12903. Press Step Over: total becomes 12904, although 0 * 3000 is zero. The debugger showed that an item with quantity zero still increased the total by 1, so the + 1 must only apply to items you actually bought. A correct version:

kotlin
fun basketTotal(items: List<BasketItem>): Int {
 var total = 0
 for (item in items) {
 if (item.quantity > 0) {
 total += item.price * item.quantity
 }
 }
 return total
}
🎯 Your Challenge

Exercise 3: Catch a crash with an exception breakpoint

Temporarily change the loop back to for (i in 0..items.size). Add a Java Exception Breakpoint for IndexOutOfBoundsException (tick both caught and uncaught), run in debug mode and tap Calculate. Find the value of i and items.size at the moment the debugger stops, then fix the loop.

In Run > View Breakpoints, click +, choose Java Exception Breakpoints, and type IndexOutOfBoundsException. (ArrayIndexOutOfBoundsException extends it, so the broader type also catches ours.) When the debugger stops, Variables shows i = 3 and items with size = 3. Since valid indexes are 0 to 2, i is one too large.

The fix is for (i in 0 until items.size) or, better, for (item in items). Remove the exception breakpoint when you are finished so it does not interrupt unrelated work.

✏️ Your Task

Open any earlier project of yours (the Dice Roller from lesson 17 is perfect). Add a Log.d inside the button's click handler that prints the rolled number, then run in Debug mode with a breakpoint on that line. Press Step Over and read the value of the variable in the Variables pane. Check that it matches what you see on screen.

Phase 10 of 11
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Pressing Run instead of Debug. The symptom: you set a breakpoint and the app never stops. The fix: use the bug icon (Debug 'app'). If the app is already running, stop it and start it in debug mode, or use the Attach Debugger to Android Process button.

Mistake #2: Reading the bottom of the stack trace first. The problem: the bottom shows framework plumbing that is the same for every crash. The fix: read the exception name and message at the top, then find the first frame in your own package.

Mistake #3: Fixing without reproducing. The wrong approach: "I think I saw it crash once, so I changed something." The fix: always write down the exact taps that trigger the bug, change one thing, and repeat those exact taps. If the bug is gone, you have evidence. If you changed three things at once, you will never know which one worked.

Mistake #4: Too much logging. The wrong code: a Log.d on every line, so Logcat becomes a wall of text. The fix: log decisions and key values only, use a clear tag per class, and filter with tag: and package:mine. Delete temporary logs afterwards.

Mistake #5: Using println in an Android app. The problem: println output may not appear where you expect in Logcat and has no tag or level, so you cannot filter it. The fix: use Log.d(TAG, "...").

Mistake #6: Stepping into library code by accident. The symptom: you press F7 on a line and land in a file you do not recognise. The fix: press Step Out (Shift + F8) to return, and use Step Over by default. Only step into functions you wrote and want to inspect.

Mistake #7: Leaving breakpoints or sensitive logs in the app. The problem: breakpoints make later runs seem to hang, and logs can leak private data. The fix: review Run > View Breakpoints and your Log calls before you share or release an app.

Mistake #8: Believing the first idea. The problem: you assume the bug is in line 13 because that is where the crash showed, but the real cause is where a bad value was created, maybe ten lines earlier. The fix: use the Frames list and Variables to walk back to where the wrong value first appeared.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 11
Knowledge Check

Question 1. Name the six Logcat levels from least to most serious and say which function writes an error.

Verbose, Debug, Info, Warn, Error, Assert. An error is written with Log.e(tag, message).

Question 2. A stack trace lists many frames. Which frame do you look at first, and why?

The first frame that belongs to your own app's package. Frames above it are library or framework code that reacted to your bad call. The line number in your frame shows where your code triggered the exception.

Question 3. You are paused on a line that calls calculateFee(order), a function you wrote. What is the difference between Step Over and Step Into?

Step Over runs the whole call to calculateFee and pauses on the next line of the current function. Step Into jumps inside calculateFee and pauses on its first line, so you can watch it run one line at a time.

Question 4. A loop runs 500 times but the bug appears only when item.id == 237. What is the best breakpoint to use?

A conditional breakpoint with the condition item.id == 237. It stays silent for the other 499 passes. A logpoint is useful too if you only want to print values rather than pause.

Question 5. What does Evaluate Expression let you do, and why is that useful?

It runs any Kotlin expression against the paused app and shows the result, so you can test a calculation, inspect an object or try a fix idea without editing code or restarting the app.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Bug Hunt: The Exam Marks Calculator

Project goal: Find and fix three planted bugs in a small marks app using Logcat, a stack trace and the debugger, and keep a written bug diary explaining each one. This is a portfolio piece that proves you can debug, which employers value highly.

Setup: Create a new Empty Activity project called Marks Check (package com.techbase.marks) and add the buggy code below. The four students scored 70, 50, 85 and 45. The correct report is: average 62 (250 divided by 4, rounded down), highest 85, and 3 students passed (a pass is 50 or above).

Requirements:

  1. Run the app and tap Check. Copy the stack trace into your bug diary, circle the first frame in your own package, and fix the crash (Bug 1).
  2. Run again. The report now shows a wrong highest mark. Use a breakpoint and Step Over to find why, recording the Variables you saw (Bug 2).
  3. The pass count is also wrong. Use a conditional breakpoint (m == 50) or Evaluate Expression to find why (Bug 3).
  4. Add at least two Log.d lines with your own tag and show the filter you used (tag:...).
  5. For each bug write three lines in the diary: symptom, cause, fix.

Stretch goal: Add an exception breakpoint and a logpoint, and describe in the diary when each is better than a normal breakpoint.

Here is the buggy starter code:

Marks.kt
package com.techbase.marks
import android.util.Log
private const val TAG = "MarksLogic"
fun average(marks: List<Int>): Int {
 var sum = 0
 for (i in 0..marks.size) {
 sum += marks[i]
 }
 return sum / marks.size
}
fun highest(marks: List<Int>): Int {
 var best = 0
 for (m in marks) {
 if (m < best) best = m
 }
 return best
}
fun passCount(marks: List<Int>): Int {
 var count = 0
 for (m in marks) {
 if (m > 50) count++
 }
 Log.d(TAG, "passCount=$count")
 return count
}
MainActivity.kt
package com.techbase.marks
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContent {
 MaterialTheme {
 Surface(modifier = Modifier.fillMaxSize()) { MarksScreen() }
 }
 }
 }
}
@Composable
fun MarksScreen() {
 val marks = listOf(70, 50, 85, 45)
 var report by remember { mutableStateOf("Tap Check") }
 Column(modifier = Modifier.padding(24.dp)) {
 Text("Marks: $marks")
 Button(onClick = {
 report = "Average ${average(marks)}, top ${highest(marks)}, passed ${passCount(marks)}"
 }) { Text("Check") }
 Text(report)
 }
}

Bug 1, the crash. The loop for (i in 0..marks.size) visits index 4 in a list of four. The stack trace shows an index-out-of-bounds exception (the exact wording varies by Android version) and the first frame in your package is average(Marks.kt:9). Fix: loop over the values themselves.

Bug 2, the highest mark. The screen shows top 0. Put a breakpoint on if (m < best) best = m and Step Over four times: best starts at 0 and never changes, because no mark is less than 0. The comparison points the wrong way. Evaluate Expression 70 < 0 to see false. Fix: use >.

Bug 3, the pass count. The screen shows passed 2, but 50 should pass. A conditional breakpoint with condition m == 50 stops on the second student, and Evaluate Expression m > 50 gives false. The operator excludes exactly 50. Fix: >=.

Marks.kt
package com.techbase.marks
import android.util.Log
private const val TAG = "MarksLogic"
fun average(marks: List<Int>): Int {
 if (marks.isEmpty()) return 0
 var sum = 0
 for (m in marks) {
 sum += m
 }
 Log.d(TAG, "sum=$sum count=${marks.size}")
 return sum / marks.size
}
fun highest(marks: List<Int>): Int {
 var best = 0
 for (m in marks) {
 if (m > best) best = m
 }
 return best
}
fun passCount(marks: List<Int>): Int {
 var count = 0
 for (m in marks) {
 if (m >= 50) count++
 }
 Log.d(TAG, "passCount=$count")
 return count
}

Expected screen after the fixes:

Average 62, top 85, passed 3

Sample bug diary entry:

Bug 3. Symptom: report said 2 students passed, but three scored 50 or more. Cause: > instead of >=, so a mark of exactly 50 failed. Fix: changed the operator. Filter used: tag:MarksLogic.

Tip for the diary: after each fix, run the original steps again. Fixing Bug 1 revealed Bugs 2 and 3, which were hidden behind the crash. This is normal: one bug often masks another.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 18 project”.

3

On github.com click New repository, name it android-lesson-18-debugging-with-android-studio, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 18 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: How Bugs Hide
  • The Practice App: Market Basket
  • Logcat: Your App's Diary
  • Reading a Stack Trace
  • The Debugger: Freezing Time
  • Advanced Breakpoints: Conditions, Logpoints and Exception Stops
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close