¿Los fragmentos realmente necesitan un constructor vacío?

258

Tengo un Fragmentcon un constructor que toma múltiples argumentos. Mi aplicación funcionó bien durante el desarrollo, pero en producción mis usuarios a veces ven este bloqueo:

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

Podría hacer un constructor vacío como sugiere este mensaje de error, pero eso no tiene sentido para mí desde entonces tendría que llamar a un método separado para terminar de configurar Fragment.

Tengo curiosidad por saber por qué este bloqueo solo ocurre ocasionalmente. Tal vez estoy usando ViewPagerincorrectamente? Ejecuto todos los Fragmentcorreos yo mismo y los guardo en una lista dentro de Activity. No uso FragmentManagertransacciones, ya que los ViewPagerejemplos que he visto no lo requerían y todo parecía funcionar durante el desarrollo.

stkent
fuente
22
En algunas versiones de Android (al menos ICS), puede ir a configuración -> opciones de desarrollador y habilitar "No mantener actividades". Hacer esto le dará una forma determinista de probar los casos en los que es necesario un constructor sin argumentos.
Keith
Tuve el mismo problema. En su lugar, estaba asignando los datos del paquete a las variables miembro (usando un ctor no predeterminado). Mi programa no se bloqueaba cuando maté la aplicación, solo estaba sucediendo cuando el programador colocó mi aplicación en segundo plano para "ahorrar espacio". La forma en que descubrí esto fue yendo a Task Mgr y abriendo un montón de otras aplicaciones, luego volviendo a abrir mi aplicación en depuración. Se estrellaba cada vez. El problema se resolvió cuando utilicé la respuesta de Chris Jenkins para usar arg de paquete.
wizurd
Te puede interesar este hilo: stackoverflow.com/questions/15519214/…
Stefan Haustein el
55
Una nota al margen para futuros lectores: si su Fragmentsubclase no declara ningún constructor en absoluto, entonces, de forma predeterminada, se hará implícitamente un constructor público vacío para usted (este es el comportamiento estándar de Java ). Usted no tiene que explícitamente declarar un constructor vacío a menos que también declararon otros constructores (por ejemplo, los que tienen argumentos).
Tony Chan
Solo mencionaré que IntelliJ IDEA, al menos para la versión 14.1, proporciona una advertencia que lo alerta sobre el hecho de que no debe tener un constructor no predeterminado en un fragmento.
RenniePet

Respuestas:

349

Ellos si.

Realmente no deberías anular el constructor de todos modos. Debe tener un newInstance()método estático definido y pasar cualquier parámetro a través de argumentos (paquete)

Por ejemplo:

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

Y, por supuesto, agarrando los args de esta manera:

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

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

Luego, creará una instancia de su administrador de fragmentos de la siguiente manera:

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

De esta manera, si se desconecta y vuelve a adjuntar, el estado del objeto se puede almacenar a través de los argumentos. Al igual que los paquetes adjuntos a las intenciones.

Motivo: lectura adicional

Pensé en explicar por qué a las personas que se preguntaban por qué.

Si marca: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/Fragment.java

Verá que el instantiate(..)método en la Fragmentclase llama al newInstancemétodo:

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 () Explica por qué, al crear una instancia, comprueba que el descriptor de acceso es publicy que el cargador de clases permite el acceso a él.

Es un método bastante desagradable en general, pero permite FragmentMangermatar y recrear Fragmentscon estados. (El subsistema de Android hace cosas similares con Activities).

Clase de ejemplo

Me preguntan mucho acerca de llamar newInstance. No confunda esto con el método de clase. Este ejemplo de clase completa debería mostrar el uso.

/**
 * 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...
}
Chris Jenkins
fuente
2
Si pausa la actividad o la destruye. Entonces vas a la pantalla de inicio y la actividad es eliminada por Android para ahorrar espacio. El estado de los fragmentos se guardará (usando los argumentos) y luego gc el objeto (normalmente). Por lo tanto, al volver a la actividad, los fragmentos deben intentar volver a crearse utilizando el estado guardado, el nuevo Predeterminado (), luego onCreate, etc. También si la actividad está tratando de ahorrar recursos (teléfono con poca memoria). .. Commonsguy debería poder explicar mejor. En resumen ¡No lo sabes! :)
Chris.Jenkins
1
@mahkie Realmente si necesita MUCHOS objetos / modelos, debe tomarlos de forma asincrónica de una base de datos o un proveedor de contenido.
Chris.Jenkins
1
@ Chris.Jenkins Lo siento si no estaba claro ... mi punto era que, a diferencia de Actividades, Fragments no deja en claro que los constructores no deben usarse para pasar / compartir datos. Y aunque el volcado / restauración está bien, creo que mantener varias copias de datos a veces puede ocupar más memoria de la que puede recuperar la destrucción de la vista. En algunos casos, podría ser útil tener la opción de tratar una colección de Actividades / Fragmentos como una unidad, para ser destruidos como un todo o para nada, entonces podríamos pasar datos a través de constructores. Por ahora, con respecto a este problema, un constructor vacío es el único que tiene.
kaay
3
¿Por qué estarías sosteniendo varias copias de datos? Los paquetes | Parcelable en realidad pasan la referencia de memoria cuando pueden entre estados / fragmentos / actividades, (en realidad causa algunos problemas de estado extraños). La única vez que Parcelable efectivamente "duplica" datos de manera efectiva es entre procesos y el ciclo de vida completo. Por ejemplo, si pasa un objeto a sus fragmentos de su actividad, su referencia de paso no es un clon. Su única sobrecarga adicional real son los objetos de fragmentos adicionales.
Chris.Jenkins
1
@ Chris.Jenkins Bueno, esa fue mi ignorancia de Parcelable. Después de leer el breve javadoc de Parcelable, y una parte de Parcel no mucho más allá de la palabra "reconstruido", no había llegado a la parte de "Objetos activos", concluyendo que solo era un serializable de bajo nivel más optimizado pero menos versátil. Por la presente, me pongo el sombrero de la vergüenza y el murmullo "Todavía no puedo compartir cosas que no se pueden usar en porcelana y hacer que la porcelana sea una molestia" :)
kaay
17

Como señaló CommonsWare en esta pregunta https://stackoverflow.com/a/16064418/1319061 , este error también puede ocurrir si está creando una subclase anónima de un Fragmento, ya que las clases anónimas no pueden tener constructores.

No hagas subclases anónimas de Fragment :-)

JesperB
fuente
1
O, como CommonsWare mencionó en esa publicación, asegúrese de declarar una Actividad / Fragmento / Receptor interno como "estático" para evitar este error.
Tony Wickham el
7

Sí, como puede ver, el paquete de soporte también crea instancias de los fragmentos (cuando se destruyen y se vuelven a abrir). Sus Fragmentsubclases necesitan un constructor público vacío, ya que esto es lo que llama el marco.

Sveinung Kval Bakken
fuente
¿El constructor de fragmentos vacíos debería llamar al constructor super () o no? Pregunto esto mientras descubro que el constructor público vacío es obligatorio. si llamar a super () no tiene sentido para un constructor público vacío
TNR
@TNR ya que todas las abstracciones Fragment tienen un constructor vacío super()sería inútil, ya que la clase padre ha roto la regla del constructor público vacío. Entonces no, no necesita pasar super()dentro de su constructor.
Chris.Jenkins
44
De hecho, no es un requisito definir explícitamente un constructor vacío en un Fragmento. Cada clase Java tiene un constructor implícito predeterminado de todos modos. Tomado de: docs.oracle.com/javase/tutorial/java/javaOO/constructors.html ~ "El compilador proporciona automáticamente un constructor predeterminado sin argumentos para cualquier clase sin constructores".
IgorGanapolsky
-6

Aquí está mi solución simple:

1 - Define tu fragmento

public class MyFragment extends Fragment {

    private String parameter;

    public MyFragment() {
    }

    public void setParameter(String parameter) {
        this.parameter = parameter;
    } 
}

2 - Cree su nuevo fragmento y complete el parámetro

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

3 - ¡Disfrútalo!

Obviamente puede cambiar el tipo y la cantidad de parámetros. Rapido y Facil.

Alecs
fuente
55
Sin embargo, esto no maneja la recarga del fragmento por parte del sistema.
Vidia