浅探美团点评的模块化框架 Shield

文章目录
  1. 1. 简介 Shield
  2. 2. 深入 Shield
    1. 2.1. 宏观设计
    2. 2.2. 框架概要
    3. 2.3. 单个页面
    4. 2.4. 模块配置
    5. 2.5. 单个模块
    6. 2.6. 视图组件
    7. 2.7. 模块管理器
    8. 2.8. 视图管理器
    9. 2.9. 数据交互
  3. 3. 参考文献

风过雁渡无声影,深入世俗不迷失。

想必,随着美团点评业务的不断壮大和复杂化,其特定业务的场景中,产生的定制化需求也越来越多。往往是,单一的页面也需要根据不同的业务、乃至不同的用户展示出不同的内容。在这样的背景下,“盾牌” Shield 便应运而生,其帮助将一个复杂的页面切分成多个模块,以利于更高效地开发与维护,如此,在移动互联网的大潮下,为整个集团内部的多种移动端 App 保驾护航。

简介 Shield

官方开源地址:Shield

Shield 是一个模块化 UI 的界面解决方案,是一款 Native (Android & iOS) 的 UI 开发框架,其不仅具备高可复用、协同开发等特性,还包括后端动态配置、动态模块化等一系列高可接入、可扩展的解决方案。

如此,就算业务逻辑越来越复杂,单一的页面 (Activity 或 Fragment) 里包含着如网络请求、数据解析、视图渲染以及打点、页面跳转等一系列逻辑,使用 Shield 可以帮助很好地开发和维护一个复杂的页面。

build.gradle 里添加依赖:

1
compile 'com.dianping.android.sdk:shieldCore:1.5.1'

深入 Shield

首先声明一下,下面提到的模块即 Agent页面即 Fragment 或 Activity

宏观设计

模块独立

一个小模块自身具备完整的生命周期,可以在不同的页面之间自由组合,同时,模块自身解耦、与大的页面也是高度解耦的,其仅关心自己设计的状态和数据

数据驱动

这一点,和 Blackboard 的框架中的驱动点如出一辙,都是以数据 Data 作为驱动,小模块的表现只取决于其依赖的数据,与具体的行为没有关系。

面向接口

往往,我们的面向对象开发,都成了面向接口开发。这里,整个模块化框架通过接口来交互及规范行为,接口的多样化实现以达成多态。

框架概要

通常,一个典型的模块化页面如大众点评的首页,其主要由一个页面和多个模块组成。

在模块配置 (AgentListConfig) 中,自行配置以确定加载哪些模块构成一个页面。

针对一个模块,分为业务逻辑部分 (AgentInterface) 和视图逻辑部分 (SectionCellInterface,包括SectionRow)。

一个完整的页面包含两个管理器:

  • 模块管理器 (AgentManager),其决定了如何创建、更新、恢复及销毁模块,还有如何将模块添加到页面中
  • 视图管理器 (CellManager),其决定了页面使用什么样的视图容器 (PageContainer) 管理视图,还有模块中的视图组件 (SectionCellInterface) 怎样添加到视图容器中

最后,问题来了,模块与页面之间、模块与模块之间怎么进行数据通信交互呢?

这里,页面持有一个支持数据订阅和通知的白板 (WhiteBoard),相当于一个媒介平台,以利于模块与页面之间、模块与模块之间进行数据通信交互。

示意图如下:

单个页面

我们已经知道,在模块化框架中,视图统一由页面的视图管理器管理。页面容器切换或扩展时,仅仅需要改变相应的视图管理器 (CellManager),不需要对模块进行调整

页面接入模块化框架大致核心的流程如下:

  • 继承 AgentManagerFragment
  • onCreateView() 中构建页面容器的内容视图 ContentView
  • onActivityCreated() 中调用 setAgentContainerView() 设置模块容器的视图
  • getCellManager() 中设定页面要使用的 CellManager (默认为 SectionRecyclerCellManager)
  • getAgentManager() 中设定页面使用的 AgentManager (默认为 LightAgentManager)
  • 注意,generaterDefaultConfigAgentList() 指定模块的配置

模块配置

上文中,我们知道,调用 generaterDefaultConfigAgentList() 来配置页面模块,其会返回一个配置列表,即一个完整页面可以有多份的配置模块,加载的是 shouldShow() 为 true 的配置,调用 getAgentInfoList() 配置具体的模块列表。比如,看源码中的StatusAgentConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class StatusAgentConfig implements AgentListConfig {

private static final String AGENT_PKG_NAME = "com.example.shield.status.agent.";

@Override
public boolean shouldShow() {
return true;
}

@Override
public Map<String, AgentInfo> getAgentInfoList() {
String[][][] agentArray = {
{
{"loading_button", AGENT_PKG_NAME + "ControlButtonAgent"},
{"loading", AGENT_PKG_NAME + "LoadingStatusAgent"}
},
{
{"loading_more", AGENT_PKG_NAME + "LoadingStatusMoreAgent"}
},
{
{"loading_more_again", AGENT_PKG_NAME + "LoadingStatusMoreAgent"}
}
};
return AgentInfoHelper.getAgents(agentArray);
}

@Override
public Map<String, Class<? extends AgentInterface>> getAgentList() {
return null;
}
}

看源码中,数组的一个维度一般包含两个元素,即模块的键 key 和模块的值:模块的包名 + 类名,亦即以

1
{ "key",  AGENT_PKG_NAME + "AgentClassName" }

表示一个模块。这样,多个模块即组成了一个完整的页面。示意图如下:

注意,模块实例以反射的方式创建,类名字符串要正确。此外,更新模块配置时,页面调用 AgentManagerFragment 里的 resetAgents() 方法重置配置,选择合适的配置后,根据现有模块和新模块的综合情况,来调用模块的生命周期。类似 Activity 里的生命周期,新增的模块会调用 onCreate() 和 onStart() 等方法,不再存在的模块调用 onStop() 和 onDestroy() 方法。

单个模块

结合上文,及总结来说,模块是有着生命周期和 Context 的视图块和逻辑片段。

通常情况下,一个模块包含一定的业务逻辑如请求接口 API、数据解析和刷新等,数据请求完成,会异步地更新视图 View,还有一定的视图逻辑如视图的渲染和展示等。不过,也有的模块仅处理业务逻辑。

一个页面中的所有模块,都由模块管理器管理,模块之间高度解耦,通过 WhiteBoard 共享数据。

总结说来,模块由统一的模块接口来规范,模块类实现了模块接口,即被视为模块可接入页面。模块接口 AgentInterface 的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface AgentInterface {

void onCreate(Bundle savedInstanceState);

void onStart();

void onResume();

void onPause();

void onStop();

void onDestroy();

Bundle saveInstanceState();

@Deprecated
void onAgentChanged(Bundle data);

void onActivityResult(int requestCode, int resultCode, Intent data);

String getIndex();

void setIndex(String index);

String getHostName();

void setHostName(String hostName);

SectionCellInterface getSectionCellInterface();

String getAgentCellName();
}

一个页面在自己的生命周期中会调用某个模块相应的生命周期,但是要求这个模块已经加载到页面中。注意,模块配置改变时,要手动调用新增或者消失模块的生命周期

模块要在页面中显示视图的话,需要调用 getSectionCellInterface() 的方法指定模块对应的视图组件。

关注模块与视图组件交互的部分,视图管理器会自动将模块中的视图组件加载到页面容器中,模块仅需要提供视图组件必需的数据。视图组件依赖的数据改变时,模块即调用updateAgentCell()方法来刷新相应的视图组件。而后,框架会自主调用视图组件中的onCreateView()updateView()方法。模块自身仅仅只需要更新视图需要的数据。

视图组件

视图组件可以在不同的模块之间复用,其正常是一个 Section & Row 的结构。一个视图组件可以有多个 Section,一个 Section 也可以有多个 Row。示意图如下:

注意,同一个模块中的不同 Section 能通过 LinkType 连接到一起,组成一个新的 Section

不同的模块之间,Section 无法通过 LinkType 连接在一起

Section 之间默认不连接

事实上,实现了 SectionCellInterface 接口的类都是一个可以复用的视图组件,视图管理器来统一管理,能加到任何页面容器中。

注意,视图组件的表现仅仅依赖相应的数据,其具有高可复用性,不同的模块若使用一样的视图组件,仅仅需要适配数据。SectionCellInterface 的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface SectionCellInterface {

int getSectionCount();

int getRowCount(int sectionPosition);

int getViewType(int sectionPosition, int rowPosition);

int getViewTypeCount();

View onCreateView(ViewGroup parent, int viewType);

void updateView(View view, int sectionPosition, int rowPosition, ViewGroup parent);
}

看源码中,视图组件的显示与隐藏以 getSectionCount() 控制,大于 0 时显示,不大于 0 时隐藏。

模块调用 updateAgentCell() 后,框架将通知视图逻辑组件进行更新,并调用对应的视图逻辑组件中的 updateView() 方法。

模块管理器

模块管理器负责对模块 (Agent) 的管理,包括了模块的整个生命周期,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface AgentManagerInterface {

void setupAgents(Bundle savedInstanceState, ArrayList<AgentListConfig> defaultConfig);

void startAgents();

void resumeAgents();

void pauseAgents();

void stopAgents();

void destroyAgents();

void onSaveInstanceState(Bundle outState);

void onActivityResult(int requestCode, int resultCode, Intent data);

void resetAgents(Bundle savedInstanceState, ArrayList<AgentListConfig> defaultConfig);

AgentInterface findAgent(String name);
}

类似 Activity 的生命周期,也很好理解。其通过 Fragment 中的 getAgentManager(),指定实现了模块管理器接口的实例。

视图管理器

视图管理器负责对视图组件的管理。源码如下:

1
2
3
4
5
6
7
8
9
10
11
public interface CellManagerInterface<T extends ViewGroup> {

void setAgentContainerView(T containerView);

//手动刷新接口
void notifyCellChanged();

void updateAgentCell(AgentInterface agent);

void updateCells(ArrayList<AgentInterface> addList, ArrayList<AgentInterface> updateList, ArrayList<AgentInterface> deleteList);
}

见码识意。注意,视图管理器要与特定的页面容器配合使用,如 Fragment 要使用 SectionRecyclerCellManager 作为视图管理器,须调用 setAgentContainerView() 设定一个 RecyclerView 类型的页面容器。

数据交互

如前文示意图,页面里的模块间,彼此之间的数据交互和通信,在 WhiteBoard 上进行,且 WhiteBoard 对象由 Fragment 持有并销毁。

WhiteBoard 类似于 RxJava 的注册与订阅。存取或获取数据等,要先调用 getWhiteBoard()。

如此,大部分页面通过 Activity + Fragment + Agent 的模块化架构支撑了大多业务的差异化定制需求,沉淀出了这份强大且灵活的“盾牌”框架。综合示例如下两张图:

拆分成模块化即:

至此,关于浅探美团点评的模块化框架 Shield 到此结束,更多细节及亮点参见 Shield 源码。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.https://tech.meituan.com/shield-opensource.html

2.https://github.com/Meituan-Dianping/Shield