当方向改变重新加载带有选项卡的活动时,片段会被初始化两次。

19

当我改变设备的方向时,重新加载带有选项卡和片段的活动出现问题。

情况如下:

我有一个活动,在操作栏中有3个选项卡。每个选项卡在主视图中加载不同的片段到FrameLayout中。如果我不改变设备的方向,一切都正常。但是当我这样做时,Android尝试初始化当前选择的片段两次,从而产生以下错误:

E/AndroidRuntime(2022): Caused by: android.view.InflateException: Binary XML file line #39: Error inflating class fragment

以下是导致错误的步骤序列:
  1. 我加载了活动,选择了第二个选项卡并改变了设备的方向。
  2. Android销毁了活动和由第二个选项卡加载的片段实例(从现在开始称为“片段2”)。然后它继续创建活动和片段的新实例。
  3. Activity.onCreate()内,我将第一个选项卡添加到操作栏中。当我这样做时,此选项卡会自动选择。这可能会在将来造成问题,但我现在不介意。 onTabSelected被调用,并创建并加载第一个片段的新实例(请参见下面的代码)。
  4. 我添加所有其他选项卡,而没有触发任何事件,这很好。
  5. 我调用ActionBar.selectTab(myTab)来选择第2个选项卡。
  6. 首先,对于第一个选项卡,onTabUnselected()被调用,然后对于第二个选项卡,onTabSelected()被调用。此序列将当前片段替换为片段2的实例(请参见下面的代码)。
  7. 接下来,在片段2实例上调用Fragment.onCreateView(),并且片段布局被填充。
  8. 这里出现问题。Android再次调用片段实例的onCreate(),然后调用onCreateView(),当我尝试(第二次)填充布局时,会产生异常。
显然问题是Android初始化了片段两次,但我不知道原因。
我尝试在重新加载活动时不选择第二个选项卡,但第二个片段仍会被初始化并且不会显示(因为我没有选择它的选项卡)。
我找到了这个问题:Android Fragments recreated on orientation change 用户基本上问了我要问的问题,但我不喜欢所选择的答案(只是一个解决方法)。必须有某种方法可以在不使用android:configChanges技巧的情况下使其正常工作。
如果不清楚,我想知道如何防止重建片段或避免其双重初始化。也很好知道为什么会发生这种情况。 :P
以下是相关代码:
public class MyActivity extends Activity  implements ActionBar.TabListener {

    private static final String TAG_FRAGMENT_1 = "frag1";
    private static final String TAG_FRAGMENT_2 = "frag2";
    private static final String TAG_FRAGMENT_3 = "frag3";

    Fragment frag1;
    Fragment frag2;
    Fragment frag3;

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

        // my_layout contains a FragmentLayout inside
        setContentView(R.layout.my_layout); 

        // Get a reference to the fragments created automatically by Android
        // when reloading the activity
        FragmentManager fm = getFragmentManager();
        this.frag1 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_1);
        this.frag2 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_2);
        this.frag3 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_3)


        ActionBar actionBar = getActionBar();

        // snip...

        // This triggers onTabSelected for the first tab
        actionBar.addTab(actionBar.newTab()
                .setText("Tab1").setTabListener(this)
                .setTag(MyActivity.TAG_FRAGMENT_1));

        actionBar.addTab(actionBar.newTab()
                .setText("Tab2").setTabListener(this)
                .setTag(MyActivity.TAG_FRAGMENT_2));
        actionBar.addTab(actionBar.newTab()
                .setText("Tab3").setTabListener(this)
                .setTag(MyActivity.TAG_FRAGMENT_3));

        Tab t = null;
        // here I get a reference to the tab that must be selected
        // snip...

        // This triggers onTabUnselected/onTabSelected
        ab.selectTab(t);

    }

    @Override
    protected void onDestroy() {
        // Not sure if this is necessary
        this.frag1 = null;
        this.frag2 = null;
        this.frag3 = null;
        super.onDestroy();
    }

    @Override  
    public void onTabSelected(Tab tab, FragmentTransaction ft) {  

        Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
        if (curFrag == null) {
            curFrag = createFragmentInstanceForTag(tab.getTag().toString());
            if(curFrag == null) { 
                // snip... 
                return;
            }
        }
        ft.replace(R.id.fragment_container, curFrag, tab.getTag().toString());
    }

    @Override  
    public void onTabUnselected(Tab tab, FragmentTransaction ft) 
    {  
        Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
        if (curFrag == null) {
            // snip... 
            return;
        }

        ft.remove(curFrag);
    }

    private Fragment getFragmentInstanceForTag(String tag) 
    {
        // Returns this.frag1, this.frag2 or this.frag3
        // depending on which tag was passed as parameter
    }

    private Fragment createFragmentInstanceForTag(String tag) 
    {
        // Returns a new instance of the fragment requested by tag
        // and assigns it to this.frag1, this.frag2 or this.frag3
    }
}
< p >该片段的代码并不重要,它只是在onCreateView()方法覆盖上返回一个充气视图。< /p >

1
只是一个建议...如果你缩小帖子的大小,你更有可能得到答案 :P。虽然我非常欣赏你包含的细节...我建议你在帖子的开头包含一些易于阅读和吸引人们注意力的摘要。 - Alex Lockwood
此外,使用“内联代码”,粗体(但不要过度使用),甚至是HTML头(<h3> </ h3>)都可以帮助组织您的文章...但不要过度滥用:P - Alex Lockwood
需要一些代码作为参考,最好是onTabSelectedonTabUnselected方法。 - StuStirling
谢谢您的建议。我考虑过帖子的长度,但我想尽可能详细地描述,因为除了最后一部分之外,一切似乎都正常运行。 - Gerardo Contijoch
9个回答

2
我有一个简单的答案:
只需在Fragment的onAttach(Activity activity)或onActivityCreated(Bundle savedInstanceState)回调中添加setRetainInstance(true)。
所以基本上,setRetainInstance(true)的作用是: 在Fragment经历以下过程时保持其状态:
- onPause(); - onStop();
它无论Activity经历什么都会保留Fragment的实例。 问题在于,如果存在太多的Fragment,则可能会对系统造成压力。
希望能有所帮助。
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);

setRetainInstance(true);

}

一如既往地欢迎纠正。敬礼,Edward Quixote。

(注:原文已经是英文)

对我来说不起作用,Fragment 会被创建两次,因此即使我将小部件的状态保存在 bundle 中,它也会在第二次重新加载时丢失。 - Miguel El Merendero
我解决了自己的问题,尽管我不知道我的解决方案是否适用于此问题:https://dev59.com/Jnvaa4cB1Zd3GeqPGrQa#26523274 - Miguel El Merendero

1

看起来,当屏幕旋转并重新启动应用程序时,它会通过调用Fragment类的默认构造函数来重新创建每个Fragment。


是的,但你如何克服这个问题?什么是正确的做法? - Eggcellentos

0
为了保护活动重新创建,请在您的Activity标签(在清单文件中)中添加configChanges,例如:
android:configChanges="keyboardHidden|orientation|screenSize"

0
将以下代码添加到清单文件中:android:configChanges="orientation|screenSize"

0

我认为你面临的问题和我曾经遇到的一样。我有一个在onCreate()中启动的json线程下载器,每次我改变方向时都会调用该线程并触发下载。我使用onSaveInstance()onRestoreInstance()来传递列表中的json响应,并结合检查列表是否为空,以便不需要额外下载。

希望这能给你一些提示。


0

我的代码有点不同,但我相信我们的问题是一样的。

onTabSelected中,我没有使用replace,而是在第一次创建片段时使用add,如果不是,则使用attach。在onTabUnselected中,我使用detach。

问题是,当视图被销毁时,我的片段仍然附加在FragmentManager上,从未被销毁。为了解决这个问题,我实现了onSaveInstanceBundle以将片段从FragmentManager中分离。

代码大致如下:

FragmentTransition ft = getSupportFragmentManager().begin();
ft.detach(myFragment);
ft.commit();

在第一次尝试中,我将那段代码放在onDestroy中,但是我收到了一个异常,告诉我我不能在onSaveInstanceBundle之后这样做,所以我将代码移到了onSaveInstanceBundle中,一切正常。

很抱歉,我工作的地方不允许我在StackOverflow上放置代码。这是我从代码中记住的内容。请随意编辑答案以添加代码。


0

我认为这是一种绝对可靠的方法,可以避免重新填充片段的根视图:

private WeakReference<View> mRootView;
private LayoutInflater mInflater;

/**
 * inflate the fragment layout , or use a previous one if already stored <br/>
 * WARNING: do not use in any function other than onCreateView
 * */
private View inflateRootView() {
    View rootView = mRootView == null ? null : mRootView.get();
    if (rootView != null) {
        final ViewParent parent = rootView.getParent();
        if (parent != null && parent instanceof ViewGroup)
            ((ViewGroup) parent).removeView(rootView);
        return rootView;
    }
    rootView = mFadingHelper.createView(mInflater);
    mRootView = new WeakReference<View>(rootView);
    return rootView;
}


@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
    mInflater=inflater!=null?inflater:LayoutInflater.from(getActivity());
    final View view = inflateRootView();
    ... //update your data on the views if needed
}

0
我遇到了同样的问题,并使用了以下解决方法:
在片段的onCreateView方法中添加以下代码:
if (mView != null) {
    // Log.w(TAG, "Fragment initialized again");
    ((ViewGroup) mView.getParent()).removeView(mView);
    return mView;
}
// normal onCreateView
mView = inflater.inflate(R.layout...)

0
我通过以下代码解决了这个问题。
private void loadFragment(){
     LogUtil.l(TAG,"loadFragment",true);
     fm = getSupportFragmentManager();
     Fragment hf = fm.findFragmentByTag("HOME");
     Fragment sf = fm.findFragmentByTag("SETTING");
     if(hf==null) {
         homeFragment = getHomeFragment();// new HomeFragment();
         settingsFragment = getSettingsFragment();// new Fragment();
         fm.beginTransaction().add(R.id.fm_place, settingsFragment, "SETTING").hide(settingsFragment).commit();
         fm.beginTransaction().add(R.id.fm_place, homeFragment, "HOME").commit();
         activeFragment = homeFragment;
     }else{
         homeFragment = hf;
         settingsFragment = sf;
         activeFragment = sf;
     }
 }

在OnCreate()中调用此方法。


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