Search code examples
androidandroid-fragmentsandroid-viewpagerfragmentmanager

Getting “java.lang.IllegalArgumentException: No view found for id” in ViewPager's Fragment that in RecyclerView


I am getting exception:

E/MessageQueue-JNI: java.lang.IllegalArgumentException: No view found for id 0x7f0a0554 (ua.com.company.app:id/vpBanners) for fragment BannerItemFragment{30698be} (4c80b228-4303-4c80-b99d-a55b8359b8c2) id=0x7f0a0554}

My hierarchy looks like so:

Screen hierarchy

My adapter for vpHome:

class HomeViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    private val mFragmentList: MutableList<Fragment> = ArrayList()
    private val mFragmentTitleList: MutableList<String> = ArrayList()

    override fun getItem(position: Int): Fragment {
        return mFragmentList[position]
    }

    override fun getCount(): Int {
        return mFragmentList.size
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return mFragmentTitleList[position]
    }

    fun addFragment(fragment: Fragment, title: String) {
        mFragmentList.add(fragment)
        mFragmentTitleList.add(title)
    }

}

And I apply it in this way:

private fun setupViewPager(viewPager: DisableSwipeViewPager) {
    vpAdapter = HomeViewPagerAdapter(childFragmentManager).apply {
        addFragment(ForYouFragment(), "for you")
        addFragment(AnotherFragment1(), "a1")
        addFragment(AnotherFragment2(), "a2")
    }
    viewPager.adapter = vpAdapter
}

Next, my SnapBannersCarouselViewHolder to handle items with ViewPager inside:

class SnapBannersCarouselViewHolder(
    private val mBinding: HomeFragmentItemSnapBannersCarouselBinding
) : RecyclerView.ViewHolder(mBinding.root) {

    companion object {
        // ...

        fun newInstance(
            inflater: LayoutInflater,
            parent: ViewGroup,
            onMoreInteractionListener: ((infoBlockId: String) -> Unit)?
        ): SnapBannersCarouselViewHolder {
            val binding =
                HomeFragmentItemSnapBannersCarouselBinding.inflate(inflater, parent, false)
            return SnapBannersCarouselViewHolder(
                binding,
                onMoreInteractionListener
            )
        }
    }

    fun bind(item: SnapBannersItem, fragmentManager: FragmentManager) {
        // ...
        val adapter = BannerPagesAdapter(fragmentManager, item.banners)
      
        with(mBinding.vpBanners) {
            // ...
            this.adapter = adapter
            offscreenPageLimit = 3
        }
    }

}

My RecyclerView adapter ForYouContentAdapter:

class ForYouContentAdapter(
    var data: List<HomeBaseItem> = emptyList(),
    var fragmentManagerRetriever: () -> FragmentManager
) : BaseRecyclerViewAdapter<HomeBaseItem>(data) {

    enum class ViewType(val value: Int) {
        // ...
        SNAP_BANNERS(6)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            // ...
            ViewType.SNAP_BANNERS.value -> SnapBannersCarouselViewHolder.newInstance(
                mInflater!!,
                parent,
                onBannersMoreInteractionListener // todo possibly change it
            )
            else -> throw RuntimeException("Can not create view holder for undefined view type")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (getItemViewType(position)) {
            // ...
            ViewType.SNAP_BANNERS.value -> {
                val vHolder = holder as SnapBannersCarouselViewHolder
                vHolder.bind(
                    getItem(position) as SnapBannersItem,
                    fragmentManagerRetriever.invoke()
                )
            }
        }
    }

}

And my fragmentManagerRetriever implementation in ForYouFragment looks like so:

private val fragmentManagerRetriever: () -> FragmentManager = {
    childFragmentManager
}

My BannerItemFragment code:

class BannerItemFragment : Fragment() {

    private lateinit var mBinding: HomeFragmentSnapBannerItemBinding

    companion object {
        // ...

        fun newInstance(
            item: RectWebViewItem
        ): BannerItemFragment {
            return BannerItemFragment()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mBinding = HomeFragmentSnapBannerItemBinding.inflate(inflater, container, false)
        return mBinding.root
    }

    // ...

}

In every place where I need to create fragments inside other fragments I am using childFragmentManager.

And when I open ForYouFragment first time, it works normally. My item with ViewPager works normally. But when I replace fragment in Activity's container (adding to back stack) being on ForYouFragment and then return back (back on HomeFragment because ForYouFragment inside HomeFragment), I am getting error.

To replace fragments I am using this method inside ForYouFragment:

private fun showAnotherFragment() {
    ActivityUtils.replaceFragmentToActivity(
        requireActivity(),
        SomeFragment.newInstance(),
        true
    )
}

And ActivityUtils code:

object ActivityUtils {

    fun replaceFragmentToActivity(
            activity: FragmentActivity,
            fragment: Fragment,
            addToBackStack: Boolean
    ) {
        replaceFragmentToActivity(activity, fragment, addToBackStack, containerId = R.id.fragmentContainer)
    }

    fun replaceFragmentToActivity(
            activity: FragmentActivity,
            fragment: Fragment,
            addToBackStack: Boolean,
            containerId: Int
    ) {
        val fragmentManager = activity.supportFragmentManager
        val transaction = fragmentManager.beginTransaction()
        transaction.replace(containerId, fragment)
        if (addToBackStack) {
            transaction.addToBackStack(null)
        }
        transaction.commitAllowingStateLoss()
    }

}

Please, help me understand why I am getting this exception?

UPD

Adapter for ViewPager inside RecyclerView:

class BannerPagesAdapter(
    fragmentManager: FragmentManager,
    var data: List<RectWebViewItem>
) : FragmentStatePagerAdapter(
    fragmentManager,
    BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
) {

    private var fragments: MutableList<BannerItemFragment> = mutableListOf()

    init {
        // initial empty fragments
        for (i in data.indices) {
            fragments.add(BannerItemFragment())
        }
    }

    override fun getItem(position: Int): Fragment {
        return BannerItemFragment.newInstance(data[position])
    }

    override fun getCount(): Int {
        return fragments.size
    }

    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        val f = super.instantiateItem(container, position)
        fragments[position] = f as BannerItemFragment
        return f
    }

}

Solution

  • Solved

    The problem was that fragments want to be attached to the ViewPager before the ViewPager is attached to its parent. This question outlined here.

    So, to solve this problem, I created custom ViewPager:

    /**
     * Use this ViewPager when you need to place ViewPager inside RecyclerView.
     * [LazyViewPager] allows you to set [PagerAdapter] in lazy way. This prevents IllegalStateException
     * in case when the fragments want to be attached to the viewpager before the viewpager is
     * attached to its parent
     */
    class LazyViewPager
    @JvmOverloads
    constructor(
        context: Context,
        attrs: AttributeSet? = null
    ) : ViewPager(context, attrs) {
    
        private var mPagerAdapter: PagerAdapter? = null
    
        override fun onAttachedToWindow() {
            super.onAttachedToWindow()
            if (mPagerAdapter != null) {
                super.setAdapter(mPagerAdapter)
            }
        }
    
        override fun onDetachedFromWindow() {
            super.onDetachedFromWindow()
            super.setAdapter(null)
        }
    
        @Deprecated("Do not use this method to set adapter. Use setAdapterLazy() instead.")
        override fun setAdapter(adapter: PagerAdapter?) {}
    
        fun setAdapterLazy(adapter: PagerAdapter?) {
            mPagerAdapter = adapter
        }
    
    }
    

    And then, instead of using setAdapter() I use setAdapterLazy().

    Also, it is important to reset adapter to null in onDetachedFromWindow().