实例化Android Fragment的最佳实践

782

我看到在应用程序中实例化新 Fragment 有两种常见的做法:

Fragment newFragment = new MyFragment();

并且

Fragment newFragment = MyFragment.newInstance();

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

public static Fragment newInstance() 
{
    MyFragment myFragment = new MyFragment();
    return myFragment;
}

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

我有什么遗漏吗?

一种方法的优势在于另一种方法吗?还是说只是好的实践?


当存在参数时,就没有选择余地了,这在这里已经得到了广泛的回答。然而,对于片段的无参构造仍然存在疑问。 - rds
1
在了解了工厂模式以及调用类不实例化对象本身如何帮助解耦之后,我认为这对于newInstance()方法来说是一个强有力的优点。我错了吗?我还没有看到过这个特定的论点被提出作为一个好处。 - Mobile Applications
16个回答

1258

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

话虽如此,将东西传递给Fragment的方法,以便在Android重新创建Fragment后它们仍然可用,是将Bundle传递给setArguments方法。

因此,例如,如果我们想将整数传递给Fragment,我们可以使用以下内容:

public static MyFragment newInstance(int someInt) {
    MyFragment myFragment = new MyFragment();

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

    return myFragment;
}

然后在Fragment的onCreate()方法中,你可以通过以下方式访问该整数:

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

即使 Fragment 被 Android 重新创建,此 Bundle 仍将可用。

还要注意: setArguments 只能在 Fragment 连接到 Activity 之前调用。

这种方法也在 Android 开发者文档中有记录:https://developer.android.com/reference/android/app/Fragment.html


7
很遗憾,静态方法无法被覆盖。 - AJD
11
@yydl,我觉得我可能有所遗漏,你无论如何都可以在这里使用一个构造函数来创建 Bundle 并调用 setArguments() 方法,因为它只会被你的代码调用(而不是在 Android 重新创建你的 fragment 时调用)。 - Mike Tunnicliffe
10
@mgibson 如果您希望在稍后重新创建片段时数据仍然可用,那么您必须使用bundle。 - yydl
141
被迫为片段创建一个无参构造函数可能是编程中最大的陷阱之一,这强制了完全不同的对象创建和初始化范式转变。如果你是 Android 的新手,并且偶然看到了这个帖子,请反复阅读上面的答案。 - rmirabelle
13
我不同意那个说法。首先,类型安全是语言问题,而不是框架问题。其次,在我看来,这个框架已经涉及到了“你的API绝不能做的事情”的领域。如果我想将国会图书馆传递到我的片段构造函数中,那么我应该被允许这样做。零参数构造函数协议基本上杀死了在片段中使用依赖注入的可能性 - 这很糟糕。 - rmirabelle
显示剩余28条评论

101

我认为使用newInstance()的唯一好处如下:

  1. 你可以将所有用于片段的参数捆绑到一个单独的位置,这样每次实例化片段时就不必再写下面的代码了。

Bundle args = new Bundle();
args.putInt("someInt", someInt);
args.putString("someString", someString);
// Put any other arguments
myFragment.setArguments(args);
  • 这是告诉其他类它期望哪些参数能够正常工作的好方法(虽然如果在片段实例中没有绑定任何参数,你应该能够处理这种情况)。

  • 因此,我认为使用静态的newInstance()来实例化一个片段是一种好的做法。


    7
    1)这与将逻辑放在构造函数中有何不同?两者都是包含此逻辑的单个位置。 2)静态工厂中的参数与构造函数中的参数有何不同?两者都说明了期望的参数。我的观点是,虽然这是一种不同的范式,但与使用构造函数相比,没有明显的好处。 - RJ Cuthbertson
    4
    你不能为片段使用自定义构造函数。框架在恢复片段时使用无参构造函数。 - 500865
    6
    是的,我同意你的观点。我是说在概念上使用静态工厂模式与使用重载构造函数没有任何优劣之分。你提出的两个观点在这两种模式中都是有效的,没有一种方法比另一种更有利。Android强制使用静态工厂模式-但是使用其中任何一种都没有优势。 - RJ Cuthbertson
    @RJCuthbertson 可能的好处是能够创建和返回静态工厂方法类的子类,即为特定情况返回适当的子类。 - urgentx

    68

    还有另一种方法:

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

    2
    尝试使用支持库,但在我的片段中的onCreateView方法中,传递的bundle为空,所以我选择了setArguments/getArguments选项,并且它起作用了(对于任何阅读此内容的人)。 - Jonathan Apodaca
    1
    有趣,我以前没有见过这种方法。相比于其他实例化Fragment的方法,它有什么优势吗? - IgorGanapolsky
    25
    根据开发人员文档,“instantiate()”方法可以创建一个带有给定类名的新Fragment实例。这与调用它的空构造函数相同。 - anon
    2
    虽然它们提到了与调用空构造函数相同的内容。但是在处理Bundle参数时,"args.setClassLoader(f.getClass().getClassLoader());" 会被调用。 - Gökhan Barış Aker
    4
    API 28中,instantiate(...)方法已被弃用。此外,我认为创建片段的方法并不好。 - Yekta Sarıoğlu
    显示剩余6条评论

    55

    虽然@yydl提供了关于newInstance方法更好的原因:

    如果Android决定稍后重新创建您的Fragment,则会调用您的fragment的无参构造函数。因此,重载构造函数并不是一个解决方案。

    但仍然可以使用构造函数。为了理解这一点,首先需要了解Android使用上述解决方法的原因。

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

    在Activity的生命周期中,Android多次创建并销毁片段。这意味着,如果将数据放入片段对象本身中,则一旦销毁片段,该数据将丢失。

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

    设置此bundle的一种方法是使用静态的newInstance方法:

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

    然而,构造函数:

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

    可以完全像newInstance方法一样执行。

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

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

    作为进一步的解释,这里是Android中的Fragment类:
    /**
     * 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不会为您的片段提供无参默认构造函数。因此,这将要求您定义一个无参构造函数,这是您可以通过使用“newInstance”工厂方法避免的代码。
    编辑:Android不再允许对片段使用重载构造函数。您必须使用“newInstance”方法。

    什么情况下可以使用 android:configChanges="orientation|keyboardHidden|screenSize" ? - Luke Allison
    1
    Android Studio现在对片段中的所有非默认构造函数抛出错误,因此这不再起作用。 - Sheharyar
    6
    圣人啊,我想知道有多少机器人开发者曾经写过非机器人的代码。我们无法使用您所描述的方法是疯狂的。在任何评论中都没有令人信服的理由说明为什么我们必须使用静态工厂方法。更令人不安的是编译时会出现错误。这绝对是提供的最佳答案,表明静态工厂方法没有任何好处。 - MPavlak
    3
    有一个微妙的原因。你可以自由地创建带参数的构造函数,但仍然需要有一个无参构造函数。因为类始终具有隐式的无参构造函数,除非显式定义了带参数的构造函数。这意味着你必须显式地定义你的带参数构造函数和一个无参构造函数,否则系统将无法调用任何无参构造函数。我认为这就是为什么建议使用静态工厂方法的原因 - 这简单地减少了忘记定义无参构造函数的风险。 - JHH
    @JHH 这将在编译时失败,因此风险并不是很大。然而,问题在于 Android 拒绝了构造函数重载,这是一个关键的编程范例。 - xyz
    显示剩余3条评论

    35

    一些 kotlin 代码:

    companion object {
        fun newInstance(first: String, second: String) : SampleFragment {
            return SampleFragment().apply {
                arguments = Bundle().apply {
                    putString("firstString", first)
                    putString("secondString", second)
                }
            }
        }
    }
    

    你可以使用以下内容来获取参数:

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

    使用@JvmStatic注解是最佳实践吗? @JvmStatic fun newInstance(bundle: Bundle) = SomeFragment().apply { arguments = bundle } - AdamHurwitz

    21

    不赞同yydi在回答中所说的:

    如果Android决定以后重新创建你的Fragment,它会调用你的fragment的无参构造函数。因此,重载构造函数并不是一个解决方案。

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

    的确,Android系统可能会销毁和重建你的Fragment。所以你可以这样做:

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

    它将使您能够在稍后从getArguments()中提取someInt,即使系统重建了Fragment。这比使用static构造函数更为优雅。

    在我看来,static构造函数是无用的,不应该被使用。此外,如果将来想要扩展此Fragment并向构造函数添加更多功能,它们会对您造成限制。使用static构造函数就不能实现这一点。

    更新:

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


    5
    拥有静态方法的另一个好处是,您无法意外地从中设置属性,这一点我在之前没有提到。 - yydl
    5
    此外,关于您提到的“扩展这个片段”的观点,如果您要扩展这个类,这种方法实际上会很糟糕。调用super将导致setArguments()调用仅对子类或父类中的一个生效,而不是同时生效。 - yydl
    2
    @yydle,您可以通过调用get arguments来初始化子Bundle来避免这种情况。Java方式总是更好的选择。 - Ilya Gazman
    10
    没错,这是鼓励人们使用Google提出的模式的另一个原因。当然,我们都同意你的解决方案在技术上是100%可行的。就像很多事情有很多做法一样,问题在于是否它是最好的。我强烈认为使用构造函数并不代表这个功能的真正本质。 - yydl
    4
    我同意@yydl所说的静态创建更好。 另一个好处是可以注入未来新增的依赖项 - 构造函数不适合此操作,可能会导致更多的代码更改(或添加更多构造函数)。 - Boon
    显示剩余14条评论

    5

    我最近来到这里。但是有些东西我刚刚知道,可能会对您有所帮助。

    如果您正在使用Java,则没有太多需要更改的内容。但是对于Kotlin开发人员,以下代码片段可能会为您提供一个基础:

    • 父碎片:
    inline fun <reified T : SampleFragment> newInstance(text: String): T {
        return T::class.java.newInstance().apply {
            arguments = Bundle().also { it.putString("key_text_arg", text) }
        }
    }
    
    • 常规调用
    val f: SampleFragment = SampleFragment.newInstance("ABC")
    // or val f = SampleFragment.newInstance<SampleFragment>("ABC")
    

    您可以通过以下方式来扩展子片段类中的父init操作:
    • 您可以通过以下方式来扩展子片段类中的父init操作:
    fun newInstance(): ChildSampleFragment {
        val child = UserProfileFragment.newInstance<ChildSampleFragment>("XYZ")
        // Do anything with the current initialized args bundle here
        // with child.arguments = ....
        return child
    }
    
    

    愉快的编码。


    3

    在Android中实例化带参数的片段的最佳做法是在片段中拥有一个静态工厂方法。

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

    您应该避免使用Fragment的实例来设置您的字段。因为每当Android系统重新创建您的Fragment时,如果它感觉系统需要更多的内存,那么它将使用没有参数的构造函数重建您的Fragment。
    您可以在此处找到有关最佳实践来实例化带参数的Fragment的更多信息。

    重点不在于静态工厂方法,重点在于捆绑。只要不忘记实现无参构造函数,它与通过自有构造函数的参数进行设置是相同的。 - The incredible Jan
    是的,关键是捆绑包,就像答案中所说的那样。最好使用静态方法来传递参数,以防止忘记添加必需字段的情况发生。 - undefined

    2

    针对最佳实践的问题,我想补充一下,在使用某些 REST web 服务时,通常使用混合方法来创建片段是一个很好的主意。

    我们无法传递复杂对象,例如某个用户模型,以便显示用户片段。

    但是我们可以在 onCreate 中检查 user!=null,如果不是,则从数据层获取它,否则使用现有的。

    这样,我们既能够通过 userId 在 Android 中重新创建片段,又能够让用户操作更加迅速,并且还能够通过保存对象本身或仅其 id 来创建片段。

    类似于这样:

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

    3
    你说:“我们无法传递复杂对象,例如一些用户模型”,这是不正确的,我们可以。像这样:User user = /*...*/; //将用户放入Bundle: Bundle bundle = new Bundle(); bundle.putParcelable("some_user", user); //从参数中获取用户: User user = getArguments().getParcelable("some_user");对象必须实现Parcelable接口。链接 - Adam Varhegyi
    3
    嗯,是的,但当类别复杂且包含对其他对象的引用时...我个人更喜欢保持简单,要么我有对象,要么我没有,然后需要获取它。 - Tigra

    1
    1. 理想情况下,我们不应该在片段构造函数中传递任何内容,片段构造函数应该为空或默认。
    2. 现在的第二个问题是,如果我们想传递接口变量或参数怎么办-
      1. 我们应该使用Bundle来传递数据。
      2. 对于接口,我们可以在bundle中使用putParceble并使该接口实现parceble
      3. 如果可能,我们可以在活动中实现该接口,在片段中,我们可以在OnAttach中初始化监听器,在那里我们有上下文[(上下文) Listener]。

    这样,在配置更改(例如字体更改)期间,Activity重建侦听器将不会未初始化,并且我们可以避免空指针异常。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接