MVP模式在Android开发中的最佳实践

回想一下,你刚刚学习Android的时候,总会看到一些书上写着,Android使用的是MVC模式,Activity就是一个Controller,或许那个时候,你没有什么深刻的体会。随着经验的积累。你发现,Activity既是Controller,掌管着许许多多的业务逻辑,同时它也作为View的一部分,控制着视图层的显示。久而久之,这个Controller便显得过于重,职责不再那么单一。

于是,再后来,为了使Activity的职责更加单一,便出现了MVP,MVVM等模式,只能说各有各的优点,没有谁对谁错,一个模式有另一个模式不具有的特点,同时也不具备另一个模式具有的特点,架构的选择永远是根据业务的复杂程度来进行的。MVC有其特点,就是写代码简单啊,但是其缺点也很明显,业务复杂起来后,Activity显得过于庞大不是特别好维护。至于MVVM,个人是十分排斥这种模式的,为什么呢,在XML中写数据绑定的代码显得有点蛋疼,从而使得xml的职责不是那么单一,在我看来,xml用来作为View再好不过了,不必掺和其他任何元素进来,这样显得“不干净”。而MVP呢,我觉得在Android开发中,MVP是一个值得考虑的模式,它既没有MVVM那样,在xml中写数据绑定的代码,xml依然还是原来的配方,也没有MVC那样,拥有一个臃肿的Controller,取而代之的是更加清晰的分层,职责更加单一,当然,优点背后必然有缺点,相信用过MVP的都知道有什么缺点,那就是接口的定义会暴增。

那么什么是MVP模式呢?

  • M即Model,what to show? 也就是显示在UI上的数据,至于数据怎么来,数据库,网络等等渠道,都是属于这一层
  • V即View,how to show?也就是怎么显示数据,在Android中,通常是使用xml定义这个view,一般View中会持有Presenter的引用。
  • P即Presenter,Presenter扮演着中间联系人的作用,就好比MVC中的Controller,通常来说,Presenetr中一般会持有View和Model的引用。

这三者的联系如下图所示:

这里写图片描述

那么问题来了,该如何实现MVP模式呢?这里介绍一个开源库Mosby,github地址https://github.com/sockeqwe/mosby

本篇文章不对该库的具体实现作分析,如果对实现感兴趣的可以阅读源码,毕竟源码之前,了无秘密。在使用前,先加入对该库的依赖

dependencies {
    compile 'com.hannesdorfmann.mosby:mvp:2.0.1'
    compile 'com.hannesdorfmann.mosby:viewstate:2.0.1'
}

现在假设我们实现一个登陆功能,原来的MVC方式就是先定义好xml,然后直接在Activity中书写各种业务逻辑,导致Activity越来越庞大,而使用了MVP之后,Activity会显得十分干净。

XML的定义这里就不再贴了,两个输入框(账号和密码),一个登陆按钮。

首先,我们需要一个与服务器交互的接口,为了简单起见,我们在本地进行模拟,如果账号密码都是admin,则登陆成功,如果账号密码都是server,其他情况都返回账号或密码错误。理论上,这个需要在子线程中发起请求,再通过UI线程回调,这一步也省略,直接在主线程中判断并回调,由于是本地模拟,不会产生任何卡顿,实际使用时需严格按照子线程请求主线程回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Listener<T> {
     void onSuccess(T t);
 
     void onFailure( int code);
}
 
public class LoginApi {
     public static void login(String username, String password, Listener<String> listener) {
         if (username.equals( "admin" ) && password.equals( "admin" )) {
             listener.onSuccess( null );
         else if (username.equals( "server" ) && password.equals( "server" )) {
             listener.onFailure(LoginView.SERVER_ERROR);
         else {
             listener.onFailure(LoginView.USERNAME_OR_PASSWORD_ERROR);
         }
     }
}

业务逻辑的接口定义好了,这个LoginApi可以认为是Model层,接下来我们需要定义和Login相关的View,Presenter。

首先定义一个LoginView接口继承MvpView接口,由于登录的接口有两种情况,一种是登录成功,一种是登录失败,而登录失败的情况又有多种,于是需要通过一个状态码进行区分,于是LoginView中的接口就产生了。这里我们直接将各种错误状态定义在了LoginView中,实际使用时建议定义在一个常量类中进行统一管理。

1
2
3
4
5
6
7
8
9
10
public interface LoginView  extends MvpView {
 
     public static final int USERNAME_OR_PASSWORD_EMPTY =  0x01 ;
     public static final int USERNAME_OR_PASSWORD_ERROR =  0x02 ;
     public static final int SERVER_ERROR =  0x03 ;
 
     void onLoginSuccess();
 
     void onLoginFailure( int code);
}

然后定义一个LoginPresenter类继承MvpBasePresenter,泛型参数是LoginView,在里面调用LoginApi的接口并将接口返回。

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
public class LoginPresenter  extends MvpBasePresenter<LoginView> {
 
     public void login( final String username,  final String password) {
         if (username ==  null || username.equals( "" )) {
             LoginView view = getView();
             if (view !=  null ) {
                 view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_EMPTY);
                 return ;
             }
         else if (password ==  null || password.equals( "" )) {
             LoginView view = getView();
             if (view !=  null ) {
                 view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_EMPTY);
                 return ;
             }
         }
         Listener<String> listener =  new Listener<String>() {
             @Override
             public void onSuccess(String str) {
                 LoginView view = getView();
                 if (view !=  null ) {
                     view.onLoginSuccess();
                 }
             }
 
             @Override
             public void onFailure( int code) {
                 if (code == LoginView.USERNAME_OR_PASSWORD_ERROR) {
                     LoginView view = getView();
                     if (view !=  null ) {
                         view.onLoginFailure(LoginView.USERNAME_OR_PASSWORD_ERROR);
                     }
                 else {
                     LoginView view = getView();
                     if (view !=  null ) {
                         view.onLoginFailure(LoginView.SERVER_ERROR);
                     }
                 }
             }
         };
         LoginApi.login(username, password, listener);
     }
}

最后便是让Activity实现LoginView接口,实现LoginView中定义的接口,此外,还需要继承MvpActivity,泛型参数是LoginView和LoginPresenter,并实现抽象方法createPresenter()返回LoginPresenter,而在LoginView中定义的两个接口onLoginSuccess和onLoginFailure中,全都是UI相关的代码,整个Activity中不再有业务逻辑的代码,职责也就单一了。

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
public class LoginActivity  extends MvpActivity<LoginView, LoginPresenter>  implements View.OnClickListener, LoginView {
     private EditText etAccount;
     private EditText etPassword;
     private Button btnLogin;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
 
         etAccount = (EditText) findViewById(R.id.accout);
         etPassword = (EditText) findViewById(R.id.password);
         btnLogin = (Button) findViewById(R.id.login);
         btnLogin.setOnClickListener( this );
 
     }
 
     @NonNull
     @Override
     public LoginPresenter createPresenter() {
         return new LoginPresenter();
     }
 
     @Override
     public void onClick(View v) {
         switch (v.getId()) {
             case R.id.login:
                 onLogin();
                 break ;
 
         }
     }
 
     private void onLogin() {
         String username = etAccount.getText().toString();
         String passowrd = etPassword.getText().toString();
         getPresenter().login(username, passowrd);
     }
 
     @Override
     public void onLoginSuccess() {
         Toast.makeText( this "登陆成功" , Toast.LENGTH_SHORT).show();
     }
 
     @Override
     public void onLoginFailure( int code) {
         switch (code) {
             case LoginView.USERNAME_OR_PASSWORD_EMPTY:
                 Toast.makeText( this "账号或密码不能为空" , Toast.LENGTH_SHORT).show();
                 break ;
             case LoginView.USERNAME_OR_PASSWORD_ERROR:
                 Toast.makeText( this "账号或密码错误" , Toast.LENGTH_SHORT).show();
                 break ;
             case LoginView.SERVER_ERROR:
                 Toast.makeText( this "服务器错误" , Toast.LENGTH_SHORT).show();
                 break ;
         }
 
     }
}

特别需要注意的是,在Presenter中引用View时,一定要判断是否非空,因为这个View是WeakReference弱引用,不进行判断的话会产生空指针异常。这是这个框架不好的地方,需要多次重复判空。

以上是这个框架最基础的用法,实际使用时我们一般不会这么直接使用它的类,一般来说,我们会定义各种Base类,比如BaseView,BasePresenter,BaseActivity,BaseFragment;从而将各种公共的方法都放着里面,减少冗余。如果你要引用这个框架,实际使用时稍微注意一下这个问题就可以了。

此外,Mosby还有一个LCE模块,什么是LCE模块呢,其实就是Loading-Content-Error的全称,主要用于数据的加载,显示灯作用,它体现在一个MvpLceView这个接口上以及具体的实现MvpLceActivity和MvpLceFragment上,该接口的定义如下。

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
public interface MvpLceView<M>  extends MvpView {
 
   /**
    * Display a loading view while loading data in background.
    * <b>The loading view must have the id = R.id.loadingView</b>
    *
    * @param pullToRefresh true, if pull-to-refresh has been invoked loading.
    */
   public void showLoading( boolean pullToRefresh);
 
   /**
    * Show the content view.
    *
    * <b>The content view must have the id = R.id.contentView</b>
    */
   public void showContent();
 
   /**
    * Show the error view.
    * <b>The error view must be a TextView with the id = R.id.errorView</b>
    *
    * @param e The Throwable that has caused this error
    * @param pullToRefresh true, if the exception was thrown during pull-to-refresh, otherwise
    * false.
    */
   public void showError(Throwable e,  boolean pullToRefresh);
 
   /**
    * The data that should be displayed with {@link #showContent()}
    */
   public void setData(M data);
 
   /**
    * Load the data. Typically invokes the presenter method to load the desired data.
    * <p>
    * <b>Should not be called from presenter</b> to prevent infinity loops. The method is declared
    * in
    * the views interface to add support for view state easily.
    * </p>
    *
    * @param pullToRefresh true, if triggered by a pull to refresh. Otherwise false.
    */
   public void loadData( boolean pullToRefresh);
}

该接口中定义了5个方法,

  • showLoading 用于显示加载数据时的动画,比如进度条
  • showError 用于显示加载数据失败的内容
  • setData 当数据加载成功时,将数据进行赋值,在调用showContent之前进行调用
  • loadData 加载数据,这个方法一般是放着Activity或者Fragment中进行调用的
  • showContent 数据加载成功时显示

除此之外,我们还要使用MvpLceActivity或者MvpLceFragment,还要在xml中定义相关的View,比如errorView,contenView等等。

现在我们来实践一下,以显示一个新闻列表为例。

首先定义布局,在布局中需要声明errorView,loadingView,contentView这几个id

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
< FrameLayout xmlns:android = "http://schemas.android.com/apk/res/android"
              android:layout_width = "match_parent"
              android:layout_height = "match_parent"
     >
 
     <!-- Loading View -->
     < ProgressBar
         android:id = "@+id/loadingView"
         android:layout_width = "wrap_content"
         android:layout_height = "wrap_content"
         android:layout_gravity = "center"
         android:indeterminate = "true"
         >
 
     <!-- Content View -->
     < android.support.v4.widget.SwipeRefreshLayout
         android:id = "@+id/contentView"
         android:layout_width = "match_parent"
         android:layout_height = "match_parent"
         >
 
         < android.support.v7.widget.RecyclerView
             android:id = "@+id/recyclerView"
             android:layout_width = "match_parent"
             android:layout_height = "match_parent"
             />
 
     </ android.support.v4.widget.SwipeRefreshLayout >
 
     <!-- Error view -->
     < TextView
         android:id = "@+id/errorView"
         android:layout_width = "wrap_content"
         android:layout_height = "wrap_content"
         android:layout_gravity = "center"
         android:text = "error"
         />
 
</ FrameLayout >

定义实体类,并添加构造函数和getter,setter方法

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 class News {
     private String title;
     private String desprition;
 
     public News(String title, String desprition) {
         this .title = title;
         this .desprition = desprition;
     }
 
     public String getTitle() {
         return title;
     }
 
     public void setTitle(String title) {
         this .title = title;
     }
 
     public String getDesprition() {
         return desprition;
     }
 
     public void setDesprition(String desprition) {
         this .desprition = desprition;
     }
 
     @Override
     public String toString() {
         return "News{" +
                 "title='" + title + '\ '' +
                 ", desprition='" + desprition + '\ '' +
                 '}' ;
     }
}

定义View层接口,空接口,继承MvpLceView即可

1
2
public interface NewsView  extends MvpLceView<List<News>>{
}

定义Presenter层,调用Model层方法获取数据源,在使用getView之前,一定要调用isViewAttached()方法或者使用getView!=null进行判空。不然极有可能产生空指针异常,在onSuccess中,调用view层的setData和showContent进行数据的显示,在onFaliure中则调用showError显示数据加载失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class NewsPresenter  extends MvpBasePresenter<NewsView> {
     public void loadNews( final boolean pullToRefresh) {
         if (isViewAttached()) {
             getView().showLoading(pullToRefresh);
         }
         Listener<List<News>> listener= new Listener<List<News>>() {
             @Override
             public void onSuccess(List<News> news) {
                 if (isViewAttached()) {
                     getView().setData(news);
                     getView().showContent();
                 }
             }
             @Override
             public void onFailure( int code) {
                 if (isViewAttached()) {
                     getView().showError( new Exception( "msg:" +code), pullToRefresh);
                 }
             }
         };
 
         NewsApi.loadNews(pullToRefresh,listener);
     }
}

编写接口方法,这里同样采用模拟,不过为了有加载动画等效果的显示,这里在子线程中进行模拟,之后切回主线程,并且,为了达到服务器错误的模拟效果,使用了一个随机数,当随机数为奇数时则返回获取数据失败的场景

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
public class NewsApi {
 
     private static Handler handler =  new Handler(Looper.getMainLooper());
     private static Random random =  new Random();
 
     public static void loadNews( final boolean pullToRefresh,  final Listener<List<News>> listener) {
         new Thread( new Runnable() {
             @Override
             public void run() {
                 final List<News> list =  new ArrayList<News>();
                 News news1 =  new News( "标题1" "描述描述描述描述描述描述描述描述描述描述描述描述1" );
                 News news2 =  new News( "标题2" "描述描述描述描述描述描述描述描述描述描述描述描述2" );
                 News news3 =  new News( "标题3" "描述描述描述描述描述描述描述描述描述描述描述描述3" );
                 News news4 =  new News( "标题4" "描述描述描述描述描述描述描述描述描述描述描述描述4" );
                 News news5 =  new News( "标题5" "描述描述描述描述描述描述描述描述描述描述描述描述5" );
                 News news6 =  new News( "标题6" "描述描述描述描述描述描述描述描述描述描述描述描述6" );
 
                 list.add(news1);
                 list.add(news2);
                 list.add(news3);
                 list.add(news4);
                 list.add(news5);
 
                 if (pullToRefresh) {
                     list.add(news6);
                 }
 
                 try {
                     Thread.sleep( 3000 );
                 catch (InterruptedException e) {
                     e.printStackTrace();
                 }
 
                 handler.post( new Runnable() {
                     @Override
                     public void run() {
                         if (listener !=  null ) {
                             listener.onFailure( 1 );
                             int i = random.nextInt( 100 );
                             if (i %  2 ==  0 ) {
                                 listener.onSuccess(list);
                             else {
                                 listener.onFailure( 1000 );
                             }
                         }
 
                     }
                 });
             }
         }).start();
 
     }
 
}

对应的Activity则是继承了MvpLceActivity,重写抽象方法,理论上来说showContent和showError是不需要重写的,但是这里使用了SwipeRefreshLayout,需要将加载的那个圆圈给隐藏掉,需要重写这两个方法,调用setRefreshing设为false;getErrorMessage方法返回的字符串类型便是用来显示在errorView上的,当不是下拉刷新时,则直接显示在errorView上,否则,使用Toast进行弹出。setData方法就是数据获取成功后对数据源进行使用,比如设置到adapter并通知数据源改变。loadData方法则调用presenter中的方法进行加载即可

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
public class NewsActivity  extends MvpLceActivity<SwipeRefreshLayout, List<News>, NewsView, NewsPresenter>  implements NewsView, SwipeRefreshLayout.OnRefreshListener {
     private RecyclerView recyclerView;
     private NewsAdapter adapter;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_news);
         adapter =  new NewsAdapter();
         contentView.setOnRefreshListener( this );
         recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
         recyclerView.setLayoutManager( new LinearLayoutManager( this ));
         recyclerView.setAdapter(adapter);
         loadData( false );
     }
 
     @NonNull
     @Override
     public NewsPresenter createPresenter() {
         return new NewsPresenter();
     }
 
     @Override
     public void showContent() {
         super .showContent();
         contentView.setRefreshing( false );
     }
 
     @Override
     public void showError(Throwable e,  boolean pullToRefresh) {
         super .showError(e, pullToRefresh);
         contentView.setRefreshing( false );
     }
 
     @Override
     protected String getErrorMessage(Throwable e,  boolean pullToRefresh) {
         return "发生了错误" ;
     }
 
     @Override
     public void setData(List<News> data) {
         adapter.setNews(data);
         adapter.notifyDataSetChanged();
     }
 
     @Override
     public void loadData( boolean pullToRefresh) {
         presenter.loadNews(pullToRefresh);
     }
 
     @Override
     public void onRefresh() {
         contentView.setRefreshing( true );
         loadData( true );
     }
}

adapter就不贴了,比较简单。

最终的效果如下

这里写图片描述

可以看到,最开始是加载数据失败的情况,显示了一个TextView,如果此时点击这个TextView,就会进行加载重试,然后加载出了数据,再下拉刷新,数据加载失败,使用Toast进行弹出提醒,如果成功,则会显示数据,这一切都变得简便了。

可以看到,使用了Mosby之后,实现Mvp显得非常简单,只需要继承自该库中对应的类,就可以轻轻松松的实现Mvp模式,该库中还有一些其他模块,比如ViewState,用于页面UI数据的恢复与存储,有兴趣的可以自行查看官网,并深入学习。处理Activity和Fragment可以做View层外,View以及ViewGroup的子类都可以作为View层,自行研究。该库的内部大量使用了委托,有兴趣的可以学习一下源码。

最后,贴上全部代码。

下载地址:http://download.csdn.net/detail/sbsujjbcy/9438195

转载请注明:Android开发中文站 » MVP模式在Android开发中的最佳实践

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值