33

I have a viewPager2 and FragmentStateAdapter, and there are Fragement1, 2,3 4, I am in fragment2, and want to remove fragment3, and display fragment4 after fragment2. The problem is it always show me fragment3(data), the debug shows the fragment3 has been removed, but the displayed page still has fragment3 content.

Adpter:

class TipsAdapter(
private val items: MutableList<TripPage>,
context: FragmentActivity
) : FragmentStateAdapter(context) {

private val fragmentFactory = context.supportFragmentManager.fragmentFactory
private val classLoader = context.classLoader

override fun getItemCount(): Int = items.size

override fun createFragment(position: Int): Fragment {
    val pageInfo = items[position]
    val fragment = fragmentFactory.instantiate(classLoader, pageInfo.fragmentClass.name)
    fragment.arguments = Bundle().also { it.putParcelable(PAGE_INFO, pageInfo) }
    return fragment
}

fun getFragmentName(position: Int) = items[position].fragmentClass.simpleName

fun removeFragment(position: Int) {
    items.removeAt(position)
    notifyItemRemoved(position)
    notifyItemRangeChanged(position, items.size)
    notifyDataSetChanged()
}

}

delete fragment code:

      if ((view_pager.adapter as TipsAdapter).getFragmentName(index + 1).equals(
            TripPreFragment::class.simpleName) &&
            viewModel.shouldRemoveBulkApply()) {
            (view_pager.adapter as TipsAdapter).removeFragment(index + 1)
            view_pager.setCurrentItem(index + 1, true)
        } else {
            view_pager.setCurrentItem(index + 1, true)
        }
Xianwei
  • 2,381
  • 2
  • 22
  • 26
  • I have 2 fragments mainfragment and otherfragment . I'm adding otherfragment after every 5th position and it's not in arraylist so how to remove that otherfragment if its not in list ? I'm using viewpager2 – Bhavin Patel Jul 14 '20 at 06:40

2 Answers2

55

Finally, it works for me. when we call notifyDataSetChanged(), android will call method getItemId() in adapter, to check if the item has been updated or not. it returns the position in source code. which means in list 0..i..n, if you remove i , it becomes to 0...i...n-1, the i won't change and the data won't update in adater.

    /**
 * 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;
}

what you need to do is rewrite this method and containsItem(long),

for my case, I use the hashcode of each fragment:

class TipsAdapter(
     private val items: MutableList<TripPreferencesOptimizerPage>,
     context: FragmentActivity
    ) : FragmentStateAdapter(context) {

private val fragmentFactory = context.supportFragmentManager.fragmentFactory
private val classLoader = context.classLoader
private val pageIds= items.map { it.hashCode().toLong() }

override fun getItemCount(): Int = items.size

override fun createFragment(position: Int): Fragment {
    val pageInfo = items[position]
    val fragment = fragmentFactory.instantiate(classLoader, pageInfo.fragmentClass.name)
    fragment.arguments = Bundle().also { it.putParcelable(PAGE_INFO, pageInfo) }
    return fragment
}

fun getFragmentName(position: Int) = items[position].fragmentClass.simpleName

fun removeFragment(position: Int) {
    items.removeAt(position)
    notifyItemRangeChanged(position, items.size)
    notifyDataSetChanged()
}

override fun getItemId(position: Int): Long {
    return items[position].hashCode().toLong() // make sure notifyDataSetChanged() works
}

override fun containsItem(itemId: Long): Boolean {
    return pageIds.contains(itemId)
}
}
Rohan Pawar
  • 1,875
  • 22
  • 40
Xianwei
  • 2,381
  • 2
  • 22
  • 26
  • please find more useful information: https://stackoverflow.com/questions/7263291/viewpager-pageradapter-not-updating-the-view?rq=1 – Xianwei Sep 15 '19 at 12:43
  • Wow, you are awesome! you saved the day :) Cheers – SKG Mar 20 '20 at 15:03
  • 4
    @Xianwei that is a completely different part of API and unfortunately not relevant – NoHarmDan May 04 '20 at 11:38
  • I spent several hours trying to tackle this problem. Unfortunate thing about Android, it's not always intuitive and documentation is incomplete regarding these use cases. – Dallas Phillips Jun 28 '20 at 15:30
  • I have 2 fragments mainfragment and otherfragment . I'm adding otherfragment after every 5th position and it's not in arraylist so how to remove that otherfragment if its not in list ? – Bhavin Patel Jul 14 '20 at 06:39
  • I'm using viewpager2 – Bhavin Patel Jul 14 '20 at 06:40
  • 2
    for me every time you update the number you also need to reinitialize `pageIds = items.map { it.hashCode().toLong() }` – Mihae Kheel Apr 03 '21 at 12:18
  • Thank you for this solution. I have tried it and noticed that calling `notifyDataSetChanged` is actually not needed and does even have an ugly side effect, i.e. the view pager jumps to the first page when the last one is deleted. In most cases, you'd want to see the previous page instead. Also, as @MihaeKheel commented, updating the pageId map is needed in the `removeFragment()`method to keep page ids and items in sync. –  Sep 29 '21 at 09:30
  • what is the 'items' in the constructor of the adapter. Is it a local cache of the pager fragments ? – Dibzmania Feb 14 '22 at 07:34
  • 1
    For me, it's working without `notifyItemRangeChanged` `notifyDataSetChanged`; only added `notifyItemRemoved` – Zain Mar 18 '22 at 16:11
0

The Simplest solution is bellow..

items.removeAt(position)
notifyDataSetChanged()

Then call this from your activity or fragment

view_pager.setCurrentItem(index - 1, true)

This will remove your fragment and animate like delete item in recyclerview if you are viewing deleting fragment.