RecyclerView onRestoreInstanceState Внутри BottomSheet не работает - PullRequest
0 голосов
/ 19 января 2019

Сводка : Похоже, что это проблема с размещением RecyclerView внутри нижнего листа, так как родительский фрагмент HomeFragment содержит другой экземпляр дочернего фрагмента ContentFragment, который не вложен в нижний лист, а onRestoreInstanceState работает как положено.

Ожидаемое

При сохранении и возврате состояния RecyclerView LayoutManager в методах Fragment 'onSaveInstanceState и onViewStateRestored ожидаемый результат - отображение RecyclerView в той же позиции, что и ранее к изменению конфигурации.

Наблюдаемые

При изменении конфигурации экрана RecyclerView иногда отображается в позиции 0 , а не в позиции RecyclerView до изменения конфигурации. Он также успешно сохраняет состояние макета, как и ожидалось в некоторых случаях. Из-за случайности кажется, что может возникнуть проблема с жизненным циклом + нижний лист

  • contentRecyclerView.layoutManager!!.onSaveInstanceState() зарегистрирован как ненулевой на onSaveInstanceState.
  • savedRecyclerLayoutState зарегистрирован как ненулевой на onViewStateRestored.
  • savedRecyclerLayoutState зарегистрирован как ненулевой после загрузки adapter данных в случае SAVED.name в observeContentUpdated ниже.

Осуществление

Иерархия

ContentFragment размещается HomeFragment внутри BottomSheet фрагмента с именем bottomSheet в макете fragment_home. Макет ContentFragment fragment_content содержит contentRecyclerView.

Загрузка сохраненного состояния

onRestoreInstanceState вызывается после загрузки данных в Adapter в observeContentUpdated в случае SAVED.name. Состояние экземпляра устанавливается на null после onRestoreInstanceState, поскольку ячейки в RecyclerView являются недопустимыми и приводят к повторной загрузке данных. Это гарантирует, что восстановление происходит только один раз после изменения конфигурации.

HomeFragment.kt

initSavedBottomSheet создает нижний лист, содержащий сохраненный фрагмент 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)
}

...
}

ContentFragment.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)
}

...

}

fragment_home.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>

fragment_content.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>

1 Ответ

0 голосов
/ 19 января 2019

В качестве обходного пути для сохранения состояния 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
                    }
...