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

MethodUse 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:

StepWhat you do
1Click in the gutter next to trailDao.insert(trail) to set a breakpoint — a red marker appears.
2Launch the app with the Debug button instead of Run. Execution pauses the instant that line is about to run.
3Use Step Over to run the current line and move to the next, or Step Into to jump inside a function call like insert() itself.
4Hover 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.
5Resume Program to let execution continue normally once you've seen what you need.
Note: These are real Android Studio workflows, but this website's own project doesn't run a live Android Studio build, so there's no actual running app here to attach a debugger or Layout Inspector to.

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.

Up next: Debugging tells you what's wrong after the fact — Unit Testing (JUnit & MockK) is about writing code that catches problems automatically, every time, before they ever reach a user.