Skip to content

bugfix: restore scroll position on Contributions page after activity recreation#6725

Open
amahuli03 wants to merge 4 commits intocommons-app:mainfrom
amahuli03:6610/scroll-position-contribution-list
Open

bugfix: restore scroll position on Contributions page after activity recreation#6725
amahuli03 wants to merge 4 commits intocommons-app:mainfrom
amahuli03:6610/scroll-position-contribution-list

Conversation

@amahuli03
Copy link

Description (required)

Fixes #6610
According to the issue description, when the user changes the app theme in settings and returns to the contributions page, their scroll position was lost and the list reset to the top. More broadly, whenever the app's activity was recreated (due to a theme change, language change, screen rotation, process death, etc.) the contributions page lost the user's scroll position and reset it to the top.

This was caused by two bugs:

  • MainActivity: onRestoreInstanceState always called loadFragment(newInstance(), ...) discarding the fragment the FragmentManager had already restored with its saved state.
  • ContributionsListFragment: The scroll state was restored immediately in onViewStateRestored, but the paging list observer subsequently fired fresh data into an empty adapter, overriding it. Additionally, scrollToPosition(0) in onItemRangeInserted fired on every data load, not just genuine new uploads.

What changes did you make and why?

MainActivity.kt: reuse the FragmentManager-restored fragment

BaseActivity.onResume() calls recreate() when the theme changes. During recreation, the Android FragmentManager automatically saves and restores the active fragment, including its instance state (scroll position). However, onRestoreInstanceState was calling restoreActiveFragment(), which always called loadFragment(newInstance(), false) — replacing the restored fragment with a brand new instance that had no saved state.

The fix: use supportFragmentManager.findFragmentById() to get the fragment the system already restored. If it matches the expected type, update the activity's reference fields directly without calling loadFragment. The else branch retains the original loadFragment call as a fallback for cases where no fragment exists in the container.

ContributionsListFragment.kt: defer scroll restoration until after data loads

Even with the fragment preserved, scroll position was still lost because of a timing issue. onViewStateRestored is called before the paging list observer fires, so restoring the layout manager state immediately had no effect. The observer would then call adapter.submitList() with fresh data into an empty adapter, resetting the scroll to the top.

The fix stores the saved state in a pendingScrollState field inonViewStateRestored instead of applying it immediately, then applies it inside the submitList commit callback, which fires after the diff is applied and items are laid out.
Additionally, scrollToPosition(0) in the AdapterDataObserver was firing on every data load (including the initial load after recreation), not just when a new upload appeared. This was overriding the restored scroll position on subsequent page fetches. The fix adds a countBeforeInsertion check so scrollToPosition(0) only fires when items are inserted into an already-populated list (which is the case for a genuine new upload) and not during initial or reload data loads.

Because this fix affects MainActivity.restoreActiveFragment, which is called after theme/language change, screen rotation, and system process death, the scroll position will be restored after all of these. These are all places we should restore the user's contributions page scroll position.

Tests performed (required)

Tested betaDebug on Android 16.0 emulator with API level 36.1.

Video of feature:

I scrolled down on my contributions page, changed the theme in settings, and came back to the contributions page to the same scroll position as before. Before this fix, the page did not restore the scroll position after a theme change.
Screen_recording_20260311_180528.webm

Happy to answer any clarifying question about this or make any changes if needed!

amahuli03 and others added 4 commits March 11, 2026 17:03
…ition

when the theme changes, BaseActivity called recreate(). Previously, onRestoreInstanceState would discard the FragmentManager-restored fragment by calling loadFragment(newInstance()), creating a fresh instance with no saved state. Now we reuse the existing restored fragment and only update the activity's reference fields.
After recreation, the paging list observer fires fresh data into an empty adapter, overriding the restored layout manager state. Now defers scroll restoration until after submitList completes, and guards scrollToPosition(0) to only fire when items are added to an already-populated list (new uploads) rather than on initial data loads.
@github-actions
Copy link

✅ Generated APK variants!

@neeldoshii neeldoshii assigned neeldoshii and unassigned neeldoshii Mar 12, 2026
@neeldoshii neeldoshii self-requested a review March 12, 2026 18:28
if (fragmentName == ActiveFragment.CONTRIBUTIONS.name) {
title = getString(R.string.contributions_fragment)
loadFragment(newInstance(), false)
if (existing is ContributionsFragment) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about simplifying this block and making it DRYer? Do we really need the fragmentName anymore?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes loss of scroll position on the Contributions screen when MainActivity is recreated (theme/language change, rotation, etc.) by reusing FragmentManager-restored fragments and deferring RecyclerView scroll restoration until after list updates.

Changes:

  • MainActivity: reuse the fragment already restored into R.id.fragmentContainer instead of always replacing it with a new instance during onRestoreInstanceState.
  • ContributionsListFragment: store pending RecyclerView state and attempt to restore it after submitList completes; tighten “scroll to top” behavior to only trigger for genuine top insertions into a non-empty list.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt Avoids replacing FragmentManager-restored fragments on state restore, preserving fragment state (including scroll).
app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt Defers RV scroll state restoration and prevents unconditional scroll-to-top on initial/reload inserts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 477 to 482
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
if (null != savedInstanceState) {
val savedRecyclerLayoutState =
pendingScrollState =
BundleCompat.getParcelable(savedInstanceState, RV_STATE, Parcelable::class.java)
rvContributionsList!!.layoutManager!!.onRestoreInstanceState(savedRecyclerLayoutState)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Scroll position lost in Contributions list after theme change

4 participants