【问题标题】:Do fragments really need an empty constructor?片段真的需要一个空的构造函数吗?
【发布时间】:2023-04-07 02:40:01
【问题描述】:

我有一个Fragment,它的构造函数接受多个参数。我的应用在开发过程中运行良好,但在生产中我的用户有时会看到此崩溃:

android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment 
make sure class name exists, is public, and has an empty constructor that is public

我可以按照此错误消息的提示创建一个空的构造函数,但这对我来说没有意义,因为那时我必须调用一个单独的方法来完成 Fragment 的设置。

我很好奇为什么这种崩溃只是偶尔发生。也许我错误地使用了ViewPager?我自己实例化了所有Fragments 并将它们保存在Activity 内的列表中。我不使用FragmentManager 事务,因为我看到的ViewPager 示例不需要它,并且在开发过程中一切似乎都在工作。

【问题讨论】:

  • 在某些版本的android(至少是ICS)中,您可以转到设置-> 开发人员选项并启用“不保留活动”。这样做将为您提供一种确定性的方法来测试需要无参数构造函数的情况。
  • 我遇到了同样的问题。我将捆绑数据分配给成员变量(使用非默认 ctor)。当我杀死应用程序时,我的程序并没有崩溃——它只是在调度程序将我的应用程序放在后台以“节省空间”时才发生。我发现这一点的方法是转到 Task Mgr 并打开大量其他应用程序,然后在调试中重新打开我的应用程序。每次都崩溃。当我使用 Chris Jenkins 回答使用 bundle args 时,这个问题得到了解决。
  • 您可能对此主题感兴趣:stackoverflow.com/questions/15519214/…
  • 对未来读者的附注:如果您的 Fragment 子类根本没有声明任何构造函数,那么默认情况下会为您隐式生成一个 空的公共构造函数 (这是standard Java behavior)。您确实不必必须显式声明一个空构造函数,除非您还声明了其他构造函数(例如带参数的构造函数)。
  • 我只想提一下,IntelliJ IDEA,至少对于 14.1 版,提供了一个警告,提醒您片段中不应有非默认构造函数。

标签: android android-fragments


【解决方案1】:

是的。

你不应该真的重写构造函数。您应该定义一个newInstance() 静态方法并通过参数(包)传递任何参数

例如:

public static final MyFragment newInstance(int title, String message) {
    MyFragment f = new MyFragment();
    Bundle bdl = new Bundle(2);
    bdl.putInt(EXTRA_TITLE, title);
    bdl.putString(EXTRA_MESSAGE, message);
    f.setArguments(bdl);
    return f;
}

当然也可以这样获取参数:

@Override
public void onCreate(Bundle savedInstanceState) {
    title = getArguments().getInt(EXTRA_TITLE);
    message = getArguments().getString(EXTRA_MESSAGE);

    //...
    //etc
    //...
}

然后你会像这样从片段管理器中实例化:

@Override
public void onCreate(Bundle savedInstanceState) {
    if (savedInstanceState == null){
        getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.content, MyFragment.newInstance(
                R.string.alert_title,
                "Oh no, an error occurred!")
            )
            .commit();
    }
}

这样,如果分离并重新附加,对象状态可以通过参数存储。很像附加到 Intent 的捆绑包。

原因 - 额外阅读

我想我会为想知道为什么的人解释原因。

如果你检查:https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/Fragment.java

你会看到Fragment类中的instantiate(..)方法调用了newInstance方法:

public static Fragment instantiate(Context context, String fname, @Nullable 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);
            if (!Fragment.class.isAssignableFrom(clazz)) {
                throw new InstantiationException("Trying to instantiate a class " + fname
                        + " that is not a Fragment", new ClassCastException());
            }
            sClassMap.put(fname, clazz);
        }
        Fragment f = (Fragment) clazz.getConstructor().newInstance();
        if (args != null) {
            args.setClassLoader(f.getClass().getClassLoader());
            f.setArguments(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);
    } catch (NoSuchMethodException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": could not find Fragment constructor", e);
    } catch (InvocationTargetException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": calling Fragment constructor caused an exception", e);
    }
}

http://docs.oracle.com/javase/6/docs/api/java/lang/Class.html#newInstance() 解释为什么在实例化时它会检查访问器是 public 并且该类加载器允许访问它。

总的来说,这是一个非常讨厌的方法,但它允许FragmentManger 杀死并重新创建带有状态的Fragments。 (Android 子系统使用Activities 做类似的事情)。

示例类

我被问到很多关于致电newInstance 的问题。不要将此与类方法混淆。这个整个类的例子应该显示用法。

/**
 * Created by chris on 21/11/2013
 */
public class StationInfoAccessibilityFragment extends BaseFragment implements JourneyProviderListener {

    public static final StationInfoAccessibilityFragment newInstance(String crsCode) {
        StationInfoAccessibilityFragment fragment = new StationInfoAccessibilityFragment();

        final Bundle args = new Bundle(1);
        args.putString(EXTRA_CRS_CODE, crsCode);
        fragment.setArguments(args);

        return fragment;
    }

    // Views
    LinearLayout mLinearLayout;

    /**
     * Layout Inflater
     */
    private LayoutInflater mInflater;
    /**
     * Station Crs Code
     */
    private String mCrsCode;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCrsCode = getArguments().getString(EXTRA_CRS_CODE);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mInflater = inflater;
        return inflater.inflate(R.layout.fragment_station_accessibility, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mLinearLayout = (LinearLayout)view.findViewBy(R.id.station_info_accessibility_linear);
        //Do stuff
    }

    @Override
    public void onResume() {
        super.onResume();
        getActivity().getSupportActionBar().setTitle(R.string.station_info_access_mobility_title);
    }

    // Other methods etc...
}

【讨论】:

  • 如果您暂停或销毁活动。因此,您转到主屏幕,然后 Android 会终止 Activity 以节省空间。片段状态将被保存(使用参数)然后 gc 对象(通常)。因此,在返回活动时,片段应该尝试使用保存的状态重新创建,new Default() 然后 onCreate 等......此外,如果活动试图节省资源(低内存电话)它可能会删除刚刚暂停的对象.. Commonsguy应该能够更好地解释。总之你不知道! :)
  • @mahkie 真的,如果您需要大量对象/模型,您应该从数据库或 ContentProvider 异步获取它们。
  • @Chris.Jenkins 对不起,如果我不清楚......我的意思是,与活动不同,片段并没有明确指出构造函数不能用于传递/共享数据。虽然转储/恢复很好,但我相信保存多个数据副本有时会占用比视图破坏可以重新获得的更多内存。在某些情况下,可以选择将活动/片段的集合视为一个单元,将其作为一个整体销毁或根本不销毁 - 然后我们可以通过构造函数传递数据。目前,关于这个问题,空的构造函数是唯一的。
  • 您为什么要持有多个数据副本? Bundles|Parcelable 实际上会在状态/片段/活动之间传递内存引用(实际上它会导致一些奇怪的状态问题),Parcelable 真正有效地“复制”数据的唯一时间是在进程和整个生命周期之间。例如。如果您将对象从活动传递给片段,则传递的引用不是克隆。您唯一真正的额外开销是额外的片段对象。
  • @Chris.Jenkins 好吧,那是我对 Parcelable 的无知。阅读了 Parcelable 的简短 javadoc,以及 Parcel 的一部分,离“重构”一词不远,我还没有到达“活动对象”部分,得出的结论是它只是一个低级的更优化但通用性较差的可序列化。我特此戴上羞耻的帽子,喃喃自语“仍然不能分享不可包裹的物品,制作可包裹物品会很麻烦”:)
【解决方案2】:

正如 CommonsWare 在这个问题 https://stackoverflow.com/a/16064418/1319061 中所指出的,如果您正在创建 Fragment 的匿名子类,也会发生此错误,因为匿名类不能有构造函数。

不要创建 Fragment 的匿名子类 :-)

【讨论】:

  • 或者,正如 CommonsWare 在那篇文章中提到的那样,确保将内部 Activity/Fragment/Reciever 声明为“静态”以避免此错误。
【解决方案3】:

是的,正如您所见,支持包也实例化了片段(当它们被破坏并重新打开时)。您的 Fragment 子类需要一个公共的空构造函数,因为这是框架调用的内容。

【讨论】:

  • 空片段构造函数是否应该调用 super() 构造函数?我问这个是因为我发现空的公共构造函数是强制性的。如果调用 super() 对空的公共构造函数没有意义
  • @TNR 因为所有 Fragment 抽象都有一个空的构造函数 super() 将是徒劳的,因为父类违反了空的公共构造函数规则。所以不,你不需要在你的构造函数中传递super()
  • 事实上,不需要在 Fragment 中显式定义一个空的构造函数。无论如何,每个 Java 类都有一个隐式的默认构造函数。取自:docs.oracle.com/javase/tutorial/java/javaOO/constructors.html ~ “编译器会自动为任何没有构造函数的类提供无参数的默认构造函数。”
【解决方案4】:

看看官方文档:Fragment:https://developer.android.com/reference/android/app/Fragment

Fragment 的所有子类都必须包含一个公共的无参数构造函数。框架通常会在需要时重新实例化一个片段类,特别是在状态恢复期间,并且需要能够找到这个构造函数来实例化它。如果无参构造函数不可用,在状态恢复过程中某些情况下会出现运行时异常。

【讨论】:

    【解决方案5】:

    这是我的简单解决方案:

    1 - 定义你的片段

    public class MyFragment extends Fragment {
    
        private String parameter;
    
        public MyFragment() {
        }
    
        public void setParameter(String parameter) {
            this.parameter = parameter;
        } 
    }
    

    2 - 创建新片段并填充参数

        myfragment = new MyFragment();
        myfragment.setParameter("here the value of my parameter");
    

    3 - 尽情享受吧!

    显然您可以更改参数的类型和数量。 快速简单。

    【讨论】:

    • 这不处理系统重新加载片段。
    猜你喜欢
    • 1970-01-01
    • 2011-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-02
    • 2016-10-01
    • 2020-08-26
    • 2011-04-08
    相关资源
    最近更新 更多