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
- Make your scratch copy with
applicationIdSuffix = ".m13", add Arjun's firstpercentChange, and open Lasalgaon onion. Set the breakpoint, attach the debugger, and reproduce the three evaluations above. - Make the breakpoint conditional on
change < 0and open a few prices. Which prices fell since yesterday? - Add the
%dversion and reproduce the crash. Find it three ways: Studio's Logcat withis:crash,adb logcat -b crash -d, and an exception breakpoint onIllegalFormatConversionExceptionthat stops before the app dies. - Replace a
Log.dyou would have added with a logging breakpoint, as in the answer above, and rotate the phone with it on. - 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.
- Harder: fix
percentChangeproperly, as a function returningDouble, 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
- Debug your app — Debuggable builds, starting a debug session and attaching to a running process.
- Using breakpoints (IntelliJ IDEA) — Every breakpoint type and option, including conditions and Evaluate and log; Android Studio's debugger is this one.
- View logs with Logcat — The whole query language:
is:crash,is:stacktrace,age:, negation, regular expressions and operators. - Debug your layout with Layout Inspector — Starting it, Deep Inspect, snapshots and comparing with a mock-up.
- Debug your Compose UI — Recomposition and skip counts, semantics in the inspector, and Compose UI Check.
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