Đoạn MyFragment không được đính kèm với Hoạt động


393

Tôi đã tạo một ứng dụng thử nghiệm nhỏ thể hiện vấn đề của mình. Tôi đang sử dụng ActionBarSherlock để triển khai các tab với (Sherlock) Fragment.

Mã của tôi: 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();
    }
}

Tôi đã thêm Thread.sleepphần để mô phỏng dữ liệu tải xuống. Các mã trong onPostExecutelà để mô phỏng việc sử dụng Fragment.

Khi tôi xoay màn hình rất nhanh giữa ngang và dọc, tôi nhận được Ngoại lệ ở onPostExecutemã:

java.lang.IllegalStateException: Fragment MyFragment {410f6060} không được đính kèm với Activity

Tôi nghĩ rằng đó là bởi vì một cái mới MyFragmentđã được tạo ra trong thời gian đó và đã được đính kèm với Hoạt động trước khi AsyncTaskkết thúc. Các mã trong onPostExecutecác cuộc gọi khi không được chú ý MyFragment.

Nhưng làm thế nào tôi có thể sửa lỗi này?


1
Bạn nên sử dụng xem từ lạm phát mảnh vỡ. mView = inflater.inflate(R.layout.my_layout, container, false) Và bây giờ sử dụng chế độ xem này khi bạn muốn nhận tài nguyên : mView.getResources().***. Nó giúp tôi sửa lỗi này.
cáo

@foxis Điều đó làm rò rỉ Contextcái được gắn vào `mView` của bạn.
nhaarman

Có thể tôi chưa kiểm tra nó. Để tránh rò rỉ, làm thế nào để có được null mViewtrong onDestroy?
cáo

Câu trả lời:


774

Tôi đã tìm thấy câu trả lời rất đơn giản isAdded()::

Trả về truenếu đoạn hiện đang được thêm vào hoạt động của nó.

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

Để tránh onPostExecutebị gọi khi Fragmentkhông được gắn vào Activitylà hủy bỏ AsyncTaskkhi tạm dừng hoặc dừng Fragment. Sau đó isAdded()sẽ không cần thiết nữa. Tuy nhiên, nên giữ kiểm tra này tại chỗ.


Trong trường hợp của tôi khi tôi khởi chạy Một ứng dụng khác Ý định từ ... thì tôi cũng gặp lỗi tương tự ... có bất kỳ lỗi nào không?
CoDe

1
developer.android.com/reference/android/app/áp ... cũng có isDetached(), đã được thêm vào API cấp 13
Lucas Jota

5
Khi trên API <11, bạn đang sử dụng developer.android.com/reference/android/support/v4/app/iêu nơi nó sẽ hoạt động.
nhaarman

Tôi gặp phải vấn đề này khi tôi sử dụng DialogFragment. Sau khi bỏ qua hộp thoạiFragment, tôi đã thử bắt đầu một hoạt động khác. Sau đó, lỗi này xảy ra. Tôi đã tránh được lỗi này bằng cách gọi hàm notify () sau startActivity. Vấn đề là đoạn đó đã bị tách ra khỏi Activity.
Ataru

28

Vấn đề là bạn đang cố truy cập tài nguyên (trong trường hợp này là chuỗi) bằng cách sử dụng getResource (). GetString (), sẽ cố gắng lấy tài nguyên từ Activity. Xem mã nguồn này của lớp 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 là đối tượng chứa Hoạt động của bạn.

Vì Hoạt động có thể không được đính kèm, lệnh gọi getResource () của bạn sẽ ném Ngoại lệ.

Giải pháp được chấp nhận IMHO không phải là cách để đi vì bạn chỉ đang che giấu vấn đề. Cách chính xác là lấy tài nguyên từ nơi khác luôn được đảm bảo tồn tại, như bối cảnh ứng dụng:

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

Tôi đã sử dụng giải pháp này bởi vì tôi cần phải thực thi getString()khi đoạn của tôi bị tạm dừng. Cảm ơn
Geekarist

24

Tôi đã phải đối mặt với hai kịch bản khác nhau ở đây:

1) Khi tôi muốn tác vụ không đồng bộ kết thúc bằng mọi cách: hãy tưởng tượng onPostExecute của tôi lưu trữ dữ liệu nhận được và sau đó gọi người nghe để cập nhật chế độ xem, để hiệu quả hơn, tôi muốn tác vụ kết thúc bằng mọi cách để tôi có sẵn dữ liệu khi người dùng truy cập trở lại. Trong trường hợp này tôi thường làm điều này:

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

2) Khi tôi muốn tác vụ không đồng bộ chỉ kết thúc khi có thể cập nhật chế độ xem: trường hợp bạn đang đề xuất ở đây, tác vụ chỉ cập nhật chế độ xem, không cần lưu trữ dữ liệu, do đó, không có manh mối nào để tác vụ kết thúc nếu chế độ xem không còn được hiển thị. Tôi làm việc này:

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

Tôi không thấy có vấn đề gì với điều này, mặc dù tôi cũng sử dụng một cách (có thể) phức tạp hơn bao gồm khởi chạy các tác vụ từ hoạt động thay vì các đoạn.

Mong điều này giúp được ai đó! :)


18

Vấn đề với mã của bạn là cách bạn đang sử dụng AsyncTask, bởi vì khi bạn xoay màn hình trong suốt chuỗi ngủ:

Thread.sleep(2000) 

AsyncTask vẫn hoạt động, đó là vì bạn đã không hủy bỏ cá thể AsyncTask đúng cách trong onDestroy () trước khi đoạn được xây dựng lại (khi bạn xoay) và khi ví dụ AsyncTask này (sau khi xoay) chạy trênPostExecute (), điều này sẽ cố gắng tìm các tài nguyên với getResource () với thể hiện đoạn cũ (một thể hiện không hợp lệ):

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

tương đương với:

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

Vì vậy, giải pháp cuối cùng là quản lý phiên bản AsyncTask (để hủy nếu điều này vẫn hoạt động) trước khi đoạn được xây dựng lại khi bạn xoay màn hình và nếu bị hủy trong quá trình chuyển đổi, hãy khởi động lại AsyncTask sau khi xây dựng lại bằng cờ boolean:

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

    }
}

thay vào đó nếu getResources().***sử dụng được Fragments.this.getResource().***giúp đỡ
Mitchs

17

Họ là giải pháp khá lừa cho việc này và rò rỉ mảnh vỡ từ hoạt động.

Vì vậy, trong trường hợp getResource hoặc bất cứ thứ gì phụ thuộc vào bối cảnh hoạt động truy cập từ Fragment, nó luôn kiểm tra trạng thái hoạt động và trạng thái đoạn như sau

 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 () là đủ vì: boolean công khai cuối cùng isAdded () {return mhost! = null && mAdded; }
Nguyễn

Trong trường hợp của tôi, kiểm tra này không khó khăn, vẫn gặp sự cố mặc dù tôi đã thêm chúng.
David

@David, isAddedlà đủ. Tôi chưa bao giờ thấy một tình huống khi getString()đã bị rơi nếu isAdded == true. Bạn có chắc chắn một hoạt động đã được hiển thị và một đoạn được đính kèm?
CoolMind

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

cũng hoạt động trong một số trường hợp. Chỉ cần phá vỡ mã thực thi từ nó và đảm bảo ứng dụng không bị sập


10

Tôi đã đối mặt với cùng một vấn đề, tôi chỉ cần thêm cá thể singletone để lấy tài nguyên như được giới thiệu bởi Erick

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

bạn cũng có thể dùng

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

Hy vọng điều này có thể giúp cho bạn.


2

Tôi gặp phải vấn đề tương tự khi hoạt động cài đặt ứng dụng với các tùy chọn được tải có thể nhìn thấy. Nếu tôi thay đổi một trong các tùy chọn và sau đó làm cho nội dung hiển thị xoay và thay đổi tùy chọn một lần nữa, nó sẽ bị sập với thông báo rằng đoạn (lớp Tùy chọn của tôi) không được đính kèm với một hoạt động.

Khi gỡ lỗi, nó trông giống như Phương thức onCreate () của PreferencesFragment đã được gọi hai lần khi nội dung hiển thị được xoay. Thế là đủ lạ rồi. Sau đó, tôi đã thêm kiểm tra isAdded () bên ngoài khối nơi nó sẽ chỉ ra sự cố và nó đã giải quyết vấn đề.

Dưới đây là mã của người nghe cập nhật tóm tắt sở thích để hiển thị mục mới. Nó nằm trong phương thức onCreate () của lớp Preferences của tôi, mở rộng lớp 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");
                    }
                }
            }
        };
        // ...
    }

Tôi hy vọng điều này sẽ giúp những người khác!


0

Nếu bạn mở rộng Applicationlớp và duy trì một đối tượng Ngữ cảnh 'toàn cầu' tĩnh, như sau, thì bạn có thể sử dụng nó thay vì hoạt động để tải tài nguyên Chuỗi.

public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

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

Nếu bạn sử dụng điều này, bạn có thể thoát khỏi Toastvà tải tài nguyên mà không phải lo lắng về vòng đời.


5
Tôi đang bị hạ thấp nhưng không ai giải thích được tại sao. Các bối cảnh tĩnh thường xấu nhưng tôi nghĩ rằng đó không phải là rò rỉ bộ nhớ nếu bạn có tham chiếu Ứng dụng tĩnh.
Anthony Chuinard

Câu trả lời của bạn bị hạ thấp bởi vì đây chỉ là một giải pháp không phù hợp. Kiểm tra giải pháp được chia sẻ bởi @nhaarman
Vivek Kumar Srivastava

0

Trong trường hợp của tôi, các phương thức phân đoạn đã được gọi sau

getActivity().onBackPressed();

0

Một bài viết cũ, nhưng tôi đã ngạc nhiên về câu trả lời được bình chọn nhiều nhất.

Giải pháp thích hợp cho việc này là hủy bỏ asynctask trong onStop (hoặc bất cứ nơi nào thích hợp trong đoạn của bạn). Bằng cách này, bạn không giới thiệu rò rỉ bộ nhớ (một asynctask giữ tham chiếu đến đoạn bị phá hủy của bạn) và bạn có thể kiểm soát tốt hơn những gì đang diễn ra trong đoạn của mình.

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

1
Câu trả lời được đánh giá cao nhất bao gồm điều này. Ngoài ra, cancelcó thể không ngăn chặn onPostExecuteđược gọi.
nhaarman

Gọi hủy sẽ đảm bảo onPostExecute sẽ không bao giờ được gọi, cả hai cuộc gọi đều thực hiện trên cùng một chuỗi do đó bạn được đảm bảo rằng nó sẽ không được gọi sau khi gọi hủy
Raz
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.