20

I have a RecyclerView.ViewHolder which will add different fragment into its FrameLayout based on the instance of the object passed. The problem comes where it is almost impossible to add fragment into the ViewHolder. Take note that I already passed the FragmentManager from the parent. Initially I try with this code

public void setSomething(boolean A) {
    if (A) {
         mFragmentManager.beginTransaction()
            .replace(mBinding.typeContainerLayout.getId(), new FragmentA())
            .commit();
    } else {
        mFragmentManager.beginTransaction()
            .replace(mBinding.typeContainerLayout.getId(), new FragmentB())
            .commit();
    }
}

The problem with this code is that all the ViewHolder share the same id, thus only a single ViewHolder can add the fragment. In my RecyclerView, only the first cell added the fragment. To tackle this problem, I create another FrameLayout and add it into typeContainerLayout. Now my code become like this.

public void setSomething(boolean A) {
    FrameLayout frameLayout = new FrameLayout(mContext);
    frameLayout.setId(View.generateViewId());
    mBinding.typeContainerLayout.removeAllViews();
    mBinding.typeContainerLayout.addView(frameLayout)

    if (A) {
         mFragmentManager.beginTransaction()
            .replace(frameLayout.getId(), new FragmentA())
            .commit();
    } else {
        mFragmentManager.beginTransaction()
            .replace(frameLayout.getId(), new FragmentB())
            .commit();
    }
}

Now each ViewHolder has added the fragment correctly and has their own fragment. However the problem comes when I added like 5 ViewHolder and trying to scroll down the RecyclerView, a runtime error occurred which state

java.lang.IllegalArgumentException: No view found for id 0x4 (unknown) for fragment FragmentA{7c55a69 #0 id=0x4 FragmentA}
                      at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1292)
                      at android.support.v4.app.FragmentManagerImpl.moveFragmentsToInvisible(FragmentManager.java:2323)
                      at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2136)
                      at android.support.v4.app.FragmentManagerImpl.optimizeAndExecuteOps(FragmentManager.java:2092)
                      at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1998)
                      at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:709)
                      at android.os.Handler.handleCallback(Handler.java:739)
                      at android.os.Handler.dispatchMessage(Handler.java:95)
                      at android.os.Looper.loop(Looper.java:148)
                      at android.app.ActivityThread.main(ActivityThread.java:5417)
                      at java.lang.reflect.Method.invoke(Native Method)
                      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
                      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

My guess is that either the id conflicted at some point, or the view got destroyed due to the ViewHolder pattern. So my question is that.

1) Is there any workaround to it?

2) Is there any better practice than adding fragment. The reason I add fragment is so that the logic for the sub item of the ViewHolder can all be located in a single fragment. Of course I can just put both the views for the fragments into the ViewHolder xml. And just setVisible() depending on the condition. But that will just make my ViewHolder contain too many logic.

In case someone is confused why I need fragment. This is what I am trying to achieve. The image

Ginsan
  • 291
  • 1
  • 5
  • 13
  • 1
    You don't use fragments with recyclerview. You probably need to use [ViewPager](https://developer.android.com/reference/android/support/v4/view/ViewPager.html) – Vygintas B Feb 13 '17 at 14:15
  • For the work around, you might add a FrameLayout in the view holder to get an Id. Usually, we use Adapters by extending RecyclerView.Adapter – jdesesquelles Feb 13 '17 at 14:20
  • I am having a list of items. Depending on the instance of the Object, the bottom part of the ViewHolder for the item is different. Thats why I used fragment. All of them share the same top part. – Ginsan Feb 13 '17 at 14:20
  • jdesesquelles, I have all those Adapter code working correctly already. Just the ViewHolder part. I did the adding FrameLayout thing and I get the `IllegalArgumentException` – Ginsan Feb 13 '17 at 14:22
  • You could also set visible and hide the corresdonding element of the Item layout depending on the type of items ? If the ViewHolder class is within the Adapter, the extra logic is still in the adapter. – jdesesquelles Feb 13 '17 at 14:23
  • There might be something wrong with your fragment, either in the Xml or the Java Code. – jdesesquelles Feb 13 '17 at 14:26
  • jdesesquelles, I also thought of that. I just scare that the adapter will have too many logic cramped. I am still trying to find the best practice for this kind of problem. If there aren't any, then maybe I will just use the set visible solution. – Ginsan Feb 13 '17 at 14:28
  • But the fragment is added correctly for the first few items. Just that when I scroll too fast, it comes out the error. – Ginsan Feb 13 '17 at 14:30
  • 1
    dont use `FragmentManager`: add your fragment in item layout xml instead – pskink Feb 13 '17 at 14:31
  • pskink, but that won't be dynamic right? – Ginsan Feb 13 '17 at 14:32
  • dynamic? what you mean? – pskink Feb 13 '17 at 14:37
  • pskink, I mean that the fragment that I wanted to add changed at runtime depending on the user's action, and changes towards the list of items – Ginsan Feb 13 '17 at 14:39
  • i have no idea what do you need `Fragment`s for: why don you use `RecyclerView.Adapter#getItemViewType(int position)` ? – pskink Feb 13 '17 at 14:44
  • I added a picture link that shows what I am trying to achieve. That should reason out why I need fragment. They all share the same top part. Thats why – Ginsan Feb 13 '17 at 14:52
  • just use tag in your item layouts – pskink Feb 13 '17 at 14:57
  • pskink, the container `FrameLayout` or the fragment? – Ginsan Feb 13 '17 at 15:03
  • https://developer.android.com/training/improving-layouts/reusing-layouts.html#Include – pskink Feb 13 '17 at 15:19
  • 1
    I had the same problem. And I solved it. Just set new unique id to your container layout and you will able to add any fragment to you recyclerview item. This answer helped me. https://stackoverflow.com/a/42994810/1931613. For example, myContainerLayout.setId(SystemClock.currentThreadTimeMillis().toInt()) – Kiryl Ivanou Sep 06 '19 at 14:09
  • Generating new ids worked - thanks! – Starwave Oct 13 '21 at 17:55
  • You should use `View.generateViewId()` to generate new IDs instead. It makes sure that there are no ID collisions. – ThePBone Jul 15 '22 at 02:47

4 Answers4

12

You should know exactly that recyclerview created your holder and drew a view for him, so you need to attach a fragment in onViewAttachedToWindowmethod of your adapter. In your "attaching fragment" method you should check if fragment manager already contains those fragments to prevent of creating multiple instances.

Adapter:

 override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {    
    if(holder is FragmentsHolder){
        attachFragmentToContainer(holder.flContainer)            
    }
    super.onViewAttachedToWindow(holder)
}

Method realization:

 fun attachFragmentToContainer(containerId: Int) {
    val fragment = if (fragmentManager?.fragments?.firstOrNull { it is YourFragment } == null)
        YourFragment.instance()
    else
        null

    if (fragment != null) {
        fragmentManager?.beginTransaction()
            ?.add(containerId, fragment)
            ?.commitNowAllowingStateLoss()
    }
}

This tested on big amount of users - no crashes, good perfomance.

Artur Antonyan
  • 578
  • 6
  • 11
  • 1
    Is this solution any better than just calling `fragmentManager.beginTransaction().replace(holder.flContainer.id, fragment).commitNowAllowingStateLoss()` in `onViewAttachedToWindow`? – lasec0203 Jul 12 '21 at 06:55
2

Short answer: you shouldn't use fragments inside a recyclerView, that's not what they're intended for.

Long answer: here

Community
  • 1
  • 1
Hanan Rofe Haim
  • 870
  • 6
  • 11
  • 1
    Maybe you are right. It is a bad design to use fragment. Thanks for the answer – Ginsan Feb 15 '17 at 12:32
  • Glad to help, you can mark my answer if it helped :) – Hanan Rofe Haim Feb 15 '17 at 12:36
  • 6
    I dont think that this is a correct answer, of course we can use fragments in recyclerView. If you have a complex UI in your app sometimes it necessary to use some kind of a hack. Please check my answer below : https://stackoverflow.com/a/56324168/5533118 – Artur Antonyan May 27 '19 at 10:25
  • 6
    This answer is not correct. `androidx` `ViewPager2` is using `Fragment`s inside of a RecyclerView. See the `placeFragmentInViewHolder` method of the Adapter: https://github.com/androidx/androidx/blob/androidx-master-dev/viewpager2/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java – user1185087 Aug 12 '20 at 10:24
2

I faced the same problem and resolved it by using the addOnChildAttachStateChangeListener RecyclerView callback on my onBindViewHolder callback :

listView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
        @Override
        public void onChildViewAttachedToWindow(@NotNull View view) {
            //Create your fragment container here using the view param and add your fragment to the container
        }

        @Override
        public void onChildViewDetachedFromWindow(@NotNull View view) {
            //Destroy the fragment container here

        }
    });
0

onBindViewHolder:

yourFragment fragment = new yourFragment();
    try {
        viewHolder.Fragment(fragment);
    }catch (Exception e){
        Toast.makeText(context, "BindViewHolder"+e.getMessage(), Toast.LENGTH_SHORT).show();
    }

ViewHolder:

class ViewHolder extends RecyclerView.ViewHolder {
    TextView name;
    FrameLayout container;

    private ViewHolder(@NonNull View itemView) {
        super(itemView);

        container = itemView.findViewById(R.id.fragmentContainer);
    }

    private void Fragment(final Fragment fragment) {
        FragmentTransaction transaction = ((AppCompatActivity) context).getSupportFragmentManager()
                .beginTransaction();
        try {
            transaction.add(R.id.fragmentContainer, fragment)
                    .commit();
        } catch (Exception e) {
            Toast.makeText(context, "ViewHolder " + e.getMessage(), Toast.LENGTH_SHORT).show();
        }
    }
}