Livy
Livy

Reputation: 663

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.

Upvotes: 15

Views: 7806

Answers (2)

antonyt
antonyt

Reputation: 21883

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).

Upvotes: 17

Emmanuel
Emmanuel

Reputation: 13223

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).

Upvotes: 4

Related Questions