Logging & Logcat
Before reaching for a debugger, most Android developers reach for a log statement first. The Log class writes timestamped messages to Logcat, Android Studio's live log viewer — the single most-used tool for understanding what a running app is actually doing, in RecipeBox or any other project.
The Log Methods
Log exposes one method per severity level. Every call takes a tag (usually the class name, used for filtering) and a message.
| Method | Level | When to use it |
|---|---|---|
| Log.v() | Verbose | Extremely detailed, noisy tracing — rarely left in code |
| Log.d() | Debug | General development-time tracing of app flow |
| Log.i() | Info | Notable, expected events (e.g., "user signed in") |
| Log.w() | Warning | Something recoverable but worth noticing (e.g., a retry) |
| Log.e() | Error | Something failed — typically paired with an exception |
Filtering Logcat in Android Studio
A running device or emulator can easily produce thousands of log lines a minute from the system and every installed app — filtering is what makes Logcat usable day to day.
By tag: type a tag (e.g., RecipeBox) into the Logcat search box to see only messages logged with that exact tag.
By log level: the level dropdown (Verbose/Debug/Info/Warn/Error) shows that level and everything more severe — selecting "Warn" hides Verbose/Debug/Info but shows Warn and Error.
By process: the process dropdown restricts output to a single running app, so system noise and other installed apps don't clutter the view.
Worked Example: Tracing a Network Fetch
Meaningful log statements at each step of a flow make it possible to see exactly where things go wrong, without attaching a debugger. Here they trace RecipeBox fetching a recipe's details over the network (see HTTP Basics & Threads and Retrofit (Java) + Gson for the networking layer itself).
private static final String TAG = "RecipeBox";
public void fetchRecipe(String recipeId) {
Log.d(TAG, "fetchRecipe: starting for id=" + recipeId);
api.getRecipe(recipeId).enqueue(new Callback<Recipe>() {
@Override
public void onResponse(Call<Recipe> call, Response<Recipe> response) {
if (!response.isSuccessful()) {
Log.w(TAG, "fetchRecipe: server returned code " + response.code());
return;
}
Log.d(TAG, "fetchRecipe: success, title=" + response.body().title);
}
@Override
public void onFailure(Call<Recipe> call, Throwable t) {
Log.e(TAG, "fetchRecipe: network call failed", t);
}
});
}
- The first Log.d() confirms the method was actually called with the expected ID — useful when a UI action seems to do nothing.
- Log.w() for a non-2xx response distinguishes "the server rejected this" from a full network failure.
- Passing t directly to Log.e(TAG, message, t) automatically prints the exception's full stack trace beneath the message.
Log.getStackTraceString()
When an exception needs to be logged somewhere that doesn't accept a Throwable directly (for example, concatenated into a larger message string), Log.getStackTraceString() converts it to text.
try {
saveRecipeToDisk(recipe);
} catch (IOException e) {
String details = Log.getStackTraceString(e);
Log.e(TAG, "saveRecipeToDisk failed:\n" + details);
}
Keep Logging Out of Release Builds
Verbose debug logging left running in a released app wastes CPU, can leak information, and clutters any user-facing device logs. The ProGuard / R8 shrinker covered earlier can be configured with rules that strip specific logging calls (commonly Log.d and Log.v) out of release builds entirely, so the calls simply don't exist in the shipped APK.
An Alternative: Timber
Many real-world apps don't call Log directly at all — they use Timber, a small, popular logging wrapper library. Conceptually, Timber auto-generates the tag from the calling class (no more repeating TAG everywhere), and makes it straightforward to plant a "tree" that silently discards debug logs in release builds while still forwarding errors to a crash-reporting service. This lesson only needs the awareness that Timber exists as the common next step once raw Log calls start feeling repetitive.