什么是MVP
- ** View ** 是显示数据和用户操作交互的层级。在android上,它可以是activity、fragment、android.view.View 或者Dialog。
- ** Model ** 是一个数据访问层。比如database API 和 远程server API。
- ** Presenter ** 是从Model给View提供data的层级。Presenter也处理后台任务。
在android上,MVP是一种把后台任务从activities/views/fragments的生命周期相关事件中独立出来的方式。使用这种方式,app变得更加简单、可复用、代码变得更加短。
为什么使用MVP
1. 保持严格的简单
- 现在大多数的app还是处于View-Model模式
- 程序里面设计大量的View间的交互
使用这种View-Model模式你的app就会像这样:
这个图片不复杂?那你想象一下每个View可以在不确定的时机显示与不显示。不要忘记Views的状态保持与恢复哦。再附上几个后台任务给临时View,现在复杂了不?
还有一个超级复杂的对象——activity,几乎与一切相连。
而MVP
- 复杂的任务被分割为简单的task,使得更加容易地解决问题。
- 对象更小、bug更少、易于debug。
- 可测试性更好。
*** MVP中的View层变得如此简单,当请求数据时它甚至不需要回调。视图逻辑变得非常线性。***
2. 后台任务
无论你写activity、fragment还是自定义View,你可以把所有与后台任务连接的方法都放到一个不同的外部或者静态类里面。这样的话你的后台任务就不会与activity相连,这样的话就不会有activity泄漏,也不会太过于依赖activity。我们把这样的一个对象叫做** Presenter **。
这里有几种不同的方法来处理后台任务,但是没有一个能像** Presenter **这样可靠。
** 为何会这样 **
下面这张表展示了几种处理配置变化和OOM问题的方案:
Case 1 A configuration | Case 2 An activity restart | Case 3 A process restart | |
---|---|---|---|
Dialog | reset | reset | reset |
Activity, View, Fragment | save/restore | save/restore | save/restore |
Fragment with setRetainInstance(true) | no change | save/restore | save/restore |
Static variables and threads | no change | no change | reset |
** 结论 ** 现在你看,设置了setRetainInstance(true) 的fragment在这里也没啥用
| |A configuration change, An activity restart | A processrestart | |:-----------------------------------------|:------------------------------------------:|:----------------:| | Activity, View, Fragment, DialogFragment | save/restore | save/restore | | Static variables and threads | no change | reseteset |
** 现在比较容易看出,我们只需要写两段代码来完成恢复一个任何一种case:**
- 保存、恢复 Activity, View, Fragment, DialogFragment;
- 重启后台请求
第一部分可以通过常规的android API实现,第二部分是** Presenter 的工作, Presenter 只需要记住哪个请求应该执行,假如在执行期间进程restart了那么这个 Presenter **也会restart。
一个简单的示例
这个简单的示例将会加载和显示来自远程server的一些记录,假如发生错误将会显示一个toast。 我建议使用Rxjava来构建** Presenter **,因为这个库使你易于控制数据流。
非MVP
public class MainActivity extends Activity {
public static final String DEFAULT_NAME = "Chuck Norris";
private ArrayAdapter<ServerAPI.Item> adapter;
private Subscription subscription;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView)findViewById(R.id.listView);
listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
requestItems(DEFAULT_NAME);
}
@Override
protected void onDestroy() {
super.onDestroy();
unsubscribe();
}
public void requestItems(String name) {
unsubscribe();
subscription = App.getServerAPI()
.getItems(name.split("\\s+")[0], name.split("\\s+")[1])
.delay(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<ServerAPI.Response>() {
@Override
public void call(ServerAPI.Response response) {
onItemsNext(response.items);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable error) {
onItemsError(error);
}
});
}
public void onItemsNext(ServerAPI.Item[] items) {
adapter.clear();
adapter.addAll(items);
}
public void onItemsError(Throwable throwable) {
Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
}
private void unsubscribe() {
if (subscription != null) {
subscription.unsubscribe();
subscription = null;
}
}
}
这个示例中的缺陷:
- 每次用户选择屏幕时都会开始一个请求,导致了多余的请求和短暂的白屏。
- 假如用户高频率的旋转屏幕这将导致内存泄漏,每次callback保持了一个指向MainActivity引用(请求还在执行),会导致OOM问题和意味着程序变慢。
MVP
这个示例仅仅是用来演示,在实际开发中不会使用一个静态变量来保存** presenter **。
public class MainPresenter {
public static final String DEFAULT_NAME = "Chuck Norris";
private ServerAPI.Item[] items;
private Throwable error;
private MainActivity view;
public MainPresenter() {
App.getServerAPI()
.getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1])
.delay(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<ServerAPI.Response>() {
@Override
public void call(ServerAPI.Response response) {
items = response.items;
publish();
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
error = throwable;
publish();
}
});
}
public void onTakeView(MainActivity view) {
this.view = view;
publish();
}
private void publish() {
if (view != null) {
if (items != null)
view.onItemsNext(items);
else if (error != null)
view.onItemsError(error);
}
}
}
从技术上来讲,MainPresenter有三个线程事件:onNext, onError, onTakeView。They join in publish() method and onNext or onError values become published to a MainActivity instance that has been supplied with onTakeView.
public class MainActivity extends Activity {
private ArrayAdapter<ServerAPI.Item> adapter;
private static MainPresenter presenter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView)findViewById(R.id.listView);
listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
if (presenter == null)
presenter = new MainPresenter();
presenter.onTakeView(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.onTakeView(null);
if (isFinishing())
presenter = null;
}
public void onItemsNext(ServerAPI.Item[] items) {
adapter.clear();
adapter.addAll(items);
}
public void onItemsError(Throwable throwable) {
Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
}
}
MainActivity创建MainPresenter并且使之在onCreate/onDestroy生命周期之间。MainActivity用了一个静态变量来持有MainPresenter,所以每次进程重新启动会导致OOM事件,MainActivity应该判断这个MainPresenter是否存在来决定是否需要重建。
是的,这看起来检查有点臃肿并且还使用了一个静态变量,但是后面我会展示一个更好的写法。
** 这里主要想表达的是: **
- 这个示例app在每次旋转屏幕时不会重新启动一个请求。
- 假如这个进程重启,这个示例会重新加载数据。
- 当MainActivity实例被销毁时MainPresenter不会持有MainActivity引用。所以当屏幕旋转式不会OOM,也不会重复发送请求。
使用了 Nucleus库
public class MainPresenter extends RxPresenter<MainActivity> {
public static final String DEFAULT_NAME = "Chuck Norris";
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
App.getServerAPI()
.getItems(DEFAULT_NAME.split("\\s+")[0], DEFAULT_NAME.split("\\s+")[1])
.delay(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.compose(this.<ServerAPI.Response>deliverLatestCache())
.subscribe(new Action1<ServerAPI.Response>() {
@Override
public void call(ServerAPI.Response response) {
getView().onItemsNext(response.items);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
getView().onItemsError(throwable);
}
});
}
}
@RequiresPresenter(MainPresenter.class)
public class MainActivity extends NucleusActivity<MainPresenter> {
private ArrayAdapter<ServerAPI.Item> adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView)findViewById(R.id.listView);
listView.setAdapter(adapter = new ArrayAdapter<>(this, R.layout.item));
}
public void onItemsNext(ServerAPI.Item[] items) {
adapter.clear();
adapter.addAll(items);
}
public void onItemsError(Throwable throwable) {
Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_LONG).show();
}
}
正如你所看到的,这个示例比上一个又更加简单和短小。Nucleus负责创建、销毁、保存presenter,从View附加、分离presenter,自动发送请求结果到正确的View。
MainPresenter
的代码非常短小,因为使用了deliverLatestCache()
操作来推迟所有数据源发射的数据和错误,直到View变为可见。它会在内存中缓存数据使得配置发生改变时可以复用。
MainActivity
的代码非常短小,因为presenter的创建由NucleusActivity
来管理,所有你需要做的只是写一行@RequiresPresenter(MainPresenter.class)
注解。
View回收和View堆栈
一般来说,在用户与你的你的app进行交互时Views的依附和分离是很随机的。你可以很像这样随意的乱操作app,presenters将会比这些操作活的更久,因为它是在后台持续工作。
最佳实践
在Presenter里面保存你的请求参数
原则很简单:Presenter的主要目的管理请求。所以View不应该处理和重启请求。从View的角度来说,后台任务应该是一直看不见的、只返回结果或者错误并且没有callback的。
public class MainPresenter extends RxPresenter<MainActivity> {
private String name = DEFAULT_NAME;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (savedState != null)
name = savedState.getString(NAME_KEY);
...
@Override
protected void onSave(@NonNull Bundle state) {
super.onSave(state);
state.putString(NAME_KEY, name);
}
我推荐使用一个优秀的库 Icepick ,它可以缩短你的代码和简化程序逻辑(这个库没有使用运行时注解,所有的事情发生在编译时)。这个库是很好的ButterKnife使用者。
public class MainPresenter extends RxPresenter<MainActivity> {
@Icicle String name = DEFAULT_NAME;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
Icepick.restoreInstanceState(this, savedState);
...
@Override
protected void onSave(@NonNull Bundle state) {
super.onSave(state);
Icepick.saveInstanceState(this, state);
}
假如你有超过两个请求参数这个library也可以很自然保存。你可以创建一个BasePresenter
并且把Icepick放到这个里面,然后所有的子类就可以通过注解@Icicle
自动获得保存field。你将不再需要实现onSave
方法。这种方法同样对activity、fragment、view的状态有效。
在onTakeView
里执行main线程的短暂请求
有时你有一个短暂的数据查询,比如从database读取少量的数据。如果你可以通过Nucleus很容易地创建一个可重新启动的请求,那你就没有必要再所有地方都使用这种强大的tool。假如你在fragment创建时实例化一个后台请求,用户将会看见一个短暂的白屏,即使你这个查询只花费很少的时间。
所以,为了使得代码精简、用户开心,用主线程。
不要试图用你的Presenter去控制View
这样做不好————会导致你的应用程序变得复杂,因为这会走向一条不自然的路子。
**最自然的做法是:做一个控制流, user -> view -> presenter -> model -> data **。最终用户将使用应用程序,并且应用程序的所有控制来源都是来自用户。所以,控制应该来源于用户而不是应用程序结构的内部。
但是假如你的控制流像这样:** user -> view -> presenter -> view -> presenter -> model -> data **,这就违背了KISS原则。