Android的MVP设计模式

9,233 阅读6分钟
原文链接: blog.waynell.com

随着Android的不断发展,APP的功能越来越强大,UI也越来越复杂,对于Android开发者来说UI层在程序开发过程中担任了越来越多的职责。通常一个APP是由多种数据模型(Model)和多种视图(View)组成,如果我们直接使用Model-View设计模型,那这将使得我们的程序代码变得复杂、耦合度高、不利于单元测试和代码重构。

什么是MVP设计模式

  • View是用户交互层,在Android中它可能是Activity、Fragment、View、Dialog
  • Model是数据访问层,例如数据库访问API和远程服务器访问API
  • Presenter是View和Model的中间纽带,负责后台数据处理任务和UI层的交互

在Android的MVP设计模式中,它很好的从activities/views/fragments中分离了后台任务,使得它们独立于自身的生命周期事件。这种设计模式使得程序变得更简单,程序可靠性也极大地增加,程序代码更加短小精悍和易于维护。

为什么要使用MVP设计模式

  1. Keep It Stupid Simple (让它够蠢够简单,简称Kiss)

    The Kiss Principl是一种设计理念,它能让你编写出更好的代码(建议大家阅读下原文,会受益匪浅)

    让我们先来看看传统的Model-View设计模式中存在的弊端:

    mv

    从图中可以看出,Model和View完全耦合在一起,或许你觉得并没有那么糟糕,但是不要忘记了,在Android中,Activity和Fragment也是一种View,并且它们是如此复杂,各种生命周期,各种事件传递,各种状态的保存和恢复等待,这都能让你的代码结构变得复杂不堪,而且很难进行重用、重构、单元测试和调试

    特别是对于Fragment,看似美好,实则痛苦,在之前的工作中遇到不少关于Fragment的奇葩问题,很难找到问题所在,而且Fragment的源码写得十分晦涩,这里有一篇国外开发者写的对Fragment的吐槽文章,有兴趣的可以看看Advocating Against Android Fragments,文中得出这么一个结论 WTFs/min = 2^fragment count (-_-!!!)

    接下来再看看MVP设计模式:

    mvp

    它将复杂的任务独立分离成若干个小任务,View不再直接访问数据,全交由Presenter来控制,Presenter访问数据,并控制View的状态,这些都使得程序设计变得更简单,View的逻辑也更加清晰,对单元测试、代码重用等角度来说也是十分有益的

  2. 后台任务

    无论何时,在你编写一个Activity、Fragment或者自定义View的时候,都应该将处理后台耗时操作任务的所有方法放到不同的外部类或者静态类中,这样做的好处是能够使得后台任务不会被关联到Activity、Fragment或者View,这样也就不会导致内存泄露。我们就称这类对象为“Presenter”

如何实现MVP

在实现MVP之前,首先我们应该知道Android程序在设备发生变化或者内存不足时会发生什么,请看下表

A configuration change An activity restart A process restart
Dialog reset reset reset
Activity, View, Fragment save/restore save/restore save/restore
Fragment with setRetainInstance(true) no change save/restore save/restore
Static variables and threads no change no change reset

从表中可以看出:

  • 当设置变化时,例如横竖屏切换,Activity会被销毁并立即被重建,而View、Fragment、Dialog也自然如此,除了Fragment被设置为setRetainInstance(true)状态时不会被销毁重建,不影响静态变量和线程状态

    Some device configurations can change during runtime (such as screen orientation, keyboard availability, and language). When such a change occurs, Android recreates the running activity (the system calls onDestroy(), then immediately calls onCreate()).

  • 当用户开启了“Don’t keep activities”选项或者内存不足时,Activity、Dialog、View、Fragment(包括设置为setRetainInstance(true)状态的)都会被销毁重建,不影响静态变量和线程状态

  • 当系统内存不足,且程序不在前台运行时,当再次回到程序时,程序的进程有可能会被重启,这会导致静态变量和线程状态都会被重置

了解了以上规则后,可以得出以下两点结论:

  1. 保存/恢复Activity,View,Fragment等视图的状态
  2. 只有在进程被重启时再重新执行后台数据请求行为,而不是在视图的重启中

下面来看两个实例,一个是没有使用MVP模式,另外一个是使用了MVP模式

public class MainActivity extends Activity {
    public static final String DEFAULT_NAME = "Chuck Norris";

    private ArrayAdapter 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() {
                @Override
                public void call(ServerAPI.Response response) {
                    onItemsNext(response.items);
                }
            }, new Action1() {
                @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;
        }
    }
}

以上示例存在两个明显的问题:

  1. 如当用户每翻转一次手机时,数据请求都被执行一次,这样会造成较差的用户体验和影响程序的性能
  2. 导致内存泄露,因为每一次数据请求时都会注册一个回调的匿名内部类,这样就会导致Activity被hold住从而导致内存泄露
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() {
                @Override
                public void call(ServerAPI.Response response) {
                    items = response.items;
                    publish();
                }
            }, new Action1() {
                @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只在构造函数中执行后台数据请求任务,并通过onTakeView(MainActivity view)函数来注入Activity对象,这样当数据请求任务完成后,就可以使用MainActivity的对象来刷新UI,这样就将数据请求的UI操作封装在一起

public class MainActivity extends Activity {

    private ArrayAdapter 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的静态成员变量presenter,并在onCreate()中当presenter == null时才新创建实例,并在onDestroy()中通过isFinishing()来判断Activity是被用户主动销毁的而不是由于内存不足或者横竖屏切换时,来销毁presenter对象。使用静态成员变量的好处在于程序进程中不会受Activity的影响重复创建MainPresenter对象,而只是在程序进程被重启静态对象被回收时才重新创建。

从以上两种代码实例可以很清楚看到MVP模式的好处,简洁、高效并且更加符合KISS编程理念。

英文原文出处 Introduction to Model-View-Presenter on Android