Fragment的onCreateView和onActivityCreated方法被调用两次。

107
我正在使用Android 4.0 ICS和片段(fragment)开发一个应用程序。
考虑这个修改后的示例,来自ICS 4.0.3(API级别15)API演示应用程序。
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;
    }
}

这是运行此示例并旋转手机后检索到的输出:

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

我的问题是,为什么onCreateView和onActivityCreated会被调用两次?第一次带有保存状态的Bundle,第二次带有null savedInstanceState?
这会导致在旋转时保留片段状态时出现问题。

2
我认为这个问题可以与https://dev59.com/Dmoy5IYBdhLWcg3wkOwK#8678705相关联。 - marioosh
5个回答

45

我也曾经困惑了一段时间,因为Dave的解释有点难懂,所以我会发布我的(显然有效的)代码:

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) {
    }
}

如您所见,这与Android示例非常相似,除了构造函数中没有分离,并使用replace而不是add

经过长时间的思考和试错,我发现在构造函数中找到片段似乎可以神奇地解决双重onCreateView问题(我假设当通过ActionBar.setSelectedNavigationItem()路径保存/恢复状态时,onTabSelected调用时它最终会变为空)。


与“previous ref. TabListener” Android 示例完美配合 - 谢谢。最新的Android“TabListener ref. sample”[截至2013年9月4日]真的非常、非常错误。 - Grzegorz Dev
ft.commit() 方法调用在哪里? - MSaudi
不错的解决方法。但是为什么使用替换而不是添加呢?我没有看到任何显著的区别。 - Muhammad Babar
在我的情况下,罪魁祸首是使用了错误片段的标签。 - WindRider
1
@MuhammadBabar,请查看https://dev59.com/JWAg5IYBdhLWcg3wvNCF。如果您使用`add`而不是`replace`并旋转屏幕,则会有许多片段的`onCreateView()`。 - CoolMind
显示剩余4条评论

29

我遇到了一个简单的 Activity 单独搭载一个 Fragment 的问题(有时会被替换)。后来我发现我只在 Fragment 中使用了 onSaveInstanceState,并在 onCreateView 中检查了它,而在 Activity 中没有。

在设备旋转时,包含 Fragments 的 Activity 会被重新启动并调用 onCreate。在那里,我附加了所需的 Fragment(第一次启动时是正确的)。

在设备旋转时,Android 首先重新创建了可见的 Fragment,然后调用了包含 Activity 的 onCreate,在其中附加了我的 Fragment,从而替换了原始的可见 Fragment。

为了避免这种情况,我只需更改我的 Activity 来检查 savedInstanceState:

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
 }

 }

我甚至没有重写活动的onSaveInstanceState方法。


谢谢。它帮助我解决了AppCompatActivity + PreferenceFragmentCompat的问题,以及在首选项片段中更改方向后显示对话框时崩溃的问题,因为第二个片段创建时片段管理器为空。 - RoK

26

好的,这是我所了解到的。

我没有理解的是,当发生配置更改(手机旋转)时附加到活动的所有片段都会被重新创建并添加回活动中。(这很合理)

在TabListener构造函数中发生的是,如果找到该选项卡,则会将其分离并附加到活动中。请参见以下内容:

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

在活动的onCreate中,之前选定的选项卡是从保存的实例状态中选定的。请参见下面:

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

当选中该选项卡时,它将在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);
    }
}

被附加的片段是对onCreateView和onActivityCreated方法的第二次调用。(第一次是当系统重新创建Activity和所有已附加的片段时)第一次 onSaveInstanceState Bundle 会保存数据,但第二次不会。

解决方案是在TabListener构造函数中不要分离片段,只需让它保持附加状态。(仍需按其标记在FragmentManager中查找)另外,在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 {

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

4
提到的“不要在TabListener构造函数中分离片段”解决方案会导致标签片段重叠。我能看到其他片段的内容。对我没用。 - Aksel Fatih
@flock.dux,我不确定你所说的“重叠”是什么意思。Android会处理它们的布局,因此我们只需指定附加或分离即可。可能还有其他问题。如果您提供示例代码并提出新问题,我们可以帮您找出问题所在。 - Dave
1
我也遇到了同样的问题(来自Android的多个片段构造函数调用)。你的发现解决了我的问题:我之前不明白的是,当配置更改发生时(手机旋转),所有附加到活动的片段都会被重新创建并添加回活动中。(这很有道理) - eugene

12
这里的两个点赞答案展示了一个带有导航模式 NAVIGATION_MODE_TABS 的Activity的解决方案,但我遇到了同样的问题,它是一个 NAVIGATION_MODE_LIST 。这导致我的Fragment在屏幕方向改变时莫名其妙地失去了状态,这真的很烦人。幸运的是,由于他们提供的有用代码,我成功地解决了这个问题。
基本上,在使用列表导航时,无论你喜欢与否,当创建/重新创建活动时都会自动调用 onNavigationItemSelected()。为了防止Fragment的 onCreateView()被调用两次,这个初始自动调用 onNavigationItemSelected()应该检查Fragment是否已经存在于Activity中。如果是,则立即返回,因为没有什么可做的;如果不是,则像通常一样构造Fragment并将其添加到Activity中。执行此检查可以防止Fragment被不必要地再次创建,这就是导致 onCreateView()被调用两次的原因!
请参见下面的 onNavigationItemSelected()实现。
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;
    }
}

我从这里得到了这个解决方案的灵感。


这个解决方案适用于我在导航抽屉中遇到的类似问题。我通过ID查找现有的片段,并在重新创建之前检查它是否与新片段具有相同的类。 - William

8
我觉得这是因为您每次实例化TabListener...所以系统会从savedInstanceState重新创建您的片段,然后在onCreate中再次创建它。
您应该将其包装在if(savedInstanceState == null)中,这样只有在没有savedInstanceState时才会触发。

我认为那不正确。当我将我的addTab代码放在if块中时,片段会附加到活动中,但没有选项卡。似乎你必须每次在onCreate方法中添加选项卡。我会继续研究这个问题,并在我更好地理解后发布更多信息。 - Dave

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