Module 09: Debugging, Testing & Troubleshooting
Objective: Master Android Studio's debugging tools, learn to read crash logs, and perform basic unit testing.
Frontend Equivalents: Debugging Tools
| Frontend | Android | Description |
|---|---|---|
| Chrome DevTools Console | Logcat | Log output |
console.log() | Log.d(TAG, msg) | Print logs |
debugger statement | Breakpoint | Pause execution |
| Network panel | OkHttp Logging Interceptor | Network request debugging |
| Application panel | Device File Explorer | View app data |
| Performance panel | Android Profiler | Performance analysis |
| Jest / Vitest | JUnit | Unit testing framework |
| Playwright / Cypress | Espresso | UI testing framework |
1. Logcat Logging System
1.1 Log Levels
import android.util.Log;
// Log levels (from lowest to highest severity)
Log.v("TAG", "Verbose log"); // Most detailed, usually disabled
Log.d("TAG", "Debug log"); // Debug information
Log.i("TAG", "Info log"); // General information
Log.w("TAG", "Warning log"); // Warnings
Log.e("TAG", "Error log"); // Errors
Log.wtf("TAG", "WTF log"); // Critical error (What a Terrible Failure)
// With exception information
try {
// ...
} catch (Exception e) {
Log.e("TAG", "Operation failed", e); // Third parameter is the exception, prints the stack trace
}1.2 Logcat Filtering
In Android Studio's Logcat panel:
# Filter by level
level:ERROR # Show Error and above only
# Filter by TAG
tag:MainActivity # Show a specific TAG only
# Search by keyword
"network error" # Search for keywords
# Filter by process
pid:12345 # By process ID
# Common combinations
level:ERROR tag:Retrofit # Show Retrofit error logs1.3 Practical TAG Conventions
// Approach 1: Class name as TAG (most common)
private static final String TAG = "MainActivity";
// Approach 2: Auto-derive class name
private static final String TAG = MyActivity.class.getSimpleName();
// Approach 3: Global TAG
public class AppConstants {
public static final String LOG_TAG = "MyApp";
}1.4 Logging Performance and Production
// ❌ Avoid: logging in tight loops or concatenating large objects
for (User user : users) {
Log.d(TAG, "user=" + user.toString()); // concatenation runs even in Release
}
// ✅ Recommended: guard with Log.isLoggable to skip work in Release
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "user=" + user.toString());
}
// ✅ Even better: use Timber and plant a no-op tree in Release
dependencies {
implementation 'com.jakewharton.timber:timber:5.0.1'
}Frontend analogy: Similar to guarding
console.logwithif (process.env.NODE_ENV === 'development')to avoid leaking debug info or wasting cycles in production.
2. Crash Log Analysis
2.1 Common Crash Types
# NullPointerException (most common)
java.lang.NullPointerException: Attempt to invoke virtual method
'void android.widget.TextView.setText(java.lang.CharSequence)'
on a null object reference
at com.example.app.MainActivity.onCreate(MainActivity.java:42)
# Analysis:
# - Cause: TextView is null, likely forgot findViewById or used wrong ID
# - Location: MainActivity.java line 42
# IllegalStateException
java.lang.IllegalStateException: Fragment not attached to a context.
at androidx.fragment.app.Fragment.requireContext(Fragment.java:909)
# Analysis:
# - Cause: Fragment has already detached but code is still accessing Context
# - Common in async callbacks where the Fragment has been destroyed
# OutOfMemoryError
java.lang.OutOfMemoryError: Failed to allocate a 52428812 byte allocation
# Analysis:
# - Cause: Loaded an oversized image or created too many objects
# - Fix: Use Glide for image loading, reduce memory usage
# ANR (Application Not Responding)
"main" thread blocked for 6000ms
# Analysis:
# - Cause: Long-running operation on the main thread (network, database, file I/O)
# - Fix: Move long-running operations to background threads2.2 Crash Troubleshooting Steps
- Check the exception type:
NullPointerException,IllegalStateException, etc. - Read the exception message: The error description, e.g., "on a null object reference"
- Look at the top of the stack trace: The first line from your own code (usually starting with
com.example.app.Xxx) - Check the line number: Navigate to the corresponding line in the source file
- Trace upward: Find the root cause in the call chain
2.3 ANR and Main-Thread Blocking
ANRs are usually caused by blocking the main thread. Typical mistake:
// ❌ Wrong: network call on the main thread
public void onClick(View v) {
URL url = new URL("https://api.example.com/data");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.getInputStream(); // NetworkOnMainThreadException or ANR
}Use StrictMode during development to catch main-thread violations early:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.penaltyDialog()
.build());
}
}
}Frontend analogy: StrictMode is like Chrome's Long Tasks API or React Profiler: it surfaces code that blocks the main/render thread.
3. Android Studio Debugger
3.1 Breakpoint Debugging
- Set a breakpoint: Click to the left of the line number; a red dot appears
- Start debugging: Click the Debug button (bug icon) or press
Shift+F9 - Debug actions:
| Button | Shortcut | Description |
|---|---|---|
| Step Over | F8 | Execute current line without entering methods |
| Step Into | F7 | Enter the method body |
| Step Out | Shift+F8 | Exit the current method |
| Resume | F9 | Continue to the next breakpoint |
| Stop | Ctrl+F2 | Stop debugging |
3.2 Viewing Variables
- Variables panel: View all variables in the current scope
- Watches: Add custom expression monitors
- Evaluate Expression (
Alt+F8): Execute expressions at the breakpoint
3.3 Conditional Breakpoints
Right-click the breakpoint and set a Condition:
// Only pause when a specific condition is met
position == 5
user.getName().equals("John")3.4 Layout Inspector
Tools -> Layout Inspector: Similar to the Elements panel in Chrome DevTools
- View the view hierarchy of the current screen
- Inspect properties of each View (dimensions, margins, colors, etc.)
- 3D view rotation to examine layer relationships
3.5 Database Inspector
View -> Tool Windows -> App Inspection -> Database:
- View Room/SQLite database contents in real time
- Execute SQL queries
- Observe changes in LiveData query results
3.6 Network Debugging with OkHttp Logging Interceptor
In the android3 OkHttp samples, callbacks often need inspection of headers and bodies. Add an interceptor to diagnose network issues quickly:
// app/build.gradle
dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
}HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging)
.build();| Level | Output |
|---|---|
NONE | No logs |
BASIC | Method, URL, status code, response size |
HEADERS | Includes request/response headers |
BODY | Includes request/response bodies (most verbose, good for Debug) |
Frontend analogy: Equivalent to Chrome DevTools Network details, or Axios
interceptors.request/responselogging.
4. Performance Analysis Tools
4.1 Android Profiler
View -> Tool Windows -> Profiler:
| Panel | Purpose | Frontend Equivalent |
|---|---|---|
| CPU | Method execution time, thread state | Performance panel |
| Memory | Memory allocation, GC, memory leaks | Memory panel |
| Network | Network request timeline | Network panel |
| Energy | Battery consumption analysis | No equivalent |
4.2 Memory Leak Detection
// Common leak scenarios:
// 1. Static reference to an Activity
public class BadUtils {
static Activity activity; // Never do this
}
// 2. Anonymous inner class holding outer reference
handler.postDelayed(new Runnable() { // Anonymous class holds Activity reference
@Override
public void run() {
textView.setText("delayed"); // Leaks if Activity has been destroyed
}
}, 10000);
// 3. Failing to unregister listeners
@Override
protected void onCreate(Bundle savedInstanceState) {
someService.addListener(this); // Register
}
// Forgot to unregister in onDestroy -> leak
@Override
protected void onDestroy() {
super.onDestroy();
someService.removeListener(this); // Unregister
}Use LeakCanary to automatically detect memory leaks:
// app/build.gradle
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
}4.3 More Memory Leak Patterns
// 4. Handler holding the Activity
public class MainActivity extends AppCompatActivity {
private Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
// Anonymous inner class implicitly holds outer MainActivity
}
};
}
// Fix: use a static inner class + WeakReference, or remove callbacks in onDestroy
// 5. AsyncTask holding the Activity
new AsyncTask<Void, Void, String>() {
@Override protected String doInBackground(Void... voids) { /* long work */ return ""; }
@Override protected void onPostExecute(String s) {
textView.setText(s); // crash or leak if Activity is gone
}
}.execute();
// Fix: static inner class / ViewModel + Executor / Kotlin Coroutines
// 6. RxJava subscription not disposed
disposable = observable.subscribe();
// Fix: call disposable.dispose() in onDestroyFrontend analogy: Like closures in JS that capture DOM/component references and prevent GC, Android's anonymous inner classes, Handlers, and async tasks often implicitly hold the Context.
5. Unit Testing
5.1 JUnit Basics
// src/test/java/com/example/app/UserTest.java
import org.junit.Test;
import org.junit.Before;
import static org.junit.Assert.*;
public class UserTest {
private User user;
@Before
public void setUp() {
user = new User("John", "john@example.com");
}
@Test
public void testGetName() {
assertEquals("John", user.getName());
}
@Test
public void testEmail() {
assertNotNull(user.getEmail());
assertTrue(user.getEmail().contains("@"));
}
@Test
public void testValidation() {
assertThrows(IllegalArgumentException.class, () -> {
user.setAge(-1);
});
}
}5.2 Mockito (Mocking Dependencies)
import static org.mockito.Mockito.*;
public class UserListPresenterTest {
@Mock
private UserListContract.View mockView;
@Mock
private UserRepository mockRepository;
private UserListPresenter presenter;
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
presenter = new UserListPresenter(mockRepository);
presenter.attachView(mockView);
}
@Test
public void testLoadUsers_success() {
List<User> expectedUsers = Arrays.asList(
new User("John", "john@example.com"),
new User("Jane", "jane@example.com")
);
// Mock repository behavior
doAnswer(invocation -> {
DataCallback<List<User>> callback = invocation.getArgument(0);
callback.onSuccess(expectedUsers);
return null;
}).when(mockRepository).getUsers(any());
// Execute
presenter.loadUsers();
// Verify
verify(mockView).showLoading();
verify(mockView).hideLoading();
verify(mockView).showUsers(expectedUsers);
verify(mockView, never()).showError(anyString());
}
@Test
public void testLoadUsers_error() {
doAnswer(invocation -> {
DataCallback<List<User>> callback = invocation.getArgument(0);
callback.onError(new RuntimeException("Network error"));
return null;
}).when(mockRepository).getUsers(any());
presenter.loadUsers();
verify(mockView).showError("Network error");
verify(mockView, never()).showUsers(any());
}
@After
public void tearDown() {
presenter.detachView();
}
}5.3 Running Tests
# Run all unit tests
./gradlew test
# Run a specific test class
./gradlew test --tests "com.example.app.UserTest"
# In Android Studio: right-click the test class -> Run 'UserTest'5.4 Espresso UI Testing Basics
// src/androidTest/java/com/example/app/LoginActivityTest.java
@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {
@Rule
public ActivityScenarioRule<LoginActivity> activityRule =
new ActivityScenarioRule<>(LoginActivity.class);
@Test
public void login_showsHomeScreen() {
onView(withId(R.id.et_username)).perform(typeText("admin"));
onView(withId(R.id.et_password)).perform(typeText("123456"));
onView(withId(R.id.btn_login)).perform(click());
onView(withText("Welcome")).check(matches(isDisplayed()));
}
}// app/build.gradle
dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}Frontend analogy: Espresso is like Playwright/Cypress: it finds views, simulates clicks and input, and asserts UI state for end-to-end testing.
6. Common Troubleshooting Checklist
6.1 Gradle Sync Failures
| Problem | Solution |
|---|---|
SDK location not found | Check sdk.dir in local.properties |
Could not find com.android.tools.build:gradle | Check AGP version and network connectivity |
Unsupported class file major version | Upgrade/downgrade the JDK version |
Dependency resolution failed | Check repository mirror configuration |
6.2 Compilation Errors
| Problem | Solution |
|---|---|
cannot find symbol class R | Clean the project: Build -> Clean Project |
Program type already present | Dependency conflict; check for duplicate dependencies |
Method count exceeded 64K | Enable multidex |
AAPT: resource not found | Check resource file names and IDs |
6.3 Runtime Errors
| Problem | Solution |
|---|---|
ClassNotFoundException | ProGuard obfuscated the class; add keep rules |
NetworkOnMainThreadException | Network requests must run on background threads |
PermissionDeniedException | Check Manifest permission declarations + runtime permissions |
IllegalStateException: Fragment not attached | Fragment lifecycle issue |
6.4 Useful adb Commands
# List connected devices
adb devices
# Install/uninstall APK
adb install app-debug.apk
adb uninstall com.example.app
# Show current Activity (useful to verify which screen is open)
adb shell dumpsys activity activities | grep mResumedActivity
# Filter app logs (common production debugging)
adb logcat -s MyApp:D
# Screenshot / screen recording
adb shell screencap /sdcard/screen.png
adb pull /sdcard/screen.png ./screen.png
adb shell screenrecord /sdcard/demo.mp4
# Restart adb server (fixes connection issues)
adb kill-server && adb start-serverFrontend analogy: adb is like the combination of
npxand browser DevTools CLI tools: a Swiss Army knife for interacting with running devices.
7. Runtime Permissions
// Android 6.0+ dangerous permissions require runtime requests
private static final int CAMERA_PERMISSION_CODE = 100;
// Check permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// Request permission
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
CAMERA_PERMISSION_CODE);
} else {
// Permission already granted, proceed
openCamera();
}
// Handle permission result
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == CAMERA_PERMISSION_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
Toast.makeText(this, "Camera permission is required for this feature", Toast.LENGTH_SHORT).show();
}
}
}Practical tip: The
chapter06/src/main/java/com/example/chapter06/util/PermissionUtil.javaclass inandroid3wraps permission checking, requesting, and result validation into reusable helpers. When taking over legacy projects, consider centralizing permission logic like this instead of repeating boilerplate in every Activity.
Frontend Developer Cheat Sheet
| Frontend Debugging Concept | Android Equivalent | Notes |
|---|---|---|
console.log() | Log.d(TAG, msg) | Log output |
| Chrome Console | Logcat panel | Log viewer |
debugger | Breakpoint | Pause execution |
| Elements panel | Layout Inspector | UI inspection |
| Application panel | Device File Explorer | File browsing |
| Network panel | OkHttp LoggingInterceptor | Network debugging |
npm test | ./gradlew test | Run tests |
Jest expect() | JUnit assertEquals() | Assertions |
jest.fn() / sinon.spy() | Mockito mock() / verify() | Mocking and verification |
| Lighthouse | Android Profiler | Performance analysis |
| Error Boundary | try-catch + crash reporting | Error handling |
Next Steps
With debugging and testing skills under your belt, finish up with Module 10: Legacy Project Guide & Kotlin Migration for a comprehensive guide to taking on real-world projects.