Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android fragment factory method vs constructor overloading

First of all, I already know that the FragmentManager often destroys then re-creates the Fragment using the default constructor. The coders must save important things in a Bundle of arguments once in the factory method, then take them out every time the Fragment is re-created in onCreate(Bundle).

public class MyFragment extends Fragment {
    private static final String MY_STRING_CONSTANT = "param";
    private int mIntegerMember;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mIntegerMember= getArguments().getInt(MY_STRING_CONSTANT);
    }
}

My question is, is there any difference between this:

// Inside MyFragment.java
public MyFragment() {
    // No-argument constructor required by the FragmentManager.
}
public static MyFragment newInstance(int param) {
    // Factory method
    MyFragment fragment = new MyFragment();
    Bundle args = new Bundle();
    args.putInt(MY_STRING_CONSTANT, param);
    fragment.setArguments(args);
    return fragment;
}

// Somewhere else
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.add(R.id.frame_layout, MyFragment.newInstance(123)).commit();

And this:

// Inside MyFragment.java
public MyFragment() {
    // No-argument constructor required by the FragmentManager.
}
public MyFragment(int param) {
    // Parameterized constructor
    Bundle args = new Bundle();
    args.putInt(MY_STRING_CONSTANT, param);
    setArguments(args);
}

// Somewhere else
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.add(R.id.frame_layout, new MyFragment(123)).commit();

I can see nothing which prevents the FragmentManager from calling the no-argument constructor. And the data I save in the parameterized constructor (in a Bundle object) will be preserved and restore during onCreate(), just like when I use the factory method.

like image 858
Livy Avatar asked May 31 '14 04:05

Livy


People also ask

Why is it recommended to use only the default constructor to create a fragment?

Traditionally, a Fragment instance could only be instantiated using its default empty constructor. This is because the system would need to reinitialize it under certain circumstances like configuration changes and the app's process recreation.

What is a fragment factory?

FragmentFactory is relatively recent addition to Android toolbox which allows you to use non-default constructors (i.e. constructors with arguments) to instantiate your Fragments. While this might sound like a small nuance, it is, in fact, a major step forward.

How do you instantiate a fragment?

In this example, to instantiate MyFragment you need to call; Fragment fragment = MyFragment. newInstance("Gunhan", 28); If android decides to recreate your activity and fragment, it is going to call no-argument constructor and the system guarantees to pass the arguments bundle to it.

What are fragments in Android?

A Fragment represents a reusable portion of your app's UI. A fragment defines and manages its own layout, has its own lifecycle, and can handle its own input events. Fragments cannot live on their own--they must be hosted by an activity or another fragment.


2 Answers

Android never directly invokes a non-default constructor (nor a factory method) - technically, it doesn't really matter which you use. You can call setArguments (in an arbitrary method, even in a constructor) any time before you add the Fragment and that bundle will be saved/restored for you if the Fragment is recreated. Views also have special constructors invoked by Android, but you can make your own with arbitrary arguments if you wish (they just won't get invoked by Android).

Code for Fragment.setArguments:

/**
 * Supply the construction arguments for this fragment.  This can only
 * be called before the fragment has been attached to its activity; that
 * is, you should call it immediately after constructing the fragment.  The
 * arguments supplied here will be retained across fragment destroy and
 * creation.
 */
public void setArguments(Bundle args) {
    if (mIndex >= 0) {
        throw new IllegalStateException("Fragment already active");
    }
    mArguments = args;
}

Code for Fragment.instantiate:

    /**
     * Create a new instance of a Fragment with the given class name.  This is
     * the same as calling its empty constructor.
     *
     * @param context The calling context being used to instantiate the fragment.
     * This is currently just used to get its ClassLoader.
     * @param fname The class name of the fragment to instantiate.
     * @param args Bundle of arguments to supply to the fragment, which it
     * can retrieve with {@link #getArguments()}.  May be null.
     * @return Returns a new fragment instance.
     * @throws InstantiationException If there is a failure in instantiating
     * the given fragment class.  This is a runtime exception; it is not
     * normally expected to happen.
     */
    public static Fragment instantiate(Context context, String fname, Bundle args) {
        try {
            Class<?> clazz = sClassMap.get(fname);
            if (clazz == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = context.getClassLoader().loadClass(fname);
                sClassMap.put(fname, clazz);
            }
            Fragment f = (Fragment)clazz.newInstance();
            if (args != null) {
                args.setClassLoader(f.getClass().getClassLoader());
                f.mArguments = args;
            }
            return f;
        } catch (ClassNotFoundException e) {
            throw new InstantiationException("Unable to instantiate fragment " + fname
                    + ": make sure class name exists, is public, and has an"
                    + " empty constructor that is public", e);
        } catch (java.lang.InstantiationException e) {
            throw new InstantiationException("Unable to instantiate fragment " + fname
                    + ": make sure class name exists, is public, and has an"
                    + " empty constructor that is public", e);
        } catch (IllegalAccessException e) {
            throw new InstantiationException("Unable to instantiate fragment " + fname
                    + ": make sure class name exists, is public, and has an"
                    + " empty constructor that is public", e);
        }
    }

Fragment.instantiate is called when Android wants to create an instance of your Fragment. It simple calls through to Class.newInstance, which is a Java method to create a class using the default, zero-arg constructor. Looking at this code, there seems to be no problem with creating additional constructor and in calling setArguments within it.

As an a convention, it is typical to use a factory method when working with Fragments. Most official sample Fragment code also uses factory methods. Here are some possible reasons why:

  1. If you are writing a custom constructor (with arguments), you will have to specify a zero-arg constructor as well. A common mistake is to create a custom constructor but forget to define a zero-arg constructor - this will lead to a crash when Android tries to invoke the zero-arg constructor when recreating your Fragment.

  2. When creating a custom constructor, you may be tempted to directly assign the constructor arguments to fields. This is how pretty much any other Java class is written (and therefore how you will naturally want to write classes). Since Android will only invoke the zero-arg constructor on a Fragment, this data will not be available to any recreated instances. As you already know, using setArguments is the way to solve this. Even though you can do this within a constructor, using a factory method makes it more obvious that this class cannot be constructed in a normal way, reducing the possibility of committing the above mistake (or similar).

like image 114
antonyt Avatar answered Sep 28 '22 07:09

antonyt


The FragmentManager implementation calls the default constructor of the Fragment. I would think that it would involve a lot of overhead to be able to determine which arguments to pass to a non-default constructor, so the Android team decided to go the Bundle route. If you do use a non-default constructor the data you pass to it, will not be kept during recreation, hence you will end up with null pointers. By using the setArguments()/getArguments() mechanism you guarantee that FragmentManager will initialize the Fragment correctly.

When you do this call:

transaction.add(R.id.frame_layout, new MyFragment(123));

it will be guaranteed that the first time will be fine. Now, lets say that the user rotates the screen (setRetainInstance() is not set), the FragmentManager will create a new instance of the Fragment by calling:

new MyFragment(); //the default constructor.

This means that all the variables that were supposed to be initialized in the non-default constructor will be null.

The docs tell us to avoid overloading Fragment's constructor, I would stick to their rules. You might get some unexpected behavior if you try to overload the constructor (even if you do setArguments() in the overloaded constructor).

like image 28
Emmanuel Avatar answered Sep 28 '22 07:09

Emmanuel