Фрагмент MyFragment не приєднаний до активності


393

Я створив невеликий тестовий додаток, який представляє мою проблему. Я використовую ActionBarSherlock для реалізації вкладок з (Sherlock) фрагментами.

Мій код: 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();
    }
}

Я додав Thread.sleepчастину для імітації завантаження даних. Код у програмі onPostExecuteповинен імітувати використання Fragment.

Коли я дуже швидко обертаю екран між пейзажем та портретом, я отримую виняток у onPostExecuteкоді:

java.lang.IllegalStateException: фрагмент MyFragment {410f6060} не доданий до активності

Я думаю, це тому, MyFragmentщо тим часом було створено нове, яке було приєднано до Діяльності до AsyncTaskзавершення. Код у onPostExecuteвикликах без приєднання MyFragment.

Але як я можу це виправити?


1
Слід використовувати вид з надувного фрагмента. mView = inflater.inflate(R.layout.my_layout, container, false) І тепер використовувати цю точку зору , коли ви хочете , щоб отримати ресурси: mView.getResources().***. Це допомагає мені виправити цю помилку.
лисиця

@foxis Це витікає те, Contextщо приєднано до вашого `mView`.
nhaarman

Можливо, я ще цього не перевіряю. Щоб уникнути протікання, як mViewнасправді отримати null в onDestroy?
лисиця

Відповіді:


774

Я знайшов дуже просту відповідь isAdded():

Повернути, trueякщо фрагмент наразі доданий до його активності.

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

Для того, щоб уникнути onPostExecuteвід виклику , коли Fragmentне прив'язаний до Activityце скасувати , AsyncTaskколи паузи або зупинки Fragment. Тоді isAdded()більше не треба було б. Однак доцільно тримати цю перевірку.


У моєму випадку, коли я запускаю іншу програму "Намір" від ..., тоді я отримую таку ж помилку ... будь-яка пропозиція?
CoDe

1
developer.android.com/reference/android/app/… ... є також те isDetached(), що додано на рівні 13 API
Лукас Джота,

5
Якщо ви користуєтесь API <11, ви використовуєте developer.android.com/reference/android/support/v4/app/… там, де він буде працювати.
nhaarman

Я зіткнувся з цією проблемою, коли використовував DialogFragment. Після відхилення діалогуFragment я спробував розпочати іншу діяльність. Потім сталася ця помилка. Я уникнув цієї помилки, викликавши dismiss () після startActivity. Проблема полягала в тому, що фрагмент уже відключений від "Діяльності".
Атару

28

Проблема полягає в тому, що ви намагаєтеся отримати доступ до ресурсів (у цьому випадку рядків) за допомогою getResources (). GetString (), який намагатиметься отримати ресурси з Діяльності. Дивіться цей вихідний код класу 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 - це об'єкт, який містить вашу активність.

Оскільки Діяльність може бути не приєднана, ваш виклик getResources () видасть Виняток.

Прийняте рішення IMHO - це не такий шлях, як ви просто приховуєте проблему. Правильний спосіб - просто отримати ресурси з іншого місця, яке завжди гарантовано існує, як-от контекст програми:

youApplicationObject.getResources().getString(...)

Я використав це рішення, тому що мені потрібно було виконати, getString()коли мій фрагмент був призупинений. Спасибі
Geekarist

24

Тут я стикався з двома різними сценаріями:

1) Коли я хочу, щоб асинхронне завдання все-таки закінчилося: уявіть, що мій сайт onPostExecute зберігає отримані дані, а потім зателефонує слухачеві, щоб оновити представлення даних, щоб бути більш ефективним, я хочу, щоб завдання все-таки закінчилося, щоб у мене були дані готові, коли користувач керує ними назад. У такому випадку я зазвичай роблю це:

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

2) Коли я хочу, щоб асинхронне завдання закінчилося лише тоді, коли перегляди можна буде оновити: у випадку, який ви тут пропонуєте, завдання лише оновлює перегляди, не потрібно зберігати дані, тому у нього немає підказки для завершення завдання, якщо перегляди є більше не показувались. Я роблю це:

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

Я не знайшов жодних проблем з цим, хоча я також використовую (можливо) більш складний спосіб, який включає в себе запуск завдань із діяльності замість фрагментів.

Бажайте, щоб це допомагало комусь! :)


18

Проблема з вашим кодом полягає в тому, як ви використовуєте AsyncTask, тому що, коли ви обертаєте екран під час потоку сну:

Thread.sleep(2000) 

AsyncTask все ще працює, це тому, що ви не скасували екземпляр AsyncTask належним чином в onDestroy () перед тим, як фрагмент відновиться (коли ви обертаєтесь) і коли цей самий екземпляр AsyncTask (після обертання) запускається onPostExecute (), це намагається знайти ресурси з getResources () зі старим екземпляром фрагмента (недійсний примірник):

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

що еквівалентно:

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

Таким чином, остаточне рішення - керувати екземпляром AsyncTask (скасувати, якщо це все ще працює) перед тим, як фрагмент відновиться під час повороту екрана, а якщо його скасувати під час переходу, перезавантажте AsyncTask після реконструкції за допомогою булевого прапора:

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

    }
}

натомість, якщо getResources().***використовувати Fragments.this.getResource().***допомогли
Prabs

17

Вони є досить хитрим рішенням для цього та витік фрагменту від діяльності.

Так що у випадку getResource або будь-якого іншого, що залежить від контексту діяльності, який отримує доступ від Fragment, завжди перевіряють стан активності та стан фрагментів, як описано нижче

 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


        }
    }

7
isAdded () достатньо, тому що: остаточний загальнодоступний логічний isAdded () {повернути mHost! = null && mAdded; }
NguyenDat

У моєму випадку ця перевірка не приховується, все ще отримують збої, незважаючи на те, що я додав їх.
Девід

@David, isAddedдосить. Я ніколи не бачив ситуації, коли getString()б аварія, якби isAdded == true. Ви впевнені, що було показано активність та додано фрагмент?
CoolMind

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

працює також у деяких випадках. Просто перериває виконання коду від нього і переконайтеся, що додаток не виходить з ладу


10

Я зіткнувся з тією ж проблемою, що я просто додаю екземпляр singletone, щоб отримати ресурс, про який говорив Ерік

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

ви також можете використовувати

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

Сподіваюся, це допоможе.


2

Я зіткнувся з подібними проблемами, коли було видно активність налаштувань програми із завантаженими налаштуваннями. Якщо я змінив би одне з налаштувань, а потім змусив би вміст дисплея обертатись і знову змінив налаштування, воно зірветься з повідомленням про те, що фрагмент (мій клас "Налаштування") не був приєднаний до активності.

При налагодженні це виглядало так, що метод OnCreate () методу PreferencesFragment викликався двічі, коли вміст дисплея обертався. Це вже було досить дивно. Потім я додав чек isAdded () поза блоком, де це вказуватиме на збій, і це вирішило проблему.

Ось код слухача, який оновлює резюме налаштувань, щоб показати новий запис. Він розташований у методі onCreate () мого класу Preferences, який розширює клас 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");
                    }
                }
            }
        };
        // ...
    }

Я сподіваюся, що це допоможе іншим!


0

Якщо ви розширите Applicationклас і підтримуєте статичний об’єкт "глобального" контексту, як описано нижче, ви можете використовувати це замість діяльності для завантаження ресурсу String.

public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

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

Якщо ви користуєтесь цим, ви можете піти Toastі завантажувати ресурси, не турбуючись про життєві цикли.


5
Мене зволікають, але ніхто не пояснив, чому. Статичний контекст зазвичай поганий, але я вважав, що це не витік пам'яті, якщо у вас є статична посилання на програму.
Ентоні Чуйнард

Ваша відповідь є оскарженою, оскільки це просто неправе рішення. Перевірте рішення, яке поділилося @nhaarman
Vivek Kumar Srivastava

0

У моєму випадку методи фрагмента були викликані після

getActivity().onBackPressed();

0

Старий пост, але я був здивований найбільш відповідальною відповіддю.

Правильним рішенням для цього має бути скасування асинтакта в inStop (або де це доречно у вашому фрагменті). Таким чином, ви не вводите витік пам'яті (асинтакт, який зберігає посилання на ваш знищений фрагмент), і ви краще контролюєте, що відбувається у вашому фрагменті.

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

1
Ця найбільш відповідна відповідь включає це. Крім того, це cancelможе не завадити onPostExecuteвикликати його.
nhaarman

Скасування виклику гарантує, що onPostExecute ніколи не буде викликаний, обидва виклики виконуються в одному потоці, отже, ви гарантовано не будете викликати після скасування дзвінка
Raz
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.