2012-06-06 17 views
323

Ich habe eine kleine Test-App erstellt, die mein Problem darstellt. Ich verwende ActionBarSherlock, um Tabs mit (Sherlock) Fragmenten zu implementieren.Fragment MyFragment nicht an Aktivität angefügt

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 hinzugefügt, um die Thread.sleep Teil das Herunterladen von Daten zu simulieren. Der Code in onPostExecute soll die Verwendung des Fragment simulieren.

Wenn ich den Bildschirm sehr schnell zwischen Quer- und Hochformat drehen, erhalte ich eine Ausnahme bei dem onPostExecute Code:

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

Ich denke es ist, weil eine neue MyFragment in der Zwischenzeit erstellt wurde, und wurde an die Aktivität vor der AsyncTask abgeschlossen. Der Code in onPostExecute ruft eine nicht verbundene MyFragment.

Aber wie kann ich das beheben?

+0

Du solltest View from Fragment inflater verwenden. 'mView = inflater.inflate (R.layout.my_layout, container, false)' Und jetzt benutze diese Ansicht, wenn du Ressourcen bekommen willst: 'mView.getResources(). ***'. Es hilft mir, diesen Fehler zu beheben. – foxis

+0

@foxis Das leckt den 'Context', der an Ihre' mView' angehängt ist. – nhaarman

+0

Vielleicht überprüfe ich es noch nicht. Um Leck zu vermeiden, wie um Null 'mView' in onDestroy zu erhalten? – foxis

Antwort

685

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

Return true wenn das Fragment wird derzeit auf seine Aktivität hinzugefügt.

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

Um onPostExecute von zu vermeiden, wenn die Fragment zum Activity nicht angebracht genannt wird, um die AsyncTask abzubrechen, wenn eine Pause oder die Fragment zu stoppen. Dann wäre isAdded() nicht mehr nötig. Es ist jedoch ratsam, diese Überprüfung beizubehalten.

+0

In meinem Fall, wenn ich eine andere Anwendung Intent von ... starte, bekomme ich denselben Fehler ... irgendeinen Vorschlag? – CoDe

+0

Funktioniert nicht in API Level 10 oder weniger –

+1

@Lucas warum ist das? – nhaarman

22

ich konfrontiert habe zwei verschiedene Szenarien hier:

1) Wenn ich die asynchrone Aufgabe wollen sowieso beenden: sich vorstellen, meine OnPostExecute speichert Daten empfangen und dann einen Zuhörer rufen Ansichten zu aktualisieren, so, effizienter zu sein Ich möchte, dass die Aufgabe trotzdem beendet wird, damit die Daten bereit sind, wenn der Benutzer zurückkommt. In diesem Fall habe ich dies in der Regel tun:

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

2) Wenn ich die asynchrone Aufgabe wollen nur beenden, wenn Ansichten aktualisiert werden kann: der Fall, dass Sie hier vorschlägt sind, aktualisiert die Aufgabe nur die Ansichten, keine Datenspeicherung benötigt, so hat es keine Ahnung für die Aufgabe zu beenden, wenn Ansichten nicht mehr angezeigt werden.Ich tue dies:

@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 kein Problem damit gefunden habe, obwohl ich auch eine (vielleicht) zum Zweck eines komplexeren Art und Weise, die Einführung Aufgaben aus der Tätigkeit anstelle des Fragments enthält.

Wünsche dies hilft jemandem! :)

18

Das Problem mit Ihrem Code ist der Weg, den Sie die AsyncTask verwenden, denn wenn Sie den Bildschirm im Schlaf Gewinde drehen:

Thread.sleep(2000) 

die AsyncTask noch funktionieren, ist es, weil Sie didn Wenn Sie die AsyncTask-Instanz in onDestroy() vor dem erneuten Erstellen des Fragments (beim Rotieren) ordnungsgemäß abbrechen und dieselbe AsyncTask-Instanz (nach dem Drehen) onPostExecute() ausführt, versucht sie, die Ressourcen mit getResources() mit der alten Fragmentinstanz zu finden (eine ungültige Instanz):

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

, die gleich ist:

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

So ist die endgültige Lösung ist es, die AsyncTask Instanz verwalten (zu stornieren, wenn dies noch funktioniert), bevor das Fragment neu erstellt, wenn Sie den Bildschirm drehen, und wenn während des Übergangs abgebrochen, starten Sie den AsyncTask nach der Rekonstruktion mit Hilfe eines boolean Flag:

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

    } 
} 
+0

statt, wenn 'getResources(). ***' mit 'Fragments.this.getResource(). ***' geholfen – Prabs

10

konfrontiert ich das gleiche Problem, das ich die Singletone Instanz nur hinzufügen Ressource zu erhalten, wie von Erick bezeichnet

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

können Sie auch

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

ich dies helfen wird, hoffen verwenden.

0

Wenn Sie die Klasse Application erweitern und ein statisches "globales" Context-Objekt wie folgt verwalten, können Sie das 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 diese verwenden, können Sie weg mit Toast und Laden von Ressourcen, ohne sich um Lifecycles besorgniserregend.

+5

Ich werde downvoted, 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. –

+0

Ihre Antwort ist downvoted, weil dies nur ein Hack nicht die richtige Lösung ist. Prüfe Lösung, die von @nhaarman geteilt wird –

2

Ich sah ähnliche Probleme, wenn die Anwendung Einstellungen Aktivität mit den geladenen Einstellungen sichtbar war. Wenn ich eine der Voreinstellungen ändern und dann den Anzeigeinhalt drehen und die Einstellung erneut ändern würde, würde dies mit einer Fehlermeldung enden, dass das Fragment (meine Preferences-Klasse) keiner Aktivität zugeordnet war.

Beim Debugging sah es so aus, als ob die onCreate() -Methode von PreferencesFragment zweimal aufgerufen wurde, wenn der Inhalt der Anzeige gedreht wurde. Das war schon seltsam genug. Dann fügte ich den isAdded() - Check außerhalb des Blocks hinzu, wo er auf den Absturz hinwies und das Problem löste.

Hier ist der Code des Listeners, der die Zusammenfassung der Einstellungen aktualisiert, um den neuen Eintrag anzuzeigen. Es ist in der onCreate() -Methode meiner Vorlieben Klasse befindet, 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 andere helfen!

14

Das Problem ist, dass Sie versuchen, mit getResources(). GetString() auf Ressourcen zuzugreifen (in diesem Fall Zeichenfolgen), die versuchen, die Ressourcen von der Aktivität abzurufen. Sehen Sie 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 hält.

Da die Aktivität möglicherweise nicht angefügt ist, löst der Aufruf getResources() eine Ausnahme aus.

Die akzeptierte Lösung IMHO ist nicht der Weg zu gehen, da Sie nur das Problem verbergen. Der richtige Weg ist nur die Ressourcen erhalten von woanders, dass immer existieren garantiert, wie der Anwendungskontext:

youApplicationObject.getResources().getString(...) 
+0

Ich benutzte diese Lösung, weil ich 'getString()' ausführen musste, als mein Fragment pausiert wurde. Danke – Geekarist

13

Ihr sind ziemlich Trick Lösung für dieses und Leck-Fragment aus Aktivität.

So im Fall von getResource oder irgendetwas ein, die auf der Aktivität Kontext von Fragmente Zugriff abhängig ist immer Aktivitätsstatus überprüfen und Fragmente Status als

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 


     } 
    } 
+5

isAdded() ist genug, weil: final public boolean isAdded() { zurück mHost! = Null && mAdded; } – NguyenDat

0

In meinem Fall Fragment Methoden aufgerufen wurde nach

folgt
getActivity().onBackPressed(); 
0

Ein alter Beitrag, aber ich war überrascht über die am meisten angenommene Antwort.

Die richtige Lösung dafür sollte sein, die asynctask in onStop (oder wo immer in Ihrem Fragment) abzubrechen. Auf diese Weise führen Sie kein Speicherleck ein (eine Asyntask, die einen Verweis auf Ihr zerstörtes Fragment enthält) und Sie haben eine bessere Kontrolle darüber, was in Ihrem Fragment vor sich geht.

@Override 
public void onStop() { 
    super.onStop(); 
    mYourAsyncTask.cancel(true); 
} 
+1

Die meistbeachtete Antwort enthält dies. Außerdem kann 'cancel' nicht verhindern, dass' onPostExecute' aufgerufen wird. – nhaarman

+0

Calling Cancel garantiert onPostExecute wird nie aufgerufen, beide Aufrufe werden auf demselben Thread ausgeführt, daher ist garantiert, dass sie nach dem Aufruf von Cancel nicht aufgerufen werden – Raz

3
if (getActivity() == null) return; 

Werke auch in einigen Fällen. Brich einfach die Codeausführung ab und vergewissere dich, dass die App nicht abstürzt

Verwandte Themen