Fragment MyFragment non attaché à l'activité

393

J'ai créé une petite application de test qui représente mon problème. J'utilise ActionBarSherlock pour implémenter des onglets avec des fragments (Sherlock).

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

J'ai ajouté la Thread.sleeppartie pour simuler le téléchargement des données. Le code dans le onPostExecuteest de simuler l'utilisation du Fragment.

Lorsque je fais pivoter l'écran très rapidement entre paysage et portrait, j'obtiens une exception au onPostExecutecode:

java.lang.IllegalStateException: Fragment MyFragment {410f6060} non attaché à Activity

Je pense que c'est parce qu'un nouveau MyFragmenta été créé entre-temps et a été attaché à l'activité avant la AsyncTaskfin. Le code dans fait onPostExecuteappel à un non attaché MyFragment.

Mais comment puis-je résoudre ce problème?

nhaarman
la source
1
Vous devez utiliser la vue depuis le gonfleur de fragments. mView = inflater.inflate(R.layout.my_layout, container, false) Et maintenant utiliser ce point de vue lorsque vous souhaitez obtenir des ressources: mView.getResources().***. Cela m'aide à corriger ce bogue.
foxis
@foxis Qui fuit le Contextqui est attaché à votre `mView`.
nhaarman
Peut-être que je ne le vérifie pas encore. Pour éviter les fuites, que diriez-vous d'obtenir null mViewdans onDestroy?
foxis

Réponses:

774

J'ai trouvé la réponse très simple isAdded():

Retourne truesi le fragment est actuellement ajouté à son activité.

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

Pour éviter onPostExecuted'être appelé lorsque le Fragmentn'est pas attaché au Activityest d'annuler le AsyncTasklorsque vous mettez en pause ou arrêtez le Fragment. Alors isAdded()ce ne serait plus nécessaire. Cependant, il est conseillé de garder ce contrôle en place.

nhaarman
la source
Dans mon cas, lorsque je lance une autre application à partir de ... alors j'obtiens la même erreur ... une suggestion?
CoDe
1
developer.android.com/reference/android/app/… ... il y a aussi isDetached(), qui a été ajouté au niveau API 13
Lucas Jota
5
Lorsque vous utilisez l'API <11, vous utilisez developer.android.com/reference/android/support/v4/app/… où cela fonctionnera.
nhaarman
J'ai rencontré ce problème lorsque j'ai utilisé DialogFragment. Après avoir rejeté dialogFragment, j'ai essayé de démarrer une autre activité. Ensuite, cette erreur s'est produite. J'ai évité cette erreur en appelant shutdown () après startActivity. Le problème était que ce fragment était déjà détaché de l'activité.
Ataru
28

Le problème est que vous essayez d'accéder aux ressources (dans ce cas, les chaînes) à l'aide de getResources (). GetString (), qui tentera d'obtenir les ressources de l'activité. Voir ce code source de la classe Fragment:

 /**
  * 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 est l'objet qui détient votre activité.

Parce que l'activité peut ne pas être attachée, votre appel getResources () lèvera une exception.

La solution acceptée à mon humble avis n'est pas la voie à suivre car vous cachez simplement le problème. La bonne façon est simplement d'obtenir les ressources d'un autre endroit qui est toujours garanti d'exister, comme le contexte de l'application:

youApplicationObject.getResources().getString(...)
Tiago
la source
J'ai utilisé cette solution parce que je devais exécuter getString()lorsque mon fragment était en pause. Merci
Geekarist
24

J'ai fait face à deux scénarios différents ici:

1) Quand je veux que la tâche asynchrone se termine de toute façon: imaginez que mon onPostExecute stocke les données reçues, puis appelle un écouteur pour mettre à jour les vues donc, pour être plus efficace, je veux que la tâche se termine quand même afin que les données soient prêtes lorsque l'utilisateur arrive retour. Dans ce cas, je fais généralement ceci:

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

2) Lorsque je veux que la tâche asynchrone se termine uniquement lorsque les vues peuvent être mises à jour: le cas que vous proposez ici, la tâche ne met à jour que les vues, aucun stockage de données nécessaire, donc il n'y a aucun indice pour que la tâche se termine si les vues sont ne plus être montré. Je fais ça:

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

Je n'ai trouvé aucun problème avec cela, bien que j'utilise également une méthode (peut-être) plus complexe qui inclut le lancement de tâches à partir de l'activité au lieu des fragments.

Je souhaite que cela aide quelqu'un! :)

luixal
la source
18

Le problème avec votre code est la façon dont vous utilisez AsyncTask, car lorsque vous faites pivoter l'écran pendant votre thread de sommeil:

Thread.sleep(2000) 

AsyncTask fonctionne toujours, c'est parce que vous n'avez pas annulé correctement l'instance AsyncTask dans onDestroy () avant la reconstruction du fragment (lorsque vous faites pivoter) et lorsque cette même instance AsyncTask (après rotation) s'exécute sur PostExecute (), cela essaie de trouver les ressources avec getResources () avec l'ancienne instance de fragment (une instance non valide):

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

ce qui équivaut à:

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

La solution finale consiste donc à gérer l'instance AsyncTask (pour annuler si cela fonctionne toujours) avant la reconstruction du fragment lorsque vous faites pivoter l'écran, et si elle est annulée pendant la transition, redémarrez AsyncTask après la reconstruction à l'aide d'un indicateur booléen:

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
la source
à la place si getResources().***vous Fragments.this.getResource().***
avez
17

Ils sont une solution assez astucieuse pour cela et une fuite de fragment de l'activité.

Donc, dans le cas de getResource ou de tout ce qui dépend du contexte d'activité accédant à partir de Fragment, il faut toujours vérifier l'état de l'activité et l'état des fragments comme suit

 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
la source
7
isAdded () suffit car: final public booléen isAdded () {return mHost! = null && mAdded; }
NguyenDat
Dans mon cas, ces vérifications ne sont pas suffisantes, et continuent de se bloquer malgré que j'aie ajouté celles-ci.
David
@David, isAddedc'est assez. Je n'ai jamais vu une situation où getString()s'était écrasé si isAdded == true. Êtes-vous sûr qu'une activité a été montrée et qu'un fragment a été attaché?
CoolMind
14
if (getActivity() == null) return;

fonctionne également dans certains cas. Interrompt juste l'exécution du code et assurez-vous que l'application ne plante pas

superutilisateur
la source
10

J'ai fait face au même problème que j'ajoute juste l'instance de monotone pour obtenir la ressource comme indiqué par Erick

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

vous pouvez aussi utiliser

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

J'espère que cela vous aidera.

Aristo Michael
la source
2

J'ai rencontré des problèmes similaires lorsque l'activité des paramètres de l'application avec les préférences chargées était visible. Si je modifiais l'une des préférences, puis fais pivoter le contenu d'affichage et modifiais à nouveau la préférence, il se plantait avec un message indiquant que le fragment (ma classe Préférences) n'était pas attaché à une activité.

Lors du débogage, il semblait que la méthode onCreate () du PreferencesFragment était appelée deux fois lorsque le contenu de l'affichage pivotait. C'était déjà assez étrange. Ensuite, j'ai ajouté la vérification isAdded () en dehors du bloc où cela indiquerait le crash et cela a résolu le problème.

Voici le code de l'auditeur qui met à jour le résumé des préférences pour afficher la nouvelle entrée. Il est situé dans la méthode onCreate () de ma classe Preferences qui étend la classe PreferenceFragment:

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");
                    }
                }
            }
        };
        // ...
    }

J'espère que cela aidera les autres!

ohgodnotanotherone
la source
0

Si vous étendez la Applicationclasse et maintenez un objet contextuel «global» statique, comme suit, vous pouvez l'utiliser à la place de l'activité pour charger une ressource String.

public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

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

Si vous l'utilisez, vous pouvez vous en sortir Toastet charger les ressources sans vous soucier des cycles de vie.

Anthony Chuinard
la source
5
Je suis déçu, mais personne n'a expliqué pourquoi. Les contextes statiques sont généralement mauvais, mais je pensais que ce n'était pas une fuite de mémoire si vous avez une référence d'application statique.
Anthony Chuinard
Votre réponse est rejetée parce que ce n'est qu'une solution de hack pas appropriée. Vérifier la solution partagée par @nhaarman
Vivek Kumar Srivastava
0

Dans mon cas, des méthodes de fragment ont été appelées après

getActivity().onBackPressed();
CoolMind
la source
0

Un vieux post, mais j'ai été surpris de la réponse la plus votée.

La bonne solution pour cela devrait être d'annuler la tâche asynctrone dans onStop (ou, le cas échéant, dans votre fragment). De cette façon, vous n'introduisez pas de fuite de mémoire (un asynctasque gardant une référence à votre fragment détruit) et vous avez un meilleur contrôle de ce qui se passe dans votre fragment.

@Override
public void onStop() {
    super.onStop();
    mYourAsyncTask.cancel(true);
}
Raz
la source
1
La réponse la plus votée comprend cela. De plus, cancelne peut pas empêcher onPostExecuted'être invoqué.
nhaarman
L'appel d'annulation garantit que onPostExecute ne sera jamais appelé, les deux appels s'exécutent sur le même thread, vous avez donc la garantie qu'il ne sera pas appelé après l'appel d'annulation
Raz