I frammenti hanno davvero bisogno di un costruttore vuoto?


258

Ho un Fragmentcon un costruttore che accetta più argomenti. La mia app ha funzionato bene durante lo sviluppo, ma in produzione i miei utenti a volte vedono questo crash:

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

Potrei creare un costruttore vuoto come suggerisce questo messaggio di errore, ma per me non ha senso da allora dovrei chiamare un metodo separato per completare la configurazione di Fragment.

Sono curioso di sapere perché questo incidente si verifica solo occasionalmente. Forse sto usando in ViewPagermodo errato? Ho un'istanza di tutti Fragmenti messaggi personali e li salvo in un elenco all'interno di Activity. Non utilizzo le FragmentManagertransazioni, poiché gli ViewPageresempi che ho visto non lo richiedono e tutto sembra funzionare durante lo sviluppo.


22
in alcune versioni di Android (almeno ICS), puoi andare su Impostazioni -> Opzioni sviluppatore e abilitare "Non mantenere le attività". In questo modo ti darà un modo deterministico di testare i casi in cui è necessario un costruttore no-arg.
Keith,

Ho avuto lo stesso problema. Stavo assegnando invece i dati del bundle alle variabili membro (usando un ctor non predefinito). Il mio programma non si è arrestato in modo anomalo quando ho ucciso l'app, ma stava succedendo solo quando lo scheduler ha messo la mia app sul backburner per "risparmiare spazio". Il modo in cui l'ho scoperto è andare su Task Mgr e aprire un sacco di altre app, quindi riaprire la mia app in debug. Si è schiantato ogni volta. Il problema è stato risolto quando ho usato la risposta di Chris Jenkins per usare i bundle args.
wizurd,

Potresti essere interessato a questa discussione: stackoverflow.com/questions/15519214/…
Stefan Haustein,

5
Una nota a margine per i futuri lettori: se la tua Fragmentsottoclasse non dichiara alcun costruttore, per impostazione predefinita verrà implicitamente creato un costruttore pubblico vuoto (questo è il comportamento standard di Java ). Non è necessario dichiarare esplicitamente un costruttore vuoto a meno che non siano stati dichiarati anche altri costruttori (ad esempio quelli con argomenti).
Tony Chan,

Devo solo dire che IntelliJ IDEA, almeno per la versione 14.1, fornisce un avviso che avvisa del fatto che non si dovrebbe avere un costruttore non predefinito in un frammento.
RenniePet,

Risposte:


349

Si lo fanno.

Non dovresti davvero scavalcare il costruttore comunque. Dovresti avere un newInstance()metodo statico definito e passare qualsiasi parametro tramite argomenti (bundle)

Per esempio:

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

E ovviamente afferrando gli argomenti in questo modo:

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

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

Quindi creeresti un'istanza dal gestore dei frammenti in questo modo:

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

In questo modo, se disconnesso e ricollegato, lo stato dell'oggetto può essere memorizzato tramite gli argomenti. Proprio come i bundle associati a Intents.

Motivo: lettura aggiuntiva

Ho pensato di spiegare perché per le persone che si chiedono perché.

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

Vedrai il instantiate(..)metodo nella Fragmentclasse chiama il newInstancemetodo:

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 () Spiega perché, all'istanza, verifica che l'accessor sia publice che quel caricatore di classi ne consenta l'accesso.

È un metodo piuttosto brutto nel complesso, ma consente FragmentMangerdi uccidere e ricreare Fragmentscon gli stati. (Il sottosistema Android fa cose simili con Activities).

Classe di esempio

Mi viene chiesto molto sulla chiamata newInstance. Non confonderlo con il metodo class. Questo esempio di intera classe dovrebbe mostrare l'utilizzo.

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

2
Se metti in pausa l'attività o la distruggi. Quindi vai alla schermata principale e l'attività viene quindi interrotta da Android per risparmiare spazio. Lo stato dei frammenti verrà salvato (usando args) quindi gc l'oggetto (normalmente). Quindi al ritorno all'attività i frammenti dovrebbero provare a essere ricreati usando lo stato salvato, il nuovo Default () quindi onCreate ecc ... Anche se l'attività sta cercando di risparmiare risorse (telefono mem basso) Potrebbe rimuovere gli oggetti appena messi in pausa .. Commonsguy dovrebbe essere in grado di spiegare meglio. Insomma non lo sai! :)
Chris.Jenkins,

1
@mahkie Davvero se hai bisogno di MOLTI oggetti / modelli, dovresti prenderli in modo asincrono da un database o ContentProvider.
Chris.Jenkins,

1
@ Chris.Jenkins Scusate se non ero chiaro ... il mio punto era che, a differenza delle attività, i frammenti non chiariscono che i costruttori non devono essere usati per passare / condividere i dati. E mentre il dumping / ripristino va bene, credo che il possesso di più copie dei dati a volte possa occupare più memoria di quanto possa riacquistare la distruzione vista. In alcuni casi, potrebbe essere utile avere la possibilità di trattare un insieme di attività / Frammenti come unità, per essere distrutti in tutto o niente - poi siamo riusciti a passare i dati tramite costruttori. Per ora, riguardo a questo problema, un costruttore vuoto è l'unico ad avere.
Kaay

3
Perché dovresti avere diverse copie dei dati? Bundles | Parcelable in realtà passa il riferimento di memoria quando può tra stati / frammenti / attività (causa effettivamente alcuni strani problemi di stato), l'unica volta che Parcelable in realtà "duplica" effettivamente i dati è tra processi e ciclo di vita completo. Ad esempio se passi un oggetto ai tuoi frammenti dalla tua attività, il tuo riferimento di passaggio non è un clone. L'unico vero sovraccarico extra sono gli oggetti frammento aggiuntivi.
Chris.Jenkins,

1
@ Chris.Jenkins Bene, quella, quindi, era la mia ignoranza di Parcelable. Dopo aver letto il breve javadoc di Parcelable e una parte di Parcel non molto distante dalla parola "ricostruita", non avevo raggiunto la parte "Oggetti attivi", concludendo che si trattava solo di una serializzazione di basso livello più ottimizzata ma meno versatile. Con la presente indosso il cappello della vergogna e del borbottio "Non riesco ancora a condividere i nonparcelables e fare i pacchi può essere un problema" :)
Kaay

17

Come notato da CommonsWare in questa domanda https://stackoverflow.com/a/16064418/1319061 , questo errore può verificarsi anche se si sta creando una sottoclasse anonima di un frammento, poiché le classi anonime non possono avere costruttori.

Non creare sottoclassi anonime di Fragment :-)


1
Oppure, come menzionato da CommonsWare in quel post, assicurati di dichiarare un'attività interna / frammento / ricevitore come "statica" per evitare questo errore.
Tony Wickham,

7

Sì, come puoi vedere il pacchetto di supporto crea anche un'istanza dei frammenti (quando vengono distrutti e riaperti). Le tue Fragmentsottoclassi necessitano di un costruttore vuoto pubblico poiché questo è ciò che viene chiamato dal framework.


Il costruttore di frammenti vuoti dovrebbe chiamare il costruttore super () o no? Lo sto chiedendo mentre trovo che il Costruttore pubblico vuoto sia obbligatorio. se chiamare super () non ha senso per un costruttore pubblico vuoto
TNR,

@TNR poiché tutte le astrazioni di frammenti hanno un costruttore vuoto super()sarebbe inutile, poiché la classe genitore ha infranto la regola vuota del costruttore pubblico. Quindi no, non è necessario passare super()all'interno del costruttore.
Chris.Jenkins,

4
In effetti non è un requisito definire esplicitamente un costruttore vuoto in un frammento. Ogni classe Java ha comunque un costruttore predefinito implicito. Tratto da: docs.oracle.com/javase/tutorial/java/javaOO/constructors.html ~ "Il compilatore fornisce automaticamente un costruttore predefinito senza argomento per qualsiasi classe senza costruttori."
IgorGanapolsky,

-6

Ecco la mia soluzione semplice:

1 - Definisci il tuo frammento

public class MyFragment extends Fragment {

    private String parameter;

    public MyFragment() {
    }

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

2 - Crea il tuo nuovo frammento e popola il parametro

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

3 - Divertiti!

Ovviamente puoi cambiare il tipo e il numero di parametri. Facile e veloce.


5
Questo però non gestisce il ricaricamento del frammento da parte del sistema.
Vidia,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.