AndroidMVP实践

相信大家在没有使用MVP模式进行开发的时候,视图层里面总是会有各种后台事务乱入,大大的加深了开发理解代码的难度。

针对简单的业务逻辑来说,MVC模式开发的确简单快捷,不过随着业务复杂度加大,我还是建议大家使用MVP模式进行开发。

因为MVP可以将后台事务从Activity/View/Fragment中分离出来,让它们独立于大部分生命周期事件这样一个应用将会变得简单, 整个应用可靠性可以提高10倍,应用的代码将会变短, 代码的可维护性提高。

本篇文章围绕MVP进行介绍,同时也会介绍一个库让你在Android平台上轻松的实现MVP,我们一同去了解下吧


什么是MVP

所谓MVP(Model-View-Presenter)模式。即是将APP的结构分为三层:

  • View层主要是用于展示数据并对用户行为做出反馈。在Android平台上,它可以对应为Activity, Fragment,View或者对话框。
  • Model是数据访问层,往往是数据库接口或者服务器的API。
  • Presenter层可以向View层提供来自数据访问层的数据,除此以外,他也会处理一些后台事务。

MVP有何优势缺点

  • 优势:解耦,提高维护性

    • 降低耦合度、模块职责划分明显、利于测试驱动开发、代码复用、隐藏数据、代码灵活性
    • MVP能使复杂的任务被分成细小的任务,并且很容易解决。
    • 越小的东西,bug越少,越容易debug,更好测试。
    • 在MVP模式下的View层将会变得简单,所以即便是他请求数据的时候也不需要回调函数。View逻辑变成十分直接。
  • 缺点:接口和类的数量会暴涨,代码量多

    • 由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。
    • 如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更

非MVP VC MVP

以下是非MVP 和 MVP 的代码对比

  • 非MVP

    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 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;
    }
    }
    }

    问题:
    当用户翻转屏幕时候会开始请求,应用发起了过多的请求,将会是屏幕在切换的时候呈现空白的界面。
    当用户频繁的切换屏幕,这将会造成内存泄露,请求运行时,每一个回调将会持有MainActivity的引用,让其保存在内存中。因此引起的OOM和应用反应迟缓,会引发应用的Crash。

  • MVP

    • MainPresenter
      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 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);
      }
      }
      }
    • MainActivity
      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
      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();
      }
      }
      MVP模式上:
      MainActivty构建了MainPresenter,仅将其维持在onCreate/onDestroy周期内,MainActivity持有MainPresenter的静态引用,所以每一个进程由于OOM重启时,MainActivity可以确认Presenter是否仍然存在,必要时创建。

两者对比下MVP的表现

  • 示例程序不会在每次切换屏幕的时候都开始一个新的请求(因为MainPresenter是静态的)
  • 当进程重启时,示例程序将会重新加载数据。
  • 当MainActivity销毁时,MainPresenter不会持有MainActivity的引用,因此不会在切换屏幕的时候发生内存泄漏,而且没必要去unsubscribe请求。

当然,对于上述的MVP模式写法,尚有不妥,下面给大家介绍一款更加方便简洁的框架,来实现项目的MVP构建

Nucleus

Nucleus是一个简单的Android库,它利用Model-View-Presenter模式将后台任务与应用程序的可视部分正确连接起来。

Nucleus的特点

  • 它支持在View/Fragment/Activity的Bundle中保存/恢复Presenter的状态,一个Presenter可以保存它的请求参数到bundles中,以便之后重启它们
  • 只需要一行代码,它就可以直接将请求结果和错误反馈给View,所以你不需要写!= null之类的非空判断语句。
  • 它允许一个view实例可以持有多个Presenter。不过你不能在用Dagger实例化的presenter中这样使用(传统方法).
  • 它可以用一行代码快速的将View和Presenter绑定。
  • 它提供一些现成的基类,例如: NucleusView, NucleusFragment, NucleusSupportFragment, NucleusActivity. 你可以将他们的代码拷贝出来改造出一个自己的类以利用Nucleus的presenter。
  • 支持在进程重启后,自动重新发起请求,在onDestroy方法中,自动的退订RxJava的订阅。
  • 整个图书馆都建立在KISS 的基础上。任何人都可以轻松阅读和理解。
  • 它简洁明了,每一个开发者都会理解,以上这些只用了180行代码来驱动Presenter这个类,加上230行RxJava的依赖。
  • 在进程重启的情况下Nucleus自动重启后台任务。即使在低内存设备上运行或等待长时间运行的后台任务完成时,应用程序仍是可靠的。
  • 图书馆不依赖Dagger。通过注释你就可以绑定一个Presenter。
  • Nucleus中的Presenter是一个不依赖于View的外部类,它可以自动防止与activity上下文泄漏有关的任何问题。

Nucleus的使用

Nucleus导入依赖

RxJava 1.x
  • 基础
    1
    2
    3
    4
    5
    6
    7
    8
    9
    dependencies {
    compile 'info.android15.nucleus:nucleus:6.0.0'
    }
    ~~~
    * 如果需要使用NucleusSupportFragment和NucleusFragmentActivity
    ~~~gradle
    dependencies {
    compile 'info.android15.nucleus:nucleus-support-v4:6.0.0'
    }
  • 如果需要使用NucleusAppCompatActivity
    1
    2
    3
    dependencies {
    compile 'info.android15.nucleus:nucleus-support-v7:6.0.0'
    }
  • 混淆ProGuard相关
    1
    2
    3
    -keepclassmembers class * extends nucleus.presenter.Presenter {
    <init>();
    }
RxJava 2.x
  • 基础

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    dependencies {
    compile 'info.android15.nucleus5:nucleus:7.0.0'
    }
    ~~~

    * 如果需要使用NucleusSupportFragment和NucleusFragmentActivity
    ~~~gradle
    dependencies {
    compile 'info.android15.nucleus5:nucleus-support-v4:7.0.0'
    }

    * 如果需要使用NucleusAppCompatActivity
    ~~~gradle
    dependencies {
    compile 'info.android15.nucleus5:nucleus-support-v7:7.0.0'
    }
  • 混淆ProGuard相关

    1
    2
    3
    -keepclassmembers class * extends nucleus5.presenter.Presenter {
    <init>();
    }

Nucleus简单示例

针对以上两者对比的需求,使用Nucleus实现如下:

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
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 ,并且自动向已经绑定的view发送请求的结果。
怎么样?简单吧

  • MainPresenter的代码简洁,因为它使用deliverLatestCache()的操作,延迟了由数据源发出的所有数据和错误,直到View可用。它还把数据缓存在内存中,以便它可以在Configuration change(横竖屏下)时可以被重用。
  • MainActivity的代码比较短,因为主Presenter的创作由NucleusActivity管理。当你需要绑定一个Presenter的时候,只需要添加注解@RequiresPresenter(MainPresenter.class)
  • 目前该注解耗时不超过0.3毫秒,只在实例化view的时候才会发生,因此注解在这里对性能的影响可以忽略。

Nucleus 相关类介绍

  • RxPresenter

    • deliver()只是推迟onNext、onError、onComplete的调用,直到视图有效。使用它,你只需要一次请求,就像发起登陆web服务一样
    • deliverLatest()当有新的的onNext值,将会舍弃原有的值,如果你有可更新的数据源,这将让你去除那些不需要的数据。
    • deliverLatestCache(),和deliverLatest()一样,但除了它会在内存中保存最新的结果外,当View的另一个实例可用(例如:在配置更改的时候)时,还是会触发一次。如果你不想组织请求在你的View中的保存/恢复事务(比方说,结果太大或者不能很容易地保存在Bundle中),这个方法可以让用户体验更好。
  • Presenter的生命周期

    • void onCreate(Bundle savedState) - 每一个Presenter构造时
    • void onDestroy() - 用户离开View时调用
    • void onSave(Bundle state) - 在View的onSaveInstanceState方法中调用,用于持有Presenter的状态
    • void onTakeView(ViewType view) - 在Activity或者Fragment的onResume()方法中或者android.view.View#onAttachedToWindow()调用
    • void onDropView() - 在Activity或者Fragment的onPause()方法中或者android.view.View#onDetachedFromWindow()调用
  • 关于View的回收

    • 默认只在Activity处于finish时,才在调用View的onDetachedFromWindow()/onDestroy() 销毁Presenter。
    • 对于fragment或者自定义的view,attached and detached调用比较频繁,你需要给给View一个销毁Presenter的信号。在这里,公有方法NucleusLayout.destroyPresenter() and NucleusFragment.destroyPresenter()就派上用场了。
      1
      2
      3
      4
         fragment = fragmentManager.findFragmentById(R.id.fragmentStackContainer);
      fragmentManager.popBackStackImmediate();
      if (fragment instanceof NucleusFragment)
      ((NucleusFragment)fragment).destroyPresenter();
      注:在进行replaceFragment栈和对处于底部的Fragment进行push操作时,你可能需要进行相同的操作。不过,你要记住,销毁Presenter后,你的后台任务也会停止运行,需要自己权衡是否销毁。fragment其实生命周期真够乱的,有空可以瞧瞧flow,听说可以替换它呢~

更多例子

其他推荐

  • 在数据复杂的项目中使用固定的数据结构 Android Parcelable模型变得简单auto-parcel

相关参考

其他相关推荐