Brauchen Fragmente wirklich einen leeren Konstruktor?

258

Ich habe einen Fragmentmit einem Konstruktor, der mehrere Argumente akzeptiert. Meine App hat während der Entwicklung gut funktioniert, aber in der Produktion sehen meine Benutzer manchmal diesen Absturz:

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

Ich könnte einen leeren Konstruktor erstellen, wie diese Fehlermeldung andeutet, aber das macht für mich keinen Sinn, da ich seitdem eine separate Methode aufrufen müsste, um die Einrichtung des zu beenden Fragment.

Ich bin gespannt, warum dieser Absturz nur gelegentlich vorkommt. Vielleicht benutze ich das ViewPagerfalsch? Ich instanziiere alle Fragments selbst und speichere sie in einer Liste innerhalb der Activity. Ich verwende keine FragmentManagerTransaktionen, da die ViewPagerBeispiele, die ich gesehen habe, dies nicht erforderten und alles während der Entwicklung zu funktionieren schien.

stkent
quelle
22
In einigen Android-Versionen (mindestens ICS) können Sie unter Einstellungen -> Entwickleroptionen die Option "Aktivitäten nicht beibehalten" aktivieren. Auf diese Weise erhalten Sie eine deterministische Möglichkeit, die Fälle zu testen, in denen ein Konstruktor ohne Argumente erforderlich ist.
Keith
Ich hatte das gleiche Problem. Ich habe die Bundle-Daten stattdessen Mitgliedsvariablen zugewiesen (unter Verwendung eines nicht standardmäßigen Ctors). Mein Programm stürzte nicht ab, als ich die App beendete - es geschah nur, wenn der Scheduler meine App auf den Backburner stellte, um "Platz zu sparen". Ich habe dies entdeckt, indem ich zu Task Mgr gegangen bin und eine Menge anderer Apps geöffnet und dann meine App beim Debuggen erneut geöffnet habe. Es stürzte jedes Mal ab. Das Problem wurde behoben, als ich Chris Jenkins Antwort verwendete, um Bundle-Argumente zu verwenden.
Wizurd
Dieser Thread könnte Sie interessieren: stackoverflow.com/questions/15519214/…
Stefan Haustein
5
Eine Randnotiz für zukünftige Leser: Wenn Ihre FragmentUnterklasse überhaupt keine Konstruktoren deklariert, wird standardmäßig implizit ein leerer öffentlicher Konstruktor für Sie erstellt (dies ist das Standardverhalten von Java ). Sie nicht müssen explizit einen leeren Konstruktor deklarieren , wenn Sie auch andere Konstrukteure (zB solche mit Argumenten) erklärt.
Tony Chan
Ich möchte nur erwähnen, dass IntelliJ IDEA, zumindest für Version 14.1, eine Warnung enthält, die Sie darauf hinweist, dass Sie keinen nicht standardmäßigen Konstruktor in einem Fragment haben sollten.
RenniePet

Antworten:

349

Ja, das tun sie.

Sie sollten den Konstruktor sowieso nicht wirklich überschreiben. Sie sollten eine newInstance()statische Methode definiert haben und alle Parameter über Argumente (Bundle) übergeben.

Beispielsweise:

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

Und natürlich die Argumente so packen:

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

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

Dann würden Sie von Ihrem Fragmentmanager aus wie folgt instanziieren:

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

Auf diese Weise kann der Objektstatus über die Argumente gespeichert werden, wenn er getrennt und erneut angehängt wird. Ähnlich wie Bündel, die an Absichten befestigt sind.

Grund - Zusätzliche Lektüre

Ich dachte, ich würde erklären, warum für Leute, die sich fragen, warum.

Wenn Sie Folgendes überprüfen: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/Fragment.java

Sie werden sehen, dass die instantiate(..)Methode in der FragmentKlasse die newInstanceMethode aufruft :

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 () Erklärt, warum bei der Instanziierung überprüft wird, ob der Accessor vorhanden ist publicund ob dieser Klassenladeprogramm den Zugriff darauf zulässt.

Alles in allem ist es eine ziemlich böse Methode, aber sie erlaubt FragmentMangeres, Fragmentsmit Zuständen zu töten und neu zu erschaffen . (Das Android-Subsystem macht ähnliche Dinge mit Activities).

Beispielklasse

Ich werde oft gefragt, ob ich anrufen soll newInstance. Verwechseln Sie dies nicht mit der Klassenmethode. Dieses ganze Klassenbeispiel sollte die Verwendung zeigen.

/**
 * 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
quelle
2
Wenn Sie die Aktivität anhalten oder zerstören. Sie gehen also zum Startbildschirm und die Aktivität wird dann von Android beendet, um Platz zu sparen. Der Fragmentstatus wird gespeichert (unter Verwendung der Argumente) und dann das Objekt (normalerweise) gc. Bei der Rückkehr zur Aktivität sollten die Fragmente versuchen, mit dem gespeicherten Status, new Default (), dann onCreate usw. neu erstellt zu werden. Auch wenn die Aktivität versucht, Ressourcen zu sparen (Low-Mem-Telefon). Möglicherweise werden die gerade angehaltenen Objekte entfernt .. Commonsguy sollte es besser erklären können. Kurz gesagt, du weißt es nicht! :)
Chris.Jenkins
1
@mahkie Wenn Sie wirklich VIELE Objekte / Modelle benötigen, sollten Sie diese asynchron aus einer Datenbank oder einem ContentProvider abrufen.
Chris.Jenkins
1
@ Chris.Jenkins Entschuldigung, wenn ich nicht klar war ... mein Punkt war, dass Fragmente im Gegensatz zu Aktivitäten nicht so deutlich machen, dass Konstruktoren nicht zum Übergeben / Teilen von Daten verwendet werden dürfen. Und während das Dumping / Wiederherstellen in Ordnung ist, glaube ich, dass das Speichern mehrerer Kopien von Daten manchmal mehr Speicher beanspruchen kann, als die Zerstörung der Ansicht wiedererlangen kann. In einigen Fällen kann es nützlich sein, die Option zu haben, eine Sammlung von Aktivitäten / Fragmenten als Einheit zu behandeln, als Ganzes oder gar nicht zerstört zu werden - dann könnten wir Daten über Konstruktoren weitergeben. In Bezug auf dieses Problem ist derzeit nur ein leerer Konstruktor verfügbar.
Kaay
3
Warum sollten Sie mehrere Kopien von Daten aufbewahren? Bundles | Parcelable übergeben tatsächlich eine Speicherreferenz, wenn dies zwischen Zuständen / Fragmenten / Aktivitäten möglich ist (dies verursacht tatsächlich einige seltsame Zustandsprobleme). Das einzige Mal, dass Parcelable Daten tatsächlich effektiv "dupliziert", liegt zwischen Prozessen und dem gesamten Lebenszyklus. Wenn Sie beispielsweise ein Objekt aus Ihrer Aktivität an Ihre Fragmente übergeben, ist Ihre Referenz kein Klon. Ihr einziger wirklicher zusätzlicher Aufwand sind die zusätzlichen Fragmentobjekte.
Chris.Jenkins
1
@ Chris.Jenkins Nun, das war meine Unkenntnis von Parcelable. Nachdem ich das kurze Javadoc von Parcelable und einen Teil von Parcel nicht weit hinter dem Wort "rekonstruiert" gelesen hatte, war ich nicht zum Teil "Aktive Objekte" gelangt und kam zu dem Schluss, dass es sich nur um eine optimierte, aber weniger vielseitige Serialisierbarkeit auf niedriger Ebene handelt. Ich ziehe hiermit den Hut der Schande an und murmle "Ich kann immer noch keine Nicht-Pakete teilen und das Herstellen von Paketen kann ein Problem sein" :)
kaay
17

Wie von CommonsWare in dieser Frage https://stackoverflow.com/a/16064418/1319061 festgestellt , kann dieser Fehler auch auftreten, wenn Sie eine anonyme Unterklasse eines Fragments erstellen, da anonyme Klassen keine Konstruktoren haben können.

Erstelle keine anonymen Unterklassen von Fragment :-)

JesperB
quelle
1
Oder stellen Sie, wie in diesem Beitrag von CommonsWare erwähnt, sicher, dass Sie eine innere Aktivität / ein Fragment / einen Empfänger als "statisch" deklarieren, um diesen Fehler zu vermeiden.
Tony Wickham
7

Ja, wie Sie sehen, instanziiert das Support-Paket auch die Fragmente (wenn sie zerstört und wieder geöffnet werden). Ihre FragmentUnterklassen benötigen einen öffentlichen leeren Konstruktor, da dies vom Framework aufgerufen wird.

Sveinung Kval Bakken
quelle
Leerer Fragmentkonstruktor sollte super () Konstruktor aufrufen oder nicht? Ich frage dies, während ich feststelle, dass ein leerer öffentlicher Konstruktor obligatorisch ist. Wenn der Aufruf von super () für einen leeren öffentlichen Konstruktor keinen Sinn ergibt
TNR
@TNR, da alle Fragmentabstraktionen einen leeren Konstruktor haben, super()wäre erfolglos, da die übergeordnete Klasse die Regel des leeren öffentlichen Konstruktors verletzt hat. Also nein, Sie müssen nicht super()in Ihrem Konstruktor übergeben.
Chris.Jenkins
4
Tatsächlich ist es nicht erforderlich, einen leeren Konstruktor in einem Fragment explizit zu definieren. Jede Java-Klasse hat sowieso einen impliziten Standardkonstruktor. Entnommen aus: docs.oracle.com/javase/tutorial/java/javaOO/constructors.html ~ "Der Compiler stellt automatisch einen Standardkonstruktor ohne Argumente für jede Klasse ohne Konstruktoren bereit."
IgorGanapolsky
-6

Hier ist meine einfache Lösung:

1 - Definieren Sie Ihr Fragment

public class MyFragment extends Fragment {

    private String parameter;

    public MyFragment() {
    }

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

2 - Erstellen Sie Ihr neues Fragment und füllen Sie den Parameter aus

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

3 - Genieße es!

Natürlich können Sie den Typ und die Anzahl der Parameter ändern. Schnell und einfach.

Alecs
quelle
5
Dies behandelt jedoch nicht das Neuladen des Fragments durch das System.
Vidia