2021年了,如何优雅地使用Fragment?(一)【超长预警】

11,563 阅读12分钟

参考:Android Fragments Doc

2021年了,相信Fragment如何创建,加载,甚至是常见的坑,大家都门清了。本文不会再讨论这些,而是希望讨论一些Fragment更好的使用方式。

简介

对于Fragment的介绍,我们从几个问题开始:

什么是Fragment?Fragment用来做什么?用Fragment有什么优点?

那么我们下面来一一进行解答:

什么是Fragment?

Fragment 表示应用界面中可重复使用的一部分。Fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独立存在,而是必须由 Activity 或另一个 Fragment 托管。Fragment 的视图层次结构会成为宿主的视图层次结构的一部分,或附加到宿主的视图层次结构。

上面是Google官方给的定义,我个人的理解就是,它是Activity里面用来区分不同功能的一个个“碎片”,大家都知道面向对象的五大原则,Android程序同样也应该遵循这些原则,但是考虑移动应用是直接面向用户的,所以我们总会遇到一个页面中装着各种各样一点联系都没有的功能的情况。

在这种情况下,我们的代码也应当遵循“高聚合,低耦合”的原则,也就是说,一个页面里的一个功能应当专注于它自己,同时和其他功能之间应当是无感知的;另外的,页面本身(也即是Activity),也应当对其中的功能仅保持最低限度的了解,这样才能增强代码的灵活性和可扩展性。

Fragment用来做什么?

如上面所说,我认为Fragment就是用来将同一个页面里面有着不同功能的部分解藕的。Android官方提供了这么一种最佳实践,避免让你自己定义一套框架来分离这些功能。

Fragment有什么优点?

如上面两个问题的答案中提到过的,

首先,Fragment拥有自己的生命周期,让你能够更好地根据不同的生命周期来控制其中的具体逻辑;

其次,Fragment是官方支持,这一方面说明它会随着Android的版本更新增加功能,同时越来越稳定;另一方面则是说明它是一个全局通用的“组件”,你可以将不同的Fragment拼装成一个页面,也可以随时将他们搬走,很方便地让它们在另一个页面当中生效;

还有,Fragment很“轻”,你可以使用它来实现“单Activity架构”;

最后,正如Fragment诞生的目的,它能够完美地为你的App中的不同功能进行解耦,提供良好的灵活性和可复用性。

如何优雅地使用Fragment?

正如Fragment的含义:“碎片”,我们应该用多种多样的Fragment来把一个页面拼起来。

让我们随便找一个复杂页面来说明Fragment的优势。 B站的“动态”页 我们来简单划分一下这个页面的结构:

页面结构

如果我们想也不想地来直接实现,那么这个页面大概可以分成这样:

  1. 顶部的两个Tab,ViewPager + Fragment。
  2. 底下的滚动页面,一个RecyclerView(或者NestedScrollView + RecyclerView)全搞定。

1 这部分没什么好说的,大家应该都是这么做的(应该不会有人这里不用ViewPager + Fragment吧?),我们简单用伪代码模拟一下。


/**
 * 动态
 */
public class MomentsFragment extends Fragment {

    /**
     * 顶部视频和综合的Tab
     */
    private ViewPager tabPager;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        // return inflater.inflate()...
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        tabPager = view.findViewById(R.id.view_pager);
        FragmentPagerAdapter adapter = new MomentsFragmentAdaper();
        // 视频 Tab
        adapter.addFragment(new VideoFragment());
        // 综合 Tab
        adapter.addFragment(new CommonFragment());
        tabPager.setAdapter(adapter);
    }
}

大概就是这个样子了。至于 2 的部分……

我们当然可以像上面说的那么做,把所有的内容往这一整页的Fragment里面一塞,剩下的1.用Adapter的Type全弄一起,2.或者写好几个View,addView,3.或者自定义一种容器来稍作解耦,然后容器提供getView方法,最后还是addView

但设想一下这么做的后果:1.假设放在Adapter里面,我们会得到一个3000行的Adapter,页面的每一部分可能仅仅是某个方法,Adapter里面还装着网络请求和用来解析网络请求的相关代码,这样的“上帝类”我们也许能忍受,但是很可能在一个月后我们就不得不审视这所有的3000行代码,而目的仅仅是因为产品经理希望我们把“最常访问”和“频道与话题”这两个部分换个位置。

上面大家当乐子看就行,大家都是老江湖了,这样的代码哪怕写出来也过不去Review这一关。这里也简单用代码表示一下:

public class VideoPageAdapter extends RecyclerView.Adapter {

    private static final int TYPE_VIDEO = 0;
    private static final int TYPE_SEARCH = 1;
    private static final int TYPE_RECENT = 2;
    private static final int TYPE_CHANNEL = 3;

    @Override
    public int getItemViewType(int position) {
        int type = TYPE_VIDEO;
        switch (position) {
            case 0:
                type = TYPE_SEARCH;
                break;
            case 1:
                type = TYPE_RECENT;
                break;
            case 2:
                type = TYPE_CHANNEL;
                break;
        }
        return type;
    }
    
    // 此处省略三千行
}

那么,关于2和3呢?稍有经验的人肯定能想到,我们应该把代表不同功能的部分分别解耦,把他们至少拆到不同的类里面去——我们可以用不同的View来代表不同的功能,但是考虑到MVP,MVVM等思想,直接用View来显然也是在给未来挖坑,那么我们就可以自定义一个容器来作为Controller或者Presenter,从而实现不同的功能……

那么,与其自己造轮子,我们何不用Fragment呢?假设我们不用Fragment,而是自定义了某个Container或者Presenter,那么我们就要在最外层的Fragment里面持有它们所有的引用,然后得在不同的生命周期里面,手动调用方法触发它们的生命周期(当然你也可以用JetPack的LifeCycle来更优雅地实现这一点)。至于这些不同的Container之间,如果我们希望他们数据互通,最坏的结果可能就是他们需要互相持有互相的引用,代码最后还是变得一团糟……

这里还是模拟一下伪代码(错误示范):

/**
 * 视频Tab
 */
public class VideoFragment extends Fragment {
    
    private ContainerOrPresenter searchPresenter;
    private ContainerOrPresenter recentPresenter;
    private ContainerOrPresenter channelPresenter;
    private ContainerOrPresenter videoPresenter;
    
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        searchPresenter = new ContainerOrPresenter();
        recentPresenter = new ContainerOrPresenter();
        // 假设两个 Presenter 需要互相通信。
        searchPresenter.setRecentPresenter(recentPresenter);
    }

    @Override
    public void onResume() {
        super.onResume();
        // 模拟繁琐的生命周期控制
        searchPresenter.onResume(this);
        recentPresenter.onResume(this);
        channelPresenter.onResume(this);
        videoPresenter.onResume(this);
    }
}

这段代码的问题就在于我们维护了完全没必要维护的生命周期,为此不得不持有了多余的引用。

所以用Fragment吧!有官方来为我们维护它的生命周期,官方还提供了Fragment + ViewModel的最佳实践让我们实现不同部分之间的数据传递,我们能够不费吹灰之力地实现MVVM架构,而未来如果我们希望把一部分功能复用或者删除,简单地操作一下Fragment就可以。这就是Fragment之于解耦,于其自带生命周期的优势所在了。

那么,回到最开始的问题,这个页面在我们加入了Fragment之后,一个更好的实现方式可能是这样……

  1. 顶部的两个Tab,ViewPager + Fragment。
  2. 底下的滚动页面:NestedScrollView + 4个Fragment。

大概这样:

我们无需知道最右边的Fragment到底哪个是哪个

我们无需在意滚动页面里面的Fragment装的到底是什么,我们只需要简单的在外层Fragment里面根据需要,new Fragment(),然后 add 即可,不同的部分所要实现的不同功能,我们可以全部放在不同的Fragment里面,而不同的Fragment之间我们通过ViewModel来通信即可。

这个思路适合几乎所有的页面。

这里我们还是用简单的代码来表示这种实践: 首先,最外层代表“动态”的Fragment我们对其进行了一些改动, 主要是添加了ViewModel,以及子Fragment的创建方式有了一些差异(这里我们过后提) 具体都在注释里面了:

/**
 * B站的“动态”Tab
 */
public class MomentsFragment extends Fragment {
    /**
     * 顶部视频和综合的Tab
     */
    private ViewPager tabPager;

    private MomentsViewModel momentsViewModel;
    private FragmentActivity hostActivity;

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        hostActivity = (FragmentActivity) context;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 这里 of 方法传入的参数可以是Fragment也可以是Activity
        // 如果此 Fragment 需要和Activity或者和Activity下的其他Fragments通讯,建议传入Activity作为参数
        // 值得注意的是,传入Activity创建的ViewModel会一直存续到Activity销毁,所以注意内存泄露问题
        momentsViewModel = ViewModelProviders.of(hostActivity).get(MomentsViewModel.class);
        momentsViewModel.newMessageLiveData.observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer integer) {
                // 这里模拟网络请求回传了“动态”tab右上角的新消息数量
            }
        });
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        // return inflater.inflate(res, container, false);
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        tabPager = view.findViewById(R.id.view_pager);
        // 这里传入的应该是 getChildFragmentManager,不做过多解释
        MomentsFragmentAdapter adapter = new MomentsFragmentAdapter(
                getChildFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
        List<String> childList = new ArrayList<>();
        // 视频 Tab,这里相比原版有一些改动,后面解释
        // 这里也能简单看出来,相比直接传入Fragment的实例,这样做其实做到了解耦
        // 我们无需关注Moments里面到底有什么,只要把对应页面的路径传入即可
        childList.add(Routes.ROUTE_VIDEO);
        // 综合 Tab
        childList.add(Routes.ROUTE_COMMON);
        adapter.setDataList(childList);
        tabPager.setAdapter(adapter);
    }

    @Override
    public void onResume() {
        super.onResume();
        // 这里模拟进行了一个网络请求
        momentsViewModel.requestNewMessage();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 避免内存泄露
        momentsViewModel.newMessageLiveData.removeObservers(this);
    }
}

严格来讲,“动态”这页的代码这样就差不多可以了。 如果还要完善细节,最多也就是修改一些UI相关的内容了(这里我们把Tab的新消息数量放在这里请求只是个模拟,实际情况下Tab可能在Fragment外层,所以没法这么做)。而后续相关逻辑的添加也会主要围绕着ViewModel,可以尽量避免对Fragment本身的改动。

顺便,这里我们提到了ARouter对于Fragment的解耦效果。下面我们简单展示ARouter+Fragment所能达到的效果。

对Fragment的依赖变成了对String的

可以看到,这下我们连具体的Fragment都不需要了,原本的实现方式我们虽然可以无需了解我们需要的Fragment是哪个,但是我们还是需要手动来 new XXXFragment,而在使用ARouter之后,我们就只需要不同Fragment对应的路径了。

形容一下就是:张三原本需要每天自己去找李四,现在有了邮局,张三把信放进邮箱就可以直接送信给李四了,而无需亲自跑一趟。

下面我们来继续展示“动态”主页的下一级,“动态-综合”

我们在通过Fragment实现具体细节之后,动态-综合这页本身作为一个容器,只需要把容器的内容填上即可,无需自己实现任何额外的逻辑,用代码标识就是:


/**
 * 动态-综合
 * 容器Fragment,用来装里面具体的 搜索,最近访问,频道,动态列表 等子模块
 */
@Route(path = Routes.ROUTE_COMMON)
public class CommonFragment extends Fragment {

    private CommonViewModel commonViewModel;
    private FragmentActivity hostActivity;

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        hostActivity = (FragmentActivity) context;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 这里 of 方法传入的参数可以是Fragment也可以是Activity
        // 如果此 Fragment 需要和Activity或者和Activity下的其他Fragments通讯,建议传入Activity作为参数
        // 值得注意的是,传入Activity创建的ViewModel会一直存续到Activity销毁,所以注意内存泄露问题
        commonViewModel = ViewModelProviders.of(this).get(CommonViewModel.class);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // 假设这个LinearLayout包裹在NestedScrollView里面,假设它叫“ll_container”
        // 借用ARouter来创建Fragment后,我们只需要四个路径,此容器Fragment的工作就全部完成啦~
        FragmentManager manager = getChildFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();
        transaction.add(R.id.ll_container, createFragmentByPath(Routes.ROUTE_SEARCH), Routes.ROUTE_SEARCH);
        transaction.add(R.id.ll_container, createFragmentByPath(Routes.ROUTE_RECENT), Routes.ROUTE_RECENT);
        transaction.add(R.id.ll_container, createFragmentByPath(Routes.ROUTE_CHANNEL), Routes.ROUTE_CHANNEL);
        transaction.add(R.id.ll_container, createFragmentByPath(Routes.ROUTE_VIDEO_LIST), Routes.ROUTE_VIDEO_LIST);
        transaction.commit();
    }

    private Fragment createFragmentByPath(String path) {
        return (Fragment) ARouter.getInstance().build(path).navigation();
    }
}

这里创建ViewModel是为了实现父Fragment和其所有子Fragment之间的通信,具体的后面再说。

然后这里我们实现一下频道Fragment和视频列表Fragment,上代码:

/**
 * 动态-综合页-频道
 */
@Route(path = Routes.ROUTE_CHANNEL)
public class ChannelFragment extends Fragment {

    private CommonViewModel commonViewModel;
    private RecyclerView channelRecyclerView;
    private RecyclerView.Adapter channelAdapter;

    private FragmentActivity hostActivity;

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        hostActivity = (FragmentActivity) context;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        commonViewModel = ViewModelProviders.of(this).get(CommonViewModel.class);
        commonViewModel.channelData.observe(this, new Observer<List<ChannelData>>() {
            @Override
            public void onChanged(List<ChannelData> channelData) {
                if (null != channelData) {
                    // channelAdapter.setDataList(channelData);
                    channelAdapter.notifyDataSetChanged();
                }
            }
        });
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // channelRecyclerView = view.findViewById(R.id.rv);
        // channelAdapter = new ChannelAdapter();
        channelRecyclerView.setLayoutManager(new GridLayoutManager(hostActivity, 2));
        channelRecyclerView.setAdapter(channelAdapter);
    }

    @Override
    public void onResume() {
        super.onResume();
        commonViewModel.requestChannel();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        commonViewModel.channelData.removeObservers(this);
    }
}
/**
 * 动态-综合页-视频列表
 */
@Route(path = Routes.ROUTE_VIDEO_LIST)
public class VideoListFragment extends Fragment {

    private CommonViewModel commonViewModel;
    private RecyclerView videoRecyclerView;
    private RecyclerView.Adapter videoAdapter;

    private FragmentActivity hostActivity;

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        hostActivity = (FragmentActivity) context;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        commonViewModel = ViewModelProviders.of(this).get(CommonViewModel.class);
        commonViewModel.channelData.observe(this, new Observer<List<ChannelData>>() {
            @Override
            public void onChanged(List<ChannelData> channelData) {
                if (null != channelData) {
                    // channelAdapter.setDataList(channelData);
                    videoAdapter.notifyDataSetChanged();
                }
            }
        });
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // videoRecyclerView = view.findViewById(R.id.rv);
        // videoAdapter = new VideoAdapter();
        videoRecyclerView.setLayoutManager(new LinearLayoutManager(hostActivity));
        videoRecyclerView.setAdapter(videoAdapter);
    }

    @Override
    public void onResume() {
        super.onResume();
        commonViewModel.requestVideo();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        commonViewModel.videoData.removeObservers(this);
    }
}

可以看到,我们从原本的一个大类负责所有的功能,逐渐变成了大类只做容器,功能细化拆分给不同的child fragment,从而提高灵活性和复用性。

这样做还有一个好处,那就是不同的页面现在可以交给不同的人来开发了,避免了冲突,提高了效率。

Fragment和ARouter

项目地址:github.com/alibaba/ARo…

上面我们提到Fragment + ARouter是一个很好的实践。

关于ARouter这里不多说,没用过的人可以百度一下。

这本身是一个用来实现模块化中不同模块通信的路由框架,它将Android里面不同Activity之间的依赖关系实现了降维打击,简化成了不同的路由地址之间的关系,从而实现解耦。

这个思路应用在Fragment上面就是Fragment的解耦(当然,ARouter对于Activity和Fragment在使用方式上有所差异,这里不详细说了,代码里有体现。)

我们这里简单说说用Fragment + ARouter好处都有啥?

我个人认为,好处如下:

  1. 解耦
  2. 传参优化&依赖注入
  3. 通过路径统一实现页面的多端统一

第一点不用说,我们原本想要创建一个Fragment,需要知道类名,现在知道路由地址path就可以了。

第二点是ARouter提供的功能,具体可以在ARouter的项目里查看,我们都知道如果给Fragment的构造函数添加参数来传参是个天坑,是个Crash的经典来源,使用ARouter可以帮我们简化许多工作。

第三点则是,当页面和路径对应起来以后,我们通过一个路径可以在安卓,ios,甚至是h5端都对应到同一个页面。理论上服务器只要下发一套这样的路径配置,我们就能把同一个页面通过不同的样式组合起来,或是轻松实现把首页的不同tab调换顺序。又比如服务器下发路径就可以在路径对应的页面上配置弹窗或者广告,悬浮窗等,十分灵活。最后还有,一个页面我们可以分别对应一个原生页和H5页,如果原生页路径找不到,这个路径(我们可以把它拼装成url)就可以直接降级打开一个web页面。

这里就简单说一下,有机会以后可以用代码补充。

Fragment和ViewModel

前面有写到过Fragment + ViewModel的优势和实践,最大好处有二:

  1. 实现Fragment与Activity,Fragment与Fragment之间的通信。
  2. 轻松实现MVVM模式。

第二点可以另起一篇文章了,这里就不多说了。 关于第一点,这同样是如今Google官方推荐的实现方式:

developer.android.google.cn/guide/fragm…

这里我就偷懒不自己写了,摘抄一些官方提供的内容:

当您需要在多个 Fragment 之间或 Fragment 与其宿主 Activity 之间共享数据时,ViewModel 是理想的选择。ViewModel 对象可存储和管理界面数据。如需详细了解 ViewModel,请参阅 ViewModel 概览。

Fragment 及其宿主 Activity 均可通过将 Activity 传入 ViewModelProvider 构造函数来使用 Activity 范围检索 ViewModel 的共享实例。ViewModelProvider 负责实例化 ViewModel 或检索它(如果已存在)。这两个组件都可以观察和修改此数据 同一 Activity 中的两个或更多 Fragment 通常需要相互通信。例如,假设有一个 Fragment 显示一个列表,另一个 Fragment 允许用户对该列表应用各种过滤器。如果 Fragment 不直接通信(这意味着,它们不再独立),这种情况可能不容易实现。此外,这两个 Fragment 还必须处理另一个 Fragment 尚未创建或不可见的情况。

这很好实现,我们只需要 ViewModelProviders.of(hostActivity).get(CommonViewModel.class);即可,只要of传入的实例是同一个,那么这个方法我们就能得到相同的ViewModel。

这两个 Fragment 可以使用其 Activity 范围共享 ViewModel 来处理这种通信。通过以这种方式共享 ViewModel,Fragment 不需要相互了解,Activity 也不需要执行任何操作来促进通信。

同样的,我们只需要 ViewModelProviders.of(getParentFragment()).get(CommonViewModel.class);即可,只要of传入的实例是同一个,那么这个方法我们就能得到相同的ViewModel。