Fragment 重建引发的 could not find Fragment constructor 问题

996 阅读3分钟

问题描述

例如 WiFiFragment 页,重写了 Fragment 默认构造函数:

class WiFiFragment(val data: String):Fragment()

当 Fragment 因为某种原因,例如旋转屏幕时 Activity 重建,App 崩溃抛出异常:

java.lang.RuntimeException:Unable to start activity ComponentInfo{xxx.Activity}: androidx.fragment.app.Fragment$InstantiationException: Unable to instantiate fragment xxx.WiFiFragment: could not find Fragment constructor
Caused by:
java.lang.NoSuchMethodException:com.ut.seasoning.view.set.net.WiFiFragment.<init> []
java.lang.Class.getConstructor0(Class.java:2332)
java.lang.Class.getConstructor(Class.java:1728)
androidx.fragment.app.Fragment.instantiate(Fragment.java:672)

解决方案

异常信息显示 WiFiFragment 实例化失败,并标出了异常出现的具体位置。根据信息查看 Fragmentinstantiate 方法发现这里的 Fragment 实例化是通过反射实现的,这个过程中会寻找默认的无参构造函数来初始化,找不到则抛出 NoSuchMethodException 异常,而我们重写的构造函数是有参的,所以会抛出异常。

/**
 * Create a new instance of a Fragment with the given class name.  This is
 * the same as calling its empty constructor, setting the {@link ClassLoader} on the
 * supplied arguments, then calling {@link #setArguments(Bundle)}.
 *
 * @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.
 * @deprecated Use {@link FragmentManager#getFragmentFactory()} and
 * {@link FragmentFactory#instantiate(ClassLoader, String)}, manually calling
 * {@link #setArguments(Bundle)} on the returned Fragment.
 */
@Deprecated
@NonNull
public static Fragment instantiate(@NonNull Context context, @NonNull String fname,
        @Nullable Bundle args) {
    try {
        Class<? extends Fragment> clazz = FragmentFactory.loadFragmentClass(
                context.getClassLoader(), fname);
        Fragment f = clazz.getConstructor().newInstance();
        if (args != null) {
            args.setClassLoader(f.getClass().getClassLoader());
            f.setArguments(args);
        }
        return f;
    } 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);
    }
}

因此,通常来说无需重写 Fragment 构造函数,若是想要传递参数给 Fragment,可使用 setArguments(Bundle) 参考代码如下:

class WiFiFragment : Fragment() {

    companion object {
        fun instance(data: String?): WiFiFragment {
            val fragment = WiFiFragment()
            val bundle = Bundle()
            bundle.putString("data", data)
            fragment.arguments = bundle
            return fragment
        }
    }

}

后记:

Android 开发文档对于 Fragment 的说明里也有提到

Default constructor. Every fragment must have an empty constructor, so it can be instantiated when restoring its activity's state. It is strongly recommended that subclasses do not have other constructors with parameters, since these constructors will not be called when the fragment is re-instantiated; instead, arguments can be supplied by the caller with setArguments(Bundle) and later retrieved by the Fragment with getArguments().

缺省构造函数。每个片段都必须有一个空的构造函数,这样在恢复其活动状态时就可以实例化它。强烈建议子类不要使用带参数的其他构造函数,因为当片段被重新实例化时,这些构造函数将不会被调用; 相反,可以使用 setArguments (Bundle)由调用者提供参数,然后使用 getArguments ()由片段检索参数。

Applications should generally not implement a constructor. Prefer onAttach(android.content.Context) instead. It is the first place application code can run where the fragment is ready to be used - the point where the fragment is actually associated with its context. Some applications may also want to implement onInflate(Activity, AttributeSet, Bundle) to retrieve attributes from a layout resource, although note this happens when the fragment is attached.

应用程序通常不应实现构造函数。首选 onAttach (android.content。背景)。它是应用程序代码可以在片段可以使用的地方运行的第一个地方——片段实际上与其上下文相关联的地方。一些应用程序可能还希望实现 on膨胀(Activity、 AttributeSet、 Bundle)来从布局资源中检索属性,但是请注意,当附加片段时会发生这种情况。

参考资料:

为什么 Fragment 需要一个无参构造函数
Android Fragment 要你何用?2.0版本
Fragment