Fragment MyFragment nie jest dołączony do działania

393

Stworzyłem małą aplikację testową, która reprezentuje mój problem. Używam ActionBarSherlock do implementacji zakładek z fragmentami (Sherlock).

Mój kod: 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();
    }
}

Dodałem Thread.sleepczęść do symulacji pobierania danych. Kod w to onPostExecutema symulować użycie Fragment.

Kiedy bardzo szybko obracam ekran między poziomym a pionowym, pojawia się wyjątek w onPostExecutekodzie:

java.lang.IllegalStateException: Fragment MyFragment {410f6060} nie jest dołączony do działania

Myślę, że dzieje się tak, ponieważ MyFragmentw międzyczasie utworzono nowy i został on dołączony do działania przed AsyncTaskzakończeniem. Kod onPostExecutewywołuje nieprzyłączony MyFragment.

Ale jak mogę to naprawić?

nhaarman
źródło
1
Powinieneś użyć widoku z fragmentu inflatora. mView = inflater.inflate(R.layout.my_layout, container, false) A teraz użyć tego widoku, gdy chcesz uzyskać zasobów: mView.getResources().***. Pomaga mi naprawić ten błąd.
foxis
@ Foxis That, który przecieka, Contextktóry jest dołączony do twojego `mView`.
nhaarman
Być może jeszcze tego nie sprawdzam. Aby uniknąć wycieku, możesz uzyskać mViewzerowanie w usłudze Destroy?
foxis

Odpowiedzi:

774

Znalazłem bardzo prostą odpowiedź isAdded():

Zwróć, truejeśli fragment jest obecnie dodawany do swojej aktywności.

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

Aby uniknąć onPostExecutewywoływania, gdy Fragmentnie jest on dołączony do, Activitynależy anulować AsyncTaskprzerwanie lub zatrzymanie Fragment. Wtedy isAdded()nie byłoby już konieczne. Jednak wskazane jest, aby utrzymać tę kontrolę na miejscu.

nhaarman
źródło
W moim przypadku, gdy uruchamiam Inną aplikację Zamiar od ... wtedy pojawia się ten sam błąd ... jakikolwiek błąd?
CoDe
1
developer.android.com/reference/android/app/… ... jest też isDetached(), że został dodany na poziomie API 13
Lucas Jota
5
Korzystając z interfejsu API <11, używasz developer.android.com/reference/android/support/v4/app/… tam, gdzie będzie działać.
nhaarman
Napotkałem ten problem, gdy korzystałem z DialogFragment. Po zamknięciu okna dialogowego Fragment próbowałem rozpocząć inną aktywność. Następnie wystąpił ten błąd. Uniknąłem tego błędu, wywołując metodęISSISS () po uruchomieniu StartActivity. Problem polegał na tym, że fragment został już odłączony od działania.
Ataru
28

Problem polega na tym, że próbujesz uzyskać dostęp do zasobów (w tym przypadku ciągów znaków) za pomocą getResources (). GetString (), który spróbuje uzyskać zasoby z działania. Zobacz ten kod źródłowy klasy 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 jest przedmiotem, który trzyma twoją aktywność.

Ponieważ działanie może nie zostać dołączone, wywołanie getResources () spowoduje zgłoszenie wyjątku.

Przyjęte rozwiązanie IMHO nie jest dobrym rozwiązaniem, ponieważ tylko ukrywasz problem. Prawidłowym sposobem jest po prostu zdobycie zasobów z innego miejsca, które zawsze istnieje, na przykład kontekst aplikacji:

youApplicationObject.getResources().getString(...)
Tiago
źródło
Użyłem tego rozwiązania, ponieważ musiałem wykonać, getString()gdy mój fragment został wstrzymany. Dzięki
Geekarist
24

Mam tu do czynienia z dwoma różnymi scenariuszami:

1) Gdy chcę mimo to zakończyć zadanie asynchroniczne: wyobraź sobie, że mój onPostExecute przechowuje odebrane dane, a następnie wywołuje słuchacza w celu zaktualizowania widoków, aby być bardziej wydajnym, chcę, aby zadanie i tak zakończyło się, więc mam dane gotowe, gdy użytkownik zadzwoni plecy. W takim przypadku zwykle robię to:

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

2) Gdy chcę, aby zadanie asynchroniczne zakończyło się tylko wtedy, gdy widoki mogą zostać zaktualizowane: w przypadku, gdy proponujesz tutaj, zadanie aktualizuje tylko widoki, nie wymaga przechowywania danych, więc nie ma pojęcia, że ​​zadanie może zakończyć się, jeśli widoki są nie jest już pokazywany. Robię to:

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

Nie znalazłem z tym problemu, chociaż używam również (być może) bardziej złożonego sposobu, który obejmuje uruchamianie zadań z działania zamiast fragmentów.

Szkoda, że ​​to komuś pomaga! :)

luixal
źródło
18

Problem z kodem polega na sposobie korzystania z AsyncTask, ponieważ podczas obracania ekranu podczas wątku uśpienia:

Thread.sleep(2000) 

AsyncTask nadal działa, ponieważ nie anulowałeś poprawnie instancji AsyncTask w onDestroy () przed odbudowaniem fragmentu (po obróceniu), a gdy ta sama instancja AsyncTask (po obróceniu) działa na PostPostExecute (), to próbuje znaleźć zasoby za pomocą getResources () ze starym wystąpieniem fragmentu (niepoprawne wystąpienie):

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

co jest równoważne z:

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

Tak więc ostatecznym rozwiązaniem jest zarządzanie instancją AsyncTask (aby anulować, jeśli nadal działa), zanim fragment zostanie odbudowany po obróceniu ekranu, a jeśli zostanie anulowany podczas przejścia, zrestartuj AsyncTask po rekonstrukcji za pomocą flagi logicznej:

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
źródło
zamiast tego, jeśli getResources().***korzystasz z Fragments.this.getResource().***pomocy
Prabs
17

Ich rozwiązanie jest dość trudne i wyciek fragmentu z działalności.

Tak więc w przypadku getResource lub czegokolwiek, co jest zależne od kontekstu aktywności uzyskującego dostęp z Fragmentu, zawsze sprawdza się status aktywności i status fragmentów w następujący sposób

 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
źródło
7
isAdded () wystarczy, ponieważ: końcowy publiczny boolean isAdded () {return mHost! = null && mAdded; }
NguyenDat
W moim przypadku te kontrole nie są wystarczające, mimo to wciąż się zawieszają.
David
@David, isAddedwystarczy. Nigdy nie widziałem sytuacji, w której getString()się rozbił isAdded == true. Czy na pewno pokazano aktywność i dołączono fragment?
CoolMind
14
if (getActivity() == null) return;

działa również w niektórych przypadkach. Po prostu przerywa wykonanie kodu i upewnia się, że aplikacja nie ulega awarii

superUser
źródło
10

Napotkałem ten sam problem, po prostu dodałem instancję singletone, aby uzyskać zasób, o którym wspomniał Erick

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

możesz także użyć

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

Mam nadzieję, że to pomoże.

Aristo Michael
źródło
2

Napotkałem podobne problemy, gdy aktywność ustawień aplikacji z załadowanymi preferencjami była widoczna. Gdybym zmienił jedną z preferencji, a następnie sprawiłby, że wyświetlana zawartość zostanie obrócona i ponownie zmieniona, spowodowałoby to awarię z komunikatem, że fragment (moja klasa Preferencji) nie został dołączony do działania.

Podczas debugowania wyglądało to tak, jakby metoda onCreate () metody PreferencesFragment była wywoływana dwukrotnie podczas obracania wyświetlanej treści. To już było dość dziwne. Następnie dodałem kontrolę isAdded () poza blokiem, która wskazywałaby awarię i rozwiązała problem.

Oto kod detektora, który aktualizuje podsumowanie preferencji, aby pokazać nowy wpis. Znajduje się w metodzie onCreate () mojej klasy Preferences, która rozszerza klasę 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");
                    }
                }
            }
        };
        // ...
    }

Mam nadzieję, że to pomoże innym!

ohgodnotanotherone
źródło
0

Jeśli rozszerzysz Applicationklasę i utrzymasz statyczny „globalny” obiekt kontekstu w następujący sposób, możesz użyć go zamiast działania do załadowania zasobu String.

public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

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

Jeśli go użyjesz, możesz uciec Toasti ładować zasoby bez martwienia się o cykle życia.

Anthony Chuinard
źródło
5
Jestem poniżany, ale nikt nie wyjaśnił dlaczego. Konteksty statyczne są zwykle złe, ale pomyślałem, że nie jest to wyciek pamięci, jeśli masz statyczne odniesienie do aplikacji.
Anthony Chuinard,
Twoja odpowiedź została odrzucona, ponieważ jest to tylko hack, niewłaściwe rozwiązanie. Sprawdź rozwiązanie udostępnione przez @nhaarman
Vivek Kumar Srivastava
0

W moim przypadku wywołano metody fragmentów

getActivity().onBackPressed();
CoolMind
źródło
0

Stary post, ale byłem zaskoczony najbardziej pozytywnie ocenioną odpowiedzią.

Właściwym rozwiązaniem tego problemu powinno być anulowanie asynchroniki w programie onStop (lub w dowolnym miejscu w swoim fragmencie). W ten sposób nie wprowadzasz przecieku pamięci (asynctask z odniesieniem do twojego zniszczonego fragmentu) i masz lepszą kontrolę nad tym, co dzieje się w twoim fragmencie.

@Override
public void onStop() {
    super.onStop();
    mYourAsyncTask.cancel(true);
}
Raz
źródło
1
Najbardziej pozytywna odpowiedź obejmuje to. Ponadto cancelnie może uniemożliwić onPostExecutewywołania.
nhaarman
Wywołanie anulowania gwarantuje, że onPostExecute nigdy nie zostanie wywołany, oba połączenia zostaną wykonane w tym samym wątku, dlatego masz gwarancję, że nie zostanie ono wywołane po wywołaniu anulowania
Raz