BottomSheet内嵌RecyclerView的onRestoreInstanceState失效问题
I’ve run into this exact frustrating issue before, and that random success/failure you’re seeing is definitely tied to how BottomSheet interacts with the fragment lifecycle during configuration changes. Let’s break down what’s happening and how to fix it:
The Problem Breakdown
- Setup: Your
ContentFragment(with itscontentRecyclerView) is hosted inside a BottomSheet inHomeFragment’s layout. When this same fragment is used outside the BottomSheet, scroll position restores perfectly every time. - Symptom: After a config change (like screen rotation), the RecyclerView sometimes jumps back to position 0—even though:
- You’re successfully saving the layout state in
onSaveInstanceState - The saved state is non-null in
onViewStateRestoredand even after loading data into the adapter
- You’re successfully saving the layout state in
- Root Cause: BottomSheet’s lifecycle and layout timing don’t align with standard fragment behavior. When you try to restore the RecyclerView state right after loading data, the BottomSheet might still be collapsing/transitioning, or the RecyclerView hasn’t finished measuring its final visible size. This means the layout manager can’t properly apply the saved state.
Reliable Fixes to Try
1. Delay Restoration Until the BottomSheet Is Fully Expanded
BottomSheet’s state restoration happens after the fragment’s view state is restored. Wait until it’s fully expanded before applying the RecyclerView state:
// In ContentFragment's onViewCreated override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Get reference to the BottomSheetBehavior (adjust based on your layout structure) val parentView = requireView().parent as View val bottomSheetBehavior = BottomSheetBehavior.from(parentView) bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { if (newState == BottomSheetBehavior.STATE_EXPANDED && savedRecyclerLayoutState != null) { // Post to the RecyclerView's message queue to ensure layout is complete contentRecyclerView.post { contentRecyclerView.layoutManager?.onRestoreInstanceState(savedRecyclerLayoutState) savedRecyclerLayoutState = null // Prevent duplicate restoration } } } override fun onSlide(bottomSheet: View, slideOffset: Float) {} }) }
2. Wait for RecyclerView to Finish Layout
Even if the BottomSheet is expanded, the RecyclerView might not have finished measuring its size. Use a global layout listener to trigger restoration once the view is ready:
// In ContentFragment after your adapter is populated with data private fun restoreRecyclerStateIfReady() { if (savedRecyclerLayoutState == null) return contentRecyclerView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { // Remove the listener to avoid repeated calls contentRecyclerView.viewTreeObserver.removeOnGlobalLayoutListener(this) contentRecyclerView.layoutManager?.onRestoreInstanceState(savedRecyclerLayoutState) savedRecyclerLayoutState = null } }) }
3. Combine Data Load and BottomSheet State Checks
For maximum reliability, only restore when both the data is loaded AND the BottomSheet is expanded:
// In ContentFragment private var isDataLoaded = false private var isBottomSheetExpanded = false private var savedRecyclerLayoutState: Parcelable? = null override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) savedRecyclerLayoutState = contentRecyclerView.layoutManager?.onSaveInstanceState() outState.putParcelable("recycler_state", savedRecyclerLayoutState) } override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) savedRecyclerLayoutState = savedInstanceState?.getParcelable("recycler_state") } // Call this after your adapter is populated with data private fun onDataLoaded() { isDataLoaded = true tryRestoreState() } // Call this when BottomSheet reaches STATE_EXPANDED private fun onBottomSheetExpanded() { isBottomSheetExpanded = true tryRestoreState() } private fun tryRestoreState() { if (isDataLoaded && isBottomSheetExpanded && savedRecyclerLayoutState != null) { contentRecyclerView.post { contentRecyclerView.layoutManager?.onRestoreInstanceState(savedRecyclerLayoutState) savedRecyclerLayoutState = null } } }
Why This Works
The core issue is timing: when you originally tried to restore the state right after data loading, the BottomSheet wasn’t fully ready to display the RecyclerView at its full size. By waiting for both the BottomSheet to be expanded and the RecyclerView to finish its layout, you ensure the layout manager has the correct view dimensions to apply the saved scroll position consistently every time.
内容的提问来源于stack exchange,提问作者adamhurwitz.eth

