使用mosby框架 应用MVP模式实现邮件客户端

本文介绍了如何使用mosby框架在Android应用中实施MVP模式,分享了避免过度架构、机智使用继承、区分MVP与MVC、结合EventBus优化通信等最佳实践。此外,还探讨了MVP的可扩展性、适用于不同类型的视图以及与服务、ViewState的交互等关键点。
摘要由CSDN通过智能技术生成

本文内容结合: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,可以看看下图


presenter并没有替代controller,而是和view内部的controller(点击事件处理并call对应的presenter方法)协作。那些UI相关的,比如做动画,隐藏进度条显示listview仍然是controller的职责,而presenter则负责告诉说数据加载完了,可以隐藏进度条显示数据了,或者需要先要loading动画了。

Tip4. 严格分离Model, View, Presenter
和business logic相关的,和view的状态相关的(showLoading, showError)都属于presenter

Tip5. Presentation Model
(好神奇,笔者我也只听过View Model)
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,甚至可能引发空指针。
解决方案就是引入Presentation Model,它是一个专门为了GUI优化的model class:
public class UserPresentationModel {
  String fullname;
  int ranking;

  public UserPresentationModel(String fullname, int ranking) { ... }
}
这样就保证了ranking总会被设置为一个具体的值,当滑动列表的时候也不会去做CPU相关的fullname计算,因为Presentation Model是在其他线程做实例化的)。UserView显示的不是List(User),而是List<UserPresentationModel>。

这个transformer的工作在哪里做呢?由于是在异步线程的,所以放在presenter(如果用了RXJava,则可以直接用map()或者flatMap())。如果需要同时支持手机和平板的显示,则可以创建PhoneUserPresenter 和TabletUserPresenter,并使用不同的PresentationModelTransformer。

Tip6. 导航是UI相关的事
不要让Presenter去做start intent(Android最先实例化的是activity和fragment,而不是presenter)

Tip7. onActivityResult()和MVP
如果启动了一个相机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);
  }

  ...

}


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值