Les fragments ont-ils vraiment besoin d'un constructeur vide?

258

J'ai un Fragmentavec un constructeur qui prend plusieurs arguments. Mon application a bien fonctionné pendant le développement, mais en production, mes utilisateurs voient parfois ce plantage:

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

Je pourrais créer un constructeur vide comme le suggère ce message d'erreur, mais cela n'a pas de sens pour moi car je devrais alors appeler une méthode distincte pour terminer la configuration de Fragment.

Je suis curieux de savoir pourquoi cet accident ne se produit qu'occasionnellement. Peut-être que j'utilise ViewPagermal? J'instancie tous les Fragments moi-même et les enregistre dans une liste à l'intérieur du Activity. Je n'utilise pas de FragmentManagertransactions, car les ViewPagerexemples que j'ai vus ne l'exigeaient pas et tout semblait fonctionner pendant le développement.

stkent
la source
22
dans certaines versions d'Android (au moins ICS), vous pouvez aller dans Paramètres -> Options développeur et activer "Ne pas garder les activités". Cela vous donnera un moyen déterministe de tester les cas où un constructeur sans argument est nécessaire.
Keith
J'ai eu ce même problème. J'attribuais les données du bundle à la place aux variables membres (en utilisant un ctor non par défaut). Mon programme ne plantait pas lorsque j'ai tué l'application - cela ne se produisait que lorsque le planificateur mettait mon application en veilleuse pour «économiser de l'espace». La façon dont j'ai découvert cela est en allant à Task Mgr et en ouvrant une tonne d'autres applications, puis en rouvrant mon application dans le débogage. Il s'est écrasé à chaque fois. Le problème a été résolu lorsque j'ai utilisé la réponse de Chris Jenkins pour utiliser les arguments de bundle.
wizurd
Vous pourriez être intéressé par ce sujet: stackoverflow.com/questions/15519214/…
Stefan Haustein
5
Remarque pour les futurs lecteurs: si votre Fragmentsous-classe ne déclare aucun constructeur, alors par défaut un constructeur public vide sera implicitement créé pour vous (c'est un comportement Java standard ). Vous n'avez pas à déclarer explicitement un constructeur vide à moins que vous n'ayez également déclaré d'autres constructeurs (par exemple ceux avec des arguments).
Tony Chan
Je mentionnerai simplement qu'IntelliJ IDEA, au moins pour la version 14.1, fournit un avertissement vous avertissant du fait que vous ne devriez pas avoir de constructeur non par défaut dans un fragment.
RenniePet

Réponses:

349

Oui, ils le font.

De toute façon, vous ne devriez pas vraiment remplacer le constructeur. Vous devez avoir une newInstance()méthode statique définie et passer tous les paramètres via des arguments (bundle)

Par exemple:

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

Et bien sûr, saisir les arguments de cette façon:

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

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

Ensuite, vous instancieriez à partir de votre gestionnaire de fragments comme suit:

@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 cette façon, s'il est détaché et re-attaché, l'état de l'objet peut être stocké via les arguments. Tout comme les bundles attachés à Intents.

Motif - Lecture supplémentaire

J'ai pensé expliquer pourquoi les gens se demandent pourquoi.

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

Vous verrez la instantiate(..)méthode dans la Fragmentclasse appelle la newInstanceméthode:

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 () Explique pourquoi, lors de l'instanciation, il vérifie que l'accesseur est publicet que ce chargeur de classe permet d'y accéder.

C'est une méthode assez méchante dans l'ensemble, mais elle permet FragmentMangerde tuer et de recréer Fragmentsavec des états. (Le sous-système Android fait des choses similaires avec Activities).

Exemple de classe

On me demande beaucoup d'appeler newInstance. Ne confondez pas cela avec la méthode de classe. Cet exemple de classe entière doit montrer l'utilisation.

/**
 * 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
la source
2
Si vous interrompez l'activité ou la détruisez. Vous accédez donc à l'écran d'accueil et l'activité est ensuite supprimée par Android pour économiser de la place. L'état des fragments sera enregistré (en utilisant les arguments) puis gc l'objet (normalement). Donc, en revenant à l'activité, les fragments devraient essayer d'être recréés en utilisant l'état enregistré, nouveau Default () puis onCreate etc ... Aussi si l'activité essaie d'économiser des ressources (téléphone à faible mémoire) Il peut supprimer les objets qui se sont arrêtés .. Commonsguy devrait être en mesure de mieux expliquer. En bref, vous ne savez pas! :)
Chris.Jenkins
1
@mahkie Vraiment, si vous avez besoin de BEAUCOUP d'objets / modèles, vous devez les récupérer de manière asynchrone à partir d'une base de données ou d'un fournisseur de contenu.
Chris.Jenkins
1
@ Chris.Jenkins Désolé si je n'ai pas été clair ... mon point était que, contrairement aux activités, les fragments ne rendent pas aussi clair que les constructeurs ne doivent pas être utilisés pour transmettre / partager des données. Et bien que le dumping / la restauration soit correct, je pense que la détention de plusieurs copies de données peut parfois prendre plus de mémoire que la destruction de vues ne peut en regagner. Dans certains cas, il pourrait être utile d'avoir l'option de traiter une collection d'activités / fragments comme une unité, à détruire dans son ensemble ou pas du tout - alors nous pourrions transmettre des données via des constructeurs. Pour l'instant, concernant ce problème, un constructeur vide est le seul à avoir.
kaay
3
Pourquoi détiendriez-vous plusieurs copies de données? Bundles | Parcelable passe en fait la référence de mémoire quand il peut entre les états / fragments / activités, (cela provoque des problèmes d'état étranges en fait). Par exemple, si vous passez un objet à vos fragments de votre activité, votre référence de passage n'est pas un clone. Votre seul véritable surcoût supplémentaire est les objets fragments supplémentaires.
Chris.Jenkins
1
@ Chris.Jenkins Eh bien, c'était donc mon ignorance de Parcelable. Après avoir lu le court javadoc de Parcelable, et une partie de Parcel non loin du mot "reconstruit", je n'avais pas atteint la partie "Active Objects", concluant que c'était juste un Serializable bas niveau plus optimisé mais moins polyvalent. Par la présente, je porte le chapeau de la honte et du marmonnement "Je ne peux toujours pas partager de non-colisables et la fabrication de colis peut être un problème" :)
kaay
17

Comme indiqué par CommonsWare dans cette question https://stackoverflow.com/a/16064418/1319061 , cette erreur peut également se produire si vous créez une sous-classe anonyme d'un fragment, car les classes anonymes ne peuvent pas avoir de constructeurs.

Ne faites pas de sous-classes anonymes de Fragment :-)

JesperB
la source
1
Ou, comme CommonsWare l'a mentionné dans cet article, assurez-vous de déclarer une activité / un fragment / un récepteur interne comme "statique" pour éviter cette erreur.
Tony Wickham
7

Oui, comme vous pouvez le voir, le support-package instancie également les fragments (lorsqu'ils sont détruits et rouverts). Vos Fragmentsous-classes ont besoin d'un constructeur public vide car c'est ce que le framework appelle.

Sveinung Kval Bakken
la source
Le constructeur de fragment vide devrait appeler le constructeur super () ou non? Je pose cette question car je constate que le constructeur public vide est obligatoire. si appeler super () n'a pas de sens pour un constructeur public vide
TNR
@TNR car toutes les abstractions de fragments ont un constructeur vide super()serait inutile, car la classe parente a violé la règle du constructeur public vide. Donc non vous n'avez pas besoin de passer à l' super()intérieur de votre constructeur.
Chris.Jenkins
4
En fait, il n'est pas obligatoire de définir explicitement un constructeur vide dans un fragment. De toute façon, chaque classe Java a un constructeur implicite par défaut. Extrait de: docs.oracle.com/javase/tutorial/java/javaOO/constructors.html ~ "Le compilateur fournit automatiquement un constructeur par défaut sans argument pour toute classe sans constructeur."
IgorGanapolsky
-6

Voici ma solution simple:

1 - Définissez votre fragment

public class MyFragment extends Fragment {

    private String parameter;

    public MyFragment() {
    }

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

2 - Créez votre nouveau fragment et remplissez le paramètre

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

3 - Profitez-en!

Évidemment, vous pouvez changer le type et le nombre de paramètres. Rapide et facile.

Alecs
la source
5
Cela ne gère cependant pas le rechargement du fragment par le système.
Vidia