Testing, Debugging and Performance in Depth

Android Development with Kotlin: 21 Real Projects · Production Android · 4 min read · Updated

Debug a slow screen

  1. You notice a slow screen or an ANR report.
  2. Use Profiler, Layout Inspector or the trace to find what blocks or redraws too much.
  3. Fix the real cause: move work off the main thread, add keys, reduce recomposition.
  4. Measure again with a benchmark so you know it improved.

Lesson 14 gave you the basics. This lesson covers what teams use every day: good test doubles, UI tests, debugging tools and the performance work that makes an app feel fast.

Unit tests with fakes and MockK

A test should check one behaviour. To isolate a class, replace its dependencies with test doubles. A *fake* is a simple working version (an in-memory repository). A *mock* records calls and returns answers you set. MockK is the Kotlin library for mocks, and Mockito is the Java-based alternative. Prefer fakes for repositories, and mocks for things that are hard to fake.

class FakeRepo : OrderRepository {
    var placed = mutableListOf<List<Item>>()
    override suspend fun place(cart: List<Item>): Result<OrderId> {
        placed.add(cart); return Result.success(OrderId("A1"))
    }
}

@Test fun emptyCartIsRejected() = runTest {
    val useCase = PlaceOrder(FakeRepo())
    assertTrue(useCase(emptyList()).isFailure)
}

@Test fun callsRepoOnce() = runTest {
    val repo = mockk<OrderRepository>()
    coEvery { repo.place(any()) } returns Result.success(OrderId("A1"))
    PlaceOrder(repo)(listOf(item))
    coVerify(exactly = 1) { repo.place(any()) }
}

runTest from kotlinx-coroutines-test runs suspend code and skips delays. Test Flows with the Turbine library (flow.test { awaitItem() }).

UI tests

  • Compose UI tests: createComposeRule(), onNodeWithText("Save").performClick(), assertIsDisplayed(). Add Modifier.testTag("list") where text is not enough.
  • Espresso tests XML views: onView(withId(R.id.save)).perform(click()). It still matters for older apps.
  • Integration tests check several parts together, for example a ViewModel with a real in-memory Room database (Room.inMemoryDatabaseBuilder).
  • End-to-end tests run a full user flow on a device or emulator. They are slow, so keep a few for the key paths such as login and purchase.
  • Test-driven development: write a failing test, make it pass, then tidy. It suits business rules such as price calculations.

Debugging

  • Logcat: filter by package and level; use Log.d(TAG, message). Read crash stack traces from the bottom of the "Caused by" chain up to the first line from your package.
  • Breakpoints and the debugger: step through code and inspect values; use conditional breakpoints in loops.
  • Layout Inspector: shows the UI tree and, in Compose, how often each composable recomposes.
  • Network Inspector: see requests and responses.
  • App Inspection also shows the database and background tasks.

Memory, ANRs and profiling

A memory leak happens when something holds a reference to an object that should be freed, such as a static field pointing to an Activity. LeakCanary (a debug dependency) detects leaks and prints the chain. An ANR (Application Not Responding) appears when the main thread is blocked for too long, usually by disk or network work on the main thread; read the trace in Play Console or /data/anr and move the work to a background coroutine. The Android Profiler shows CPU, memory, network and energy over time.

Faster startup and smoother UI

  • Startup: do the minimum in Application.onCreate, initialise libraries lazily or with App Startup, and use the SplashScreen API.
  • Baseline Profiles: a file that tells Android which code to pre-compile, which can improve startup and first scrolls noticeably. Generate it with the Macrobenchmark library.
  • Macrobenchmark measures startup and scrolling of the whole app. Microbenchmark measures one function.
  • UI: stable keys, avoid unnecessary recomposition, avoid nested scrolling lists, and load images at display size.
  • Battery and network: batch requests, use caching, avoid wake locks, and prefer WorkManager.
  • Size: enable R8 and resource shrinking, use AAB, compress images (WebP), and remove unused libraries. Check the APK Analyzer.

Your task

  1. Write a fake repository and test a use case with it.
  2. Write one Compose UI test that types in a field and presses a button.
  3. Add LeakCanary to a debug build and look for a leak you create on purpose.
  4. Run the APK Analyzer on a release build and list the three largest items.

How to check it worked

  • Tests do not depend on the network.
  • You can read a crash trace to your own code.
  • You can name three ways to speed up startup.

Frequently asked questions

Fake or mock?

Use a fake when a simple working version is easy to write. Use a mock when you need to check that a call happened or force an error.

What causes an ANR?

Blocking the main thread for several seconds, typically with disk, network or heavy computation. Move that work to a background coroutine.

What is a Baseline Profile?

A list of important code paths that Android compiles ahead of time, so the app starts faster and scrolls more smoothly from the first run.