本文内容结合:http://hannesdorfmann.com/android/mosby-playbook/
前置内容:http://hannesdorfmann.com/android/mosby/
apk下载体验:点击打开链接
不了解MVP是什么的,可以看我的上一篇文章:http://blog.csdn.net/marktheone/article/details/46662873
该应用基于mosby,但并不真连接到了POP3或者IMAP服务器,数据均在app开始时随机生成,且没有持久层比如本地sqlite数据库。
Tip1. 不要过度架构
不要过度分析,一个干净的软件架构是必要的,但是在一定时候必须开始写代码。尽管计划设计很重要,但真实世界中我们总是需要重构的。
java开发者总是陷入类似的问题:不要过度泛型所有东西。
比如有个ImageProxy,要做的事情很简单,过一个image的url,load到那个那篇,但是其中可能会根据imageView大小,网络状况,设备来对url重写,比如把http://www.example.com/foo.jpg变成http://www.example.com/foo.jpg?quality=high&width=400。你是不是会把load图和cache图抽象化?然后使用Picasso进行实现接口,这样以后就可以使用其他图片库如Glide, Universal Image Loader来替换;然后你可能会实现一个策略模式来动态加载正确版本的图片,这样以后就能在运行时做预设置和配置化。但事实上呢?可能3年以后,你仍然只需要最初做的这些功能,那些可配置化,替换Picasso压根不会发生。
所以保持你的presenter简单,不要花太多时间去想:怎么可以让这个presenter可扩展,来应对一年以后可能的某功能。
Tip2. 机智地使用继承
如果有个base的MVP View,实现了在ListView上的下拉刷新,并且在app中几乎所有的界面都用了。如果有一天某个需求要做一个某有下拉刷新的listview,某个开发可能就直接继承了那个base view,然后用了某个黑科技把下拉刷新的效果给去了(比如override后内部是个空实现),搞得代码难以维护。更好的方法是重构原来listview,默认没有下拉刷新,再继承一个,添加下拉刷新效果。但如此一来,就会弄出庞大的继承层级。而新加入团队的开发者,真地能选对base class来实现某个新功能吗。
Tip3. 不要把MVP当做MVC的变种
MVP不是MVC,可以看看下图
public class User {
String firstname;
String lastname;
int ranking;
public String getFullname(){
return firstname +" "+lastname;
}
}
比如我们的UserView想展示全名和一个ranking,我们会给User加上ranking和getFullname(),但在web api过来的json数据中并没有ranking,这就搞得很困惑,而在我们没有计算时,ranking通常为0。如果ranking是别的什么object,甚至可能引发空指针。
public class UserPresentationModel {
String fullname;
int ranking;
public UserPresentationModel(String fullname, int ranking) { ... }
}
这样就保证了ranking总会被设置为一个具体的值,当滑动列表的时候也不会去做CPU相关的fullname计算,因为Presentation Model是在其他线程做实例化的)。UserView显示的不是List(User),而是List<UserPresentationModel>。
如果启动了一个相机app,拍摄照片后,有没有必要把结果传回给presenter?这要看实际情况:只是想在ImageView展示该图片吗?Presenter是负责和状态协作的,所以只是展示图片的话通常不会修改状态,所以可以直接在view内部去做。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
Bundle extras = data.getExtras();
Bitmap imageBitmap = (Bitmap) extras.get("data");
imageView.setImageBitmap(imageBitmap);
}
}
如果需要做一个图片处理的话(比如弄个缩略图),那就需要由presenter来控制了(比如在处理图片的时候显示个loading)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
Bundle extras = data.getExtras();
Bitmap imageBitmap = (Bitmap) extras.get("data");
presenter.makeThumbnail(imageBitmap);
}
}
Tip8. MVP和EventBus
EventBus是一个有名的事件总线的库,通过观察者模式来做到控件解耦。和RXJava的区别在于RXJava需要订阅一个具体的可观察者,而EventBus则可以是某种Event。
应用场景
1. 业务逻辑事件
比如登入后,一些view会自动刷新
并不是通过Activity的onResume,而是LoginPresent抛出了一个事件,来通知其他presenter告知用户已经登入了。其他presenter则会触发重载页面更新view。
2. UI事件
比如不同fragment,或者activity和fragment之间的交互,不需要通过google推荐的listener回调方法。
Tip9. 乐观传播
其实是类似假feed的东西,在实际数据更新成功前,先给用户一个反馈(比如点赞,打星),在MVP模式中,我们完全在P层中做这个,通过clean architecture和EventBus,一切也变得很简单。
public class BaseRxMailPresenter<V extends BaseMailView<M>, M>
extends BaseRxAuthPresenter<V, M> {
@Inject public BaseRxMailPresenter(MailProvider mailProvider, EventBus eventBus) {
super(mailProvider, eventBus);
}
public void starMail(final Mail mail, final boolean star) {
// optimistic propagation
if (star) {
eventBus.post(new MailStaredEvent(mail.getId()));
} else {
eventBus.post(new MailUnstaredEvent(mail.getId()));
}
mailProvider.starMail(mail.getId(), star)
.subscribe(new Subscriber<Mail>() {
@Override public void onCompleted() {
}
@Override public void onError(Throwable e) {
// Oops, something went wrong, "undo"
if (star) {
eventBus.post(new MailUnstaredEvent(mail.getId()));
} else {
eventBus.post(new MailStaredEvent(mail.getId()));
}
if (isViewAttached()) {
if (star) {
getView().showStaringFailed(mail);
} else {
getView().showUnstaringFailed(mail);
}
}
}
@Override public void onNext(Mail mail) {
}
});
// Note: that we don't cancel this operation in detachView().
// We want to ensure that this operation finishes
}
...
}
Tip 10. MVP可扩展
一个MVP View可以包含独立的其他MVP Views
Tip 11. 不是所有View需要MVP
比如静态页面
Tip 12. 每个MVP view只展示一种model
比如抽屉里要展示account和menu,最好分成AccountView和MenuView两个
AccountManager <- AccountPresenter <- AccountView
MailProvider <- MenuPresenter <- MenuView
Tip 13. Android服务
Android Service显然是业务逻辑,所以应该由presenter和其交流。
下面的代码时通过EventBus,SendMailService和写操作presenter交互。
public class WritePresenter extends MvpBasePresenter<WriteView> {
private EventBus eventBus;
private IntentStarter intentStarter;
public void writeMail(Context context, Mail mail) {
getView().showLoading();
intentStarter.sendMailViaService(context, mail);
}
public void onEventMainThread(MailSentErrorEvent errorEvent){
if (isViewAttached()){
getView().showError(errorEvent.getException());
}
}
public void onEventMainThread(MailSentEvent event){
if (isViewAttached()){
getView().finishBecauseSuccessful();
}
}
}
Tip 14. 仅当你有LCE View的时候才使用LCE(Load Content Error)
Tip 15. 写自定义的ViewState
已登陆为例
public class LoginViewState implements ViewState<LoginView> {
final int STATE_SHOW_LOGIN_FORM = 0;
final int STATE_SHOW_LOADING = 1;
final int STATE_SHOW_ERROR = 2;
int state = STATE_SHOW_LOGIN_FORM;
public void setShowLoginForm() {
state = STATE_SHOW_LOGIN_FORM;
}
public void setShowError() {
state = STATE_SHOW_ERROR;
}
public void setShowLoading() {
state = STATE_SHOW_LOADING;
}
/**
* Is called from Mosby to apply the view state to the view.
* We do that by calling the methods from the View interface (like the presenter does)
*/
@Override public void apply(LoginView view, boolean retained) {
switch (state) {
case STATE_SHOW_LOADING:
view.showLoading();
break;
case STATE_SHOW_ERROR:
view.showError();
break;
case STATE_SHOW_LOGIN_FORM:
view.showLoginForm();
break;
}
}
}
public class LoginFragment extends MvpViewStateFragment<LoginView, LoginPresenter>
implements LoginView {
@Override public void showLoginForm() {
// Set View state
LoginViewState vs = (LoginViewState) viewState;
vs.setShowLoginForm();
errorView.setVisibility(View.GONE);
...
}
@Override public void showError() {
// Set the view state
LoginViewState vs = (LoginViewState) viewState;
vs.setShowError();
if (!isRestoringViewState()) {
// Enable animations only if not restoring view state
loginForm.clearAnimation();
Animation shake = AnimationUtils.loadAnimation(getActivity(), R.anim.shake);
loginForm.startAnimation(shake);
}
errorView.setVisibility(View.VISIBLE);
...
}
@Override public void showLoading() {
// Set the view state
LoginViewState vs = (LoginViewState) viewState;
vs.setShowLoading();
errorView.setVisibility(View.GONE);
...
}
}
Tip16. 测试自定义ViewState
@Test
public void testShowLoginForm(){
final AtomicBoolean loginCalled = new AtomicBoolean(false);
LoginView view = new LoginView() {
@Override public void showLoginForm() {
loginCalled.set(true);
}
@Override public void showError() {
Assert.fail("showError() instead of showLoginForm()");
}
@Override public void showLoading() {
Assert.fail("showLoading() instead of showLoginForm()");
}
@Override public void loginSuccessful() {
Assert.fail("loginSuccessful() instead of showLoginForm()");
}
};
LoginViewState viewState = new LoginViewState();
viewState.setShowLoginForm();
viewState.apply(view, false);
Assert.assertTrue(loginCalled.get());
}
Tip 17. ViewState变种
Tip18. 并非每一种UI表示都是一个ViewState
比如列表空时候的默认页面,其实也是showing content的状态
Tip19. 并不是所有View需要ViewState
Tip20. 避免backstack中的fragment,以及子fragment
fragment相关的争论很多,而其中大部分都是对的,fragment的生命周期在你将其丢到backstack和使用子fragment(fragment里面包含fragment)的时候则会更加诡异
Tip21. Mosby支持ViewGroup的MVP
如果不想使用fragment可以用ViewGroup代替。
Tip22. Mosby支持delegate(委托)
好处1. 让你可以加一些功能包括roboguice
好处2. 改变mosby默认行为(比如默认横竖屏切换时候重新创建presenter,可以改成从hashmap里面存取)
public class StatisticsDialog extends DialogFragment
implements StatisticsView, MvpViewStateDelegateCallback<StatisticsView, StatisticsPresenter> {
@InjectView(R.id.contentView) RecyclerView contentView;
@InjectView(R.id.loadingView) View loadingView;
@InjectView(R.id.errorView) TextView errorView;
@InjectView(R.id.authView) View authView;
StatisticsPresenter presenter;
ViewState<StatisticsView> viewState;
MailStatistics data;
StatisticsAdapter adapter;
// Delegate
private FragmentMvpDelegate<StatisticsView, StatisticsPresenter> delegate =
new FragmentMvpViewStateDelegateImpl<>(this);
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
delegate.onCreate(savedInstanceState);
}
@Override public void onDestroy() {
super.onDestroy();
delegate.onDestroy();
}
@Override public void onPause() {
super.onPause();
delegate.onPause();
}
@Override public void onResume() {
super.onResume();
delegate.onResume();
}
@Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_statistics, container, false);
}
@Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
delegate.onViewCreated(view, savedInstanceState);
ButterKnife.inject(this, view);
adapter = new StatisticsAdapter(getActivity());
contentView.setAdapter(adapter);
contentView.setLayoutManager(new LinearLayoutManager(getActivity()));
}
@Override public void onStart() {
super.onStart();
delegate.onStart();
}
@Override public void onStop() {
super.onStop();
delegate.onStop();
}
@Override public void onAttach(Activity activity) {
super.onAttach(activity);
delegate.onAttach(activity);
}
@Override public void onDetach() {
super.onDetach();
delegate.onDetach();
}
@Override public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
delegate.onActivityCreated(savedInstanceState);
}
@Override public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
delegate.onSaveInstanceState(outState);
}
...
}