碎片MyFragment未附加到活动。

438

我已经创建了一个小型测试应用程序,它代表了我的问题。 我正在使用ActionBarSherlock来实现带有(Sherlock)Fragments的选项卡。

我的代码: 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: Fragment MyFragment{410f6060} not attached to Activity

我认为这是因为在此期间创建了一个新的MyFragment,并在AsyncTask完成之前将其附加到Activity上。在onPostExecute中的代码调用了未连接的MyFragment
但是我该如何解决这个问题?

1
你应该使用片段填充器中的视图。mView = inflater.inflate(R.layout.my_layout, container, false) 现在,当您想要获取资源时,请使用此视图:mView.getResources().***。它帮助我修复了这个错误。 - foxis
@foxis 这会泄漏附加到您的mViewContext - nhaarman
也许我还没有检查它。为了避免泄漏,在 onDestroy 中获取 null 的 mView 怎么样? - foxis
14个回答

842

我找到了非常简单的答案:isAdded()

如果片段当前已添加到其活动中,则返回true

@Override
protected void onPostExecute(Void result){
    if(isAdded()){
        getResources().getString(R.string.app_name);
    }
}
为了避免在Fragment未附加到Activity时调用onPostExecute,可以在暂停或停止Fragment时取消AsyncTask。这样,isAdded()将不再需要。但是,建议保留此检查。

在我的情况下,当我从...启动另一个应用程序意图时,我遇到了相同的错误...有什么建议吗? - CoDe
2
http://developer.android.com/reference/android/app/Fragment.html#isAdded() ...还有一个isDetached(),它是在API级别13中添加的。 - Lucas Jota
5
如果使用 API <11,您需要使用 http://developer.android.com/reference/android/support/v4/app/Fragment.html#isAdded(),它会起作用。请注意不要改变原意。 - nhaarman
当我使用DialogFragment时,我遇到了这个问题。在对话框片段被取消后,我尝试启动另一个活动。然后出现了这个错误。通过在startActivity之后调用dismiss(),我避免了这个错误。问题是该片段已从Activity分离。 - Ataru

38

问题在于您正在尝试使用getResources().getString()访问资源(在此情况下是字符串),这将尝试从Activity中获取资源。请参阅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是保存您的Activity的对象。

由于Activity可能未附加,因此调用getResources()将引发异常。

在我看来,接受的解决方案并不是正确的方式,因为它只是隐藏了问题。正确的方法是从始终保证存在的其他位置获取资源,例如应用程序上下文:

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

我使用了这个解决方案,因为我需要在我的片段暂停时执行 getString()。谢谢。 - Geekarist
更新:根据文档,“在 Build.VERSION_CODES#R 之后,资源必须通过 Activity 或 Context 获取。Application#getResources() 在多窗口或辅助显示器上可能报告错误的值。” 因此,您可以使用 Context.getResources() 方法获取 Resources 的实例。 - MGLabs

26

我在这里面遇到了两种不同的情况:

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

我并未发现这方面的问题,虽然我也使用了一种(可能)更复杂的方式,它包括从活动中启动任务而不是片段。

希望这能帮助到某些人!:)


24

这个问题有一些棘手的解决方案和来自Activity的碎片泄漏。

因此,在Fragment从Activity访问getResource或其他依赖于Activity上下文的内容时,需要始终检查Activity状态和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()足够了,因为: 最终的public boolean isAdded()方法如下: 返回 mHost != null && mAdded; - NguyenDat
在我的情况下,这些检查还不够,我仍然会遇到崩溃问题,尽管我已经添加了它们。 - David
@David,isAdded 就足够了。我从未见过 getString()isAdded == true 的情况下崩溃。你确定已经显示了一个活动并附加了一个片段吗? - CoolMind

18
您的代码问题在于您使用AsyncTask的方式,因为当您在睡眠线程期间旋转屏幕时:
Thread.sleep(2000) 

如果 AsyncTask 还在运行,那是因为你没有在 onDestroy() 中正确地取消 AsyncTask 实例,导致在重建片段(旋转时)时,同一个 AsyncTask 实例(旋转后)运行 onPostExecute() 时,会使用旧的片段实例(无效实例)来查找资源:

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

    }
}

使用Fragments.this.getResource().***代替getResources().***可以帮助。 - Prabs

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

在某些情况下也可以起作用。它会打断代码执行,确保应用程序不会崩溃。


10

我遇到了同样的问题,我只是按照 Erick 所说的添加了单例实例以获取资源。

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

您也可以使用

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

我希望这能有所帮助。


2
当应用程序设置活动和加载的首选项可见时,我遇到了类似的问题。如果我更改其中一个首选项,然后使显示内容旋转并再次更改首选项,则会崩溃,并显示一条消息,指出片段(我的首选项类)未附加到活动上。
在调试时,当显示内容旋转时,PreferencesFragment的onCreate()方法被调用了两次,这已经非常奇怪了。然后我在它会导致崩溃的块之外添加了isAdded()检查,并解决了问题。
以下是更新首选项摘要以显示新条目的侦听器代码。它位于我的首选项类中的onCreate()方法中,该类扩展了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");
                    }
                }
            }
        };
        // ...
    }

我希望这篇文章能够帮助到其他人!


1
如果您扩展Application类并维护一个静态的“全局”Context对象,如下所示,则可以使用它来加载字符串资源,而不是使用activity。
public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

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

如果您使用这个,您可以不用担心生命周期问题而轻松使用Toast和资源加载。

7
我被贴了负面评价,但没有人解释原因。静态上下文通常不好,但我认为如果你有一个静态的应用程序引用,它就不是内存泄漏。 - Anthony Chuinard
1
你的回答被踩了,因为这只是一个hack而不是正确的解决方案。请查看@nhaarman分享的解决方案。 - Vivek Kumar Srivastava

1

我在Xamarin Android中遇到了类似的错误消息"Fragment MyFragment not attached to Context"

这个错误消息是因为调用了这个资源而导致的。

button.Text = Resources.GetString(Resource.String.please_wait)

我通过在Xamarin Android中使用解决了这个问题。

if (Context != null && IsAdded){ 
    button.Text = Resources.GetString(Resource.String.please_wait);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接