From b0c179dee2f07b175c8a2a5ff79acf0d2a66b04c Mon Sep 17 00:00:00 2001
From: Jakub Gielzak <jgielzak@google.com>
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}
+ * <p>
+ * Lifecycle within {@link RecyclerView}:
+ * <ul>
+ * <li>{@link RecyclerView.ViewHolder} initially an empty {@link FrameLayout}, serves as a
+ * re-usable container for a {@link Fragment} in later stages.
+ * <li>{@link RecyclerView.Adapter#onBindViewHolder} we ask for a {@link Fragment} for the
+ * position. If we already have the fragment, or have previously saved its state, we use those.
+ * <li>{@link RecyclerView.Adapter#onAttachedToWindow} we attach the {@link Fragment} to a
+ * container.
+ * <li>{@link RecyclerView.Adapter#onViewRecycled} and
+ * {@link RecyclerView.Adapter#onFailedToRecycleView} we remove, save state, destroy the
+ * {@link Fragment}.
+ * </ul>
+ */
+public abstract class FragmentStateAdapter extends
+        RecyclerView.Adapter<FragmentViewHolder> 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<Fragment> mFragments = new LongSparseArray<>();
+    private final LongSparseArray<Fragment.SavedState> mSavedStates = new LongSparseArray<>();
+    private final LongSparseArray<Integer> 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.
+     * <p>
+     * The adapter will be responsible for the Fragment lifecycle:
+     * <ul>
+     *     <li>The Fragment will be used to display an item.</li>
+     *     <li>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.
+     * </ul>
+     * @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<Long> 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.
+     * <p>
+     * TODO(b/122670460): add lint rule
+     * When overriding, also override {@link #containsItem(long)}.
+     * <p>
+     * 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.
+     * <p>
+     * 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

