ViewPager2 crash
K

9

20

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()
        }
    }

}
Kettie answered 5/5, 2020 at 5:56 Comment(2)
Provide the code of the activitie/fragment where you have the VP2 and the tabs' code.Anciently
@inspire_coding please check added code.Kettie
K
32

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

Kettie answered 5/5, 2020 at 8:43 Comment(4)
I assume that on a retained fragment lazy property cause memory leak because of holding a reference to the old view which leads to crashGabar
@Gabar you are answer is right , when i set adapter = null in onPasue that crash is goneHitandrun
@Gabar actually this is not happening in regular scenario. it is just happen with advance navigation usage with BottomTabbar.Kettie
Thank you so much. It worked like a charm. I just changed the lazy init to lateinitFinger
F
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
}
Fiord answered 13/7, 2020 at 15:5 Comment(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?Sung
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.Neediness
S
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()
}
Sundsvall answered 19/7, 2021 at 14:40 Comment(0)
H
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
    }
}
Hitandrun answered 17/9, 2020 at 6:47 Comment(0)
A
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
    }
})
Anciently answered 5/5, 2020 at 6:51 Comment(2)
Why did you remove the "Solved" mark? Is't it working?Anciently
i got idea from your response but your answer is not solution of problem.Kettie
E
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()
}
Ephram answered 27/5, 2021 at 19:52 Comment(0)
S
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) {
Slosberg answered 12/12, 2022 at 14:39 Comment(0)
O
0

In the accepted answer the reason is correct, I will share a solution without using laterinit:

private var _profileAdapter: ProfileVpAdapter? = null

private val profileVPAdapter: ProfileVpAdapter
        get() = _profileAdapter ?: ProfileVpAdapter(this).also {
            _profileAdapter = it
        }

override fun onDestroyView() {
        super.onDestroyView()
        _profileAdapter = null
    }

Use in fragment profileVPAdapter

Oxygen answered 15/5, 2024 at 4:28 Comment(2)
nullable and lateinit are almost same thing @Oxygen :)Kettie
but still nullable is easier to control and nullable is more predictable. And it’s better not to allow either one or the other :) That’s why I suggested this option..Oxygen
L
-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
}
Lawabiding answered 10/3, 2021 at 21:19 Comment(1)
The first sentence is part incomprehensible. Is a word missing? Please respond by editing (changing) your answer, not here in comments (without "Edit:", "Update:", or similar - the answer should appear as if it was written today). Thanks in advance.Amylum

© 2022 - 2025 — McMap. All rights reserved.