Fragment MyFragment nicht an Aktivität angehängt

393

Ich habe eine kleine Test-App erstellt, die mein Problem darstellt. Ich verwende ActionBarSherlock, um Registerkarten mit (Sherlock-) Fragmenten zu implementieren.

Mein Code: TestActivity.java

public class TestActivity extends SherlockFragmentActivity {
    private ActionBar actionBar;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setupTabs(savedInstanceState);
    }

    private void setupTabs(Bundle savedInstanceState) {
        actionBar = getSupportActionBar();
        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

        addTab1();
        addTab2();
    }

    private void addTab1() {
        Tab tab1 = actionBar.newTab();
        tab1.setTag("1");
        String tabText = "1";
        tab1.setText(tabText);
        tab1.setTabListener(new TabListener<MyFragment>(TestActivity.this, "1", MyFragment.class));

        actionBar.addTab(tab1);
    }

    private void addTab2() {
        Tab tab1 = actionBar.newTab();
        tab1.setTag("2");
        String tabText = "2";
        tab1.setText(tabText);
        tab1.setTabListener(new TabListener<MyFragment>(TestActivity.this, "2", MyFragment.class));

        actionBar.addTab(tab1);
    }
}

TabListener.java

public class TabListener<T extends SherlockFragment> implements com.actionbarsherlock.app.ActionBar.TabListener {
    private final SherlockFragmentActivity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public TabListener(SherlockFragmentActivity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
    }

    /* The following are each of the ActionBar.TabListener callbacks */

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        SherlockFragment preInitializedFragment = (SherlockFragment) mActivity.getSupportFragmentManager().findFragmentByTag(mTag);

        // Check if the fragment is already initialized
        if (preInitializedFragment == null) {
            // If not, instantiate and add it to the activity
            SherlockFragment mFragment = (SherlockFragment) SherlockFragment.instantiate(mActivity, mClass.getName());
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            ft.attach(preInitializedFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        SherlockFragment preInitializedFragment = (SherlockFragment) mActivity.getSupportFragmentManager().findFragmentByTag(mTag);

        if (preInitializedFragment != null) {
            // Detach the fragment, because another one is being attached
            ft.detach(preInitializedFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        // User selected the already selected tab. Usually do nothing.
    }
}

MyFragment.java

public class MyFragment extends SherlockFragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... params) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                }
                return null;
            }

            @Override
            protected void onPostExecute(Void result){
                getResources().getString(R.string.app_name);
            }

        }.execute();
    }
}

Ich habe den Thread.sleepTeil hinzugefügt , um das Herunterladen von Daten zu simulieren. Der Code in der onPostExecutesoll die Verwendung der simulieren Fragment.

Wenn ich den Bildschirm sehr schnell zwischen Quer- und Hochformat drehe, wird beim onPostExecuteCode eine Ausnahme angezeigt :

java.lang.IllegalStateException: Fragment MyFragment {410f6060} nicht an Aktivität angehängt

Ich denke, das liegt daran, dass MyFragmentin der Zwischenzeit eine neue erstellt wurde, die vor der AsyncTaskFertigstellung an die Aktivität angehängt wurde. Der Code in onPostExecuteruft einen nicht angehängten auf MyFragment.

Aber wie kann ich das beheben?

Nhaarman
quelle
1
Sie sollten die Ansicht vom Fragmentinflater verwenden. mView = inflater.inflate(R.layout.my_layout, container, false) Verwenden Sie diese Ansicht jetzt, wenn Sie Ressourcen abrufen möchten : mView.getResources().***. Es hilft mir, diesen Fehler zu beheben.
Foxis
@foxis Das leckt das Context, was an dein` mView` angehängt ist.
Nhaarman
Vielleicht überprüfe ich es noch nicht. Um ein Leck zu vermeiden, wie wäre es mit null mViewin onDestroy?
Foxis

Antworten:

774

Ich habe die sehr einfache Antwort gefunden: isAdded():

Rückgabe, truewenn das Fragment derzeit zu seiner Aktivität hinzugefügt wird.

@Override
protected void onPostExecute(Void result){
    if(isAdded()){
        getResources().getString(R.string.app_name);
    }
}

Um zu vermeiden, onPostExecutedass Sie angerufen werden, wenn das Fragmentnicht an das angeschlossen Activityist, müssen Sie das AsyncTaskbeim Anhalten oder Stoppen des abbrechen Fragment. Dann isAdded()wäre das nicht mehr nötig. Es ist jedoch ratsam, diese Überprüfung beizubehalten.

Nhaarman
quelle
In meinem Fall, wenn ich eine andere Anwendung starte Absicht von ... dann erhalte ich den gleichen Fehler ... irgendeinen Vorschlag?
CoDe
1
developer.android.com/reference/android/app/… ... gibt es auch isDetached(), das auf API-Level 13 hinzugefügt wurde
Lucas Jota
5
Unter API <11 verwenden Sie developer.android.com/reference/android/support/v4/app/…, wo es funktionieren wird.
Nhaarman
Ich hatte dieses Problem, als ich DialogFragment verwendete. Nachdem ich dialogFragment geschlossen hatte, habe ich versucht, eine andere Aktivität zu starten. Dann trat dieser Fehler auf. Ich habe diesen Fehler vermieden, indem ich entlassen () nach startActivity aufgerufen habe. Das Problem war, dass das Fragment bereits von der Aktivität getrennt war.
Ataru
28

Das Problem ist, dass Sie versuchen, mit getResources (). GetString () auf Ressourcen (in diesem Fall Zeichenfolgen) zuzugreifen, um die Ressourcen aus der Aktivität abzurufen. Siehe diesen Quellcode der Fragment-Klasse:

 /**
  * Return <code>getActivity().getResources()</code>.
  */
 final public Resources getResources() {
     if (mHost == null) {
         throw new IllegalStateException("Fragment " + this + " not attached to Activity");
     }
     return mHost.getContext().getResources();
 }

mHost ist das Objekt, das Ihre Aktivität enthält.

Da die Aktivität möglicherweise nicht angehängt ist, löst Ihr Aufruf von getResources () eine Ausnahme aus.

Die akzeptierte Lösung ist meiner Meinung nach nicht der richtige Weg, da Sie nur das Problem verstecken. Der richtige Weg besteht darin, die Ressourcen von einem anderen Ort abzurufen, von dem immer garantiert wird, dass er vorhanden ist, wie z. B. dem Anwendungskontext:

youApplicationObject.getResources().getString(...)
Tiago
quelle
Ich habe diese Lösung verwendet, weil ich sie ausführen musste, getString()als mein Fragment angehalten wurde. Danke
Geekarist
24

Ich habe hier zwei verschiedene Szenarien gesehen:

1) Wenn ich möchte, dass die asynchrone Aufgabe trotzdem beendet wird: Stellen Sie sich vor, mein onPostExecute speichert empfangene Daten und ruft dann einen Listener auf, um Ansichten zu aktualisieren. Um effizienter zu sein, möchte ich, dass die Aufgabe trotzdem beendet wird, damit ich die Daten bereit habe, wenn der Benutzer kommt zurück. In diesem Fall mache ich normalerweise Folgendes:

@Override
protected void onPostExecute(void result) {
    // do whatever you do to save data
    if (this.getView() != null) {
        // update views
    }
}

2) Wenn die asynchrone Aufgabe nur beendet werden soll, wenn Ansichten aktualisiert werden können: In dem hier vorgeschlagenen Fall aktualisiert die Aufgabe nur die Ansichten, es ist kein Datenspeicher erforderlich, sodass keine Ahnung besteht, ob die Aufgabe beendet werden kann, wenn Ansichten vorhanden sind nicht mehr gezeigt. Ich mache das:

@Override
protected void onStop() {
    // notice here that I keep a reference to the task being executed as a class member:
    if (this.myTask != null && this.myTask.getStatus() == Status.RUNNING) this.myTask.cancel(true);
    super.onStop();
}

Ich habe kein Problem damit gefunden, obwohl ich auch eine (vielleicht) komplexere Methode verwende, die das Starten von Aufgaben aus der Aktivität anstelle der Fragmente umfasst.

Wünschte, das hilft jemandem! :) :)

luixal
quelle
18

Das Problem mit Ihrem Code ist die Art und Weise, wie Sie die AsyncTask verwenden, denn wenn Sie den Bildschirm während Ihres Sleep-Threads drehen:

Thread.sleep(2000) 

Die AsyncTask funktioniert immer noch. Dies liegt daran, dass Sie die AsyncTask-Instanz in onDestroy () nicht ordnungsgemäß abgebrochen haben, bevor das Fragment neu erstellt wurde (wenn Sie es drehen) und wenn dieselbe AsyncTask-Instanz (nach dem Drehen) onPostExecute () ausgeführt wird, wird versucht, dies zu finden die Ressourcen mit getResources () mit der alten Fragmentinstanz (eine ungültige Instanz):

getResources().getString(R.string.app_name)

was äquivalent ist zu:

MyFragment.this.getResources().getString(R.string.app_name)

Die endgültige Lösung besteht also darin, die AsyncTask-Instanz zu verwalten (abzubrechen, wenn dies noch funktioniert), bevor das Fragment beim Drehen des Bildschirms neu erstellt wird. Wenn die Fragmentierung während des Übergangs abgebrochen wird, starten Sie die AsyncTask nach der Rekonstruktion mithilfe eines booleschen Flags neu:

public class MyFragment extends SherlockFragment {

    private MyAsyncTask myAsyncTask = null;
    private boolean myAsyncTaskIsRunning = true;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if(savedInstanceState!=null) {
            myAsyncTaskIsRunning = savedInstanceState.getBoolean("myAsyncTaskIsRunning");
        }
        if(myAsyncTaskIsRunning) {
            myAsyncTask = new MyAsyncTask();
            myAsyncTask.execute();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean("myAsyncTaskIsRunning",myAsyncTaskIsRunning);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if(myAsyncTask!=null) myAsyncTask.cancel(true);
        myAsyncTask = null;

    }

    public class MyAsyncTask extends AsyncTask<Void, Void, Void>() {

        public MyAsyncTask(){}

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            myAsyncTaskIsRunning = true;
        }
        @Override
        protected Void doInBackground(Void... params) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {}
            return null;
        }

        @Override
        protected void onPostExecute(Void result){
            getResources().getString(R.string.app_name);
            myAsyncTaskIsRunning = false;
            myAsyncTask = null;
        }

    }
}
Erick Reátegui Diaz
quelle
stattdessen, wenn mit getResources().***Hilfe Fragments.this.getResource().***geholfen
Prabs
17

Sie sind ziemlich trickreiche Lösung dafür und Leck von Fragmenten aus der Aktivität.

Im Fall von getResource oder etwas anderem, das vom Aktivitätskontext abhängt, auf den über Fragment zugegriffen wird, wird der Aktivitätsstatus und der Fragmentstatus immer wie folgt überprüft

 Activity activity = getActivity(); 
    if(activity != null && isAdded())

         getResources().getString(R.string.no_internet_error_msg);
//Or any other depends on activity context to be live like dailog


        }
    }
Vinayak
quelle
7
isAdded () reicht aus, weil: final public boolean isAdded () {return mHost! = null && mAdded; }
NguyenDat
In meinem Fall sind diese Überprüfungen nicht ausreichend und es kommt immer noch zu Abstürzen, obwohl ich diese hinzugefügt habe.
David
@ David, isAddedist genug. Ich habe nie eine Situation gesehen, in der getString()es abgestürzt war isAdded == true. Sind Sie sicher, dass eine Aktivität angezeigt und ein Fragment angehängt wurde?
CoolMind
14
if (getActivity() == null) return;

funktioniert auch in einigen Fällen. Unterbricht einfach die Codeausführung und stellt sicher, dass die App nicht abstürzt

superUser
quelle
10

Ich hatte das gleiche Problem, ich habe nur die Singletone-Instanz hinzugefügt, um die von Erick angegebene Ressource zu erhalten

MainFragmentActivity.defaultInstance().getResources().getString(R.string.app_name);

Sie können auch verwenden

getActivity().getResources().getString(R.string.app_name);

Ich hoffe das wird helfen.

Aristo Michael
quelle
2

Ich hatte ähnliche Probleme, als die Aktivität der Anwendungseinstellungen mit den geladenen Einstellungen sichtbar war. Wenn ich eine der Einstellungen ändern und dann den Anzeigeinhalt drehen und die Einstellung erneut ändern würde, stürzt die Meldung ab, dass das Fragment (meine Voreinstellungsklasse) nicht an eine Aktivität angehängt war.

Beim Debuggen sah es so aus, als würde die onCreate () -Methode des PreferencesFragment zweimal aufgerufen, wenn der Anzeigeinhalt gedreht wurde. Das war schon seltsam genug. Dann habe ich den isAdded () - Check außerhalb des Blocks hinzugefügt, der auf den Absturz hinweist, und das Problem behoben.

Hier ist der Code des Listeners, der die Zusammenfassung der Einstellungen aktualisiert, um den neuen Eintrag anzuzeigen. Es befindet sich in der onCreate () -Methode meiner Preferences-Klasse, die die PreferenceFragment-Klasse erweitert:

public static class Preferences extends PreferenceFragment {
    SharedPreferences.OnSharedPreferenceChangeListener listener;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // ...
        listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                // check if the fragment has been added to the activity yet (necessary to avoid crashes)
                if (isAdded()) {
                    // for the preferences of type "list" set the summary to be the entry of the selected item
                    if (key.equals(getString(R.string.pref_fileviewer_textsize))) {
                        ListPreference listPref = (ListPreference) findPreference(key);
                        listPref.setSummary("Display file content with a text size of " + listPref.getEntry());
                    } else if (key.equals(getString(R.string.pref_fileviewer_segmentsize))) {
                        ListPreference listPref = (ListPreference) findPreference(key);
                        listPref.setSummary("Show " + listPref.getEntry() + " bytes of a file at once");
                    }
                }
            }
        };
        // ...
    }

Ich hoffe das wird anderen helfen!

ohgodnotanotherone
quelle
0

Wenn Sie die ApplicationKlasse wie folgt erweitern und ein statisches 'globales' Kontextobjekt verwalten, können Sie dieses anstelle der Aktivität zum Laden einer String-Ressource verwenden.

public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

    @Override
    public void onCreate() {
        super.onCreate();
        GLOBAL_APP_CONTEXT = this;
    }
}

Wenn Sie dies verwenden, können Sie mit dem ToastLaden von Ressourcen durchkommen, ohne sich um Lebenszyklen sorgen zu müssen.

Anthony Chuinard
quelle
5
Ich werde abgelehnt, aber niemand hat erklärt, warum. Statische Kontexte sind normalerweise schlecht, aber ich war der Meinung, dass es kein Speicherverlust ist, wenn Sie eine statische Anwendungsreferenz haben.
Anthony Chuinard
Ihre Antwort wird abgelehnt, da dies nur ein Hack ist, der keine richtige Lösung darstellt. Überprüfen Sie die von @nhaarman
Vivek Kumar Srivastava
0

In meinem Fall wurden Fragmentmethoden aufgerufen

getActivity().onBackPressed();
CoolMind
quelle
0

Ein alter Beitrag, aber ich war überrascht über die am besten bewertete Antwort.

Die richtige Lösung hierfür sollte darin bestehen, die Asynctask in onStop (oder wo immer dies in Ihrem Fragment angemessen ist) abzubrechen. Auf diese Weise führen Sie keinen Speicherverlust ein (eine Asynctask, die einen Verweis auf Ihr zerstörtes Fragment enthält) und haben eine bessere Kontrolle darüber, was in Ihrem Fragment vor sich geht.

@Override
public void onStop() {
    super.onStop();
    mYourAsyncTask.cancel(true);
}
Raz
quelle
1
Die am besten bewertete Antwort beinhaltet dies. Auch cancelverhindern kann nicht onPostExecuteaus wird aufgerufen.
Nhaarman
Das Aufrufen von Abbrechen garantiert, dass onPostExecute niemals aufgerufen wird. Beide Aufrufe werden im selben Thread ausgeführt. Sie können also sicher sein, dass es nach dem Aufruf von cancel nicht aufgerufen wird
Raz