片段onCreateView和onActivityCreated调用了两次


100

我正在使用Android 4.0 ICS和片段开发应用程序。

考虑一下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被调用两次?第一次使用具有已保存状态的捆绑包,第二次使用具有已保存状态的null实例?

这导致在旋转时保持片段状态的问题。


2
我认为这个问题可能与stackoverflow.com/a/8678705/404395
marioosh

Answers:


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最终将为null)保存/恢复状态)。


效果很好!你救了我一夜的睡眠!谢谢您:)
jaibatrik

如果您要删除类变量并从调用中删除参数,也可以使用fragment.getClass()。getName()
Ben Sewards 2013年

与“先前的参考TabListener” Android示例-tnx完美配合。最新的Android“ TabListener参考样本”(截至2013年6月4日)确实是非常错误的。
Grzegorz Dev

ft.commit()方法调用在哪里?
MSaudi

1
@MuhammadBabar,请参阅stackoverflow.com/questions/23248789/…。如果使用add而不是replace旋转屏幕,将会有很多片段onCreateView()
CoolMind

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方法的第二次调用。(第一次是系统重新创建活动性和所有附加的片段时)onSavedInstanceState 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构造函数中的片段”解决方案导致选项卡片段彼此重叠。我可以看到其他片段的内容。它对我不起作用。
阿克塞尔·法提赫

@ flock.dux我不确定彼此重叠是什么意思。Android会照顾它们的布局,因此,我们只需指定Attach或detach。肯定还有更多的事情要做。也许如果您用示例代码提出一个新问题,我们可以弄清楚您的情况。
戴夫

1
我遇到了同样的问题(来自Android的多个片段构造函数调用)。您的发现解决了我的问题:我不了解的是,当配置发生更改(电话旋转)时,附加到活动的所有片段都将重新创建并重新添加到活动中。(这很有意义)
eugene

26

我也遇到了一个简单的活动,该活动仅包含一个片段的问题(有时会被替换)。然后,我意识到我仅在片段中使用onSaveInstanceState(并且在活动中未使用onCreateView来检查saveInstanceState)。

在设备打开时,包含片段的活动将重新启动,并调用onCreated。在那里,我确实附加了所需的片段(在第一次启动时是正确的)。

在设备上,Android首先重新创建可见的片段,然后调用包含我的片段的包含活动的onCreate,从而替换了原始的可见片段。

为避免这种情况,我只是将我的活动更改为检查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
 }

 }

我什至没有覆盖活动的onSaveInstanceState。


谢谢。它帮助我使用AppCompatActivity + PreferenceFragmentCompat并在方向更改后在首选项片段中显示对话框时崩溃,因为片段管理器在创建第二个片段时为空。
韩国

12

这里的两个赞成答案显示了带有导航模式的活动的解决方案NAVIGATION_MODE_TABS,但是我遇到了与相同的问题NAVIGATION_MODE_LIST。当屏幕方向改变时,这使我的碎片莫名其妙地丢失了状态,这真令人讨厌。幸运的是,由于他们有用的代码,我设法弄清楚了。

基本上,当使用列表导航时,``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()`将被调用两次!

请参阅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查找现有片段,并在重新创建它之前检查它是否具有与新片段相同的类。
威廉

8

在我看来,这是因为您每次都实例化TabListener ...,因此系统正在从savedInstanceState重新创建您的片段,然后在onCreate中再次进行操作。

您应该将其包装在中,if(savedInstanceState == null)以便仅在没有saveInstanceState时才触发。


我认为那是不对的。当我将我的addTab代码包装在if块中时,该片段将附加到活动中,但是没有选项卡。似乎您每次都必须在onCreate方法中添加选项卡。我会继续研究并发布更多,以使我更好地理解。
戴夫2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.