How to Test Infinite Scroll on Android (Complete Guide)

Infinite scroll is a UI pattern that loads additional content automatically as the user reaches the end of a list. On Android, it is commonly implemented with RecyclerView, Paging 3 library, or custom

June 15, 2026 · 19 min read · How-To Guides

Why Infinite Scroll Matters on Android

Infinite scroll is a UI pattern that loads additional content automatically as the user reaches the end of a list. On Android, it is commonly implemented with RecyclerView, Paging 3 library, or custom AbsListView subclasses. The pattern improves perceived performance by avoiding explicit pagination controls, but it also couples UI behavior to data fetching, lifecycle events, and thread management. When any part of that chain fails, the user sees a frozen spinner, duplicated items, or a sudden jump back to the top of the list.

From a product perspective, infinite scroll drives engagement: users stay longer when new items appear without interruption. From a testing perspective, the pattern creates a moving target. Traditional scripted tests that navigate to a fixed position and assert a single element miss the dynamic nature of the list. They also ignore the myriad ways the loading mechanism can be triggered—by a quick flick, a slow drag, an accessibility gesture, or a system‑initiated scroll due to IME changes. Consequently, bugs that only manifest after dozens of loads, under memory pressure, or when the device is rotated often escape detection until they reach production.

Testing infinite scroll therefore requires a strategy that exercises the list across many scroll distances, varied timing, and different device states while observing both UI correctness and underlying data‑source behavior. The following sections lay out a complete approach, from manual exploration to automated scripts and persona‑driven autonomous testing.

Common Failure Modes in Production

Understanding what can go wrong helps focus test effort. Below are the most frequent categories of infinite‑scroll defects observed in Android apps released to the Play Store.

CategorySymptomTypical Root Cause
Data‑source exhaustionList stops loading after N pages; shows empty state or stale footerPagingSource returns null after a certain page, or network API returns empty array without signalling end‑of‑list
Duplicate entriesSame item appears twice consecutively after a scrollFailure to deduplicate keys when merging new page with existing list; PagingDataAdapter not using stable IDs
Spinner leakageProgress bar remains visible after data arrivesLoading state flag not cleared on error or success; coroutine not cancelled on configuration change
ANR on main threadUI freezes for >5 seconds during scrollHeavy work (JSON parsing, DB query) executed on UI thread inside RecyclerView.ViewHolder.bind
Memory overflowApp crashes with OutOfMemoryError after scrolling 30+ pagesBitmaps or large objects held in ViewHolder without recycling; ListAdapter retains stale references
Accessibility breakageTalkBack skips newly loaded items or announces “loading” indefinitelyNew views not marked as importantForAccessibility; live region not updated
Security/privacy leakPrefetched data includes PII that should not be cachedPagingSource loads more data than needed for prefetch; data persisted in Room without encryption
Rotation lossScroll position resets to top after device rotationSavedStateHandle not implemented; PagingData not retained across ViewModel recreation
Network‑retry loopEndless spinner appears when network fluctuatesRetry policy does not respect maxAttempts; each failure triggers a new load request
Edge‑gesture conflictSwipe‑to‑refresh triggers unintended load moreBoth swipeRefreshLayout and RecyclerView onTouchListener consume the same motion event

Each of these defects can be reproduced with a combination of manual steps, automated checks, and stress‑testing techniques. The next section defines a test matrix that covers the necessary dimensions.

Test Matrix Overview

A systematic matrix ensures that happy‑path, error‑path, accessibility, and security aspects are all exercised. The table below lists test dimensions (rows) against scroll scenarios (columns). Each cell indicates the recommended verification technique.

Test Dimension \ Scroll ScenarioInitial Load (0‑5 items)Mid‑Scroll (10‑30 pages)Fast Flick ( >100 dp/ms )Slow Drag ( <10 dp/ms )Accessibility Gesture (TalkBack swipe)Rotation Mid‑ScrollLow Memory ( <150 MB free )Network Flakiness ( 30% loss )
Data correctnessVerify first page matches APIEnsure no duplicates, correct orderingSame as mid‑scroll, plus check for dropped framesVerify incremental loading triggers at correct thresholdNew items announced, live region updatedScroll position preserved after rotationList continues loading without OOMRetry logic respects back‑off, no infinite spinner
UI stabilityNo overlapping views, proper item decorFooter/spinner appears only when neededNo jank >16ms per frame (measured via FrameMetrics)Smooth scroll, no stutterFocus moves to newly loaded itemUI does not flash or resetUI remains responsive, no dropped framesSpinner shows, then hides on success/error
State persistenceViewModel holds initial PagingDataPagingData survives configuration changeSame as mid‑scrollSame as mid‑scrollTalkBack reads updated stateScroll offset restored, PagingData not reloadedPagingData retained, no reload triggeredNetwork state reflected in UI after recovery
Resource usageMemory baseline ~20MBMemory growth <5MB per 10 pagesPeak memory <80MBSame as mid‑scrollAccessibility overlay does not leakNo memory spike on rotationHeap stays <120MB, GC frequency normalNo leak of HttpClient or coroutine scopes
Accessibility complianceContentDescription on each itemNew items receive proper labelsLabels appear within 200ms of appearanceLabels appear promptlyTalkBack reads each new item as it appearsFocus not lost after rotationContrast ratios unchanged under low memoryError messages announced via accessibility
Security/privacyNo prefetch beyond visible rangePrefetch window respects configured sizeNo extra prefetch triggered by flingPrefetch respects drag speedNo exposure of PII in accessibility eventsNo caching of sensitive data after rotationNo persistence of prefetched data to diskNo transmission of auth tokens on retry loops
Error handlingNetwork error shows retry buttonError state appears, allows manual retryError state does not mask new dataError state appears after slow dragTalkBack announces error messageError state persists across rotationError state shown under low memory if data fetch failsRetry attempts capped, fallback to cached data

How to Use the Matrix

  1. Select a scenario (e.g., Fast Flick) and run through each dimension, marking Pass/Fail.
  2. Automate the repetitive checks (data correctness, UI stability, resource usage) with instrumented tests.
  3. Reserve manual or exploratory testing for dimensions that rely on human perception (accessibility announcements, visual jank, focus behavior).
  4. Iterate: if a scenario fails a dimension, add a targeted test case that isolates the failure condition (e.g., simulate low memory with adb shell am set-debug-app -w com.example.app persisistent).

The matrix also serves as a communication tool with product and UX teams: they can see which aspects of infinite scroll are covered and where additional design work (e.g., prefetch tuning) may be needed.

Manual Testing Approach

Manual exploration remains valuable for catching subtle UX glitches that automated assertions may overlook. Below is a step‑by‑step protocol that a tester can follow on a physical device or emulator.

Preparation

  1. Install the app variant you intend to test (debug or release). Ensure logging is enabled (adb logcat -v threadtime).
  2. Clear app data (adb shell pm clear com.example.app) to start from a clean state.
  3. Enable developer options: Show CPU usage, Show surface updates, and Enable strict mode to detect disk/network on main thread.
  4. Set up accessibility service: Turn on TalkBack, enable “Explore by touch”, and optionally enable “Speak passwords” if you need to verify that no credentials are spoken.
  5. Prepare network throttling: Use adb shell netem or a tool like Charles Proxy to simulate 3G, packet loss, or latency.

Execution

StepActionObservation
1Launch the app, navigate to the infinite‑scroll screen.Verify initial loading spinner disappears after first page renders.
2Perform a slow drag (≈5 dp/ms) downwards.New items should appear smoothly; spinner should show only when data fetch starts.
3Perform a fast flick (≈150 dp/ms).List should keep up; no blank spaces or duplicate items.
4Rotate device to landscape, then back to portrait.Scroll position should be preserved; no flash of stale data.
5Enable TalkBack, swipe right to move focus through items.Each newly loaded item should receive focus and be announced; live region should update when spinner appears/disappears.
6Simulate low memory: adb shell am set-debug-app -w com.example.app persistant then open a memory‑heavy app (e.g., Chrome with many tabs) to pressure RAM.Continue scrolling; app should not crash with OOM; if it does, note the exact page count.
7Introduce 30% packet loss via network throttle.Observe retry behavior; ensure spinner does not stay forever and that error UI appears after max retries.
8After many scrolls (≈200 items), open the overflow menu and trigger a manual refresh (swipe‑to‑refresh).List should reset to first page correctly; no duplicate header.
9Leave the app in the background for 5 minutes, then return.List should restore to same position; background prefetch should not have consumed excess battery.
10Examine logcat for StrictMode violations, GC spikes, or NetworkOnMainThreadException.Any occurrence flags a defect to be fixed.

Documentation

Manual testing is time‑consuming but essential for validating the subjective feel of infinite scroll—something that automated frame‑timing checks can quantify but not fully interpret.

Automated Testing with Espresso/UIAutomator

Automated checks give confidence that regressions are caught early. The following patterns work well for infinite scroll on Android.

Espresso Basics

Espresso synchronizes with the UI thread and is ideal for verifying item correctness and simple interactions.


@RunWith(AndroidJUnit4::class)
class InfiniteScrollTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class)

    @Test
    fun `loads next page on scroll`() {
        // 1. Verify first page contains expected IDs
        onView(withId(R.id.recycler_view))
            .perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(0))
        onView(withId(R.id.item_text)).check(matches(withText("Item 1")))

        // 2. Scroll to near the end to trigger load more
        onView(withId(R.id.recycler_view))
            .perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(19))

        // 3. Wait for new items (use IdlingResource tied to PagingState)
        val pagingIdle = object : IdlingResource {
            override fun getName() = "PagingIdle"
            override fun isIdleNow(): Boolean {
                val adapter = activityRule.scenario.onActivity { it.findViewById<RecyclerView>(R.id.recycler_view).adapter }
                return adapter?.itemCount ?: 0 > 20
            }
            override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {}
        }
        IdlingRegistry.getInstance().register(pagingIdle)

        // 4. Verify that item 21 appears
        onView(withId(R.id.item_text))
            .check(matches(withText("Item 21")))

        IdlingRegistry.getInstance().unregister(pagingIdle)
    }
}

Key points:

UIAutomator for Scroll Dynamics

UIAutomator works across app boundaries and can expose low‑level scroll metrics, making it suitable for performance and jank checks.


public class ScrollPerformanceTest extends UiAutomatorTestCase {

    private UiDevice device;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        device.pressHome();
        // Launch the app
        Context ctx = InstrumentationRegistry.getTargetContext();
        final Intent intent = ctx.getPackageManager()
                .getLaunchIntentForPackage("com.example.app");
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
        ctx.startActivity(intent);
        device.wait(Until.hasObject(By.pkg("com.example.app").depth(0)), 5000);
    }

    public void testFastFlickJank() throws UiObjectNotFoundException {
        // Locate RecyclerView
        UiObject list = device.findObject(new UiSelector()
                .resourceId("com.example.app:id/recycler_view"));

        // Record frame timestamps before flick
        long[] before = device.executeShellCommand(
                "dumpsys gfxinfo com.example.app | grep -A 20 \"Draw\"").getBytes();

        // Perform a fast flick (150 dp/ms)
        list.swipeUp(30); // 30 steps approximates a quick gesture

        // Wait for UI to settle
        SystemClock.sleep(500);

        // Capture frame timestamps after flick
        long[] after = device.executeShellCommand(
                "dumpsys gfxinfo com.example.app | grep -A 20 \"Draw\"").getBytes();

        // Compute jank: frames >16ms
        int jankCount = computeJank(after);
        assertTrue("Excessive jank detected: " + jankCount + " frames", jankCount <= 2);
    }

    private int computeJank(long[] frameTimes) {
        int over = 0;
        for (long t : frameTimes) {
            if (t > 16) over++;
        }
        return over;
    }
}

This test:

Integrating with CI

Automated tests give fast feedback on functional correctness and performance regressions, but they cannot replace the exploratory power of a human tester or a persona‑driven agent that tries unconventional gestures.

Leveraging SUSA for Autonomous Exploration

SUSA (SUSATest) is an autonomous QA platform that explores an app without pre‑written scripts. It builds a behavior model from actual interactions and can simulate a variety of user personas. Applying SUSA to infinite scroll surfaces bugs that scripted tests never think to trigger.

How SUSA Approaches Infinite Scroll

  1. Screen Discovery – On launch, SUSA identifies the RecyclerView as a scrollable container and notes the presence of a loading indicator.
  2. Persona‑Driven Strategies
  1. State Tracking – SUSA records each unique screen state (identified by view hierarchy hash) and the transitions that lead to it. If a scroll results in a state already seen, it marks the edge as a *dead end* and avoids re‑exploring it unnecessarily.
  2. Learning Loop – After each run, the platform updates its internal graph: successful load‑more transitions are weighted higher, while paths that lead to crashes or ANRs are deprioritized for future runs but flagged for review.

Concrete Example: Finding a Duplicate‑Item Bug

Suppose the app’s PagingSource fails to deduplicate when the network returns overlapping IDs after a retry. A scripted test that scrolls exactly 20 items then asserts item 21 would miss this because the duplication only appears after a network glitch triggers a retry on page 3.

SUSA’s *Impatient* persona might:

When the merged list contains duplicate IDs, SUSA’s oracle (based on heuristics like “same contentDescription appearing consecutively”) raises an alert, captures a screenshot, and logs the exact sequence of events (including timestamps and network state).

Integrating SUSA into Your Workflow

Because SUSA explores the state space driven by actual interaction patterns, it often reaches combinations of gestures, device states, and network conditions that a manual tester would overlook due to time constraints, and that a scripted test would never consider because the test author did not imagine them.

Limitations to Keep in Mind

When used alongside traditional Espresso/UIAutomator checks, SUSA provides a complementary safety net that catches the “unknown unknowns” of infinite scroll.

Accessibility and Security Considerations

Infinite scroll touches two non‑functional domains that are often neglected in functional test plans: accessibility and data privacy. Below are concrete checks to add to your test suite.

Accessibility Checks

CheckMethodExpected Result
ContentDescription on each itemEspresso: onView(withId(R.id.item_text)).check(matches(withContentDescription(notNullValue())))Every visible item has a non‑empty description.
Live region for loading stateVerify that a view with android:accessibilityLiveRegion="polite" updates when spinner appears/disappears.TalkBack announces “Loading more items” when spinner visible, “Finished loading” when hidden.
Focus order after new itemsUse UIAutomator to perform a TalkBack swipe, then check getFocusedChild() on RecyclerView.Focus moves to the first newly loaded item, not stuck on footer.
Touch target sizeRun androidx.test.espresso.accessibility.AccessibilityChecks.enable(); run the test suite.No view fails the minimum 48 dp touch target rule.
Contrast ratioUse the Accessibility Scanner tool on a screenshot of the list after several pages.All text meets WCAG AA (≥4.5:1) for normal text, ≥3:1 for large text.
Screen reader announces duplicatesIf duplicate items appear, TalkBack will read the same description twice in succession.No duplicate announcements unless the data truly repeats (which should be flagged as a bug).

Automating these checks can be done with the built‑in Espresso accessibility rules:


@get:Rule
val accessibilityRule = AccessibilityChecks.EnableRule()

Security / Privacy Checks

Infinite scroll often prefetches additional pages to reduce latency. Prefetching can inadvertently pull sensitive data that should remain server‑side or be encrypted at rest.

CheckTechnique
Prefetch window sizeVerify that the PagingConfig’s prefetchDistance is no greater than 2–3 pages for lists containing PII.
Disk cache inspectionAfter a scrolling session, run adb shell run-as com.example.app ls /data/data/com.example.app/cache/ and inspect any files for unencrypted JSON or images containing personal data.
Network request inspectionUse a proxy (e.g., mitmproxy) to confirm that requests for pages beyond the visible range do not include authentication tokens in query parameters or headers that could be logged.
Memory leak of sensitive objectsLeakCanary should not report any retained User, Message, or Payment ViewModel after scrolling and then backgrounding the app.
Permission usageEnsure the app does not request READ_EXTERNAL_STORAGE or ACCESS_FINE_LOCATION solely for infinite scroll; such permissions are unnecessary for pure UI scrolling.

A simple test to assert that no PII appears in logs:


@Test
fun `no personal data in logcat`() {
    // Perform scrolling that triggers prefetch
    val scenario = ActivityScenario.launch(MainActivity::class)
    scenario.onActivity { 
        // scroll to trigger 5 pages
        val rv = findViewById<RecyclerView>(R.id.recycler_view)
        rv.scrollToPosition(200)
    }
    // Capture recent logcat
    val logs = executeShellCommand("logcat -d -t 500")
    assertFalse(logs.contains("email@"))
    assertFalse(logs.contains("ssn"))
    scenario.close()
}

By integrating accessibility and security verification into the same test suite that covers functional correctness, you reduce the chance that a performance optimization inadvertently creates a compliance risk.

Edge Cases That Appear Only in Production

Even with exhaustive test matrices, some defects surface only after the app reaches real users under specific conditions. Below are the most elusive infinite‑scroll bugs observed in post‑release monitoring, along with tactics to surface them earlier.

1. Battery‑Driven Throttling

On devices with aggressive battery‑saver policies, the system may lower CPU frequency or throttle background services while the app is in the foreground but not interacting. This can cause the paging loader to miss its deadline, resulting in a stale spinner.

Detection:

2. Input Method Editor (IME) Interference

When a user taps an edit‑text inside a list item (e.g., to comment), the IME may cause the RecyclerView to layout again, resetting scroll position if the adapter does not handle onSaveInstanceState correctly.

Detection:

3. Multi‑Window / Split‑Screen Mode

On foldables or tablets, users may run the app side‑by‑side with another. The system may deliver onConfigurationChanged with a new screenWidthDp while the list is mid‑scroll, causing the RecyclerView to request a new layout and lose its prefetch state.

Detection:

4. Network Handoff (Wi‑Fi to Cellular)

When the device switches from Wi‑Fi to cellular mid‑scroll, the underlying socket may be torn down. If the paging library does not recreate the request correctly, the list stalls.

Detection:

5. Database Cursor Exhaustion (Room + Paging 3)

If the backing DataSource is a LimitOffsetDataSource over a Room query that uses LIMIT/OFFSET, high offset values can cause slow queries and eventual cursor exhaustion on low‑end devices.

Detection:

6. Overdraw from Item Decorations

Custom ItemDecoration that draws dividers or backgrounds can cause excessive overdraw when many items are on screen, leading to dropped frames on low‑GPU devices.

Detection:

7. Accessibility Service Conflict

Some third‑party accessibility services (e.g., font changers, screen dimmers) intercept touch events and can delay or distort scroll gestures, causing the app to miss the threshold for loading more.

Detection:

8. Memory Pressure from Background Services

A music playback service or location tracker running in the same process can consume RAM, triggering early GC cycles during scrolling, which manifests as periodic UI freezes.

Detection:

Mitigation Strategies to Incorporate Early

By intentionally reproducing these production‑only conditions in a controlled environment, you shrink the gap between lab testing and real‑world reliability.

Checklist and Takeaways

Quick‑Reference Checklist

AreaItemHow to Verify
FunctionalFirst page loads correctlyEspresso assertion on first item text
Load‑more triggers at correct thresholdScroll to itemCount‑prefetchDistance, verify spinner appears
No duplicate itemsCollect item IDs in a Set while scrolling, assert size equals count
Error state shown on network failureThrottle network to 100% loss, verify retry UI appears
Position retained after rotationRotate device, compare computeVerticalScrollOffset() before/after
PerformanceFrame time <16 ms during fast flickUIAutomator + dumpsys gfxinfo jank count ≤2
Memory growth <5 MB per 10 pagesdumpsys meminfo delta after scrolling 50 pages
No main‑thread disk/networkStrictMode enabled, logcat clear of violations
AccessibilityContentDescription present on all itemsAccessibilityChecks.EnableRule()
Live region announces loading/spinnerTalkBack feedback captured via AccessibilityEvent
Focus moves to newly loaded itemsUIAutomator focus check after scroll
Security/PrivacyPrefetch window ≤2 pages for PII listsInspect PagingConfig in debug build
No unencrypted PII in cacherun-as cache directory scan after scroll
No auth tokens leaked in network logsProxy inspection of requests beyond visible range
Production‑LikeBattery saver enabled does not stall loaderToggle power mode, watch load‑more latency
IME open/close does not reset scrollFocus EditText, type, back, verify offset
Split‑screen mode maintains stateLaunch in multi‑window, scroll, rotate, verify
Network handoff recovers gracefullyToggle Wi‑Fi/mobile data while scrolling, observe retry logic
Low‑end device (≤1 GB RAM) stays responsiveRun on Android Go emulator, measure frame drops
Third‑party accessibility service does not block load‑moreInstall service, repeat scroll, compare load‑more count

Core Takeaways

  1. Treat infinite scroll as a state machine, not a static list. Each

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free