Logging & Debugging Tools
Every lesson up to this point has been about building TrailLog's features. This one is about figuring out what's actually happening when a feature doesn't behave the way you expect — a trail that won't save, a screen that renders with the wrong layout, a Firestore listener that never fires. Debugging isn't a separate skill from Android development; it's the skill you use every day while doing it. This lesson covers the everyday toolkit: logging, Logcat, the Layout Inspector, the step-through debugger, and a couple of tools that catch mistakes before they become bug reports.
Log and Logcat: Where Everything Ends Up
Android's built-in Log class writes timestamped messages that show up in
Logcat — the live, streaming console built into Android Studio (and available from
the command line via adb logcat). Every call takes a tag (usually the
class name, so you can tell where a message came from) and a message string:
class TrailRepository {
companion object {
private const val TAG = "TrailRepository"
}
suspend fun saveTrail(trail: Trail) {
Log.d(TAG, "Saving trail: ${trail.name}, distance=${trail.distanceKm}km")
try {
trailDao.insert(trail)
} catch (e: Exception) {
Log.e(TAG, "Failed to save trail ${trail.name}", e)
}
}
}
Passing the exception itself as the last argument to Log.e (as above) prints the full
stack trace along with the message — genuinely worth doing every time, since the stack trace is
usually what actually points at the bug.
Log Levels: Which One to Reach For
| Method | Use it for |
|---|---|
Log.v (Verbose) | Extremely fine-grained detail, rarely left in code — the noisiest level. |
Log.d (Debug) | Everyday development messages — "here's what this function did," useful while actively working on a feature. |
Log.i (Info) | Notable but expected events — "user signed in," "sync completed." |
Log.w (Warn) | Something unexpected happened but TrailLog recovered on its own — worth a look, not an emergency. |
Log.e (Error) | An actual failure — an exception was caught, an operation didn't complete. |
Filtering Logcat in Android Studio
A running app can produce hundreds of log lines a second once the system and every library gets
involved — finding TrailLog's own messages in that noise is a genuinely essential daily skill, not
an advanced one. Logcat's filter bar lets you narrow the stream three ways at once: by
tag (e.g. only TrailRepository), by log level (e.g.
only Warn and above, to spot problems fast), and by process (only TrailLog's own
process, cutting out every other app and system service running on the device). Combining a tag
filter with a minimum level is usually the fastest way to isolate exactly what one part of the app
is doing.
The Layout Inspector: Seeing the UI Hierarchy Live
When a screen renders with the wrong size, the wrong overlap, or a view that's invisible for no obvious reason, reading XML and Kotlin code often isn't enough — you need to see what the layout engine actually did. Android Studio's Layout Inspector connects to a running app and shows the live view hierarchy as an inspectable tree: every view's real position, size, and attribute values, and a 3D exploded view of how views stack on top of each other. For something like a trail card that mysteriously overlaps the toolbar, this turns "guess and rebuild" into "look and see exactly what's wrong."
Breakpoints and Step-Through Debugging
Logging tells you what already happened; the debugger lets you pause execution and inspect it
as it happens. A quick practical walkthrough for TrailLog's saveTrail()
function above:
| Step | What you do |
|---|---|
| 1 | Click in the gutter next to trailDao.insert(trail) to set a breakpoint — a red marker appears. |
| 2 | Launch the app with the Debug button instead of Run. Execution pauses the instant that line is about to run. |
| 3 | Use Step Over to run the current line and move to the next, or Step Into to jump inside a function call like insert() itself. |
| 4 | Hover over any variable (or check the Variables panel) to inspect its current value — e.g. confirm trail.distanceKm really is what you expected before it's saved. |
| 5 | Resume Program to let execution continue normally once you've seen what you need. |
Timber: A More Convenient Logging Wrapper
Raw Log calls work fine, but many real apps add Timber, a small,
popular logging library, for a couple of genuine conveniences: it auto-derives the tag from the
calling class so you never type TAG by hand, and it makes it easy to strip verbose
logging out of release builds entirely (by installing a no-op tree in production) while keeping
full logging in debug builds. A Timber call looks almost identical to Log:
Timber.d("Saving trail: ${trail.name}") // no tag needed — Timber infers it automatically
You don't need Timber to follow the rest of this track — it's included here as conceptual awareness, since you'll likely run into it reading other people's Android codebases.
StrictMode: Catching Main-Thread Mistakes Before They Ship
Coroutines Basics made the case that disk and network
access must never run on the main thread. StrictMode is the tool that actually
enforces this during development: enabled in debug builds, it detects accidental main-thread disk
reads, network calls, and a few other slow operations, and can be configured to log a warning or
even crash the app immediately when one slips through. In practice, StrictMode is
exactly what would catch you if you (or a teammate) ever forgot a
withContext(Dispatchers.IO) around a Room query or a network call — a fast, loud
signal in development, instead of a slow, silent janky screen a user notices in production.