使用MVP实现的列表框架

MVC

对于Android来说,它的界面开发就涉及到了模型——视图——控制器这3者的交互。

  • 在Android中视图层一般采用XML文件对界面的描述,也就是视图V。
  • 从本地数据文件或者网络获取的数据体就是模型M。
  • 而Activity就是控制器C。

在Android系统中,Activity主要起到的作用就是解耦,把视图View和模型Model进行分离,两者在Activity中进行绑定或者完成其他逻辑。

MVC模式

MVP

MVP模式能解除View和Model的耦合,同时带来更好的可扩张性和可测试性,保证了系统的整洁性和灵活性。在传统MVC模式下,Activity不仅要控制视图还要跟模型打交道,导致Activity的业务逻辑过多,代码基本无法复用。测试也比较麻烦。

在Android上,业务逻辑和数据存储关系紧密,而在MVC模式下,Activity中往往被塞入很多的业务逻辑代码,而MVP模式下,我们把业务逻辑和数据存储从Activity中剥离出来,由中间人Presenter负责,而Activity只负责组织和管理View,通过接口暴露对View的操作即可。
由此可知MVP中三个角色的作用:

  • Presenter——交互中间人:Presenter负责沟通View和Model,它从Model层检索数据后,放回给View层,使得View和Model之间没有耦合,也就是把业务逻辑从View角色中剥离出来。
  • View——用户界面:View通常是指Activity、Fragment或者某个View控件,它包含一个Presenter成员变量。一般View需要实现一个逻辑接口,将View上的操作通过接口传递给Presenter。而Presenter调用View的逻辑接口将数据结果返回给View元素。
  • Model——数据的存取:Model角色负责提供数据的存取功能。Presenter可以通过Model层存储、获取数据,Model就像一个数据仓库。具体来说,就是封装了数据库DAO或者网络获取数据。

MVP模式

业务分析

以优惠链为例,当我们进入客户端的时候,首先我们需要展示一个带有头部的列表,具体看图:
优惠链首页

进入应用后,我们首先初始化头部,首先需要从服务端拉取数据,然后将每一项的信息显示到列表中。点击某项数据时进入到令一个页面,该页面加载详细内容。因此,我们的业务逻辑大概有:

  1. 服务器请求数据
  2. 将数据加载到列表中

    具体有数据重置、加载数据、重新加载、加载更多的四个业务需求对应的方法,我们把它们抽象出来,作为接口暴露给View层:

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

    /**
    * 重置
    */
    void reset();

    /**
    * 加载新的数据源
    * @param dataSource 数据源
    */
    void load(@NonNull PageCallable<E> dataSource);

    /**
    * 重新加载
    */
    void reload();

    /**
    * 加载更多
    */
    void loadMore();
    }

Presenter的继承关系.png
方法暴露出来后,我们针对Presenter的默认实现是DataSetLoaderPresenter,代码如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class DataSetLoaderPresenter<E> implements DataSetPresenter<E>,
LoaderManager.LoaderCallbacks<DataSet<E>> {

负责调度数据存取,也就是model层
private LoaderManager loaderManager;

// DataSetUiController接口,代表了View层
private DataSetUiController<E> uiController;
/**
* 关联 UI
*/
public void onTakeUi(Context context,
LoaderManager loaderManager,
DataSetUiController<E> uiController) {
this.context = context;
this.loaderManager = loaderManager;
this.uiController = uiController;
}
// load的具体实现,它使用loaderManager负责
public void load(@NonNull PageCallable<E> dataSource) {
Timber.v("load");
changeDataSource(dataSource);
if (loaderManager != null) {
Bundle args = new Bundle();
args.putSerializable(ARG_ACTION, DataSetLoadAction.LOAD);
// 由Loader这个任务管理进行网络访问
loaderManager.initLoader(loaderId, args, this);
}
}
// 获取数据时,调用View的showLoading函数更新UI
protected void notifyUiLoading(DataSetLoadAction loadAction) {
if (uiController != null) {
uiController.showLoading(loadAction);
}
}
// 数据加载完,调用View的showArticles函数将数据传递给View显示
protected void notifyUiLoadResult(DataSetLoadAction loadAction, DataSet<E> data) {
if (uiController != null) {
uiController.showLoadResult(loadAction, data);
}
}
// 数据加载完,调用View的showLoadError函数将数据传递给View显示
protected void notifyUiLoadError(DataSetLoadAction loadAction, Exception error) {
if (uiController != null) {
uiController.showLoadError(loadAction, error);
}
}

在DataSetLoaderPresenter中,持有了View和Model的引用,分别是uiController和loaderManager。uiController是主界面的逻辑接口,代表了View接口角色,用于Presenter回调View的操作。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface DataSetUiController<E> {
/**
* UI 重置
*/
void reset();

/**
* UI 表现加载中状态
*/
void showLoading(DataSetLoadAction action);

/**
* UI 表现加载成功
*/
void showLoadResult(DataSetLoadAction action, @NonNull DataSet<E> result);

/**
* UI 表现加载失败
*/
void showLoadError(DataSetLoadAction action, @Nullable Exception error);
}

而LoaderManager负责对数据的存取操作,用于在网络上加载数据。

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
@Override
public final Loader<DataSet<E>> onCreateLoader(int id, Bundle args) {
DataSetLoadAction loadAction = (DataSetLoadAction) args.getSerializable(ARG_ACTION);
Timber.v("onCreateLoader - action: %s", loadAction);
return createDataSetLoader(loadAction)
.setListener(this);
}

@Override
public void onLoadFinished(Loader<DataSet<E>> loader, DataSet<E> data) {
DataSetLoader<E> dataSetLoader = (DataSetLoader<E>) loader;
Timber.v("onLoadFinished - action: %s", dataSetLoader.loadAction);
DataSetLoadAction loadAction = dataSetLoader.loadAction;
if (data != null) {
notifyUiLoadResult(loadAction, data);
} else {
notifyUiLoadError(loadAction, dataSetLoader.getException());
}
}

@Override
public void onLoaderReset(Loader<DataSet<E>> loader) {
DataSetLoader<E> dataSetLoader = (DataSetLoader<E>) loader;
Timber.v("onLoaderReset - action: %s", dataSetLoader.loadAction);
dataSetLoader.setListener(null);
notifyUiReset();
}

最后是界面的具体实现,这里我们根据列表的不同使用场景抽取出了它们的一些共性,于是UiController有很深的继承关系和多种实现。个人觉得这也是该模块比较难以理解的地方。下面是UiController的继承关系:
UiController的继承关系.png

  • DataSetUiController:定义了UI重置、加载状态的UI更新、加载成功和加载失败的UI更新的接口
  • AbsDataSetUiController:DataSet 数据的 UI 控制器,主要负责 dataSet/empty/loading 等各个状态对应 UI 展示上的处理
  • DataSetAbsListUiController:使用 AbsListView 展示列表的DataSetUiController
  • DataSetHeaderListUiController:使用带有头部的ListView的DataSetUiController
  • DataSetRecyclerUiController:使用RecyclerView展示列表的DataSetUiController

    我们定义了抽象类AbsDataSetUiController实现DataSetUiController接口,由它负责处理数据加载变动时的UI控制,例如加载数据时显示加载提示,加载完成之后隐藏。
    然后为了适配不同的数据集容器(例如ListView、GridView、RecyclerView),我们又抽取出了一层抽象类,它们就是DataSetAbsListUiController和DataSetRecyclerUiController。它们负责处理数据集和对应适配器的关联和设置,只留下创建适配器的抽象方法,留待用户实现。同时实现了下拉更新接口和处理了与上拉更新有关的列表状态变化,只留下对下拉更新和上拉更新的逻辑处理接口,留待用户自己实现。
    最后DataSetHeaderListUiController是一个带头部的ListUiContorller。

而抽象类UiController如何在Activity、Fragment或者其他View中使用呢?
我们在Activity、Fragment或者其他View中通过内部类实现UiController,并使用代理模式,使UiController代理主界面对应的方法,具体代码如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
public abstract class AbsDataSetListActivity<E> extends FragmentActivity {

protected static final int LOAD_ITEMS = 1;

private DataSetLoaderPresenter<E> presenter;

@Nullable
protected DataSetHeaderListUiController<E> uiController;

public abstract View onCreateView();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = onCreateView();
setContentView(view);

uiController = createUiController(view);
uiController.configure();
// 关联UI和Presenter
presenter().onTakeUi(this, getSupportLoaderManager(), uiController);
}

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

@Override
public void onPause() {
presenter().onPause();
super.onPause();
}

@Override
public void onDestroy() {
presenter().onDropUi();
if (uiController != null) {
uiController.destroy();
uiController = null;
}
super.onDestroy();
}

public ListView listView() {
if (uiController != null) {
return uiController.getListView();
}
return null;
}

@NonNull
protected DataSetLoaderPresenter<E> createPresenter() {
return new DataSetLoaderPresenter<>(LOAD_ITEMS);
}

@NonNull
public DataSetLoaderPresenter<E> presenter() {
if (presenter == null) {
presenter = createPresenter();
}
return presenter;
}

@NonNull
protected DataSetHeaderListUiController<E> createUiController(View view) {
return new UiController(view);
}

protected void configureSwipe(@NonNull SwipeRefreshLayout swipe) {
Resources resources = swipe.getResources();
swipe.setColorSchemeColors(resources.getIntArray(R.array.common_swipe_refresh_colors));
}

@NonNull
protected abstract ArrayAdapter<E> createDataSetAdapter(@NonNull ListView listView);

protected void onLoadMoreDataSet() {
presenter().loadMore();
}

protected void onRefreshDataSet() {
presenter().reload();
}

protected void onListItemClick(ListView listView, View v, int position, long id) {
}

protected class UiController extends DataSetHeaderListUiController<E> {

protected UiController(View view) {
super(view);
}

@Override
protected void configureSwipe(@NonNull SwipeRefreshLayout swipe) {
super.configureSwipe(swipe);
AbsDataSetListActivity.this.configureSwipe(swipe);
}

@NonNull
@Override
protected ArrayAdapter<E> createDataSetAdapter(@NonNull ListView listView) {
return AbsDataSetListActivity.this.createDataSetAdapter(listView);
}

@Override
protected void onScrolledToLast() {
if (getSupportLoaderManager().hasRunningLoaders())
return;
AbsDataSetListActivity.this.onLoadMoreDataSet();
}

@Override
protected void showError(DataSetLoadAction action, @Nullable Exception error) {
Toasts.show(AbsDataSetListActivity.this, R.string.common__failed_to_load);
}

@Override
public void onRefresh() {
AbsDataSetListActivity.this.onRefreshDataSet();
}

@Override
protected void onListItemClick(ListView listView, View v, int position, long id) {
AbsDataSetListActivity.this.onListItemClick(listView, v, position, id);
}
}
}

最后用户只需继承对应的Activity或者Fragment同时初始化UI和实现适配器即可。

总结

  1. 使用MVP将业务逻辑分离开,针对不同的UI界面的需求,我们新建不同的UiController,但是对于控制业务的Presenter我们却可以一直使用同一个。提高了代码的复用。
  2. 逻辑更加清晰,针对UI界面,我们无需关心数据的获取,只需调用相应的Presenter接口即可。而对于Presenter,也只需要调用相应的UiController和Medol接口即可。

热评文章