RizTech Academy logo
RizTech Academy
Testing, Profiling and DebuggingLesson 1 of 530 min

Logcat, breakpoints and the layout inspector

Arjun adds one small thing to the price detail screen: how much the price moved since yesterday as a percentage, so a farmer can see at a glance that ₹55 on a ₹2,525 onion is a small move. He runs it, it says "₹55 up on yesterday (0%)", and he ships it to Priya to try. She sends back a photo of every price saying 0%. He changes one line, and now the screen does not open at all. This lesson follows both bugs with the three tools you will use every day for the rest of your career: Logcat for what happened, the debugger for why, and the Layout Inspector for what is actually on the screen. Module 1 met each of them for a minute; this is the full treatment.

Every run in this lesson is a scratch copy of Mandi Bhav installed as com.riztechacademy.mandibhav.m13, so the real app on the emulator is never touched. Make your own copy the same way (applicationIdSuffix = ".m13" in defaultConfig) before you start breaking things.

The bug that does not crash

Arjun's first version, in ui/PriceWeek.kt beside describeChange:

/** How much the price moved since yesterday, as a percentage of yesterday's. */
fun percentChange(change: Long, yesterday: Long): Long {
    val percent = change / yesterday * 100
    return percent
}

and on the detail screen, inside the changeSinceYesterday?.let { change -> … } block:

val yesterday = detail.week[detail.week.size - 2].modalPrice
Text(
    text = "${describeChange(change)} (${percentChange(change, yesterday)}%)",
    style = MaterialTheme.typography.bodyLarge,
)

Opened from a deep link on the emulator, Lasalgaon onion showed exactly what Priya saw:

₹2,580 per quintal
Modal (most common) price
Min ₹1,250 · Max ₹2,950
₹55 up on yesterday (0%)

Nothing in Logcat, because nothing went wrong as far as Android is concerned. The code did what it says. Logcat finds failures; it cannot find code that runs perfectly and computes the wrong answer. For that you need to stop the app and look inside it.

A breakpoint

Click in the gutter beside val percent = change / yesterday * 100 to set a breakpoint, a red dot, and start the app with Debug (the bug icon) instead of Run. If the app is already running, you do not need to restart it: Attach Debugger to Android Process in the Run menu attaches to the running process. Then open the price. The app freezes on that line, before it runs, and the Debug window shows the stack on one side and the variables on the other.

Android Studio's debugger is a window you click through, so to show you its output as text I attached the command-line debugger that ships with the JDK, jdb, to the same process, with a breakpoint on the same line. This is what the debugger saw:

Breakpoint hit: "thread=main", com.riztechacademy.mandibhav.ui.PriceWeekKt.percentChange(), line=39 bci=0

main[1]   [1] com.riztechacademy.mandibhav.ui.PriceWeekKt.percentChange (PriceWeek.kt:39)
  [2] com.riztechacademy.mandibhav.ui.PriceDetailScreenKt.PriceDetailContent$lambda$0$0$1 (PriceDetailScreen.kt:230)
  [3] com.riztechacademy.mandibhav.ui.PriceDetailScreenKt.$r8$lambda$LonvAF5QOqcuFw2JPrW_0eIclHg (null)
  [4] com.riztechacademy.mandibhav.ui.PriceDetailScreenKt$$ExternalSyntheticLambda3.invoke (D8$$SyntheticClass:0)
  [5] androidx.compose.runtime.internal.ComposableLambdaImpl.invoke (ComposableLambda.kt:131)

main[1] Local variables:
change = 55
yesterday = 2525

Studio shows the same: the frames list, and change = 55, yesterday = 2525 in the Variables pane. Now the most useful button in the debugger, Evaluate Expression (Alt + F8 on Windows and Linux, Option + F8 on a Mac). Type any Kotlin expression over the variables in scope and it runs it in the paused app:

change / yesterday = 0
change * 100 / yesterday = 2
change * 100.0 / yesterday = 2.178218

There it is. Long divided by Long is a Long, and 55 divided by 2,525 is 0 with 55 left over, which is thrown away before the multiplication by 100 ever happens. Multiplying first gives 2, still truncated; a Double gives the real answer, 2.18%. You found the bug in about ten seconds without adding a single log line or rebuilding anything.

Note what the app was doing meanwhile. The breakpoint is on the main thread, so while you look, the app draws nothing and answers no taps. That is fine while you debug, and it is why you never leave a breakpoint on a line you do not care about: a forgotten breakpoint in a scroll handler makes the app look frozen, and the first time it happens it is very confusing.

The breakpoints worth knowing

A plain breakpoint stops every time. Right-click the red dot to make it smarter.

Kind Stops when Use it for
Line That line is about to run Almost everything
Conditional The line runs and a condition is true, such as price.market == "Nashik APMC" A bug in one item of a list of hundreds
Logging (untick Suspend, tick Evaluate and log) Never; writes an expression to the Debug console instead A log line without editing code or rebuilding
Method A function is entered or left "Is this ever called?"
Exception Any Throwable is thrown, or one class of it Stopping at the line that throws, not where it is caught

Shift and a click in the gutter creates a logging breakpoint directly. The stepping keys are the same as in any JetBrains IDE: Step Over (F8) runs the line, Step Into (F7) goes into the function it calls, Step Out (Shift + F8) finishes the current function, and Resume (F9 on Windows and Linux, Cmd + Option + R on a Mac) carries on.

Two Kotlin surprises. First, in a composable the debugger stops in lambdas with names such as PriceDetailContent$lambda$0$0$1; that is the Compose compiler's work, and the line numbers on the right are what you read. Second, inside a coroutine, Step Over can land you somewhere unexpected after a suspend call, because the function really did stop and resume. Put a breakpoint on the line after the call and press Resume instead of stepping across the suspension.

Breakpoints are for debug builds

The debugger attaches only to a debuggable app, which the debug build type is and release is not. That is deliberate: a debuggable app lets anyone with a cable read its memory. When a bug only happens in release, you cannot attach to it; you have Logcat, and the tools of lesson 5.

The bug that crashes

Arjun's fix is to compute a Double and format it with String.format. He reaches for %d, from the version that returned a Long:

/** How much the price moved since yesterday, as a percentage of yesterday's. */
fun percentChange(change: Long, yesterday: Long): String =
    String.format(Locale.ENGLISH, "%d", change * 100.0 / yesterday)

It compiles; format strings are checked when they run, not when they compile. The deep link opened the app, and it was gone a second later. Logcat, from the crash buffer, which keeps crashes after the busy main log has scrolled past them:

adb logcat -b crash -d
E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: com.riztechacademy.mandibhav.m13, PID: 6076
E AndroidRuntime: java.util.IllegalFormatConversionException: d != java.lang.Double
E AndroidRuntime: 	at java.util.Formatter$FormatSpecifier.failConversion(Formatter.java:4608)
E AndroidRuntime: 	at java.util.Formatter$FormatSpecifier.printInteger(Formatter.java:3123)
E AndroidRuntime: 	at java.util.Formatter$FormatSpecifier.print(Formatter.java:3078)
E AndroidRuntime: 	at java.util.Formatter.format(Formatter.java:2704)
E AndroidRuntime: 	at java.util.Formatter.format(Formatter.java:2640)
E AndroidRuntime: 	at java.lang.String.format(String.java:4078)
E AndroidRuntime: 	at com.riztechacademy.mandibhav.ui.PriceWeekKt.percentChange(PriceWeek.kt:40)
E AndroidRuntime: 	at com.riztechacademy.mandibhav.ui.PriceDetailScreenKt.PriceDetailContent$lambda$0$0$1(PriceDetailScreen.kt:230)
E AndroidRuntime: 	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:131)

The full trace was 124 frames long. Four mention Mandi Bhav, and two of them are lines anybody wrote; the other two are lambdas the compiler generated. That is normal for Compose, and it is the skill: read the exception line, then scan down for the first line in your own package. Here, IllegalFormatConversionException: d != java.lang.Double says exactly what is wrong (%d wants a whole number and got a Double), and PriceWeek.kt:40 says where. Everything above it is the Java formatter discovering the problem; everything below it is Compose and Android calling your code. Lesson 5 reads traces properly, including the ones from phones you will never touch.

Two more things in the same log are worth seeing once. The same exception appeared a few lines earlier under the tag ComposeInternal, which Compose logs on its way out; and the system wrote what it did about it:

W ActivityTaskManager:   Force finishing activity com.riztechacademy.mandibhav.m13/com.riztechacademy.mandibhav.MainActivity
I ActivityManager: Process com.riztechacademy.mandibhav.m13 (pid 6076) has died: fg  TOP

The fix is "%.1f", or better, no format string at all where a typed function will do:

/** How much the price moved since yesterday, as a percentage of yesterday's: 2.2 for ₹55 on ₹2,525. */
fun percentChange(change: Long, yesterday: Long): Double = change * 100.0 / yesterday

with the rounding done where the text is built, and a unit test for 55 on 2,525. Writing that test before the second attempt would have caught both bugs on your laptop; lesson 2 is about exactly that.

Logcat, beyond module 1

Module 1 gave you the query bar and package:mine. The rest of what you will reach for:

In Studio's Logcat From a terminal Shows
is:crash adb logcat -b crash Crashes only, Java or native, even after the main log has moved on
is:stacktrace — Anything that looks like a stack trace, at any level, including caught exceptions someone logged
level:WARN package:mine adb logcat --pid=$(adb shell pidof <package>) '*:W' Your app's warnings and errors
tag:StrictMode | tag:LastExit adb logcat -s StrictMode LastExit Either tag
age:5m adb logcat -c before you start, so the log holds only this run Recent lines only
-tag~:^WM- — Everything except WorkManager's own WM- tags

When the process restarts, Studio's Logcat prints PROCESS ENDED and PROCESS STARTED, which is the quickest way to notice a crash you were not looking at.

What not to log. Every Log.d in a release build is readable by anyone who connects the phone to a computer, and it costs time on every call. Module 11 kept location out of the log for that reason. Log freely in debug code, gate it with BuildConfig.DEBUG when it must stay, and never log anything a farmer would not want a stranger to read.

The Layout Inspector: what is really on screen

The third kind of bug is neither a wrong number nor a crash: something is on the screen in the wrong place, the wrong size, or not at all, or it redraws far more often than it should. Run the app, open the Running Devices window and click Toggle Layout Inspector. Studio connects to the debuggable app in the foreground and shows:

  • the Component Tree, every composable in the screen, which you can click to select;
  • the Attributes pane for the selected composable: its size, its parameters, and its semantics;
  • the screen itself, where clicking selects the composable under the pointer when Toggle Deep Inspect is on.

For Compose, the Component Tree can also show two counts per composable: how many times it recomposed and how many times it was skipped. If they are not visible, choose Show Recomposition Counts from the tree's View Options menu. Reset sets them to zero, so you can count what one tap does. A price card that recomposes when you type in the search box, or forty times during one scroll, is the start of a performance investigation; lesson 4 continues it with the profiler. The counts need the app on API 29 or later.

The semantics view is the one to learn first, because tests and TalkBack both see the screen through it. Select a price card and its Attributes show one content description, "Onion at Lasalgaon APMC, 2,580 rupees per quintal…", and none of the texts inside it: module 4's clearAndSetSemantics replaced them. You can confirm the same thing without Studio, because UI Automator reads the same tree:

adb shell uiautomator dump /data/local/tmp/ui.xml
adb shell cat /data/local/tmp/ui.xml

On the Prices screen it listed the search field, the chips and "Updated today at 3:24 AM" as texts, and each card as an android.widget.Button with an empty text and the description "Onion at Lasalgaon APMC, 2,580 rupees per quintal. Range 1,250 to 2,950 rupees. 8 November 2026." On the detail screen, where nothing merges them, Lasalgaon APMC, 8 Nov 2026 and ₹2,580 per quintal are separate texts. Hold on to that: the first Compose test in lesson 3 fails because of it.

One warning from the documentation that will matter in module 14: the inspector needs the META-INF/androidx.compose.*.version files in the APK. A packaging rule that strips them makes Compose vanish from the Component Tree.

Which tool, when

Symptom Start with Then
The app closes Logcat, is:crash Read the exception and your first frame; breakpoint there if the cause is not obvious
A wrong value, no error A breakpoint where the value is computed Evaluate Expression on the parts
Something only for one item A conditional breakpoint —
"Is this called, and with what?" A logging breakpoint A real Log.d only if you need it in later runs
Wrong size, position or missing element Layout Inspector Attributes, then semantics
Slow or janky Layout Inspector's recomposition counts The profiler (lesson 4)
Only in release Logcat from the release build Lesson 5: mapping files and exit reasons

Check your work

Why did Logcat show nothing for "0%"? Nothing failed. The code computed 55 / 2525, which is 0 in Long arithmetic, and multiplied it by 100. Logcat records failures and what you log; a wrong answer is neither.

What did the debugger show at the breakpoint? change = 55 and yesterday = 2525; evaluating change / yesterday gave 0, change * 100 / yesterday gave 2, and change * 100.0 / yesterday gave 2.178218.

What did the app do while it was stopped at the breakpoint? Nothing: the breakpoint was on the main thread, so it drew no frames and answered no taps until resumed.

What does IllegalFormatConversionException: d != java.lang.Double mean, and where was it thrown from in Mandi Bhav's code? The format %d was given a Double. The first Mandi Bhav frame was PriceWeekKt.percentChange(PriceWeek.kt:40), called from PriceDetailScreen.kt:230.

Why can you not attach the debugger to a release build? Release is not debuggable, on purpose: a debuggable app exposes its memory to anyone with a cable.

Practice 4. Shift and a gutter click on the Text( line inside the let block, with percentChange(change, yesterday) as the expression, prints the percentage every time that line runs, which is every time the item composes, and the app never pauses. Rotating the phone destroys the activity and composes the screen from nothing, so each rotation prints it again.

Practice

  1. Make your scratch copy with applicationIdSuffix = ".m13", add Arjun's first percentChange, and open Lasalgaon onion. Set the breakpoint, attach the debugger, and reproduce the three evaluations above.
  2. Make the breakpoint conditional on change < 0 and open a few prices. Which prices fell since yesterday?
  3. Add the %d version and reproduce the crash. Find it three ways: Studio's Logcat with is:crash, adb logcat -b crash -d, and an exception breakpoint on IllegalFormatConversionException that stops before the app dies.
  4. Replace a Log.d you would have added with a logging breakpoint, as in the answer above, and rotate the phone with it on.
  5. Open the Layout Inspector on the Prices screen. Turn on recomposition counts, reset them, type "pune" in the search box, and write down which composables recomposed and how many times. Then select a price card and find its content description in Attributes.
  6. Harder: fix percentChange properly, as a function returning Double, with the text rounded to one decimal place where it is shown, and write the unit tests first: 55 on 2,525 is 2.2, a fall shows a minus sign, and decide (and test) what the screen says when yesterday's price is 0, which would otherwise divide by zero.

Official documentation

Next: unit testing ViewModels and repositories, and the test that would have caught both of Arjun's bugs.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship