Skip to content

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

FrontendAndroidDescription
Chrome DevTools ConsoleLogcatLog output
console.log()Log.d(TAG, msg)Print logs
debugger statementBreakpointPause execution
Network panelOkHttp Logging InterceptorNetwork request debugging
Application panelDevice File ExplorerView app data
Performance panelAndroid ProfilerPerformance analysis
Jest / VitestJUnitUnit testing framework
Playwright / CypressEspressoUI testing framework

1. Logcat Logging System

1.1 Log Levels

java
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 logs

1.3 Practical TAG Conventions

java
// 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

java
// ❌ 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.log with if (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 threads

2.2 Crash Troubleshooting Steps

  1. Check the exception type: NullPointerException, IllegalStateException, etc.
  2. Read the exception message: The error description, e.g., "on a null object reference"
  3. Look at the top of the stack trace: The first line from your own code (usually starting with com.example.app.Xxx)
  4. Check the line number: Navigate to the corresponding line in the source file
  5. 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:

java
// ❌ 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:

java
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

  1. Set a breakpoint: Click to the left of the line number; a red dot appears
  2. Start debugging: Click the Debug button (bug icon) or press Shift+F9
  3. Debug actions:
ButtonShortcutDescription
Step OverF8Execute current line without entering methods
Step IntoF7Enter the method body
Step OutShift+F8Exit the current method
ResumeF9Continue to the next breakpoint
StopCtrl+F2Stop 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:

java
// 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:

groovy
// app/build.gradle
dependencies {
    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
}
java
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(logging)
    .build();
LevelOutput
NONENo logs
BASICMethod, URL, status code, response size
HEADERSIncludes request/response headers
BODYIncludes request/response bodies (most verbose, good for Debug)

Frontend analogy: Equivalent to Chrome DevTools Network details, or Axios interceptors.request/response logging.

4. Performance Analysis Tools

4.1 Android Profiler

View -> Tool Windows -> Profiler:

PanelPurposeFrontend Equivalent
CPUMethod execution time, thread statePerformance panel
MemoryMemory allocation, GC, memory leaksMemory panel
NetworkNetwork request timelineNetwork panel
EnergyBattery consumption analysisNo equivalent

4.2 Memory Leak Detection

java
// 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:

groovy
// app/build.gradle
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
}

4.3 More Memory Leak Patterns

java
// 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 onDestroy

Frontend 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

java
// 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)

java
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

bash
# 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

java
// 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()));
    }
}
groovy
// 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

ProblemSolution
SDK location not foundCheck sdk.dir in local.properties
Could not find com.android.tools.build:gradleCheck AGP version and network connectivity
Unsupported class file major versionUpgrade/downgrade the JDK version
Dependency resolution failedCheck repository mirror configuration

6.2 Compilation Errors

ProblemSolution
cannot find symbol class RClean the project: Build -> Clean Project
Program type already presentDependency conflict; check for duplicate dependencies
Method count exceeded 64KEnable multidex
AAPT: resource not foundCheck resource file names and IDs

6.3 Runtime Errors

ProblemSolution
ClassNotFoundExceptionProGuard obfuscated the class; add keep rules
NetworkOnMainThreadExceptionNetwork requests must run on background threads
PermissionDeniedExceptionCheck Manifest permission declarations + runtime permissions
IllegalStateException: Fragment not attachedFragment lifecycle issue

6.4 Useful adb Commands

bash
# 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-server

Frontend analogy: adb is like the combination of npx and browser DevTools CLI tools: a Swiss Army knife for interacting with running devices.

7. Runtime Permissions

java
// 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.java class in android3 wraps 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 ConceptAndroid EquivalentNotes
console.log()Log.d(TAG, msg)Log output
Chrome ConsoleLogcat panelLog viewer
debuggerBreakpointPause execution
Elements panelLayout InspectorUI inspection
Application panelDevice File ExplorerFile browsing
Network panelOkHttp LoggingInterceptorNetwork debugging
npm test./gradlew testRun tests
Jest expect()JUnit assertEquals()Assertions
jest.fn() / sinon.spy()Mockito mock() / verify()Mocking and verification
LighthouseAndroid ProfilerPerformance analysis
Error Boundarytry-catch + crash reportingError 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.

A Java Android development tutorial for frontend developers