首页 > 解决方案 > BottomSheet 内的 RecyclerView onRestoreInstanceState 不起作用

问题描述

摘要这似乎是托管RecyclerView底部表内部的问题,因为父片段HomeFragment托管子片段的另一个实例,该实例ContentFragment未嵌套在底部表中并且onRestoreInstanceState按预期执行。

预期的

在a和方法中保存和返回 aRecyclerView LayoutManager的状态时,预期的结果是显示在与配置更改之前相同的位置。 FragmentonSaveInstanceStateonViewStateRestoredRecyclerView

观测到的

在屏幕配置更改时,RecyclerView有时会显示在位置0而不是RecyclerView配置更改之前的位置。在某些情况下,它还成功地保留了预期的布局状态。由于随机性,这似乎涉及生命周期 + 底页问题。

执行

等级制度

由布局中命名的片段内部ContentFragment托管。的布局包含.HomeFragmentBottomSheetbottomSheetfragment_homeContentFragmentfragment_contentcontentRecyclerView

加载保存的状态

onRestoreInstanceState在数据加载到案例中Adapter后调用。实例状态设置为after,因为其中的单元格是可关闭的,并且会导致数据再次加载。这可确保仅在配置更改后进行一次还原。observeContentUpdatedSAVED.namenullonRestoreInstanceStateRecyclerView

HomeFragment.kt

initSavedBottomSheet创建包含已保存 Fragment 的底部工作表ContentFragment

class HomeFragment : Fragment() {

...

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putParcelable(USER_KEY, user)
    outState.putBoolean(APP_BAR_EXPANDED_KEY, isAppBarExpanded)
    outState.putBoolean(SAVED_CONTENT_EXPANDED_KEY, isSavedContentExpanded)
}

override fun onViewStateRestored(savedInstanceState: Bundle?) {
    super.onViewStateRestored(savedInstanceState)
    if (savedInstanceState != null) {
        if (savedInstanceState.getBoolean(APP_BAR_EXPANDED_KEY)) appBar.setExpanded(true)
        else appBar.setExpanded(false)
        if (savedInstanceState.getBoolean(SAVED_CONTENT_EXPANDED_KEY)) {
            swipeToRefresh.isEnabled = false
            bottomSheetBehavior.state = STATE_EXPANDED
            setBottomSheetExpanded()
        }
        updateAds()
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    homeViewModel = ViewModelProviders.of(activity!!).get(HomeViewModel::class.java)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    binding = FragmentHomeBinding.inflate(inflater, container, false)
    binding.setLifecycleOwner(this)
    binding.viewmodel = homeViewModel
    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    user = homeViewModel.getCurrentUser()
    ...
    observeSignIn(savedInstanceState)
    initSavedBottomSheet(savedInstanceState)
    ...
    initSwipeToRefresh()
    ...
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    if (savedInstanceState == null
            && childFragmentManager.findFragmentByTag(PRICEGRAPH_FRAGMENT_TAG) == null
            && childFragmentManager.findFragmentByTag(CONTENT_FEED_FRAGMENT_TAG) == null) {
        childFragmentManager.beginTransaction()
                .replace(priceContainer.id, PriceFragment.newInstance(), PRICEGRAPH_FRAGMENT_TAG)
                .commit()
        childFragmentManager.beginTransaction().replace(contentContainer.id,
                ContentFragment.newInstance(Bundle().apply {
                    putString(FEED_TYPE_KEY, MAIN.name)
                }), CONTENT_FEED_FRAGMENT_TAG)
                .commit()
    }
}

...

private fun initSavedBottomSheet(savedInstanceState: Bundle?) {
    bottomSheetBehavior = from(bottomSheet)
    bottomSheetBehavior.isHideable = false
    bottomSheetBehavior.peekHeight = SAVED_BOTTOM_SHEET_PEEK_HEIGHT
    bottomSheet.layoutParams.height = getDisplayHeight(context!!)
    if (savedInstanceState == null && homeViewModel.user.value == null)
        childFragmentManager.beginTransaction().replace(
                R.id.savedContentContainer,
                SignInDialogFragment.newInstance(Bundle().apply {
                    putInt(SIGNIN_TYPE_KEY, FULLSCREEN.code)
                }))
                .commit()
    bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
        override fun onStateChanged(bottomSheet: View, newState: Int) {
            if (newState == STATE_EXPANDED) {
                homeViewModel.bottomSheetState.value = STATE_EXPANDED
                setBottomSheetExpanded()
            }
            if (newState == STATE_COLLAPSED) {
                isSavedContentExpanded = false
                appBar.visibility = VISIBLE
                bottom_handle.visibility = VISIBLE
                bottom_handle_elevation.visibility = VISIBLE
            }
        }

        override fun onSlide(bottomSheet: View, slideOffset: Float) {}
    })
    ...
}

private fun setBottomSheetExpanded() {
    isSavedContentExpanded = true
    appBar.visibility = GONE
    bottom_handle.visibility = GONE
    bottom_handle_elevation.visibility = GONE
}

private fun initSavedContentFragment() {
    childFragmentManager.beginTransaction().replace(
            savedContentContainer.id,
            ContentFragment.newInstance(Bundle().apply { putString(FEED_TYPE_KEY, SAVED.name) }),
            SAVED_CONTENT_TAG).commit()
}

...

private fun observeSignIn(savedInstanceState: Bundle?) {
    homeViewModel.user.observe(this, Observer { user: FirebaseUser? ->
        this.user = user
        ...
        if (user != null) { // Signed in.
            ...
            if (savedInstanceState == null || savedInstanceState.getParcelable<FirebaseUser>(USER_KEY) == null) {
                initMainContent()
                initSavedContentFragment()
            }
        } else if (savedInstanceState == null)  /*Signed out.*/ initMainContent()
    })
}

private fun initMainContent() {
    (childFragmentManager.findFragmentById(R.id.contentContainer) as ContentFragment)
            .initMainContent(false)
}

fun initSwipeToRefresh() {
    homeViewModel.isSwipeToRefreshEnabled.observe(viewLifecycleOwner, Observer { isEnabled: Boolean ->
        ...
        (childFragmentManager.findFragmentById(R.id.priceContainer) as PriceFragment)
                .getPrices(false, false)
        if (homeViewModel.accountType.value == FREE) updateAds()
    }
}

private fun updateAds() {
    (childFragmentManager.findFragmentById(R.id.contentContainer) as ContentFragment)
            .updateAds(true)
    if (childFragmentManager.findFragmentById(R.id.savedContentContainer) as ContentFragment != null)
        (childFragmentManager.findFragmentById(R.id.savedContentContainer) as ContentFragment)
                .updateAds(true)
}

...
}

内容片段.kt

contentRecyclerView填充在方法initializeAdapters中。

class ContentFragment : Fragment() {

...

private var savedRecyclerLayoutState: Parcelable? = null

companion object {
    @JvmStatic
    fun newInstance(contentBundle: Bundle) = ContentFragment().apply {
        arguments = contentBundle
    }
}

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
        if (contentRecyclerView != null)
                outState.putParcelable(CONTENT_RECYCLER_VIEW_STATE,
                        contentRecyclerView.layoutManager!!.onSaveInstanceState())
}

override fun onViewStateRestored(savedInstanceState: Bundle?) {
    super.onViewStateRestored(savedInstanceState)
    if (savedInstanceState != null) {
        savedRecyclerLayoutState = savedInstanceState.getParcelable(CONTENT_RECYCLER_VIEW_STATE)
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    feedType = ContentFragmentArgs.fromBundle(arguments!!).feedType
    analytics = getInstance(FirebaseApp.getInstance()!!.applicationContext)
    contentViewModel = ViewModelProviders.of(this).get(ContentViewModel::class.java)
    homeViewModel = ViewModelProviders.of(activity!!).get(HomeViewModel::class.java)
    contentViewModel.feedType = feedType
    if (savedInstanceState == null) homeViewModel.isRealtime.observe(this, Observer { isRealtime: Boolean ->
        when (feedType) {
            SAVED.name, DISMISSED.name -> initCategorizedContent(feedType, homeViewModel.user.value!!.uid)
        }
    })
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    analytics.setCurrentScreen(activity!!, feedType, null)
    binding = FragmentContentBinding.inflate(inflater, container, false)
    binding.setLifecycleOwner(this)
    binding.viewmodel = contentViewModel
    binding.actionbar.viewmodel = contentViewModel
    binding.emptyContent.viewmodel = contentViewModel
    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    setToolbar()
    initializeAdapters()
}

override fun onDestroy() {
    moPubAdapter.destroy()
    compositeDisposable.dispose()
    super.onDestroy()
}

fun setToolbar() {
    when (feedType) {
        SAVED.name -> {
            binding.actionbar.toolbar.savedContentTitle.visibility = View.VISIBLE
        }
        DISMISSED.name -> {
            binding.actionbar.toolbar.title = getString(R.string.dismissed)
            (activity as AppCompatActivity).setSupportActionBar(binding.actionbar.toolbar)
            (activity as AppCompatActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        }
    }
}

fun initMainContent(isRealtime: Boolean) {
    contentViewModel.initializeMainContent(isRealtime).observe(viewLifecycleOwner, Observer { status ->
        if (status == SUCCESS && homeViewModel.accountType.value == FREE) updateAds(true)
    })
}

fun initCategorizedContent(feedType: String, userId: String) {
    contentViewModel.initCategorizedContent(feedType, userId)
}

fun updateAds(toLoad: Boolean) {
    var toLoad = toLoad
    moPubAdapter.loadAds(AD_UNIT_ID)
    moPubAdapter.setAdLoadedListener(object : MoPubNativeAdLoadedListener {
        override fun onAdRemoved(position: Int) {}
        override fun onAdLoaded(position: Int) {
            if (toLoad) {
                moPubAdapter.notifyDataSetChanged()
                toLoad = false
            }
        }
    })
}

private fun initializeAdapters() {
    contentRecyclerView.layoutManager = LinearLayoutManager(context)
    populateAdapterType()
    observeContentUpdated()
    ...
}

private fun observeContentUpdated() {
    when (feedType) {
        MAIN.name -> {
            contentViewModel.getMainContentList().observe(viewLifecycleOwner, Observer { homeContentList ->
                adapter.submitList(homeContentList)
                if (homeContentList.isNotEmpty()) {
                    emptyContent.visibility = GONE
                    if (savedRecyclerLayoutState != null) {
                        contentRecyclerView.layoutManager?.onRestoreInstanceState(savedRecyclerLayoutState)
                        savedRecyclerLayoutState = null
                    }
                }
            })
        }
        SAVED.name, DISMISSED.name -> {
            contentViewModel.getCategorizedContentList(
                    if (feedType == SAVED.name) SAVED
                    else if (feedType == DISMISSED.name) DISMISSED
                    else NONE
            ).observe(viewLifecycleOwner, Observer { contentList ->
                adapter.submitList(contentList)
                if (!(contentList.size == 0 && (adapter.itemCount == 1 || adapter.itemCount == 0))) {
                    emptyContent.visibility = GONE
                    if (feedType == SAVED.name) {
                        if (savedRecyclerLayoutState != null) {
                            contentRecyclerView.layoutManager?.onRestoreInstanceState(savedRecyclerLayoutState)
                            savedRecyclerLayoutState = null
                        }
                    }
                    if (feedType == DISMISSED.name)
                        contentRecyclerView.layoutManager?.onRestoreInstanceState(savedRecyclerLayoutState)
                } 
            })
        }
    }
}

private fun populateAdapterType() {
    adapter = ContentAdapter(contentViewModel)
    // FREE
    if (homeViewModel.accountType.value!! == FREE) {
        moPubAdapter = MoPubRecyclerAdapter(activity!!, adapter,
                MoPubNativeAdPositioning.MoPubServerPositioning())
    ...            
        contentRecyclerView.adapter = moPubAdapter
        // Realtime, only need to set ads once.
        if (feedType == SAVED.name || feedType == DISMISSED.name) moPubAdapter.loadAds(AD_UNIT_ID)
    } /* PAID */ else contentRecyclerView.adapter = adapter
    ItemTouchHelper(homeViewModel).build(context!!, FREE, feedType, adapter, moPubAdapter, fragmentManager!!)
            .attachToRecyclerView(contentRecyclerView)
}

...

}

片段主页.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
    <variable
        name="viewmodel"
        type="app.coinverse.home.HomeViewModel" />
</data>

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipeToRefresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white">

            <com.google.android.material.appbar.CollapsingToolbarLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:fitsSystemWindows="true"
                app:layout_scrollFlags="scroll|snap">

                <androidx.appcompat.widget.Toolbar
                    android:layout_width="match_parent"
                    android:layout_height="?attr/actionBarSize">

                    <androidx.constraintlayout.widget.ConstraintLayout
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:paddingTop="@dimen/padding_small"
                        android:paddingRight="@dimen/padding_small">

                        <ImageView
                            android:id="@+id/profileButton"
                            android:layout_width="@dimen/toolbar_button_dimen"
                            android:layout_height="@dimen/toolbar_button_dimen"
                            android:layout_gravity="start"
                            android:contentDescription="@string/profile_content_description"
                            android:src="@drawable/ic_astronaut_color_accent_24dp"
                            app:layout_constraintLeft_toLeftOf="parent" />

                    </androidx.constraintlayout.widget.ConstraintLayout>

                </androidx.appcompat.widget.Toolbar>

                <FrameLayout
                    android:id="@+id/priceContainer"
                    android:name="app.carpecoin.PriceDataFragment"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/price_graph_height"
                    app:layout_collapseMode="parallax"
                    app:layout_constraintLeft_toLeftOf="parent"
                    app:layout_constraintRight_toRightOf="parent"
                    app:layout_constraintTop_toBottomOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />

            </com.google.android.material.appbar.CollapsingToolbarLayout>

        </com.google.android.material.appbar.AppBarLayout>

        <FrameLayout
            android:id="@+id/contentContainer"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/bottomSheet"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingBottom="@dimen/margin_large"
            app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

            <ImageView
                android:id="@+id/bottom_handle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/ic_bottom_sheet_handle"
                android:contentDescription="@string/saved_bottomsheet_handle_content_description"
                android:elevation="@dimen/bottom_sheet_elevation_height"
                android:src="@drawable/ic_save_planet_dark_48dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <ImageView
                android:id="@+id/bottom_handle_elevation"
                android:layout_width="0dp"
                android:layout_height="@dimen/bottom_sheet_elevation_height"
                android:background="@color/bottom_sheet_handle_elevation"
                android:contentDescription="@string/saved_bottomsheet_handle_content_description"
                app:layout_constraintBottom_toBottomOf="@id/bottom_handle"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent" />

            <FrameLayout
                android:id="@+id/savedContentContainer"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:background="@android:color/white"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintTop_toBottomOf="@id/bottom_handle_elevation" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

片段内容.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data>

    <variable
        name="viewmodel"
        type="app.coinverse.content.ContentViewModel" />

</data>


<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:id="@+id/contentFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <include
            android:id="@+id/actionbar"
            layout="@layout/toolbar"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/contentRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/actionbar" />

        <include
            android:id="@+id/emptyContent"
            layout="@layout/empty_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/actionbar" />

    </RelativeLayout
</androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

标签: androidtwitterandroid-recyclerviewmopub

解决方案


作为保存RecyclerView状态的解决方法,可以将位置保存在实例状态中。

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    if (contentRecyclerView != null)
        when (feedType) {
            MAIN.name, DISMISSED.name ->
                outState.putParcelable(CONTENT_RECYCLER_VIEW_STATE,
                        contentRecyclerView.layoutManager!!.onSaveInstanceState())
            SAVED.name ->
                outState.putInt(CONTENT_RECYCLER_VIEW_POSITION,
                        (contentRecyclerView.layoutManager as LinearLayoutManager)
                                .findLastVisibleItemPosition())
        }
}

override fun onViewStateRestored(savedInstanceState: Bundle?) {
    super.onViewStateRestored(savedInstanceState)
    if (savedInstanceState != null)
        when (feedType) {
            MAIN.name, DISMISSED.name -> savedRecyclerLayoutState = savedInstanceState.getParcelable(CONTENT_RECYCLER_VIEW_STATE)
            SAVED.name -> savedRecyclerPosition = savedInstanceState.getInt(CONTENT_RECYCLER_VIEW_POSITION)
        }
}

为了确保保存的索引没有超出范围,需要进行检查。此外,由于RecyclerView项目被解除,因此清除保存的索引位置非常重要,这样在解除项目后RecyclerView不会更新,因为此代码片段包含在LiveData观察者中。

if (feedType == SAVED.name && savedRecyclerPosition != 0) {
                        val position: Int =
                                if (savedRecyclerPosition >= adapter.itemCount) adapter.itemCount - 1
                                else savedRecyclerPosition
                        contentRecyclerView.layoutManager?.scrollToPosition(position)
                        savedRecyclerPosition = 0
                    }

推荐阅读