From b0c179dee2f07b175c8a2a5ff79acf0d2a66b04c Mon Sep 17 00:00:00 2001 From: Jakub Gielzak Date: Tue, 16 Jul 2019 16:51:45 +0100 Subject: [PATCH] Hack for transient fragment state --- .../DemoFragmentAdapter.kt | 1 - .../FragmentStateAdapter.java | 740 ++++++++++++++++++ .../FragmentViewHolder.java | 49 ++ 3 files changed, 789 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/FragmentStateAdapter.java create mode 100644 app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/FragmentViewHolder.java diff --git a/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/DemoFragmentAdapter.kt b/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/DemoFragmentAdapter.kt index d97f56c..29f3f63 100644 --- a/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/DemoFragmentAdapter.kt +++ b/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/DemoFragmentAdapter.kt @@ -2,7 +2,6 @@ package com.example.fragmentstateadapteronfailedtorecyclerviewfailure import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter class DemoFragmentAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { override fun getItemCount(): Int { diff --git a/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/FragmentStateAdapter.java b/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/FragmentStateAdapter.java new file mode 100644 index 0000000..4864e40 --- /dev/null +++ b/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/FragmentStateAdapter.java @@ -0,0 +1,740 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fragmentstateadapteronfailedtorecyclerviewfailure; + +import static androidx.core.util.Preconditions.checkArgument; +import static androidx.lifecycle.Lifecycle.State.RESUMED; +import static androidx.lifecycle.Lifecycle.State.STARTED; +import static androidx.recyclerview.widget.RecyclerView.NO_ID; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.FrameLayout; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.ArraySet; +import androidx.collection.LongSparseArray; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.adapter.StatefulAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import java.util.Set; + +/** + * Similar in behavior to {@link FragmentStatePagerAdapter} + *

+ * Lifecycle within {@link RecyclerView}: + *

+ */ +public abstract class FragmentStateAdapter extends + RecyclerView.Adapter implements StatefulAdapter { + // State saving config + private static final String KEY_PREFIX_FRAGMENT = "f#"; + private static final String KEY_PREFIX_STATE = "s#"; + + // Fragment GC config + private static final long GRACE_WINDOW_TIME_MS = 10_000; // 10 seconds + + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + final Lifecycle mLifecycle; + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + final FragmentManager mFragmentManager; + + // Fragment bookkeeping + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + final LongSparseArray mFragments = new LongSparseArray<>(); + private final LongSparseArray mSavedStates = new LongSparseArray<>(); + private final LongSparseArray mItemIdToViewHolder = new LongSparseArray<>(); + + private FragmentMaxLifecycleEnforcer mFragmentMaxLifecycleEnforcer; + + // Fragment GC + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + boolean mIsInGracePeriod = false; + private boolean mHasStaleFragments = false; + + /** + * @param fragmentActivity if the {@link ViewPager2} lives directly in a + * {@link FragmentActivity} subclass. + * + * @see FragmentStateAdapter#FragmentStateAdapter(Fragment) + * @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle) + */ + public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) { + this(fragmentActivity.getSupportFragmentManager(), fragmentActivity.getLifecycle()); + } + + /** + * @param fragment if the {@link ViewPager2} lives directly in a {@link Fragment} subclass. + * + * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity) + * @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle) + */ + public FragmentStateAdapter(@NonNull Fragment fragment) { + this(fragment.getChildFragmentManager(), fragment.getLifecycle()); + } + + /** + * @param fragmentManager of {@link ViewPager2}'s host + * @param lifecycle of {@link ViewPager2}'s host + * + * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity) + * @see FragmentStateAdapter#FragmentStateAdapter(Fragment) + */ + public FragmentStateAdapter(@NonNull FragmentManager fragmentManager, + @NonNull Lifecycle lifecycle) { + mFragmentManager = fragmentManager; + mLifecycle = lifecycle; + super.setHasStableIds(true); + } + + @CallSuper + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + checkArgument(mFragmentMaxLifecycleEnforcer == null); + mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer(); + mFragmentMaxLifecycleEnforcer.register(recyclerView); + } + + @CallSuper + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + mFragmentMaxLifecycleEnforcer.unregister(recyclerView); + mFragmentMaxLifecycleEnforcer = null; + } + + /** + * Provide a new Fragment associated with the specified position. + *

+ * The adapter will be responsible for the Fragment lifecycle: + *

    + *
  • The Fragment will be used to display an item.
  • + *
  • The Fragment will be destroyed when it gets too far from the viewport, and its state + * will be saved. When the item is close to the viewport again, a new Fragment will be + * requested, and a previously saved state will be used to initialize it. + *
+ * @see ViewPager2#setOffscreenPageLimit + */ + public abstract @NonNull Fragment createFragment(int position); + + @NonNull + @Override + public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return FragmentViewHolder.create(parent); + } + + @Override + public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) { + final long itemId = holder.getItemId(); + final int viewHolderId = holder.getContainer().getId(); + final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH + if (boundItemId != null && boundItemId != itemId) { + removeFragment(boundItemId); + mItemIdToViewHolder.remove(boundItemId); + } + + mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry + ensureFragment(position); + + /** Special case when {@link RecyclerView} decides to keep the {@link container} + * attached to the window, but not to the view hierarchy (i.e. parent is null) */ + final FrameLayout container = holder.getContainer(); + if (ViewCompat.isAttachedToWindow(container)) { + if (container.getParent() != null) { + throw new IllegalStateException("Design assumption violated."); + } + container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (container.getParent() != null) { + container.removeOnLayoutChangeListener(this); + placeFragmentInViewHolder(holder); + } + } + }); + } + + gcFragments(); + } + + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + void gcFragments() { + if (!mHasStaleFragments || shouldDelayFragmentTransactions()) { + return; + } + + // Remove Fragments for items that are no longer part of the data-set + Set toRemove = new ArraySet<>(); + for (int ix = 0; ix < mFragments.size(); ix++) { + long itemId = mFragments.keyAt(ix); + if (!containsItem(itemId)) { + toRemove.add(itemId); + mItemIdToViewHolder.remove(itemId); // in case they're still bound + } + } + + // Remove Fragments that are not bound anywhere -- pending a grace period + if (!mIsInGracePeriod) { + mHasStaleFragments = false; // we've executed all GC checks + + for (int ix = 0; ix < mFragments.size(); ix++) { + long itemId = mFragments.keyAt(ix); + if (!mItemIdToViewHolder.containsKey(itemId)) { + toRemove.add(itemId); + } + } + } + + for (Long itemId : toRemove) { + removeFragment(itemId); + } + } + + private Long itemForViewHolder(int viewHolderId) { + Long boundItemId = null; + for (int ix = 0; ix < mItemIdToViewHolder.size(); ix++) { + if (mItemIdToViewHolder.valueAt(ix) == viewHolderId) { + if (boundItemId != null) { + throw new IllegalStateException("Design assumption violated: " + + "a ViewHolder can only be bound to one item at a time."); + } + boundItemId = mItemIdToViewHolder.keyAt(ix); + } + } + return boundItemId; + } + + private void ensureFragment(int position) { + long itemId = getItemId(position); + if (!mFragments.containsKey(itemId)) { + // TODO(133419201): check if a Fragment provided here is a new Fragment + Fragment newFragment = createFragment(position); + newFragment.setInitialSavedState(mSavedStates.get(itemId)); + mFragments.put(itemId, newFragment); + } + } + + @Override + public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) { + placeFragmentInViewHolder(holder); + gcFragments(); + } + + /** + * @param holder that has been bound to a Fragment in the {@link #onBindViewHolder} stage. + */ + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) { + ensureFragment(holder.getAdapterPosition()); + Fragment fragment = mFragments.get(holder.getItemId()); + if (fragment == null) { + throw new IllegalStateException("Design assumption violated."); + } + FrameLayout container = holder.getContainer(); + View view = fragment.getView(); + + /* + possible states: + - fragment: { added, notAdded } + - view: { created, notCreated } + - view: { attached, notAttached } + + combinations: + - { f:added, v:created, v:attached } -> check if attached to the right container + - { f:added, v:created, v:notAttached} -> attach view to container + - { f:added, v:notCreated, v:attached } -> impossible + - { f:added, v:notCreated, v:notAttached} -> schedule callback for when created + - { f:notAdded, v:created, v:attached } -> illegal state + - { f:notAdded, v:created, v:notAttached } -> illegal state + - { f:notAdded, v:notCreated, v:attached } -> impossible + - { f:notAdded, v:notCreated, v:notAttached } -> add, create, attach + */ + + // { f:notAdded, v:created, v:attached } -> illegal state + // { f:notAdded, v:created, v:notAttached } -> illegal state + if (!fragment.isAdded() && view != null) { + throw new IllegalStateException("Design assumption violated."); + } + + // { f:added, v:notCreated, v:notAttached} -> schedule callback for when created + if (fragment.isAdded() && view == null) { + scheduleViewAttach(fragment, container); + return; + } + + // { f:added, v:created, v:attached } -> check if attached to the right container + if (fragment.isAdded() && view.getParent() != null) { + if (view.getParent() != container) { + addViewToContainer(view, container); + } + return; + } + + // { f:added, v:created, v:notAttached} -> attach view to container + if (fragment.isAdded()) { + addViewToContainer(view, container); + return; + } + + // { f:notAdded, v:notCreated, v:notAttached } -> add, create, attach + if (!shouldDelayFragmentTransactions()) { + scheduleViewAttach(fragment, container); + mFragmentManager.beginTransaction() + .add(fragment, "f" + holder.getItemId()) + .setMaxLifecycle(fragment, STARTED) + .commitNow(); + mFragmentMaxLifecycleEnforcer.updateFragmentMaxLifecycle(false); + } else { + if (mFragmentManager.isDestroyed()) { + return; // nothing we can do + } + mLifecycle.addObserver(new LifecycleEventObserver() { + @Override + public void onStateChanged(@NonNull LifecycleOwner source, + @NonNull Lifecycle.Event event) { + if (shouldDelayFragmentTransactions()) { + return; + } + source.getLifecycle().removeObserver(this); + if (ViewCompat.isAttachedToWindow(holder.getContainer())) { + placeFragmentInViewHolder(holder); + } + } + }); + } + } + + private void scheduleViewAttach(final Fragment fragment, @NonNull final FrameLayout container) { + // After a config change, Fragments that were in FragmentManager will be recreated. Since + // ViewHolder container ids are dynamically generated, we opted to manually handle + // attaching Fragment views to containers. For consistency, we use the same mechanism for + // all Fragment views. + mFragmentManager.registerFragmentLifecycleCallbacks( + new FragmentManager.FragmentLifecycleCallbacks() { + @Override + public void onFragmentViewCreated(@NonNull FragmentManager fm, + @NonNull Fragment f, @NonNull View v, + @Nullable Bundle savedInstanceState) { + if (f == fragment) { + fm.unregisterFragmentLifecycleCallbacks(this); + addViewToContainer(v, container); + } + } + }, false); + } + + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + void addViewToContainer(@NonNull View v, @NonNull FrameLayout container) { + if (container.getChildCount() > 1) { + throw new IllegalStateException("Design assumption violated."); + } + + if (v.getParent() == container) { + return; + } + + if (container.getChildCount() > 0) { + container.removeAllViews(); + } + + if (v.getParent() != null) { + ((ViewGroup) v.getParent()).removeView(v); + } + + container.addView(v); + } + + @Override + public final void onViewRecycled(@NonNull FragmentViewHolder holder) { + final int viewHolderId = holder.getContainer().getId(); + final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH + if (boundItemId != null) { + removeFragment(boundItemId); + mItemIdToViewHolder.remove(boundItemId); + } + } + + @Override + public final boolean onFailedToRecycleView(@NonNull FragmentViewHolder holder) { + // This happens when a ViewHolder is in a transient state (e.g. during custom + // animation). We don't have sufficient information on how to clear up what lead to + // the transient state, so we are throwing away the ViewHolder to stay on the + // conservative side. + onViewRecycled(holder); // the same clean-up steps as when recycling a ViewHolder + return false; // don't recycle the view + } + + private void removeFragment(long itemId) { + Fragment fragment = mFragments.get(itemId); + + if (fragment == null) { + return; + } + + if (fragment.getView() != null) { + ViewParent viewParent = fragment.getView().getParent(); + if (viewParent != null) { + ((FrameLayout) viewParent).removeAllViews(); + } + } + + if (!containsItem(itemId)) { + mSavedStates.remove(itemId); + } + + if (!fragment.isAdded()) { + mFragments.remove(itemId); + return; + } + + if (shouldDelayFragmentTransactions()) { + mHasStaleFragments = true; + return; + } + + if (fragment.isAdded() && containsItem(itemId)) { + mSavedStates.put(itemId, mFragmentManager.saveFragmentInstanceState(fragment)); + } + mFragmentManager.beginTransaction().remove(fragment).commitNow(); + mFragments.remove(itemId); + } + + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + boolean shouldDelayFragmentTransactions() { + return mFragmentManager.isStateSaved(); + } + + /** + * Default implementation works for collections that don't add, move, remove items. + *

+ * TODO(b/122670460): add lint rule + * When overriding, also override {@link #containsItem(long)}. + *

+ * If the item is not a part of the collection, return {@link RecyclerView#NO_ID}. + * + * @param position Adapter position + * @return stable item id {@link RecyclerView.Adapter#hasStableIds()} + */ + @Override + public long getItemId(int position) { + return position; + } + + /** + * Default implementation works for collections that don't add, move, remove items. + *

+ * TODO(b/122670460): add lint rule + * When overriding, also override {@link #getItemId(int)} + */ + public boolean containsItem(long itemId) { + return itemId >= 0 && itemId < getItemCount(); + } + + @Override + public final void setHasStableIds(boolean hasStableIds) { + throw new UnsupportedOperationException( + "Stable Ids are required for the adapter to function properly, and the adapter " + + "takes care of setting the flag."); + } + + @Override + public final @NonNull Parcelable saveState() { + /** TODO(b/122670461): use custom {@link Parcelable} instead of Bundle to save space */ + Bundle savedState = new Bundle(mFragments.size() + mSavedStates.size()); + + /** save references to active fragments */ + for (int ix = 0; ix < mFragments.size(); ix++) { + long itemId = mFragments.keyAt(ix); + Fragment fragment = mFragments.get(itemId); + if (fragment != null && fragment.isAdded()) { + String key = createKey(KEY_PREFIX_FRAGMENT, itemId); + mFragmentManager.putFragment(savedState, key, fragment); + } + } + + /** Write {@link mSavedStates) into a {@link Parcelable} */ + for (int ix = 0; ix < mSavedStates.size(); ix++) { + long itemId = mSavedStates.keyAt(ix); + if (containsItem(itemId)) { + String key = createKey(KEY_PREFIX_STATE, itemId); + savedState.putParcelable(key, mSavedStates.get(itemId)); + } + } + + return savedState; + } + + @Override + public final void restoreState(@NonNull Parcelable savedState) { + if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) { + throw new IllegalStateException( + "Expected the adapter to be 'fresh' while restoring state."); + } + + Bundle bundle = (Bundle) savedState; + if (bundle.getClassLoader() == null) { + /** TODO(b/133752041): pass the class loader from {@link ViewPager2.SavedState } */ + bundle.setClassLoader(getClass().getClassLoader()); + } + + for (String key : bundle.keySet()) { + if (isValidKey(key, KEY_PREFIX_FRAGMENT)) { + long itemId = parseIdFromKey(key, KEY_PREFIX_FRAGMENT); + Fragment fragment = mFragmentManager.getFragment(bundle, key); + mFragments.put(itemId, fragment); + continue; + } + + if (isValidKey(key, KEY_PREFIX_STATE)) { + long itemId = parseIdFromKey(key, KEY_PREFIX_STATE); + Fragment.SavedState state = bundle.getParcelable(key); + if (containsItem(itemId)) { + mSavedStates.put(itemId, state); + } + continue; + } + + throw new IllegalArgumentException("Unexpected key in savedState: " + key); + } + + if (!mFragments.isEmpty()) { + mHasStaleFragments = true; + mIsInGracePeriod = true; + gcFragments(); + scheduleGracePeriodEnd(); + } + } + + private void scheduleGracePeriodEnd() { + final Handler handler = new Handler(Looper.getMainLooper()); + final Runnable runnable = new Runnable() { + @Override + public void run() { + mIsInGracePeriod = false; + gcFragments(); // good opportunity to GC + } + }; + + mLifecycle.addObserver(new LifecycleEventObserver() { + @Override + public void onStateChanged(@NonNull LifecycleOwner source, + @NonNull Lifecycle.Event event) { + if (event == Lifecycle.Event.ON_DESTROY) { + handler.removeCallbacks(runnable); + source.getLifecycle().removeObserver(this); + } + } + }); + + handler.postDelayed(runnable, GRACE_WINDOW_TIME_MS); + } + + // Helper function for dealing with save / restore state + private static @NonNull String createKey(@NonNull String prefix, long id) { + return prefix + id; + } + + // Helper function for dealing with save / restore state + private static boolean isValidKey(@NonNull String key, @NonNull String prefix) { + return key.startsWith(prefix) && key.length() > prefix.length(); + } + + // Helper function for dealing with save / restore state + private static long parseIdFromKey(@NonNull String key, @NonNull String prefix) { + return Long.parseLong(key.substring(prefix.length())); + } + + /** + * Pauses (STARTED) all Fragments that are attached and not a primary item. + * Keeps primary item Fragment RESUMED. + */ + class FragmentMaxLifecycleEnforcer { + private ViewPager2.OnPageChangeCallback mPageChangeCallback; + private RecyclerView.AdapterDataObserver mDataObserver; + private LifecycleEventObserver mLifecycleObserver; + private ViewPager2 mViewPager; + + private long mPrimaryItemId = NO_ID; + + void register(@NonNull RecyclerView recyclerView) { + mViewPager = inferViewPager(recyclerView); + + // signal 1 of 3: current item has changed + mPageChangeCallback = new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageScrollStateChanged(int state) { + updateFragmentMaxLifecycle(false); + } + + @Override + public void onPageSelected(int position) { + updateFragmentMaxLifecycle(false); + } + }; + mViewPager.registerOnPageChangeCallback(mPageChangeCallback); + + // signal 2 of 3: underlying data-set has been updated + mDataObserver = new DataSetChangeObserver() { + @Override + public void onChanged() { + updateFragmentMaxLifecycle(true); + } + }; + registerAdapterDataObserver(mDataObserver); + + // signal 3 of 3: we may have to catch-up after being in a lifecycle state that + // prevented us to perform transactions + mLifecycleObserver = new LifecycleEventObserver() { + @Override + public void onStateChanged(@NonNull LifecycleOwner source, + @NonNull Lifecycle.Event event) { + updateFragmentMaxLifecycle(false); + } + }; + mLifecycle.addObserver(mLifecycleObserver); + } + + void unregister(@NonNull RecyclerView recyclerView) { + ViewPager2 viewPager = inferViewPager(recyclerView); + viewPager.unregisterOnPageChangeCallback(mPageChangeCallback); + unregisterAdapterDataObserver(mDataObserver); + mLifecycle.removeObserver(mLifecycleObserver); + mViewPager = null; + } + + void updateFragmentMaxLifecycle(boolean dataSetChanged) { + if (shouldDelayFragmentTransactions()) { + return; /** recovery step via {@link #mLifecycleObserver} */ + } + + if (mViewPager.getScrollState() != ViewPager2.SCROLL_STATE_IDLE) { + return; // do not update while not idle to avoid jitter + } + + if (mFragments.isEmpty() || getItemCount() == 0) { + return; // nothing to do + } + + final int currentItem = mViewPager.getCurrentItem(); + if (currentItem >= getItemCount()) { + /** current item is yet to be updated; it is guaranteed to change, so we will be + * notified via {@link ViewPager2.OnPageChangeCallback#onPageSelected(int)} */ + return; + } + + long currentItemId = getItemId(currentItem); + if (currentItemId == mPrimaryItemId && !dataSetChanged) { + return; // nothing to do + } + + Fragment currentItemFragment = mFragments.get(currentItemId); + if (currentItemFragment == null || !currentItemFragment.isAdded()) { + return; + } + + mPrimaryItemId = currentItemId; + FragmentTransaction transaction = mFragmentManager.beginTransaction(); + + for (int ix = 0; ix < mFragments.size(); ix++) { + long itemId = mFragments.keyAt(ix); + Fragment fragment = mFragments.valueAt(ix); + + if (!fragment.isAdded()) { + continue; + } + + transaction.setMaxLifecycle(fragment, itemId == mPrimaryItemId ? RESUMED : STARTED); + fragment.setMenuVisibility(itemId == mPrimaryItemId); + } + + if (!transaction.isEmpty()) { + transaction.commitNow(); + } + } + + @NonNull + private ViewPager2 inferViewPager(@NonNull RecyclerView recyclerView) { + ViewParent parent = recyclerView.getParent(); + if (parent instanceof ViewPager2) { + return (ViewPager2) parent; + } + throw new IllegalStateException("Expected ViewPager2 instance. Got: " + parent); + } + } + + /** + * Simplified {@link RecyclerView.AdapterDataObserver} for clients interested in any data-set + * changes regardless of their nature. + */ + private abstract static class DataSetChangeObserver extends RecyclerView.AdapterDataObserver { + @Override + public abstract void onChanged(); + + @Override + public final void onItemRangeChanged(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public final void onItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { + onChanged(); + } + + @Override + public final void onItemRangeInserted(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public final void onItemRangeRemoved(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public final void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + onChanged(); + } + } +} diff --git a/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/FragmentViewHolder.java b/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/FragmentViewHolder.java new file mode 100644 index 0000000..c274cd9 --- /dev/null +++ b/app/src/main/java/com/example/fragmentstateadapteronfailedtorecyclerviewfailure/FragmentViewHolder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fragmentstateadapteronfailedtorecyclerviewfailure; + +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +/** + * {@link ViewHolder} implementation for handling {@link Fragment}s. Used in + * {@link FragmentStateAdapter}. + */ +public final class FragmentViewHolder extends ViewHolder { + private FragmentViewHolder(@NonNull FrameLayout container) { + super(container); + } + + @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) { + FrameLayout container = new FrameLayout(parent.getContext()); + container.setLayoutParams( + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + container.setId(ViewCompat.generateViewId()); + container.setSaveEnabled(false); + return new FragmentViewHolder(container); + } + + @NonNull FrameLayout getContainer() { + return (FrameLayout) itemView; + } +} -- 2.22.0.510.g264f2c817a-goog