ListView
scroll restoration
Android development
UI/UX
mobile app tips

Maintain/Save/Restore scroll position when returning to a ListView

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Users notice immediately when a long Android ListView jumps back to the top after they open a detail screen and return. ListView does not automatically preserve the exact visual position in every navigation flow, so the reliable solution is to save the first visible row plus its top offset and restore both when the list comes back.

What Needs To Be Saved

To restore the visible position accurately, you usually need two values:

  • the adapter position of the first visible row
  • the pixel offset of that row from the top of the ListView

If you save only the row index, the list may come back to the correct item but still look slightly off because the top offset is lost.

Save And Restore In A Fragment

A practical Kotlin pattern looks like this:

kotlin
1class MessagesFragment : Fragment(R.layout.fragment_messages) {
2    private lateinit var listView: ListView
3    private lateinit var adapter: ArrayAdapter<String>
4
5    private var savedIndex = 0
6    private var savedTop = 0
7
8    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
9        super.onViewCreated(view, savedInstanceState)
10
11        listView = view.findViewById(R.id.messagesList)
12        adapter = ArrayAdapter(
13            requireContext(),
14            android.R.layout.simple_list_item_1,
15            loadMessages()
16        )
17        listView.adapter = adapter
18
19        savedIndex = savedInstanceState?.getInt("index") ?: savedIndex
20        savedTop = savedInstanceState?.getInt("top") ?: savedTop
21
22        listView.post {
23            listView.setSelectionFromTop(savedIndex, savedTop)
24        }
25    }
26
27    override fun onPause() {
28        super.onPause()
29        saveScrollPosition()
30    }
31
32    override fun onSaveInstanceState(outState: Bundle) {
33        super.onSaveInstanceState(outState)
34        saveScrollPosition()
35        outState.putInt("index", savedIndex)
36        outState.putInt("top", savedTop)
37    }
38
39    private fun saveScrollPosition() {
40        val firstChild = listView.getChildAt(0) ?: return
41        savedIndex = listView.firstVisiblePosition
42        savedTop = firstChild.top
43    }
44
45    private fun loadMessages(): MutableList<String> {
46        return MutableList(200) { index -> "Message #$index" }
47    }
48}

The critical part is setSelectionFromTop, not just setSelection, because it restores both row position and pixel offset.

Why post Matters

Restoration usually needs to happen after the adapter is attached and the view has completed layout. That is why listView.post is so useful:

kotlin
listView.post {
    listView.setSelectionFromTop(savedIndex, savedTop)
}

If you restore too early, Android may ignore the request or overwrite it during the next layout pass.

Returning From A Detail Screen

When the user opens a detail screen and navigates back, the fragment instance may survive or its view may be recreated. The safer design is to store the scroll state somewhere that survives view recreation, such as a ViewModel.

kotlin
1data class ScrollState(
2    val firstVisiblePosition: Int = 0,
3    val topOffset: Int = 0
4)
5
6class MessagesViewModel : ViewModel() {
7    var scrollState: ScrollState = ScrollState()
8}

Then save into the ViewModel before leaving and restore from it when the list screen returns.

When The Data Changes

Scroll restoration works best when the dataset is stable. If rows were inserted, removed, or re-sorted while the user was away, an old position may now refer to different content.

If the list uses stable IDs, saving the item identity can be better than saving only the adapter position. Then you can find that item after reload and restore near it instead of trusting a stale index.

Common Pitfalls

The biggest mistake is saving only firstVisiblePosition and ignoring the top offset. That usually restores the right item but not the exact visible location.

Another common issue is calling setSelectionFromTop before the adapter is attached or before layout is complete. The restore call then appears to do nothing.

Developers also often forget that asynchronous data reloads can reorder the list before restoration happens. If the dataset changed, the saved index may no longer point to the same logical item.

Finally, getChildAt(0) can return null when there are no visible children. Guard that case before reading the top offset.

Summary

  • Save both the first visible row index and its top pixel offset.
  • Restore with setSelectionFromTop, not just setSelection.
  • Perform restoration after the adapter is attached and layout has completed.
  • Use savedInstanceState or a ViewModel so the state survives navigation and recreation.
  • If the dataset can change, prefer restoring by stable item identity when possible.

Course illustration
Course illustration

All Rights Reserved.