19

I am using Advance Navigation Component with BottomNavigationView.

In one tab I have ViewPager2. When I clicked on the tab for the first time, it worked fine.

Although the second time, come on that tab application keep crashing. Below is the crash log. How can I fix this?

java.lang.IllegalArgumentException
at androidx.core.util.Preconditions.checkArgument(Preconditions.java:36)
at androidx.viewpager2.adapter.FragmentStateAdapter.onAttachedToRecyclerView(FragmentStateAdapter.java:140)
at androidx.recyclerview.widget.RecyclerView.setAdapterInternal(RecyclerView.java:1206)
at androidx.recyclerview.widget.RecyclerView.setAdapter(RecyclerView.java:1158)
at androidx.viewpager2.widget.ViewPager2.setAdapter(ViewPager2.java:460)
at com..ui.home.history.HistoryFragment.setupAdapter(HistoryFragment.kt:25)
at com.
.ui.home.history.HistoryFragment.viewSetup(HistoryFragment.kt:21)
at com.****.base.BaseFragment.onViewCreated(BaseFragment.kt:37)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:332)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1187)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1356)
at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1434)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1497)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2625)
at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2577)
at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:2722)
at androidx.fragment.app.FragmentStateManager.activityCreated(FragmentStateManager.java:346)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1188)
at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2224)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1997)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1953)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1849)
at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6940)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Here is my code for the fragment:

private val adapter by lazy {
    HistoryPagerAdapter(this)
}

override fun viewSetup() {
    binding.vpBuySell.adapter = adapter
    TabLayoutMediator(
        binding.tabBuySell,
        binding.vpBuySell,
        TabLayoutMediator.TabConfigurationStrategy { tab: TabLayout.Tab, i: Int ->
           tab.text = when (i) {
                0 -> getString(R.string.buy)
                1 -> getString(R.string.sell)
                else -> getString(R.string.buy)
            }
        })
}

Here is the UI code:

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/blue_122e47">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?actionBarSize"
        android:background="@color/blue_06233e"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:titleTextColor="@color/white">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tvTitle"
            style="@style/ToolbarTitleTextStyle"
            android:text="@string/history" />

        <TextView
            android:id="@+id/btnExport"
            android:layout_width="wrap_content"
            android:layout_height="@dimen/_24sdp"
            android:layout_gravity="end"
            android:layout_marginEnd="@dimen/_8sdp"
            android:fontFamily="@font/helvetica_neue_medium"
            android:insetLeft="0dp"
            android:gravity="center"
            android:background="@drawable/shape_export_button"
            android:insetTop="0dp"
            android:insetRight="0dp"
            android:insetBottom="0dp"
            android:foreground="?selectableItemBackground"
            android:paddingBottom="@dimen/_2sdp"
            android:paddingStart="@dimen/_8sdp"
            android:paddingEnd="@dimen/_8sdp"
            android:text="@string/export"
            android:textAllCaps="false"
            android:textColor="@color/white"
            android:textSize="@dimen/_12ssp" />

    </androidx.appcompat.widget.Toolbar>


    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabBuySell"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/blue_122e47"
        app:tabIndicatorFullWidth="false"
        app:tabIndicatorGravity="bottom"
        app:tabTextAppearance="@style/HistoryTabTextStyle"
        app:tabTextColor="@color/gray_697b8b"
        app:tabSelectedTextColor="@color/white"
        app:tabIndicatorHeight="@dimen/_2sdp"
        app:tabIndicatorColor="@color/blue_47cfff"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        app:tabGravity="start"
        app:tabMode="scrollable" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/vpBuySell"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tabBuySell" />

</androidx.constraintlayout.widget.ConstraintLayout>

Here is my adapter code:

class HistoryPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {

    override fun getItemCount(): Int {
        return 2
    }

    override fun createFragment(position: Int): Fragment {
        return when (position) {
            0 -> HistoryBuyFragment()
            1 -> HistorySellFragment()
            else -> HistoryBuyFragment()
        }
    }

}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
patel dhaval r
  • 1,237
  • 1
  • 8
  • 17

8 Answers8

29

The actual error is of the lazy initialisation of the adapter. I also don't know why that happened.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
patel dhaval r
  • 1,237
  • 1
  • 8
  • 17
11

I had the same crash with my ViewPager2 implementation. In my case I was creating an adapter in onCreate and setting it to ViewPager in onViewCreated. Like this:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    adapter = FragmentAdapter(this)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.viewPager?.adapter = adapter
}

I've fixed the crash, combining adapter creation and setting into one method - onViewCreated. Like this:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    adapter = FragmentAdapter(this, month)
    binding?.viewPager?.adapter = adapter
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Demigod
  • 5,073
  • 3
  • 31
  • 49
  • 2
    while this works, does somebody know why viewpager2 adapter is not being detached from recycler view after onDestroyView is called on the fragment? Or what is the conception here? – YTerle Oct 26 '20 at 15:56
  • 3
    this works in fixing the crash, but, if you have a list in a tab, and you navigate from the tab, and then return to that screen, the list will be reinitialised. That means that you will lose the position where the list is scrolled, or any other state of that tab. Not sure if this solves the whole problem, only the crash part. – grayFox16 Jul 14 '21 at 11:33
5

I ran into this issue with BottomNavigationView when I switched between fragments back and forward. The problem was that I used Kotlin's by lazy {} definition of a value and I was setting the adapter in onViewCreated. For some reason, the fragment state could not be properly restored and it was not possible to set the adapter even after I used some "hacks" from this post, like modifying some lifecycle methods, etc.

That pager adapter is automatically restored during state restoration. This is why you might not want to persist your adapter, but always set a new in onViewCreated. Then just wait for automatic restoration to trigger and have your previous state.

Fix:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // Anything you set here will be overriden during restoration
    viewBinding.pager.adapter = DemoCollectionPagerAdapter(this)

    var mediator = TabLayoutMediator(viewBinding.tabLayout, viewBinding.pager) { tab, position ->
        when (position){
            0 -> tab.text = getString(R.string.global_rooms).uppercase()
            1 -> tab.text = getString(R.string.global_devices).uppercase()
            2 -> tab.text = getString(R.string.global_presets).uppercase()
        }
    }.attach()
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
3

I had the same error when I got back to the fragment, and I solved the problem after I read Oleh's comment that said:

I assume that on a retained fragment lazy property cause memory leak because of holding a reference to the old view which leads to crash

So I set the adapter to null in onPause like this code, and then the problem is disappeared:

override fun onPause() {
    super.onPause()
    binding?.run {
        viewPager.adapter = null
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mosa
  • 353
  • 2
  • 16
1

Try out this solution:

import androidx.viewpager2.adapter.FragmentStateAdapter

class HistoryPagerAdapter(activity: AppCompatActivity, private var itemCount: Int): FragmentStateAdapter(activity) {

    override fun getItemCount(): Int {
        return 2
    }

    override fun createFragment(position: Int): Fragment {
        return when (position) {
            0 -> HistoryBuyFragment()
            1 -> HistorySellFragment()
            else -> HistoryBuyFragment()
        }
    }

Code for fragment:

private val TAB_ITEMS_COUNT = 2

adapter = HistoryPagerAdapter((activity as AppCompatActivity), TAB_ITEMS_COUNT)
binding.vpBuySell.adapter = adapter

TabLayoutMediator(binding.tabBuySell, binding.vpBuySell) { tab, position ->
    when(position)
    {
        0 -> tab.text = getString(R.string.buy)
        1 -> tab.text = getString(R.string.sell)
    }
}.attach()

binding.tabBuySell.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener
{
    override fun onTabReselected(tabItem: TabLayout.Tab?) {}
    override fun onTabUnselected(tabItem: TabLayout.Tab?) {}
    override fun onTabSelected(tabItem: TabLayout.Tab) {
        binding.vpBuySell.currentItem = tabItem.position
    }
})
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
PeterPazmandi
  • 533
  • 10
  • 13
0

If you are using Navigation Component, the fragment's view gets recreated when you navigate back. For some reason in my case, the adapter wasn't detaching automatically when the fragment's view was destroyed. The error was: I kept an already attached adapter reference and was trying to add it to a new pager's view. Setting adapter = null inside onDestroyView() worked in my case.

override fun onDestroyView() {
    pager.adapter = null
    super.onDestroyView()
}
G_comp
  • 162
  • 3
  • 12
0

I had the same issue, also i'm having another fragment injected in the viewPager's fragment in a FragmentContainerView, i properly handled that by creating the adapter every time the fragment created/recreated, but the trick is attaching the FragmentStateAdapter to the activity's fragment manager, it prevents the recreation of the whole view when returning to your fragment and it keeps also the current selected item in your viewPager, you can use something like that:

class YourFragmentPagerAdapter(
    fragmentManager: FragmentManager,
    lifecycle: Lifecycle,
    ...
) : FragmentStateAdapter(fragmentManager, lifecycle) {
Badr At
  • 658
  • 7
  • 22
-1

When you set the viewpager2 adapter, first check viewpager already adapter, else set the adapter.

For example,

if(viewPager.adapter == null) {
     viewPager.adapter = adapter
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • The first sentence is part incomprehensible. Is a word missing? Please respond by [editing (changing) your answer](https://stackoverflow.com/posts/66573006/edit), not here in comments (***without*** "Edit:", "Update:", or similar - the answer should appear as if it was written today). Thanks in advance. – Peter Mortensen Apr 22 '22 at 11:07