¿Fragments realmente necesita un constructor vacío?


Tengo un Fragment con un constructor que toma múltiples argumentos. Mi aplicación funcionó bien durante el desarrollo, pero en la 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í ya que entonces tendría que llamar a un método separado para terminar de configurar el Fragment.

Tengo curiosidad por saber por qué este accidente solo ocurre ocasionalmente. Tal vez estoy usando el ViewPager incorrectamente? Instanciar todos los Fragment y guardarlos en una lista dentro de la Activity. No uso transacciones FragmentManager, ya que los ejemplos ViewPager que he visto no lo requerían y todo parecía estar funcionando durante el desarrollo.

Author: stkent, 2012-05-04

4 answers

Sí lo hacen.

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

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
    //...
}

Entonces puedes crear una instancia de tu 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 separa y se vuelve a unir, el estado del objeto se puede almacenar a través de los argumentos. Al igual que los paquetes unidos a Intención.

Razón-Lectura extra

Pensé en explicar por qué a la gente preguntándose por qué.

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

Verá que el método instantiate(..) en la clase Fragment llama al método 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 () Explica por qué, al instanciarlo comprueba que el accessor es public y que ese cargador de clases permite el acceso a él.

Es un método bastante desagradable en general, pero permite a FragmentManger matar y recrear Fragments con estados. (El subsistema Android hace cosas similares con Activities).

Clase de ejemplo

Me preguntan mucho acerca de llamar a newInstance, (no confunda esto con el método de clase. Todo este ejemplo de clase 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...
}
 318
Author: Chris.Jenkins,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-08 13:41:31

Como señala 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 hacer subclases anónimas de Fragmento: -)

 15
Author: JesperB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:28

Sí, como puede ver el paquete de soporte también crea instancias de los fragmentos (cuando se destruyen y se vuelven a abrir). Sus subclases Fragmnt necesitan un constructor vacío público ya que esto es lo que está siendo llamado por el framework.

 4
Author: Sveinung Kval Bakken,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-04 14:04:05

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-Crea tu nuevo fragmento y rellena el parámetro

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

3 - ¡Disfrútalo!

Obviamente puede cambiar el tipo y el número de parámetros. Rápido y fácil.

 -5
Author: Alecs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-01 13:19:49