Create fragment with custom object

Viewed 174

I was creating a fragment with a custom object this way:

public class SlideFragment extends Fragment {

    SlideData data;

    public static SlideFragment newInstance(SlideData data) {
        SlideFragment fragment = new SlideFragment(data);
        return fragment;
    }

    public SlideFragment() {
    }

    public SlideFragment(SlideData data) {
        this.data = data;
    }

    ...
}

But I realized that this is wrong and it crashes if Android recreates the fragment (it uses the default constructor so data is null).

Reading about this I've seen that I should do something like this:

    public class SlideFragment extends Fragment {
    
        SlideData data;
    
        public static SlideFragment newInstance(SlideData data) {
            SlideFragment fragment = new SlideFragment(data);
            Bundle b = new Bundle();
            b.putParcelable("data", data);
            fragment.setArguments();
            return fragment;
        }
    
        public SlideFragment() {
        }
    
        public SlideFragment(SlideData data) {
            this.data = data;
        }
    
       @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    
            View _fragment = inflater.inflate(R.layout.fragment_slide, container, false);
            Bundle b = getArguments();
            data = b.getParcelable("data");
            return _fragment;
        }
    ...
}

So I should make SlideData as a Parcelable Object, but the problem is that this object is a bit complex:

public class SlideData {

    List<SlideMessageData> messages;
    int backgroundColor;
    SlideButtonType buttonType;
    SlideButtonInterface listener;
}

public class SlideMessageData {

    String text;
    int textColour;
}

public enum SlideButtonType {
    CLOSE,
    PREVIOUS,
    NEXT
}

public interface SlideButtonInterface {
    void clickOnSlideButton();
}

To make this parcelable I was thinking to use:

  • writeList and readList for "messages".
  • writeInt and readInt for "backgroundColor" and "textColor".
  • writeString and readString for "text".
  • writeInt(buttonType.ordinal()) and SlideButtonType.values()[in.readInt()] for "buttonType".

But there is no way to make parcelable the "listener" attribute (it's normal, it's no sense).

So, how could I solve this problem? How could I make persistent my slidedata if the fragment is recreated by Android?

UPDATE:

Taking into account game over comment I've tried to subclass FragmentFactory class, but in this case I can't do it. I use FactoryFragment to create my fragments, but this case is special. I'm creating this fragment for a FragmentAdapter.

I have this:

public class SlideAdapter extends FragmentStatePagerAdapter {

    ...

    public void addSlide(SlideData data)
    {
        SlideFragment slide = SlideFragment.newInstance(data);
        listOfFragments.add(slide);
        notifyDataSetChanged();
    }

    ...
}

And this (I've added more data after game over comment):

public class SlideViewerFragment extends Fragment {

    public static SlideViewerFragment newInstance() {
        return new SlideViewerFragment();
    }

    public SlideViewerFragment() {
    }

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if(savedInstanceState != null)
            data = savedInstanceState.getBundle("data");
    }

    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBundle("data", data);
    }

    public void setData(Bundle _data)
    {
        data = _data;
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        ... 

       FragmentActivity activity = getActivity();
       FragmentManager fragmentManager = activity.getSupportFragmentManager();
       mSlideAdapter = new SlideAdapter(fragmentManager);
       adapter.setValue(mSlideAdapter);
       slideList = viewModel.getSlideListData();
       slideList.observe(myActivity, this::slideListChanged);

        return view;
    }

    private void buildSlides() {

        mSlideAdapter.clearSlides();
        for(int index = 0; index < slides.size(); index++)
        {
            SlideData slide = slides.get(index);
            mSlideAdapter.addSlide(slide);
        }
    }

   ...
}

And I'm creating my SlideViewerFragment this way:

    final FragmentManager fm = getSupportFragmentManager();
    FragmentFactory ff = fm.getFragmentFactory();
    Fragment fr = ff.instantiate(getClassLoader(), SlideViewerFragment.class.getName());
    FragmentTransaction ft = fm.beginTransaction();
    ft.setReorderingAllowed(true);
    ft.add(R.id.frame_content, fr, SlideViewerFragment.class.getName());

    Bundle _data = new Bundle();
    _data.putInt("content_type", 2);
    ((BaseFragment) fr).setData(_data);

    ft.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
    ft.commit();
    fm.executePendingTransactions();

Since I'm not using here a FragmentFactory, how could I recreate my SlideFragment in a safe manner?

Maybe the problem is in how I'm building SlideViewerFragment using instantiate? Should I use SlideViewerFragment.newInstance(data) and subclass FragmentFactory to create SlideViewerFragment using the constructor with arguments?

1 Answers

Ok so your issue is that yes,

1.) you need to pass in the data as arguments into the Fragment, otherwise process death will eat it

but

2.) FragmentPagerAdapter/FragmentStatePagerAdapter only instantiates a Fragment in getItem() when it is not yet added, so using something like pagerAdapter.addFragment(fragment) is an anti-pattern that will kill your code. If you want to dynamically create objects, you would need to pass in SlideData to the adapter, and create a SlideFragment in pagerAdapter.getItem() for the provided position, using the SlideData as arguments from the correct position. This SlideData must be constructed correctly even after process death for this to work.

Alternately, you can initialize a ViewPager with 0 and then dynamically add fragments as long as you use the following code:

public class DynamicFragmentPagerAdapter extends PagerAdapter {
    private static final String TAG = "DynamicFragmentPagerAdapter";
  
    private final FragmentManager fragmentManager;
  
    public static abstract class FragmentIdentifier implements Parcelable {
        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");
    }
}

3.) you cannot pass a listener to a Fragment. There are 3 ways to communicate from a fragment:

  • a.) via fragment.setTargetFragment(), but this was deprecated just the other day.

  • b.) via FragmentResultListener, but there is no guarantee that your Fragment will be created by a Fragment, and not an Activity. Also, an adapter would never terminate "for result", so this does not work in this scenario.

  • c.) the SlideFragment should expose an interface SlideFragment.SlideButtonInterface that is implemented by either a parent fragment OR the host activity.

For that, you need to do a hierarchical lookup:

private SlideButtonInterface slideButtonInterface = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Fragment parentFragment = null;
    while((parentFragment = getParentFragment()) != null) {
        if(parentFragment instanceof SlideButtonInterface) {
            this.slideButtonInterface = (SlideButtonInterface) parentFragment;
            break;
        }
    }

    if(slideButtonInterface == null) {
        slideButtonInterface = (SlideButtonInterface)getActivity();
    }
}

This way, either a parent fragment or an activity can handle the events of this fragment, without having to pass it in explicitly from outside.

Related