Android片段工厂方法vs构造函数重载

时间:2014-05-31 04:04:20

标签: android android-fragments

首先,我已经知道FragmentManager经常会破坏然后使用默认构造函数重新创建Fragment。编码器必须在工厂方法中将重要事项保存在一组参数中,然后每次在 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);
    }
}

我的问题是,这是否有任何区别:

// 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();

而且:

// 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();

我什么都看不到阻止FragmentManager调用无参构造函数的东西。我保存在参数化构造函数(在Bundle对象中)的数据将在 onCreate()期间保存和恢复,就像我使用工厂方法时一样。

2 个答案:

答案 0 :(得分:15)

Android永远不会直接调用非默认构造函数(也不是工厂方法) - 从技术上讲,使用它并不重要。在添加片段之前,您可以随时调用setArguments(在任意方法中,甚至在构造函数中),如果重新创建片段,将为您保存/恢复该包。视图也有Android调用的特殊构造函数,但如果您愿意,可以使用任意参数创建自己的构造函数(它们只是不会被Android调用)。

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;
}

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);
        }
    }
当Android想要创建Fragment的实例时,会调用

Fragment.instantiate。它简单地调用Class.newInstance,这是一个使用默认的零参数构造函数创建类的Java方法。查看此代码,创建其他构造函数并在其中调用setArguments似乎没有问题。

作为一种惯例,在处理Fragments时通常使用工厂方法。大多数official sample Fragment code也使用工厂方法。以下是一些可能的原因:

  1. 如果您正在编写自定义构造函数(带参数),则还必须指定零参数构造函数。一个常见的错误是创建一个自定义构造函数但忘记定义一个零参数构造函数 - 当Android在重新创建片段时尝试调用零参数构造函数时,这将导致崩溃。

  2. 创建自定义构造函数时,您可能想直接将构造函数参数分配给字段。这就是编写任何其他Java类的原因(因此您自然希望如何编写类)。由于Android只会在Fragment上调用zero-arg构造函数,因此任何重新创建的实例都无法使用此数据。如您所知,使用setArguments是解决此问题的方法。即使您可以在构造函数中执行此操作,但使用工厂方法会更明显地无法以正常方式构造此类,从而降低了提交上述错误(或类似)的可能性。

答案 1 :(得分:3)

FragmentManager实现调用Fragment的默认构造函数。我认为能够确定哪些参数传递给非默认构造函数会涉及很多开销,因此Android团队决定采用Bundle路由。如果您使用非默认构造函数,则传递给它的数据将不会在重新创建期间保留,因此您最终会得到null个指针。使用setArguments() / getArguments()机制,您可以保证FragmentManager能够正确初始化Fragment

当你这样做时:

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

将保证第一次没问题。现在,假设用户旋转了屏幕(setRetainInstance()未设置),FragmentManager将通过调用Fragment创建new MyFragment(); //the default constructor. 的新实例。

null

这意味着应该在非默认构造函数中初始化的所有变量都是Fragment

文档告诉我们要避免重载setArguments()的构造函数,我会坚持他们的规则。如果您尝试重载构造函数,则可能会遇到一些意外行为(即使您在重载的构造函数中执行{{1}})。