Сводка : Похоже, что это проблема с размещением 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>