Android 上 MVP 的介绍

发布于 2024-10-25 06:03:55 字数 20080 浏览 3 评论 0

这篇文章向你介绍 Android 平台上的 MVP 模式,从一个简浅的例子开始实践之路。文章也会介绍一个一个库让你在 Android 平台上轻松的实现 MVP

简单吗?我怎么才能从中受益?

什么是 MVP?

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

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

Android 为什么需要 MVP

理由 1:尽量简单

如果你还有读过这篇文章,请阅读它: Kiss 原则 (Keep It Stupid Simple)

  • 大部分的安卓应用只使用 View-Model 结构
  • 程序员现在更多的是和复杂的 View 打交道而不是解决业务逻辑。

当你在应用中只使用 Model-View 时,到最后,你会发现“所有的事物都被连接到一起”。

如果这张图看上去还不是很复杂,那么请你想象一下以下情况:每一个 View 在任意一个时刻都有可能出现或者消失。不要忘记 View 的保存和恢复,在临时的 view 上挂载一个后台任务。

所有的事物都被连接到一起 的替代品是一个万能对象(god object)。

god object 是十分复杂的,他的每一个部分都不能重复利用,无法轻易的测试、或者调试和重构。

With MVP

使用 MVP

复杂的任务被分成细小的任务,并且很容易解决。越小的东西,bug 越少,越容易 debug,更好测试。在 MVP 模式下的 View 层将会变得简单,所以即便是他请求数据的时候也不需要回调函数。View 逻辑变成十分直接。

理由 2:后台任务

当你编写一个 Actviity、Fragment、自定义 View 的时候,你会把所有的和后台任务相关的方法写在一个静态类或者外部类中。这样,你的 Task 不再和 Activity 联系在一起,这既不会导致内存泄露,也不依赖于 Activity 的重建。

这里有若干种方法处理后台任务,但是它们的可靠性都不及 MVP。

为什么它是可行的?

这里有一张表格,用于展示在 configuration 改变、Activity 重启、Out-Of-Memory 时,不同的应用部分会发生什么?

 情景 1情景 2情景 3
 配置改变Activity 重启进程重启
对话框重置重置重置
Activity, View, Fragment保存/恢复保存/恢复保存/恢复
Fragment with setRetainInstance(true)无变化保存/恢复保存/恢复
Static variables and threads无变化无变化重置

情景 1: 当用户切换屏幕、更改语言设置或者链接外部的模拟器时,往往意味着设置改变。 相关更多请阅读 这里

情景 2:Activity 的重启发生在当用户在开发者选项中选中了“Don't keep activities”(“中文下为 不保留活动”)的复选框,然后另一个 Activity 在最顶上的时候。

情景 3: 进程的重启发生在应用运行在后台,但是这个时候内存不够的情况下。

总结

现在你可以发现,一个调用了 setRetainInstance(true) 的 Fragment 也不奏效,我们还是需要保存/恢复 fragment 的状态,所以为简化问题,我们暂不考虑上述情况的 Fragment。 Occam's razor

 配置改变, Activity 重启进程重启
Activity, View, Fragment, DialogFragment保存/恢复保存/恢复
Static variables and threads无变化重置

现在,看上去更舒服了,我们只需要写两段代码为了恢复应用:

  • 保存/恢复 for Activity, View, Fragment, DialogFragment;
  • 重启后台请求由于进程重启

第一个部分,用 Android 的 API 可以实现。第二个部分,就是 Presenter 的作用了。Presenter 将会记住有哪些请求需要执行,当进程在执行过程中重启时,Presenter 将会出现执行它们。

一个简单的例子(no MVP)

这个例子用于从远程服务器加载数据并呈现,当发生异常时,会通过 Toast 提示。

我推荐使用 RxJava 构建 Presenter,因为这个库更容易控制数据流。

我想对创造如此简单的 API 的伙计说声谢谢,我把它用于 The Internet Chuck Norris Database

无 MVP 的 例子 00


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 模式下的 例子 01

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。他们调用了 publish() 方法,onNextonError的值将会在 MainActivity 中发布,而不是由 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();
  }
}

MainActivty 构建了 MainPresenter,将其维持在 onCreate/onDestroy 周期外,MainActivity 持有 MainPresenter 的静态引用,所以每一个进程由于 OOM 重启时,MainActivity 可以确认 Presenter 是否仍然存在,必要时创建。

当然,确认和使用静态变量可能是代码变得臃肿,稍后我们会告诉你如何好看些:。:)

重要思路:

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

Nucleus

Nucleus 是我从 Mortar 和 Keep It Stupid Simple 这篇文章得到的灵感而建立的库。

它有以下特征:

  • 它支持在 View/Fragment/Activity 的 Bundle 中保存/恢复 Presenter 的状态,一个 Presenter 可以保存请求参数,以便之后重启它们
  • 只需要一行代码,它就可以直接将请求结果或者错误反馈给 View,所以你不需要写 != null 之类的核对代码。
  • 它允许你可以有多个持有 Presenter 的实例。 不过你不能在用 Dagger 实例化的 presenter 中这样使用(传统方法).
  • 它可以用一行代码快速的将 View 和 Presenter 绑定。
  • 它提供一些现成的基类,例如: NucleusView , NucleusFragment , NucleusSupportFragment , NucleusActivity . 你可以将他们的代码拷贝出来改造出一个自己的类以利用 Nucleus 的 presenter。
  • 支持在进程重启后,自动重新发起请求,在 onDestroy 方法中,自动的退订 RxJava 的订阅。
  • 最后,它简洁明了,每一个开发者都会理解,Presenter 的驱动只用了 180 行代码,RxJava 用了 230 行代码。

使用了 Nucleus例 02

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, 向 Presenter 中添加/分离 View ,并且自动向附加的 view 发送请求。。

MainPresenter 的代码比较短,因为它使用 deliverLatestCache() 的操作,延迟了由一个数据源发出所有的数据和错误,直到 View 可用。它还把数据缓存在内存中,以便它可以在 Configuration change 时可以被重用。

MainActivity 的代码比较短,因为主 Presenter 的创作由 NucleusActivity 管理。当你需要绑定一个 Presenter 的时候,只需要添加注解 @RequiresPresenter(MainPresenter.class)

警告!注释!在 Android 中,如果你使用注解,这是最好检查以下这么做会不会降低性能。以我使用的'Galaxy S`(2010 年设备)为例,处理此批注耗时不超过 0.3 毫秒。只在实例化 view 的时候才会发生,因此注解在这里对性能的影响可以忽略。

更多例子

一个扩展的例子,带有请求参数的 Presenter: Nucleus Example

带有单元测试的例子: Nucleus Example With Tests

deliverLatestCache() 方法

这个 RxPresenter 的工具方法有三个变种:

  • deliver() will just delay all onNext, onError and onComplete emissions until a View becomes available. Use it for cases when you're doing a one-time request, like logging in to a web service. Javadoc )
  • deliver() 只是推迟 onNext、onError、onComplete 的调用,直到视图有效。使用它,你只需要一次请求,就像发起登陆 web 服务一样。 Javadoc )
  • deliverLatest() 当有新的的 onNext 值,将会舍弃原有的值,如果你有可更新的数据源,这将让你去除那些不需要的数据。 Javadoc
  • deliverLatestCache() ,和 deliverLatest() 一样,但除了它会在内存中保存最新的结果外,当 View 的另一个实例可用(例如:在配置更改的时候)时,还是会触发一次。如果你不想组织请求在你的 View 中的保存/恢复事务(比方说,结果太大或者不能很容易地保存在 Bundle 中),这个方法可以让用户体验更好。 Javadoc

Presenter 的生命周期

相比 Android 组件,Presenter 的生命周期更加简短。

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

View 的回收与 View 栈

通常来说,你的 view(比如 fragment 或者自定义的 view)在用户的交互过程中挂载与解挂(attached and detached)都是随机发生的。 这倒是不让 presenter 在 view 每次解挂(detached)的时候都销毁的一个启发。你可以在任何时候挂载与解挂 view,但是 presenter 可以在这些行为中幸存下来,继续后台的工作。

这里还存在着一个关于 View 回收的问题:一个 Fragment 在 Configuration change 或者从 stack 中弹出的情况下,不知道自身有没有解挂(detached)。

默认只在 Activity 处于 finish 时,才在调用 View 的 onDetachedFromWindow() / onDestroy() 销毁 Presenter。

所以,当你在常规的 Activity 生命周期内,销毁 View,你需要给给 View 一个销毁 Presenter 的信号。在这里,公有方法 NucleusLayout.destroyPresenter() and NucleusFragment.destroyPresenter() 就派上用场了。

例如,在我的项目中,下面的是我如何进行 FragmentManager 的 pop() 操作:

  fragment = fragmentManager.findFragmentById(R.id.fragmentStackContainer);
  fragmentManager.popBackStackImmediate();
  if (fragment instanceof NucleusFragment)
    ((NucleusFragment)fragment).destroyPresenter();

在进行replaceFragment 栈和对处于底部的 Fragment 进行push操作时,你可能需要进行相同的操作。

在 View 从 Activity 解挂(detached)时,您可能会选择摧毁 Presenter 来避免问题的发生,但是,这将意味着当 View 解挂(detached)时,后台任务无法继续进行。

所以这一节的 "view recycling"完全留你你自己考虑,也许有一天我会找到更好的解决办法,如果你有一个办法,请告诉我。

最佳实践

在 Presenter 中保存你的请求参数。

规则很简单:Presenter 的主要作用是管理请求。所以,View 不应该自己处理或者重启请求。从 View 中,我们可以看见,后台事务不会消失,总是会返回结果或者错误,而不是通过回调的方式

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

如果你有不止一对请求参数,这个库在不使用运行时注解的前提下。您可以创建 BasePresenter 并把Icepick到该类中,所有的子类将会自动保存标有 @Icicle 这一注解的变量,而你将不再需要去实现 OnSave 。这也适用于保存 Activity,Fragment,View 的状态。

在主线程中调用 onTakeView 进行即时查询 Javadoc )

有时候,你要进行少量的数据查询,如从数据库中读取少量的数据。虽然你可以很容易地用 Nucleus 创建一个可重启的请求,你不必到处使用这个强大的工具。如果你在 fragment 创建的时候初始化一个后台请求,即使只有几毫秒,用户也会看到一会儿的空白屏。因此,为了使代码更短,用户体验更好,可以使用主线程。

不要让 Presenter 控制你的 View

这不是很好的工作方式 - 由于这种不自然的方式,应用程序逻辑变得太复杂。

自然的方式是操作流由用户发起,通过 View,Presenter 和 Model,最后流向数据。毕竟,用户将使用应用,用户是控制应用程序的源头。因此,控制应该从用户开始而不是一些应用的内部结构。

从 view,到 presenter 到 model 是很直接的形式,很容易书写这样的代码。你将得到以下序列: user -> view -> presenter -> model -> data 。但是,当控制流变成这样时: user -> view -> presenter -> view -> presenter -> model -> data ,它只是违反 KISS 原则.

Fragments?不好意思它是违背了这种自然操作流程的。它们太复杂。这里是一个非常好讲诉 Fragment 的文章: 抨击 Android 的 Fragment 。fragment 的替代者 Flow 并没有简化多少东西。

MVC

如果你对 MVC(模型-View-控制器)-不要去使用。模型-View-控制器和 MVP 完全不同,不能解决接口开发者面对的问题。

What is MVC?

什么是 MVC?

  • Model 代表着应用程序的内部状态。它可以负责存储,当然也可以不考虑。
  • View 是唯一的与 MVP 相同的部分 - 它用于将模型呈现在屏幕上,应用程序的一部分。
  • Controller 表示输入装置,如键盘,鼠标或操纵杆。

MVC 在过去以键盘为驱动的应用中(比如游戏),是比较好的模式。没有窗口和图形用户界面的交互——应用接受输入(Controller),维持状态(Model),产生输出(View)。同样,数据和控制的关系是这样的。 controller -> model -> view 。这种模式是在 Android 绝对无用。

这里有一些关于 MVC 的困惑。人们(Web 开发人员)觉得他们使用 MVC,而实际上,他们使用的 MVP。许多 Android 开发者认为 Controller 是用于控制 View 的,所以他们试图在创建 View 时,从视图(View)中提取视图逻辑,交由专门的控制器控制。我个人是没有看出这种架构的好处。

在数据复杂的项目中使用固定的数据结构

在这方面, AutoValue 是十分好的库,在它的描述中,你会发现一大堆好处,我建议你阅读它。Android 平台上还有一个接口: AutoParcel 。其主要原因是,你可以四处传递,而不用关心是否在程序的某个地方被修改了。而且他们也是线程安全的。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

冷弦

暂无简介

0 文章
0 评论
785 人气
更多

推荐作者

玍銹的英雄夢

文章 0 评论 0

我不会写诗

文章 0 评论 0

十六岁半

文章 0 评论 0

浸婚纱

文章 0 评论 0

qq_kJ6XkX

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文