【问题标题】:Fragment Field is NULL after rotate device in Android在Android中旋转设备后片段字段为NULL
【发布时间】:2019-10-29 18:13:25
【问题描述】:

当我启动应用程序时一切正常,但是当我旋转到横向时它崩溃了,因为在 Fragment 中有一个字段是 NULL

我不使用setRetainInstance(true) 或将片段添加到FragmentManager我在应用启动和应用旋转时创建新片段。

ActivityOnCreate() 中,我创建Fragment 并将它们添加到viewPager,如下所示。

   protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         ParentBasicInfoFragment parentBasicInfoFragment = new ParentBasicInfoFragment();
         ParentUTCFragment parentUTCFragment = new ParentUTCFragment();
         ParentEventsFragment parentEventsFragment = new ParentEventsFragment();
         this.mFragments = new ArrayList<>();
         this.mFragments.add(parentBasicInfoFragment);
         this.mFragments.add(parentUTCFragment);
         this.mFragments.add(parentEventsFragment);
         this.viewpage.setOffscreenPageLimit(3);
         setCurrentTab(0);
         this.viewpage.setAdapter(new MainActivityPagerAdapter(getSupportFragmentManager(), this.mFragments));
    }

然后我在应用程序上有一个测试按钮,当我按下它时会像

  public void test(View view) {
      ((BaseFragment) MainActivity.this.mFragments.get(MainActivity.this.viewpage.
                getCurrentItem())).activityNotifiDataChange("hello");
  }

这将起作用,并且ViewPager 中的当前Fragments 具有正在调用的方法activityNotifiDataChange(),一切正常。

当我旋转应用程序并按下按钮执行相同操作时,activityNotifiDataChange() 被正常调用,但出现空指针异常,因为 ArrayList&lt;Fragment&gt; mFragment 现在为 NULL。

这是一个展示此行为的小型 Android Studio 项目示例: https://drive.google.com/file/d/1Swqu59HZNYFT5hMTqv3eNiT9NmakhNEb/view?usp=sharing

启动应用程序并按下名为“PRESS TEST”的按钮,然后旋转设备并再次按下按钮并观察应用程序崩溃

更新解决方案感谢@GregMoens 和@EpicPandaForce

public class MainActivityPagerAdapter extends PersistenPagerAdapter<BaseFragment> {

    private static int NUM_ITEMS = 3;

    public MainActivityPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    public int getCount() {
        return NUM_ITEMS;
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return ParentBasicInfoFragment.newInstance(0, "Page # 1");
            case 1:
                return ParentUTCFragment.newInstance(1, "Page # 2");
            case 2:
                return ParentEventsFragment.newInstance(2, "Page # 3");
            default:
                return null;
        }
    }
}

public abstract class PersistenPagerAdapter<T extends BaseFragment> extends FragmentPagerAdapter {
    private SparseArray<T> registeredFragments = new SparseArray<T>();

    public PersistenPagerAdapter(FragmentManager fragmentManager) {
        super(fragmentManager);
    }

    @Override
    public T instantiateItem(ViewGroup container, int position) {
        T fragment = (T)super.instantiateItem(container, position);
        registeredFragments.put(position, fragment);
        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        registeredFragments.remove(position);
        super.destroyItem(container, position, object);
    }

    public T getRegisteredFragment(ViewGroup container, int position) {
        T existingInstance = registeredFragments.get(position);
        if (existingInstance != null) {
            return existingInstance;
        } else {
            return instantiateItem(container, position);
        }
    }
}

【问题讨论】:

  • 这是因为当方向改变时,片段会从头开始重新创建。如果您想重新获得变量的值,您需要使用您提到的 setInstantState(true) 或上下文来设置引用。请参阅此链接以供参考link
  • 我知道它们是重新创建的,这就是为什么我在 Fragment onViewCreated() 中设置该字段,但旋转后该字段仍然为空
  • 只在if(savedInstanceState == null)... 时操作视图,否则代码再次运行。特别是 setCurrentTab(0); 打破了旋转屏幕并仍然期望看到相同标签的想法(savedInstanceState 也可用于记住实际的当前标签)。
  • this.mFragments = new ArrayList&lt;&gt;(); 因为这是错误的。你永远不应该持有对片段列表的引用。只需从 FragmentAdaptergetItem(int position) 返回 Fragment 的新实例,您就可以开始了

标签: android android-fragments


【解决方案1】:

我在您的应用中看到的主要问题是您对 FragmentPagerAdapter 的工作方式存在误解。我经常看到这种情况,这是因为课堂上缺乏好的 javadocs。应该实现适配器,以便 getItem(position) 在调用时返回一个新的片段实例。然后 getItem(position) 只会在寻呼机需要该位置的新实例时调用。您不应该预先创建片段然后将其传递给适配器。您也不应该持有对来自您的活动或父片段(如 ParentBasicInfoFragment)的片段的强引用。因为请记住,片段管理器正在管理片段,而您也在通过更新片段并保持对它们的引用来管理片段。这会导致冲突,并且在轮换之后,您尝试在未实际初始化的片段上调用 ​​activityNotifiDataChange()(未调用 onCreate())。使用调试器和跟踪对象 ID 将确认这一点。

如果您更改代码以便 FragmentPagerAdapter 在需要时创建片段并且不存储对片段或片段列表的引用,您会看到更好的结果。

【讨论】:

  • 是的,很好的解释。几乎问题是人们对 Lifecycle 以及组件如何对其做出反应有基本的误解。
  • FragmentPagerAdapter.getItem(position) 上的 javadoc 非常具有误导性,即使不是完全错误也无济于事。他们在 FragmentPagerAdapter 的 javadocs 中显示的示例实现是正确的。
  • 谢谢 听起来你是对的,我会试试的,你看我包含的示例应用代码了吗?
  • @ErikHellberg 我做到了。我认为您将遇到的唯一挑战是依赖于访问 mFragments 的代码。必须更改该代码才能通过片段管理器找到您要查找的片段。
  • 好的,该应用程序被设计为仅作为纵向运行,我现在尝试更改它
【解决方案2】:
this.mFragments = new ArrayList<>();

因为这是错误的。如果您使用 ViewPager,则永远不应持有对片段列表的引用。只需从 FragmentPagerAdaptergetItem(int position) 返回 Fragment 的新实例即可。

但要修复您的代码,您必须完全删除 mFragments

更多详情请见https://stackoverflow.com/a/58605339/2413303

【讨论】:

  • this.mFragments 可能属于FragmentAdapter
  • 不应该,super.onCreate 在进程死亡后管理片段重新创建。您不应该在其他地方手动创建它们。
  • FragmentPagerAdapterParcelable saveState()restoreState(Parcelable state, ClassLoader loader) (所以它可以处理它自己的状态),以及abstract Fragment getItem(int position) - 不知何故需要保留它们,以免总是返回new Fragment(如果不保留它们,这将是唯一可用的选项)。
  • @MartinZeitler 你应该检查源代码。 getItem() 仅在 instantiateItem 没有根据它创建的标签找到 Fragment 时调用,该标签构建为 "android:switcher:" + viewId + ":" + itemId。如果您在 FragmentPagerAdapter 之外手动创建片段,那么您最终会得到 unused 片段,但片段分页器适配器将使用系统重新创建的片段,而不是您的片段。
  • @ErikHellberg 因为应用程序第一次启动时,您手动创建的片段与适配器返回的片段和片段管理器正在管理的片段相同。旋转后,片段管理器重新创建片段,您重新创建片段,现在断开连接。
【解决方案3】:

使用setRetainInstance(true) 不是一个好方法。如果您需要相同的一些简单信息,例如:recyclerView 的位置,recyclerView 的选定项,也许某些模型(Parcelable),您可以使用方法onSaveInstanceState / onRestoreInstanceStatethere is one limitation is 1MB 来完成。 Here is an example with animations how it works.

要获得更持久的持久性,请使用SharedPreferencesRoom(Google ORM),或者您可以尝试使用ViewModel with LiveData(最好处理一些应该在用户会话期间存在的数据)。

【讨论】:

  • 增加复杂性并不能解决实际问题。
  • 我不想保留任何数据,除了当前的 ViewPager 选项卡。我希望重新创建所有内容,因为它们是由LiveData 支持的虚拟视图。我的LiveData 将在旋转后继续提供我的片段视图。在我的简单示例中,为简单起见不包括 LiveData 设置
【解决方案4】:
//change in AndroidManifest.xml file,
<activity
        android:name=".activity.YourActivity"
        android:configChanges="orientation|keyboardHidden|screenSize"
        android:screenOrientation="sensor"
         />

//YourActivity in which you defines fragment.
 //may be it helps

【讨论】:

    猜你喜欢
    • 2016-08-23
    • 1970-01-01
    • 1970-01-01
    • 2014-10-24
    • 2013-12-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-11
    相关资源
    最近更新 更多