Sparround

Performance, memory leaks & testing

A memory leak is a no-longer-needed object "hiding" from GC: something still references it. Android's classic leak sources:

  • An Activity/Context/View in a static field
  • Fragment bindings not cleared in onDestroyView
  • Unregistered listeners/receivers/callbacks
  • Anonymous inner classes (Handler, Runnable) holding the outer class
  • Activity contexts handed to singletons

Tools: LeakCanary (automatic detection in debug), the Memory Profiler (heap dump analysis). Symptoms: slowly climbing memory, OutOfMemoryError, jank from GC pauses.

The Android test pyramid:

  • Unit tests (many, fast) — ViewModel, use case, repository logic; JUnit + fakes/MockK; runTest + TestDispatcher for coroutines; Turbine for Flows.
  • Integration — Room DAOs (in-memory DB), the API layer (MockWebServer).
  • UI tests (few, slow) — Espresso (Views), Compose Testing (composeTestRule); for critical flows (login, transfers).

DI pays off here: constructor injection lets everything be swapped for fakes.

kotlin
@OptIn(ExperimentalCoroutinesApi::class)
class TransferViewModelTest {

    private val dispatcher = StandardTestDispatcher()

    @Before fun setup() { Dispatchers.setMain(dispatcher) }
    @After fun tearDown() { Dispatchers.resetMain() }

    @Test
    fun `declined transfer surfaces reason`() = runTest {
        val repo = FakeTransferRepository(
            result = TransferResult.Declined("Günlük limit aşılıb")
        )
        val vm = TransferViewModel(repo)

        vm.uiState.test {                      // Turbine
            vm.submit(amount = "5000.00", to = "AZ45...")
            advanceUntilIdle()

            val state = expectMostRecentItem()
            assertEquals("Günlük limit aşılıb", state.error)
            assertFalse(state.isLoading)
        }
    }
}

A ViewModel unit test: TestDispatcher + Turbine

The balanced answer to "what do you test?": we test risk, not every line: money math and limit logic — 100%; ViewModel state transitions — the main scenarios; mappers — against sample JSONs; UI — only critical flows (login, transfer confirmation). The coverage number is an outcome, not a goal — citing the anti-example "90% coverage but untested money logic" lands well in interviews.

🛠 Practice task

Create a leak, then catch it:

  • Leak an activity context through a static field on purpose, read the LeakCanary trace and fix it.
  • Write a ViewModel unit test: StandardTestDispatcher + runTest, covering the error path with a fake repository.
  • Get one Room DAO test passing against an in-memory DB.

Done when: all three tests are green and you can narrate the leak trace.

📚 Sources and documentation