Phân mảnh onCreateView và onActivityCreate được gọi hai lần


101

Tôi đang phát triển một ứng dụng sử dụng Android 4.0 ICS và các mảnh.

Hãy xem xét ví dụ được sửa đổi này từ ứng dụng mẫu thử nghiệm của API ICS 4.0.3 (API cấp 15):

public class FragmentTabs extends Activity {

private static final String TAG = FragmentTabs.class.getSimpleName();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    final ActionBar bar = getActionBar();
    bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    bar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);

    bar.addTab(bar.newTab()
            .setText("Simple")
            .setTabListener(new TabListener<SimpleFragment>(
                    this, "mysimple", SimpleFragment.class)));

    if (savedInstanceState != null) {
        bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
        Log.d(TAG, "FragmentTabs.onCreate tab: " + savedInstanceState.getInt("tab"));
        Log.d(TAG, "FragmentTabs.onCreate number: " + savedInstanceState.getInt("number"));
    }

}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
}

public static class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private final Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;
    private final Bundle mArgs;
    private Fragment mFragment;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        this(activity, tag, clz, null);
    }

    public TabListener(Activity activity, String tag, Class<T> clz, Bundle args) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mArgs = args;

        // Check to see if we already have a fragment for this tab, probably
        // from a previously saved state.  If so, deactivate it, because our
        // initial state is that a tab isn't shown.
        mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
        if (mFragment != null && !mFragment.isDetached()) {
            Log.d(TAG, "constructor: detaching fragment " + mTag);
            FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
            ft.detach(mFragment);
            ft.commit();
        }
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
            Log.d(TAG, "onTabSelected adding fragment " + mTag);
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            Log.d(TAG, "onTabSelected attaching fragment " + mTag);
            ft.attach(mFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            Log.d(TAG, "onTabUnselected detaching fragment " + mTag);
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        Toast.makeText(mActivity, "Reselected!", Toast.LENGTH_SHORT).show();
    }
}

public static class SimpleFragment extends Fragment {
    TextView textView;
    int mNum;

    /**
     * When creating, retrieve this instance's number from its arguments.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(FragmentTabs.TAG, "onCreate " + (savedInstanceState != null ? ("state " + savedInstanceState.getInt("number")) : "no state"));
        if(savedInstanceState != null) {
            mNum = savedInstanceState.getInt("number");
        } else {
            mNum = 25;
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.d(TAG, "onActivityCreated");
        if(savedInstanceState != null) {
            Log.d(TAG, "saved variable number: " + savedInstanceState.getInt("number"));
        }
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        Log.d(TAG, "onSaveInstanceState saving: " + mNum);
        outState.putInt("number", mNum);
        super.onSaveInstanceState(outState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Log.d(FragmentTabs.TAG, "onCreateView " + (savedInstanceState != null ? ("state: " + savedInstanceState.getInt("number")) : "no state"));
        textView = new TextView(getActivity());
        textView.setText("Hello world: " + mNum);
        textView.setBackgroundDrawable(getResources().getDrawable(android.R.drawable.gallery_thumb));
        return textView;
    }
}

}

Đây là đầu ra được truy xuất từ ​​việc chạy ví dụ này và sau đó xoay điện thoại:

06-11 11:31:42.559: D/FragmentTabs(10726): onTabSelected adding fragment mysimple
06-11 11:31:42.559: D/FragmentTabs(10726): onCreate no state
06-11 11:31:42.559: D/FragmentTabs(10726): onCreateView no state
06-11 11:31:42.567: D/FragmentTabs(10726): onActivityCreated
06-11 11:31:45.286: D/FragmentTabs(10726): onSaveInstanceState saving: 25
06-11 11:31:45.325: D/FragmentTabs(10726): onCreate state 25
06-11 11:31:45.340: D/FragmentTabs(10726): constructor: detaching fragment mysimple
06-11 11:31:45.340: D/FragmentTabs(10726): onTabSelected attaching fragment mysimple
06-11 11:31:45.348: D/FragmentTabs(10726): FragmentTabs.onCreate tab: 0
06-11 11:31:45.348: D/FragmentTabs(10726): FragmentTabs.onCreate number: 0
06-11 11:31:45.348: D/FragmentTabs(10726): onCreateView state: 25
06-11 11:31:45.348: D/FragmentTabs(10726): onActivityCreated
06-11 11:31:45.348: D/FragmentTabs(10726): saved variable number: 25
06-11 11:31:45.348: D/FragmentTabs(10726): onCreateView no state
06-11 11:31:45.348: D/FragmentTabs(10726): onActivityCreated

Câu hỏi của tôi là, tại sao onCreateView và onActivityCreate được gọi hai lần? Lần đầu tiên với Gói có trạng thái đã lưu và lần thứ hai với Trạng thái không được lưu?

Điều này gây ra vấn đề với việc giữ lại trạng thái của phân mảnh khi quay.


2
Tôi nghĩ rằng câu hỏi này có thể liên quan đến stackoverflow.com/a/8678705/404395
marioosh

Câu trả lời:


45

Tôi cũng đã vò đầu bứt tai về điều này một lúc, và vì lời giải thích của Dave hơi khó hiểu nên tôi sẽ đăng mã (dường như đang hoạt động) của mình:

private class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private Fragment mFragment;
    private Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mFragment=mActivity.getFragmentManager().findFragmentByTag(mTag);
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.replace(android.R.id.content, mFragment, mTag);
        } else {
            if (mFragment.isDetached()) {
                ft.attach(mFragment);
            }
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
    }
}

Như bạn có thể thấy, nó khá giống với mẫu Android, ngoài việc không tách rời trong hàm tạo và sử dụng thay thế thay vì thêm .

Sau nhiều lần cân nhắc và thử-và-sai, tôi nhận thấy rằng việc tìm kiếm phân đoạn trong hàm tạo dường như làm cho vấn đề onCreateView kép biến mất một cách kỳ diệu (tôi cho rằng nó chỉ kết thúc bằng rỗng đối với onTabSelected khi được gọi thông qua đường dẫn ActionBar.setSelectedNavigationItem () khi lưu / khôi phục trạng thái).


Hoạt động hoàn toàn tốt! Bạn đã cứu giấc ngủ đêm của tôi! Cảm ơn bạn :)
jaibatrik

bạn cũng có thể sử dụng segment.getClass (). getName () nếu bạn muốn xóa biến lớp và xóa tham số khỏi lời gọi
Bến Sewards

Hoạt động hoàn hảo với mẫu Android "tham chiếu trước. TabListener" - tnx. Android mới nhất "TabListener ref. Sample ref." [Như trên 4 ix 2013] thực sự, thực sự sai.
Grzegorz Dev

đâu là lời gọi phương thức ft.commit () ??
MSaudi

1
@MuhammadBabar, xem stackoverflow.com/questions/23248789/… . Nếu bạn sử dụng addthay thế replacevà xoay màn hình, bạn sẽ có nhiều mảnh vỡ ' onCreateView().
CoolMind

26

Ok, đây là những gì tôi phát hiện ra.

Điều tôi không hiểu là tất cả các đoạn được đính kèm với một hoạt động khi xảy ra thay đổi cấu hình (xoay điện thoại) đều được tạo lại và thêm lại vào hoạt động. (có lý)

Những gì đang xảy ra trong hàm tạo TabListener là tab đã được tách ra nếu nó được tìm thấy và gắn vào hoạt động. Xem bên dưới:

mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
    if (mFragment != null && !mFragment.isDetached()) {
        Log.d(TAG, "constructor: detaching fragment " + mTag);
        FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
        ft.detach(mFragment);
        ft.commit();
    }

Sau đó trong hoạt động onCreate tab đã chọn trước đó đã được chọn từ trạng thái phiên bản đã lưu. Xem bên dưới:

if (savedInstanceState != null) {
    bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
    Log.d(TAG, "FragmentTabs.onCreate tab: " + savedInstanceState.getInt("tab"));
    Log.d(TAG, "FragmentTabs.onCreate number: " + savedInstanceState.getInt("number"));
}

Khi tab được chọn, nó sẽ được gắn lại trong lệnh gọi lại onTabSelected.

public void onTabSelected(Tab tab, FragmentTransaction ft) {
    if (mFragment == null) {
        mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
        Log.d(TAG, "onTabSelected adding fragment " + mTag);
        ft.add(android.R.id.content, mFragment, mTag);
    } else {
        Log.d(TAG, "onTabSelected attaching fragment " + mTag);
        ft.attach(mFragment);
    }
}

Đoạn được đính kèm là cuộc gọi thứ hai đến các phương thức onCreateView và onActivityCreate. (Lần đầu tiên khi hệ thống tạo lại độ nhạy và tất cả các đoạn đính kèm) Lần đầu tiên Gói onSavedInstanceState sẽ lưu dữ liệu nhưng không phải lần thứ hai.

Giải pháp là không tách đoạn trong hàm tạo TabListener, chỉ cần để nó gắn vào. (Bạn vẫn cần tìm nó trong FragmentManager bằng thẻ của nó) Ngoài ra, trong phương thức onTabSelected, tôi kiểm tra xem liệu phân đoạn có bị tách ra hay không trước khi đính kèm. Một cái gì đó như thế này:

public void onTabSelected(Tab tab, FragmentTransaction ft) {
            if (mFragment == null) {
                mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
                Log.d(TAG, "onTabSelected adding fragment " + mTag);
                ft.add(android.R.id.content, mFragment, mTag);
            } else {

                if(mFragment.isDetached()) {
                    Log.d(TAG, "onTabSelected attaching fragment " + mTag);
                    ft.attach(mFragment);
                } else {
                    Log.d(TAG, "onTabSelected fragment already attached " + mTag);
                }
            }
        }

4
Giải pháp "không tách đoạn trong hàm tạo TabListener" được đề cập khiến các đoạn tab chồng lên nhau. Tôi có thể thấy nội dung các đoạn khác. Nó không hiệu quả với tôi.
Aksel Fatih

@ bầy.dux Tôi không chắc ý của bạn khi chồng lên nhau. Android quan tâm đến việc chúng được bố trí như thế nào, chúng tôi chỉ định đính kèm hoặc tách rời. Phải có nhiều hơn nữa đang diễn ra. Có thể nếu bạn hỏi một câu hỏi mới với mã ví dụ, chúng tôi có thể tìm ra điều gì đang xảy ra cho bạn.
Dave

1
Tôi đã gặp vấn đề tương tự (nhiều cuộc gọi hàm tạo phân đoạn từ Android). Phát hiện của bạn giải quyết được vấn đề của tôi: Điều tôi không hiểu là tất cả các đoạn được đính kèm với một hoạt động khi xảy ra thay đổi cấu hình (xoay điện thoại) đều được tạo lại và thêm trở lại hoạt động. (điều đó có ý nghĩa)
eugene

26

Tôi đã gặp vấn đề tương tự với một Activity đơn giản chỉ mang một đoạn (đôi khi sẽ bị thay thế). Sau đó, tôi nhận ra rằng tôi chỉ sử dụng onSaveInstanceState trong phân đoạn (và onCreateView để kiểm tra saveInstanceState) chứ không phải trong hoạt động.

Trên thiết bị, hoạt động chứa các phân đoạn được khởi động lại và onCreate được gọi. Ở đó, tôi đã đính kèm đoạn được yêu cầu (chính xác trong lần khởi động đầu tiên).

Trên thiết bị, lần đầu tiên Android tạo lại phân đoạn có thể nhìn thấy và sau đó gọi onCreate của hoạt động chứa nơi phân đoạn của tôi được đính kèm, do đó thay thế phân đoạn có thể nhìn thấy ban đầu.

Để tránh điều đó, tôi chỉ cần thay đổi hoạt động của mình để kiểm tra SaveInstanceState:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

if (savedInstanceState != null) {
/**making sure you are not attaching the fragments again as they have 
 been 
 *already added
 **/
 return; 
 }
 else{
  // following code to attach fragment initially
 }

 }

Tôi thậm chí đã không ghi đè onSaveInstanceState của hoạt động.


Cảm ơn bạn. Nó đã giúp tôi với AppCompatActivity + PreferenceFragmentCompat và gặp sự cố khi hiển thị các hộp thoại trong phân đoạn tùy chọn sau khi thay đổi hướng, vì trình quản lý phân đoạn không có trong lần tạo phân đoạn thứ hai.
RoK

12

Hai câu trả lời được ủng hộ ở đây hiển thị các giải pháp cho Hoạt động có chế độ điều hướng NAVIGATION_MODE_TABS, nhưng tôi gặp vấn đề tương tự với a NAVIGATION_MODE_LIST. Nó khiến các mảnh vỡ của tôi mất trạng thái một cách khó hiểu khi hướng màn hình thay đổi, điều này thực sự gây khó chịu. Rất may, do mã hữu ích của họ, tôi đã tìm ra nó.

Về cơ bản, khi sử dụng điều hướng danh sách, `` onNavigationItemSelected () is automatically called when your activity is created/re-created, whether you like it or not. To prevent your Fragment'sonCreateView () from being called twice, this initial automatic call toonNavigationItemSelected () should check whether the Fragment is already in existence inside your Activity. If it is, return immediately, because there is nothing to do; if it isn't, then simply construct the Fragment and add it to the Activity like you normally would. Performing this check prevents your Fragment from needlessly being created again, which is what causesonCreateView () `sẽ được gọi hai lần!

Xem cách onNavigationItemSelected()triển khai của tôi bên dưới.

public class MyActivity extends FragmentActivity implements ActionBar.OnNavigationListener
{
    private static final String STATE_SELECTED_NAVIGATION_ITEM = "selected_navigation_item";

    private boolean mIsUserInitiatedNavItemSelection;

    // ... constructor code, etc.

    @Override
    public void onRestoreInstanceState(Bundle savedInstanceState)
    {
        super.onRestoreInstanceState(savedInstanceState);

        if (savedInstanceState.containsKey(STATE_SELECTED_NAVIGATION_ITEM))
        {
            getActionBar().setSelectedNavigationItem(savedInstanceState.getInt(STATE_SELECTED_NAVIGATION_ITEM));
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState)
    {
        outState.putInt(STATE_SELECTED_NAVIGATION_ITEM, getActionBar().getSelectedNavigationIndex());

        super.onSaveInstanceState(outState);
    }

    @Override
    public boolean onNavigationItemSelected(int position, long id)
    {    
        Fragment fragment;
        switch (position)
        {
            // ... choose and construct fragment here
        }

        // is this the automatic (non-user initiated) call to onNavigationItemSelected()
        // that occurs when the activity is created/re-created?
        if (!mIsUserInitiatedNavItemSelection)
        {
            // all subsequent calls to onNavigationItemSelected() won't be automatic
            mIsUserInitiatedNavItemSelection = true;

            // has the same fragment already replaced the container and assumed its id?
            Fragment existingFragment = getSupportFragmentManager().findFragmentById(R.id.container);
            if (existingFragment != null && existingFragment.getClass().equals(fragment.getClass()))
            {
                return true; //nothing to do, because the fragment is already there 
            }
        }

        getSupportFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();
        return true;
    }
}

Tôi đã mượn nguồn cảm hứng cho giải pháp này từ đây .


Giải pháp này phù hợp với vấn đề tương tự của tôi với ngăn điều hướng. Tôi tìm phân đoạn hiện có theo ID và kiểm tra xem nó có cùng lớp với phân đoạn mới hay không trước khi tạo lại nó.
William

8

Đối với tôi, có vẻ như đó là vì bạn luôn khởi tạo TabListener của mình ... vì vậy hệ thống đang tạo lại phân đoạn của bạn từ SaveInstanceState và sau đó bạn thực hiện lại trong onCreate của mình.

Bạn nên bọc nó trong một if(savedInstanceState == null)để nó chỉ kích hoạt nếu không có SaveInstanceState.


Tôi không nghĩ điều đó chính xác. Khi tôi bọc mã addTab của mình trong khối if, phân đoạn được đính kèm với hoạt động nhưng không có tab nào. Có vẻ như bạn phải thêm các tab mỗi lần trong phương thức onCreate. Tôi sẽ tiếp tục xem xét vấn đề này và đăng nhiều hơn khi tôi hiểu rõ hơn.
Dave
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.