web-dev-qa-db-fra.com

Fragment MyFragment non attaché à l'activité

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 partie Thread.sleep pour simuler le téléchargement de données. Le code dans la onPostExecute consiste à simuler l'utilisation de la Fragment.

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

Java.lang.IllegalStateException: fragment MyFragment {410f6060} non attaché à l'activité

Je pense que c’est parce qu’un nouveau MyFragment a été créé entre-temps et qu’il était associé à l’activité avant la fin du AsyncTask. Le code dans onPostExecute appelle un MyFragment non associé.

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

377
nhaarman

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

Renvoie true si le fragment est actuellement ajouté à son activité.

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

Pour éviter que onPostExecute ne soit appelé lorsque la Fragment n'est pas rattachée à la Activity, vous devez annuler la AsyncTask lors de la mise en pause ou de l'arrêt de la Fragment. Alors, isAdded() ne serait plus nécessaire. Cependant, il est conseillé de garder cette vérification en place.

756
nhaarman

Le problème est que vous essayez d'accéder aux ressources (dans ce cas, les chaînes) à l'aide de getResources (). GetString (), qui essaiera 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 contient votre activité.

Étant donné que l'activité pourrait ne pas être attachée, votre appel à getResources () lève une exception.

La solution acceptée à mon humble avis n’est pas la solution, vous ne faites que masquer le problème. La bonne façon consiste simplement à obtenir les ressources d'un autre lieu dont l'existence est toujours garantie, comme le contexte de l'application:

youApplicationObject.getResources().getString(...)
25
Tiago

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

1) Quand je veux quand même que la tâche asynchrone se termine: imaginez mon onPostExecute stocke les données reçues, puis appelle un écouteur pour mettre à jour les vues. Par conséquent, pour être plus efficace, je veux que la tâche se termine quand même. retour. Dans ce cas, je fais habituellement ceci:

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

2) Lorsque je souhaite que la tâche asynchrone se termine uniquement lorsque les vues peuvent être mises à jour: le cas que vous proposez ici, la tâche met uniquement à jour les vues, aucun stockage de données nécessaire, de sorte qu'il n'a pas la moindre idée de son achèvement 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();
}

Cela ne me pose aucun problème, bien que j'utilise également un moyen (peut-être) plus complexe qui inclut le lancement de tâches à partir de l'activité au lieu des fragments.

J'aimerais que ça aide quelqu'un! :)

24
luixal

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 la rotation) s’exécute onPostExecute (), elle tente de trouver les ressources avec getResources () avec l'ancienne instance de fragment (une instance non valide):

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

qui équivaut à:

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

La solution finale consiste donc à gérer l'instance AsyncTask (à annuler si cela fonctionne toujours) avant la reconstruction du fragment lorsque vous faites pivoter l'écran. Si elle est annulée pendant la transition, redémarrez l'AsyncTask après 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;
        }

    }
}
18

Leur solution est assez astucieuse pour cela et la fuite d'un fragment d'activité.

Donc, dans le cas de getResource ou de tout ce qui dépend du contexte d'activité accédant à Fragment, il est toujours vérifié l'état de l'activité et le statut 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


        }
    }
17
Vinayak

J'ai rencontré le même problème que je viens d'ajouter l'instance singleton pour obtenir une 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 aidera.

10
Aristo Michael
if (getActivity() == null) return;

fonctionne également dans certains cas. Casse l'exécution du code et veille à ce que l'application ne se bloque pas

10
superUser

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 modifiait à nouveau la préférence, le message tomberait comme un message indiquant que le fragment (ma classe Préférences) n'était pas associé à une activité.

Lors du débogage, il semblait que la méthode onCreate () du PreferencesFragment était appelée deux fois lors de la rotation du contenu d'affichage. C'était déjà assez étrange. Ensuite, j'ai ajouté la vérification isAdded () en dehors du bloc, indiquant le blocage et résolvant 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!

2
ohgodnotanotherone

Un ancien message, mais j'ai été surpris par la réponse la plus votée.

La solution appropriée à cela devrait être d’annuler l’asynctask dans onStop (ou le cas échéant dans votre fragment). De cette façon, vous n'introduisez pas de fuite de mémoire (un asyncte 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);
}
0
Raz

Si vous étendez la classe Application et maintenez un objet de contexte statique 'global', 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 utilisez ceci, vous pouvez vous en sortir avec Toast et le chargement de ressources sans vous soucier des cycles de vie.

0
Anthony Chuinard

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

getActivity().onBackPressed();
0
CoolMind