I believe that FragmentStatePagerAdapter does not behave correctly when overriding getItemPosition(Object object) with the purpose of reordering the pages.
I've reimplemented the existing solution in Kotlin such that it allows you to return a String instead of a long for the item id. You can find it here or below:
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentTransaction
import android.view.View
import android.view.ViewGroup
import java.util.HashSet
import java.util.LinkedHashMap
/**
* A PagerAdapter that can withstand item reordering. See
* https://issuetracker.google.com/issues/36956111.
*
* @see android.support.v4.app.FragmentStatePagerAdapter
*/
abstract class MovableFragmentStatePagerAdapter(
private val manager: FragmentManager
) : NullablePagerAdapter() {
private var currentTransaction: FragmentTransaction? = null
private var currentPrimaryItem: Fragment? = null
private val savedStates = LinkedHashMap()
private val fragmentsToItemIds = LinkedHashMap()
private val itemIdsToFragments = LinkedHashMap()
private val unusedRestoredFragments = HashSet()
/** @see android.support.v4.app.FragmentStatePagerAdapter.getItem */
abstract fun getItem(position: Int): Fragment
/**
* @return a unique identifier for the item at the given position.
*/
abstract fun getItemId(position: Int): String
/** @see android.support.v4.app.FragmentStatePagerAdapter.startUpdate */
override fun startUpdate(container: ViewGroup) {
check(container.id != View.NO_ID) {
"ViewPager with adapter $this requires a view id."
}
}
/** @see android.support.v4.app.FragmentStatePagerAdapter.instantiateItem */
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val itemId = getItemId(position)
val f = itemIdsToFragments[itemId]
if (f != null) {
unusedRestoredFragments.remove(f)
return f
}
if (currentTransaction == null) {
// We commit the transaction later
@SuppressLint("CommitTransaction")
currentTransaction = manager.beginTransaction()
}
val fragment = getItem(position)
fragmentsToItemIds.put(fragment, itemId)
itemIdsToFragments.put(itemId, fragment)
val fss = savedStates[itemId]
if (fss != null) {
fragment.setInitialSavedState(fss)
}
fragment.setMenuVisibility(false)
fragment.userVisibleHint = false
currentTransaction!!.add(container.id, fragment)
return fragment
}
/** @see android.support.v4.app.FragmentStatePagerAdapter.destroyItem */
override fun destroyItem(container: ViewGroup, position: Int, fragment: Any) {
(fragment as Fragment).destroy()
}
/** @see android.support.v4.app.FragmentStatePagerAdapter.setPrimaryItem */
override fun setPrimaryItem(container: ViewGroup, position: Int, fragment: Any?) {
fragment as Fragment?
if (fragment !== currentPrimaryItem) {
currentPrimaryItem?.let {
it.setMenuVisibility(false)
it.userVisibleHint = false
}
fragment?.setMenuVisibility(true)
fragment?.userVisibleHint = true
currentPrimaryItem = fragment
}
}
/** @see android.support.v4.app.FragmentStatePagerAdapter.finishUpdate */
override fun finishUpdate(container: ViewGroup) {
if (!unusedRestoredFragments.isEmpty()) {
for (fragment in unusedRestoredFragments) fragment.destroy()
unusedRestoredFragments.clear()
}
currentTransaction?.let {
it.commitAllowingStateLoss()
currentTransaction = null
manager.executePendingTransactions()
}
}
/** @see android.support.v4.app.FragmentStatePagerAdapter.isViewFromObject */
override fun isViewFromObject(view: View, fragment: Any): Boolean =
(fragment as Fragment).view === view
/** @see android.support.v4.app.FragmentStatePagerAdapter.saveState */
override fun saveState(): Parcelable? = Bundle().apply {
putStringArrayList(KEY_FRAGMENT_IDS, ArrayList(savedStates.keys))
putParcelableArrayList(
KEY_FRAGMENT_STATES,
ArrayList(savedStates.values)
)
for ((f, id) in fragmentsToItemIds.entries) {
if (f.isAdded) {
manager.putFragment(this, "$KEY_FRAGMENT_STATE$id", f)
}
}
}
/** @see android.support.v4.app.FragmentStatePagerAdapter.restoreState */
override fun restoreState(state: Parcelable?, loader: ClassLoader?) {
if ((state as Bundle?)?.apply { classLoader = loader }?.isEmpty == false) {
state!!
fragmentsToItemIds.clear()
itemIdsToFragments.clear()
unusedRestoredFragments.clear()
savedStates.clear()
val fragmentIds: List = state.getStringArrayList(KEY_FRAGMENT_IDS)
val fragmentStates: List =
state.getParcelableArrayList(KEY_FRAGMENT_STATES)
for ((index, id) in fragmentIds.withIndex()) {
savedStates.put(id, fragmentStates[index])
}
for (key: String in state.keySet()) {
if (key.startsWith(KEY_FRAGMENT_STATE)) {
val itemId = key.substring(KEY_FRAGMENT_STATE.length)
manager.getFragment(state, key)?.let {
it.setMenuVisibility(false)
fragmentsToItemIds.put(it, itemId)
itemIdsToFragments.put(itemId, it)
}
}
}
unusedRestoredFragments.addAll(fragmentsToItemIds.keys)
}
}
private fun Fragment.destroy() {
if (currentTransaction == null) {
// We commit the transaction later
@SuppressLint("CommitTransaction")
currentTransaction = manager.beginTransaction()
}
val itemId = fragmentsToItemIds.remove(this)
itemIdsToFragments.remove(itemId)
if (itemId != null) {
savedStates.put(itemId, manager.saveFragmentInstanceState(this))
}
currentTransaction!!.remove(this)
}
private companion object {
const val KEY_FRAGMENT_IDS = "fragment_keys_"
const val KEY_FRAGMENT_STATES = "fragment_states_"
const val KEY_FRAGMENT_STATE = "fragment_state_"
}
}
And the Java piece:
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.view.ViewGroup;
/**
* A PagerAdapter whose {@link #setPrimaryItem} is overridden with proper nullability annotations.
*/
public abstract class NullablePagerAdapter extends PagerAdapter {
@Override
public void setPrimaryItem(@NonNull ViewGroup container,
int position,
@Nullable Object object) {
// `object` is actually nullable. It's even in the dang source code which is hilariously
// ridiculous:
// `mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);`
}
}