返回

Android Fragment 返回视图重建问题完美解决

Android

Fragment 返回时,如何判断视图是否被重建?

开发 Android 应用时,经常会遇到 Fragment 之间跳转的情况。有时候,从 Fragment B 返回 Fragment A 后,会发现 A 的视图被重新创建,导致一些不希望发生的状态变化,比如,进度条(ProgressBar)重新显示。这种情况让人有点头疼。今天聊聊这个问题该怎么解决。

问题复现

假设有两个 Fragment:A 和 B。Fragment A 用于展示和编辑个人信息,包含一个 ProgressBar,在数据加载时显示。Fragment B 用于选择城市。

从 A 跳转到 B,通常会使用 FragmentTransaction.replace()。在 B 中选择完城市或点击返回键后,用 FragmentManager.popBackStack() 返回 A。此时,A 的视图会被重建,ProgressBar 会再次出现。即使 A 之前已经加载过数据,ProgressBar 的 visibility 已经被设置成 GONE 了,但因为视图重建,它又变成了可见状态。

原本的期望是:在 onCreateView() 方法中,savedInstanceState 参数不应该为空,可以从中恢复之前保存的状态,并隐藏 ProgressBar。但实际上,savedInstanceState 总是 null

问题根源:Fragment 的生命周期和回退栈

要弄清这个问题,先要搞明白 Fragment 的生命周期和回退栈的机制。

当使用 replace() 方法切换 Fragment 时,原来的 Fragment A 会经历 onPause(), onStop(), 甚至是 onDestroyView(), 但不会 onDestroy().
popBackStack() 返回时,Fragment A 会重新创建视图,依次调用 onCreateView(), onActivityCreated(), onStart(), 和 onResume(). 注意: 这里的重点是 onCreateView() 又被调用了一次。

因为没有主动做特殊的保存和恢复工作, 所以 savedInstanceState 参数在 onCreateView() 中拿到的始终是null

解决办法

既然知道了问题所在,那么,可以这样搞定它:

1. 使用 ViewModel

ViewModel 是一个好东西,它的生命周期独立于 Fragment。即使 Fragment 被销毁重建,ViewModel 中的数据仍然保留。

  • 原理: ViewModel 专门用来存放和管理与 UI 相关的数据。当 Fragment 被销毁重建后,重新获取同一个 ViewModel 实例,数据就可以无缝恢复。

  • 代码示例:

    // 定义 ViewModel
    public class MyViewModel extends ViewModel {
        private boolean dataLoaded = false;
        // ... 其他数据
    
        public boolean isDataLoaded() {
            return dataLoaded;
        }
    
        public void setDataLoaded(boolean dataLoaded) {
            this.dataLoaded = dataLoaded;
        }
        //... 其他getter/setter
    }
    
    // 在 Fragment A 中使用
    public class FragmentA extends Fragment {
    
        private ProgressBar progressBar;
        private MyViewModel viewModel;
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_a, container, false);
            progressBar = view.findViewById(R.id.progressBar);
    
            // 获取 ViewModel 实例
            viewModel = new ViewModelProvider(this).get(MyViewModel.class);
    
            if (viewModel.isDataLoaded()) {
                progressBar.setVisibility(View.GONE);
            } else {
                // 加载数据
                loadData();
            }
    
            return view;
        }
         private void loadData() {
             //模拟加载
            new Handler().postDelayed(() -> {
                viewModel.setDataLoaded(true);
                 progressBar.setVisibility(View.GONE);
            },2000);
        }
    
    }
    
  • 安全建议: ViewModel 配合 LiveData 使用更香,LiveData 能感知生命周期,避免内存泄漏。

  • 进阶使用技巧 如果有多个 Fragment 共享同一份数据,可以将 ViewModelProvider 的构造函数的 this 改为 requireActivity(),使得这些 Fragment 可以访问到同一个 ViewModel 实例。

2. 使用 setRetainInstance(true)

setRetainInstance(true) 可以让 Fragment 在配置变更(如屏幕旋转)时,实例不被销毁,直接保留。不过要注意,这货跟回退栈一起使用时有点 tricky。

  • 原理: setRetainInstance(true) 可以使 Fragment 实例跨越 Activity 的重建而被保留。 但这仅适用于配置变更, 比如屏幕旋转, 且需要小心内存泄露的问题。当 Fragment 从回退栈返回时,它仍然会重建视图。所以,还需要结合其他方法。

  • 代码示例:

    public class FragmentA extends Fragment {
    
        private ProgressBar progressBar;
        private boolean dataLoaded = false;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true); // 保持 Fragment 实例
              if (savedInstanceState != null) {
                    dataLoaded = savedInstanceState.getBoolean("dataLoaded", false);
             }
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_a, container, false);
            progressBar = view.findViewById(R.id.progressBar);
    
            if (dataLoaded) {
                progressBar.setVisibility(View.GONE);
            } else {
                //加载数据
                loadData();
            }
    
            return view;
        }
    
         @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putBoolean("dataLoaded", dataLoaded);
        }
    
         private void loadData() {
             //模拟加载
            new Handler().postDelayed(() -> {
                 dataLoaded = true;
                 progressBar.setVisibility(View.GONE);
            },2000);
        }
    }
    
  • 安全建议: 使用 setRetainInstance(true) 时,要确保 Fragment 中没有持有 Activity 的引用,否则会导致内存泄漏。最佳实践是只用它来保留数据,不保留和 View 相关的状态。 和View 相关的状态可以在 onSaveInstanceState 里去保留。

  • 进阶使用技巧:setRetainInstance(true) 同时使用 setUserVisibleHint() 来控制可见状态,虽然麻烦但不失为一种有效方法。

3. 重写 onHiddenChanged()

可以重写 onHiddenChanged() 方法,在 Fragment 重新变为可见时,判断数据是否已经加载,从而控制 ProgressBar 的显示。

  • 原理: 当 Fragment 的可见状态发生变化时(包括从不可见到可见),onHiddenChanged() 方法会被调用。可以利用这个时机进行状态检查。

  • 代码示例:

    public class FragmentA extends Fragment {
    
        private ProgressBar progressBar;
        private boolean dataLoaded = false;
          private boolean viewCreated = false;
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
              View view = inflater.inflate(R.layout.fragment_a, container, false);
               progressBar = view.findViewById(R.id.progressBar);
             viewCreated = true;
    
            if (dataLoaded) {
                 progressBar.setVisibility(View.GONE);
            }else{
                loadData();
             }
              return view;
        }
         @Override
        public void onHiddenChanged(boolean hidden) {
             super.onHiddenChanged(hidden);
                //当 Fragment 不是隐藏 并且 View 创建完成
                if (!hidden && viewCreated) {
                       if(dataLoaded){
                           progressBar.setVisibility(View.GONE);
                       }
                }
         }
          private void loadData() {
              //模拟加载
                new Handler().postDelayed(()->{
                dataLoaded = true;
                    progressBar.setVisibility(View.GONE);
                },2000);
    
         }
         @Override
        public void onDestroyView() {
            super.onDestroyView();
              viewCreated = false;
        }
    }
    
  • 安全建议: 这个方式适合处理那些仅在用户可见状态变化时需要的操作。不要在这个方法里面处理耗时逻辑。

  • 进阶使用技巧 : 和 FragmentTransaction.show(),FragmentTransaction.hide() 配合使用,可以更加精确地控制Fragment 的可见状态,达到更优的效果。

4.在onResume() 里处理

Fragment 可见的时候会触发 onResume(),也可以在这个里面检查状态.

  • 原理: onResume() 方法是 Fragment 可见时必定执行的方法之一,利用好这一点.

  • 代码实现 :

 public class FragmentA extends Fragment {
   private ProgressBar progressBar;
     private boolean dataLoaded = false;
      @Override
      public View onCreateView(LayoutInflater inflater, ViewGroup container,
                               Bundle savedInstanceState) {
             View view = inflater.inflate(R.layout.fragment_a, container, false);
          progressBar = view.findViewById(R.id.progressBar);
           if(!dataLoaded){
              loadData();
          }
           return view;
     }

     @Override
     public void onResume() {
             super.onResume();
             if(dataLoaded){
                 progressBar.setVisibility(View.GONE);
            }
     }

     private void loadData() {
       //模拟数据加载
         new Handler().postDelayed(() -> {
            dataLoaded = true;
            progressBar.setVisibility(View.GONE);
      }, 2000);
     }
}

  • 进阶使用技巧 : 和方法3配合使用,效果更好。

总结

通过上面的介绍,应该清楚如何在 Fragment 返回时处理视图重建的情况了吧。推荐优先使用 ViewModel,其他的方法可以灵活选用. 最重要的事情就是:搞明白 Fragment 的生命周期和回退栈.