介绍Model-View-Presenter在Android中的应用

这篇文章是我学习MVP模式时翻译的,原文是Konstantin Mikheev所写,传送门
因英语水平有限,翻译的很生硬,基本靠Google,请见谅。以下是译文。


这篇文章我会通过一个最简单的例子去一步步介绍MVP模式在Android中的最佳实践。同时我也会介绍一个使MVP模式在Android开发中变简单的library。

简单?怎么才能从中获益呢?

什么是MVP

View层是用来显示数据和相应数据操作的。在Android中,它可能是Activity,Fragment,View或者Dialog。

Model层是数据访问层,例如数据库API或者远程服务器访问API。

Presenter层提供View层和Model层的数据进行联系。Presenter层也可以控制后台Task。

在Android中,MVP模式可以把后台线程从Activitys/Views/Fragments中脱离出来,使它们在大部分生命周期事件中更加独立。这样的应用变得更加简单,整个程序的稳定性提升了10倍不止,应用代码变得更加少,代码可维护性变得更加友善,开发者的生命变得更加开心。

在Android中,为什么是MVP

原因1:保持简单傻瓜

如果你还没读过这篇文章,请读一遍:The Kiss Principle

·大多数Android程序仅仅使用了View-Model模式。
·程序员需要参与View的复杂性,而不是解决业务。

在应用中你仅仅使用Model-View,最后会落得“一切连接这一切”的状态。

技术分享

如果这个示例图看起来不是那么复杂,那么想想每个View可能随时消失和随时出现。别忘了保存和恢复view的状态。为临时View attache 几个后台任务,蛋糕准备好了!

另一种“一切连接着一切”就是上帝对象。

技术分享

上地对象太过于复杂;代码块不能被重复利用,测试或方便的debug和重构。

使用MVP模式

技术分享

·复杂的任务被分解成简单的任务,并且容易解决。
·更小的对象,更少的bug,更简单debug。
·可测试。

原因2:后台任务

无论何时,你写Activity,Fragment或者自定义View,你可以把所有方法与后台任务的外部或者静态类联系起来。这样,你的后台任务将不会和一个Activity联系,不会造成内存泄露和不用Activity来消费。我叫这样的对象为“Presenter”。

有那么几种处理后台线程的方法,但都是不可靠的,不过MVP是可靠的。

为什么MVP可以

通过这个视图,显示了不同的应用控件,在发生configuration发生改变或者内存溢出的时候发生了什么。每一个Android开发者都应该知道这个视图,然而这样一个视图并不是每个开发都知道。

技术分享

Case 1:当用户旋转屏幕,改变语言设置, attache 一个外部显示器,等情况,通常Configuration会发生变化。更多关于[Configuration]
(http://developer.android.com/reference/android/R.attr.html#configChanges)请阅读链接。

Case 2:当用户在开发者设置里面选择了“Don’t keep activities”或者其他Activity到最顶层,Activity会发生restart。

Case 3:没有足够的内存和应用进入后台,process会restart。

最后
现在你可以看到,Fragment当中设置setRetainInstance(true)在这里是没用帮助的,我们只需要设置save/restore就可以。因此,我们可以简单的去除Fragment的setRetainInstance方法,来限制问题的数量。

技术分享

|Activity, View, Fragment, DialogFragment Static variables and threads | save/restore no change | save/restore reset |
现在看起来爽多了。我们在应用任何情况下,只需要写两段代码就可以完成restore:

·Activity,View,Fragment,DialogFragment的save/restore。
·线程restart,restart后台请求。

第一部分,是Android API提供的方法。第二部分是Presenter层的工作。Presenter只要记住那些请求需要被执行,如果一个进程执行期间restart,Presenter将会重新执行它们。

一个例子

这个例子将加载服务器上得数据来显示一些items。如果出现错误将显示一个toast。

我推荐使用RxJava来构建Presenter,因为这个Library控制数据流很简单。

我也要感谢创建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的引用,并且request运行的时候会把MainActivity保持在内存中。这绝对有可能导致因为内存溢出而应用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();//onNext
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    error = throwable;
                    publish();//onError
                }
            });
    }

    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()方法,onNext或者onError事件会发布到MainActivity实例。

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

MainActivity 创建 MainPresenter,并且保持MainPresenter在onCreate和onDestory周期之外。MainActivity用一个静态变量引用MainPresenter,当由于OOM导致线程重启,MainActivity会检查MainPresenter是否还存在,如果不存在就去创建它。

是的,检查和使用静态变量起来有那么点臃肿,但是稍后我会给大家看如何写的更加优雅。:)

主要思想:

·例子应用不会在每次用户翻转屏幕的时候重新请求。
·如果线程被重启,再次加载数据。
·当MainActivity被销毁后,MainPresenter不会保持应用

MainActivity实例,这样当旋转屏幕的时候就不会内存泄露,而且也没有取消请求。

Nucleus

Nuleus 是我创建的一个library,灵感来自于Mortarlibrary和Keep It Stupid Simple这篇文章。

下面是功能列表:

·支持在View/Fragment/Activity状态Bundle中save/restore Presenter的状态。Presenter能够存储请求参数到重新启动。

·提供一个工具,通过一行代码可以把请求结果和错误放到正确的view当中去,因此无需再写`!=null`来检查。

·一个Presenter可以被多个view实例引用。如果Presenter实例使用[Dagger],就不能被多个view引用。

·通过一行代码就可以把Presenter和view进行绑定。

·提供view的基类:NucleusView, NucleusFragment, NucleusSupportFragment, NucleusActivity。你也可以从他们当中copy代码到任何类当中来利用Nucleus的Presenters。

·Presenter在线程重启之后能够自动restart。在`onDestroy`自动取消注册RxJava。

·最后,要保持简单明了,让每一个开发者都能够看懂。这里通过大约180行代码来驱动Presenter,230行RxJava代码来支持。

使用Nuleus例子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, attache 或 detached一个view到Presenter,并且把请求结果自动发送到被 attache 的View当中。

MainPresenter的代码比较少是因为通过deliverLatestCache()操作,期延迟了数据和错误,直到view是可用的,才会把数据和错误送到view里。它把数据缓存到内存中,这样当configuration改变的时候,数据还是可用的。

MainActivity的代码比较少是因为Presenter的创建由NucleusActivity来管理。所有你需要绑定presenter的类,只需要在类上声明@RequiresPresenter(MainPresenter.class)注释。

警告!注释!在Android世界中,如果你使用注释,这是一个很好的做法,这不会降低性能。我已Galaxy S(2010的设备)作为基准测试,处理注释只会花去0.3ms。这种注视只会发生在view中,所以我认为注释是对系统性能没有消耗的。

更多例子

这是一个参数持久性的例子测试列子

deliverLatestCache()方法

这是RxPresenter的一个方法,它有三种版本:

·deliver()延迟onNext,onError和onComplete到view变成可用的才会释放。当你做一次请求的时候可以使用它,例如登录到web service。Javadoc

·deliverLatest()如果有一个新的onNext可用,将会抛弃老的onNext。如果你有数据需要更新,这将不会积累没有必要的数据。Javadoc

·deliverLastestCache()deliverLatest()比较相似,它保存最后一次数据在内存中,当另一个view变成可用的(例如:configuration 改变),它将重新发送数据到view。如果你不想save/restore请求结果到你的view中(返回结果比较大或者不方便存储到Bundle中),这个方法将允许你去做出更好的用户体验。Javadoc

Presenter的生命周期

Presenter的生命周期与Android的控件相比,明显少一些。

·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 onDropView() - Activity或者Fragment调用onPause(),或者在android.view.View#onDetachedFromWindow()期间。Javadoc

View回收和View栈

通常你的view(Fragment和自定义view)在与用户的交互下随机 attache 和 detached。每次view被 detached的时候不去销毁Presenter,这可能是一个好主意。你可以任何时间 detached和 attache view,presenter会比这些动作活的更持久,继续后台的工作。

联想到view的回收,有个问题:fragment无法知道是否因为配置改变或者被弹出栈被 detached。

Nucleus的意见是:销毁presenter只能发生在view的onDetachedFromWindow()/onDestroy()并且activity是finish的。所以,如果你销毁view是在正常的activity生命周期,你可发出信号来通知presenter也应该被销毁。这里有两个方法可以用NucleusLayout.destroyPresenter()NucleusFragment.destroyPresenter()

举个例子,下面是我在我的项目里面如何管理Fragment pop()操作:

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

你可以对replace Fragment做类似的操作。压栈操作的时候也可以。

每次view从Activity detached的时候,你可以决定去销毁presenter来避免这个问题,但是你也将在view被detach的时候失去后台任务。

因此,view回收这部分,完全取决于你。也许,我会找到更好的解决方案,如果你知道,请告诉我。

最佳实践

把你的请求参数放在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);
    }

我建议使用Icepicklibrary。无需使用注解,就可以减少代码量,并且简化应用逻辑——这一切都发生在编译过程中。可以配合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);
    }

如果你有超过2个的请求参数,这个library会存储它们。你可以创建一个BasePresenter,并且把Icepick放在类里,这样所有的子类将会获得@Icicle,无需再次实现onSave。这也工作在activity,Fragment和view。

在onTakeView主线程中,执行一个即时查询Javadoc

有时候,你要查询一段小数据,例如从数据库中读取一小段数据。虽然你可以用Nucleus简单的创建一个请求,但是你不必到处使用Nucleus。如果在一个Fragment创建的过程中创建一个后台请求,用户会看到一个空白屏幕一小会儿,尽管这个请求就几毫秒。因此,为了是代码更简短,更友善,使用主线程吧。

不要尝试用Presenter控制你的View

这么做不是个好方式——应用的逻辑会变得更复杂,这是不正常的方式。

正常的方式是,控制流应该是从用户,通过View,到Presenter,再到Model。用户是控制应用程序的一个来源。因此我们的控制流应该是从用户开始,而不是从应用的内部的结构。

当控制流是从View到Presenter,然后Presenter到Model,这是一个线性流,这样很好写代码。这样你得到了一个简单的序列,user->view->presenter->model->data。但是,当控制流是这个样子的:user->view->presenter->view->presenter->model->data,这只是违反了KISS原则。

Fragment?是的,Fragment有时候会违反正常的控制流。他们太复杂了。这里有一篇不错的文章,关于思考Fragment:Advocating Against Android Fragments。但是Flow也没有简化太多。

MVC

如果你熟悉MVC,别用了。MVC完全不同于MVP,MVC并没有解决开发面临的问题。

什么是MVC?

·Model应用内部的逻辑部分。负责数据存储。
·View唯一和MVP共同的部分,应用中呈现到屏幕的部分。
·Controller输入设备,例如键盘,鼠标,操纵杆。

当你有一台电脑和一个用键盘简单驱动的游戏的时候,MVC出现有很长一段时间了。没有windows,没有图形交互界面,应用程序接收输入(Controller),维持一些状态(Model),产生输出(View)。控制流是这样的:Controller->Model->View。这种模式绝对不能用在Android中。

有很多混淆的MVC模式。人们相信他们使用的是MVC,实际上他们可能用的是MVP(Web开发)。很多Android开发,认为Controller就是控制View,因此他们尝试抽取View的逻辑代码来减少View的代码,用Controller来控制View。我个人没看到这种方式有任何好处。

使用不可变数据结构的复杂关系数据库项目

AutoValue是一个这样的library,在它的描述中写了一堆好处,我推荐看看它。AutoParcel是AutoValue一个Android项目。使用的主要原因是,不用改变对象,通过AutoParcel转换,而不用关心其影响了应用程序的其他部分。他们都是线程安全的。

结尾

尝试MVP,并且分享给你的朋友。:)

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。