实例化新Android片段的最佳实践

Best practice for instantiating a new Android Fragment

我看到了两种在应用程序中实例化新片段的一般实践:

1
Fragment newFragment = new MyFragment();

1
Fragment newFragment = MyFragment.newInstance();

第二个选项使用静态方法newInstance(),通常包含以下方法。

1
2
3
4
5
public static Fragment newInstance()
{
    MyFragment myFragment = new MyFragment();
    return myFragment;
}

起初,我认为主要的好处是我可以重载newInstance()方法以在创建片段的新实例时提供灵活性,但我也可以通过为片段创建重载的构造函数来实现这一点。

我错过什么了吗?

一种方法比另一种方法有什么好处?或者只是个好的练习?


如果Android稍后决定重新创建片段,它将调用片段的无参数构造函数。因此,重载构造函数不是解决方案。

这么说来,把东西传递到你的片段中,以便在Android重新创建一个片段后它们可用,方法是将一个包传递到setArguments方法。

因此,例如,如果我们想将一个整数传递给片段,我们将使用如下方法:

1
2
3
4
5
6
7
8
9
public static MyFragment newInstance(int someInt) {
    MyFragment myFragment = new MyFragment();

    Bundle args = new Bundle();
    args.putInt("someInt", someInt);
    myFragment.setArguments(args);

    return myFragment;
}

稍后在片段onCreate()中,您可以使用以下方法访问该整数:

1
getArguments().getInt("someInt", 0);

即使这个片段是由Android以某种方式重新创建的,这个捆绑包也是可用的。

另请注意:只有在片段附加到活动之前,才能调用setArguments

这种方法也记录在Android开发者参考资料中:https://developer.android.com/reference/android/app/fragment.html


我看到的使用newInstance()的唯一好处是:

  • 您将有一个单独的地方,在那里片段使用的所有参数都可以打包,并且您不必每次实例化片段时都编写下面的代码。

    1
    2
    3
    4
    5
    Bundle args = new Bundle();
    args.putInt("someInt", someInt);
    args.putString("someString", someString);
    // Put any other arguments
    myFragment.setArguments(args);
  • 这是告诉其他类它希望哪些参数能够忠实地工作的一个好方法(尽管如果片段实例中没有绑定任何参数,您应该能够处理案例)。

  • 因此,我的看法是,使用静态newInstance()来实例化片段是一个很好的实践。


    还有另一种方法:

    1
    Fragment.instantiate(context, MyFragment.class.getName(), myBundle)


    虽然@yydl给出了一个令人信服的理由来解释为什么newInstance方法更好:

    If Android decides to recreate your Fragment later, it's going to call
    the no-argument constructor of your fragment. So overloading the
    constructor is not a solution.

    仍然可以使用构造函数。要了解这是为什么,首先我们需要了解为什么Android使用上述解决方案。

    在使用片段之前,需要一个实例。android调用YourFragment()(无参数构造函数)来构造片段的实例。这里,您编写的任何重载构造函数都将被忽略,因为Android不知道要使用哪个构造函数。

    在活动的生命周期中,碎片会像上面一样被创建,并被Android销毁多次。这意味着,如果将数据放入片段对象本身,那么一旦片段被销毁,它将丢失。

    为了解决这个问题,Android要求您使用Bundle(调用setArguments())存储数据,然后可以从YourFragment访问该数据。参数Bundles受Android保护,因此保证是持久的。

    设置这个包的一种方法是使用静态newInstance方法:

    1
    2
    3
    4
    5
    6
    7
    8
    public static YourFragment newInstance (int data) {
        YourFragment yf = new YourFragment()
        /* See this code gets executed immediately on your object construction */
        Bundle args = new Bundle();
        args.putInt("data", data);
        yf.setArguments(args);
        return yf;
    }

    但是,构造函数:

    1
    2
    3
    4
    5
    public YourFragment(int data) {
        Bundle args = new Bundle();
        args.putInt("data", data);
        setArguments(args);
    }

    可以做与newInstance方法完全相同的事情。

    当然,这会失败,这也是Android希望您使用newInstance方法的原因之一:

    1
    2
    3
    public YourFragment(int data) {
        this.data = data; // Don't do this
    }

    作为进一步的解释,这里是Android的fragment类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
     * 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;
    }

    请注意,android要求只在构造时设置参数,并保证这些参数将被保留。

    编辑:正如在@ JHH的注释中指出的,如果您提供了一个需要一些参数的自定义构造函数,那么Java不会为您的片段提供一个没有ARG的默认构造函数。因此,这需要您定义一个no-arg构造函数,这是使用newInstance工厂方法可以避免的代码。

    编辑:Android不再允许对片段使用重载的构造函数。您必须使用newInstance方法。


    我不同意YYDI的回答说:

    If Android decides to recreate your Fragment later, it's going to call
    the no-argument constructor of your fragment. So overloading the
    constructor is not a solution.

    我认为这是一个很好的解决方案,这正是Java核心语言开发的原因。

    的确,Android系统可以摧毁并重新创建你的Fragment。所以你可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public MyFragment() {
    //  An empty constructor for Android System to use, otherwise exception may occur.
    }

    public MyFragment(int someInt) {
        Bundle args = new Bundle();
        args.putInt("someInt", someInt);
        setArguments(args);
    }

    即使系统重新创建了Fragment,它也允许您从getArguments()后者中拉出someInt。这比static构造函数更优雅。

    依我看,static构造器是无用的,不应该使用。另外,如果将来您希望扩展这个Fragment,并向构造函数添加更多功能,它们也会限制您。使用static构造函数,您不能这样做。

    更新:

    Android增加了检查,将所有非默认构造函数标记为一个错误。出于上述原因,我建议禁用它。


    一些Kotlin代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    companion object {
        fun newInstance(first: String, second: String) : SampleFragment {
            return SampleFragment().apply {
                arguments = Bundle().apply {
                    putString("firstString", first)
                    putString("secondString", second)
                }
            }
        }
    }

    你可以用这个得到参数:

    1
    2
    val first: String by lazy { arguments?.getString("firstString") ?:"default"}
    val second: String by lazy { arguments?.getString("secondString") ?:"default"}


    在android中使用参数的实例片段的最佳实践是在片段中使用静态工厂方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static MyFragment newInstance(String name, int age) {
        Bundle bundle = new Bundle();
        bundle.putString("name", name);
        bundle.putInt("age", age);

        MyFragment fragment = new MyFragment();
        fragment.setArguments(bundle);

        return fragment;
    }

    您应该避免使用片段的实例设置字段。因为每当Android系统重新创建片段时,如果它觉得系统需要更多的内存,那么它将使用不带参数的构造函数重新创建片段。

    您可以在这里找到更多关于用参数实例化片段的最佳实践的信息。


    由于关于最佳实践的问题,我想补充一点,在使用一些RESTWeb服务时,使用混合方法创建片段通常是个好主意。

    为了显示用户片段,我们不能传递复杂的对象,例如一些用户模型。

    但我们能做的就是签入那个用户的onCreate!=null,如果不是,则从数据层将其引入,否则使用现有的。

    这样,我们既可以在Android进行碎片创建时通过user id重新创建,也可以在用户操作时快速创建,还可以通过保持对象本身或仅其id来创建碎片。

    类似这样的事情:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class UserFragment extends Fragment {
        public final static String USER_ID="user_id";
        private User user;
        private long userId;

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            userId = getArguments().getLong(USER_ID);
            if(user==null){
                //
                // Recreating here user from user id(i.e requesting from your data model,
                // which could be services, direct request to rest, or data layer sitting
                // on application model
                //
                 user = bringUser();
            }
        }

        public static UserFragment newInstance(User user, long user_id){
            UserFragment userFragment = new UserFragment();
            Bundle args = new Bundle();
            args.putLong(USER_ID,user_id);
            if(user!=null){
                userFragment.user=user;
            }
            userFragment.setArguments(args);
            return userFragment;

        }

        public static UserFragment newInstance(long user_id){
            return newInstance(null,user_id);
        }

        public static UserFragment newInstance(User user){
            return newInstance(user,user.id);
        }
    }


    setArguments()是无用的。它只会带来混乱。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class MyFragment extends Fragment {

        public String mTitle;
        public String mInitialTitle;

        public static MyFragment newInstance(String param1) {
            MyFragment f = new MyFragment();
            f.mInitialTitle = param1;
            f.mTitle = param1;
            return f;
        }

        @Override
        public void onSaveInstanceState(Bundle state) {
            state.putString("mInitialTitle", mInitialTitle);
            state.putString("mTitle", mTitle);
            super.onSaveInstanceState(state);
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
            if (state != null) {
                mInitialTitle = state.getString("mInitialTitle");
                mTitle = state.getString("mTitle");
            }
            ...
        }
    }


    实例化片段的最佳方法是使用默认片段。实例化方法或创建工厂方法来实例化片段。警告:总是在片段中创建一个空的构造函数,而还原片段内存将引发运行时异常。


    我相信我有一个更简单的解决办法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MyFragment extends Fragment{

       private String mTitle;
       private List<MyObject> mObjects;

       public static MyFragment newInstance(String title, List<MyObject> objects)
       MyFragment myFrag = new MyFragment();
       myFrag.mTitle = title;
       myFrag.mObjects = objects;
       return myFrag;
       }