Skip to content

Instantly share code, notes, and snippets.

@Zhuinden
Last active April 26, 2024 09:32
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Zhuinden/ef743346eda60a314d2a100eeaf069d5 to your computer and use it in GitHub Desktop.
Save Zhuinden/ef743346eda60a314d2a100eeaf069d5 to your computer and use it in GitHub Desktop.
Dynamic FragmentPagerAdapter
public class DynamicFragmentPagerAdapter extends PagerAdapter {
private static final String TAG = "DynamicFragmentPagerAdapter";
private final FragmentManager fragmentManager;
public static abstract class FragmentIdentifier implements Parcelable { //should be concrete children with @Parcelize if possible, don't forget CREATOR field
private final String fragmentTag;
private final Bundle args;
public FragmentIdentifier(@NonNull String fragmentTag, @Nullable Bundle args) {
this.fragmentTag = fragmentTag;
this.args = args;
}
protected FragmentIdentifier(Parcel in) {
fragmentTag = in.readString();
args = in.readBundle(getClass().getClassLoader());
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(fragmentTag);
dest.writeBundle(args);
}
protected final Fragment newFragment() {
Fragment fragment = createFragment();
Bundle oldArgs = fragment.getArguments();
Bundle newArgs = new Bundle();
if(oldArgs != null) {
newArgs.putAll(oldArgs);
}
if(args != null) {
newArgs.putAll(args);
}
fragment.setArguments(newArgs);
return fragment;
}
protected abstract Fragment createFragment();
}
private ArrayList<FragmentIdentifier> fragmentIdentifiers = new ArrayList<>();
private FragmentTransaction currentTransaction = null;
private Fragment currentPrimaryItem = null;
public DynamicFragmentPagerAdapter(FragmentManager fragmentManager) {
this.fragmentManager = fragmentManager;
}
private int findIndexIfAdded(FragmentIdentifier fragmentIdentifier) {
for (int i = 0, size = fragmentIdentifiers.size(); i < size; i++) {
FragmentIdentifier identifier = fragmentIdentifiers.get(i);
if (identifier.fragmentTag.equals(fragmentIdentifier.fragmentTag)) {
return i;
}
}
return -1;
}
public void addFragment(FragmentIdentifier fragmentIdentifier) {
if (findIndexIfAdded(fragmentIdentifier) < 0) {
fragmentIdentifiers.add(fragmentIdentifier);
notifyDataSetChanged();
}
}
public void removeFragment(FragmentIdentifier fragmentIdentifier) {
int index = findIndexIfAdded(fragmentIdentifier);
if (index >= 0) {
fragmentIdentifiers.remove(index);
notifyDataSetChanged();
}
}
@Override
public int getCount() {
return fragmentIdentifiers.size();
}
@Override
public void startUpdate(@NonNull ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this
+ " requires a view id");
}
}
@SuppressWarnings("ReferenceEquality")
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (currentTransaction == null) {
currentTransaction = fragmentManager.beginTransaction();
}
final FragmentIdentifier fragmentIdentifier = fragmentIdentifiers.get(position);
// Do we already have this fragment?
final String name = fragmentIdentifier.fragmentTag;
Fragment fragment = fragmentManager.findFragmentByTag(name);
if (fragment != null) {
currentTransaction.attach(fragment);
} else {
fragment = fragmentIdentifier.newFragment();
currentTransaction.add(container.getId(), fragment, fragmentIdentifier.fragmentTag);
}
if (fragment != currentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
if (currentTransaction == null) {
currentTransaction = fragmentManager.beginTransaction();
}
currentTransaction.detach((Fragment) object);
}
@SuppressWarnings("ReferenceEquality")
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment) object;
if (fragment != currentPrimaryItem) {
if (currentPrimaryItem != null) {
currentPrimaryItem.setMenuVisibility(false);
currentPrimaryItem.setUserVisibleHint(false);
}
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
currentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(@NonNull ViewGroup container) {
if (currentTransaction != null) {
currentTransaction.commitNowAllowingStateLoss();
currentTransaction = null;
}
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return ((Fragment) object).getView() == view;
}
@Override
public Parcelable saveState() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList("fragmentIdentifiers", fragmentIdentifiers);
return bundle;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
Bundle bundle = ((Bundle)state);
bundle.setClassLoader(loader);
fragmentIdentifiers = bundle.getParcelableArrayList("fragmentIdentifiers");
notifyDataSetChanged();
}
}
@DDihanov
Copy link

DDihanov commented Apr 28, 2021

Add notifyDataSetChanged(); after line 162 in "fragmentIdentifiers = bundle.getParcelableArrayList("fragmentIdentifiers");

because it crashes with a java.lang.IllegalStateException: The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: 0, found: ... exception on state restore

Works great otherwise! (better than ViewPager2' new adapter :D)

@Zhuinden
Copy link
Author

Oh yeah, that makes sense. Surprising that I hadn't been notified of that, I somewhat figured that restoration would occur before trying to render, but alas. Added.

Works great otherwise!

Glad to hear! I made this specifically because ViewPager2 claims in its documentation that "dynamic fragment count" isn't possible with regular ViewPager, but that isn't true at all.

@Zhuinden
Copy link
Author

Reminder that you cannot use anonymous fragment identifiers because the fragment identifier requires a CREATOR field for Parcelable to work, otherwise it wouldn't be able to recreate the createFragment() lambdas.

So,

        public static final Creator<FragmentIdentifier> CREATOR = new Creator<FragmentIdentifier>() {
            @Override
            public FragmentIdentifier createFromParcel(Parcel in) {
                return new FragmentIdentifier(in); // <-- abstract
            }

            @Override
            public FragmentIdentifier[] newArray(int size) {
                return new FragmentIdentifier[size];
            }
        };

Using @Parcelize data class for your concrete identifiers is preferred.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment