今天我想分享我在Android上实现MVP(Model-View-Presenter)模式的方法。如果你对MVP模式还不熟悉,或者不了解为什么要在Android应用中使用MVP模式,推荐你先阅读这篇维基百科文章这篇博客

使用Activity和Fragment作为View合适么?

目前,在很多使用了MVP模式的Android项目中,主流做法是将Activity和Fragment作为视图层来进行处理。而Presenters通常是通过继承被视图层实例化或者注入的对象来得到的。我认可这种方式可以节省掉那些让人厌烦的”import android.*”语句,并且将Presenters从Activity的生命周期中分离出来, 这使项目后续的维护会变得简便很多。但另一方面, Activity有一个很复杂的生命周期(Fragment的生命周期可能会更复杂)。而这些生命周期很有可能对项目的业务逻辑有非常重要的影响。Activity可以获取Context和各种Android系统服务。Activity可以发送Intent,启动Service和执行FragmentTransisitons等等。在我看来,这些错综复杂的方面不应该是视图层涉及的领域(视图的功能只是显示数据,从用户那里获取输入数据。在理想情况下,视图应该避免业务逻辑,无需单元测试)。基于上述原因,我对目前的主流做法并不赞同,所以我尝试使用Activity和Fragment作为Presenters。

使用Activity和Fragment作为Presenters

1、去除所有的view

将Activity和Fragment作为Presenter最大的困难就是如何将关于UI的逻辑分离出来。我的解决方案是:让需要作为Presenter的Activity或者Fragment来继承一个抽象的类。这样关于View各种组件的初始化以及逻辑,都可以在继承了抽象类的方法中进行操作。而当继承了该抽象类的class需要对某些组件进行操作的时候,只需要调用继承自抽象类的方法而不必考虑Presenter类型。在抽象类里面会有一个实例化的接口,这个接口里面的初始化方法就会对view进行实例化,这个接口我称为Vu,如下所示:

1
2
3
4
public interface Vu { 
    void init(LayoutInflater inflater, ViewGroup container);
    View getView();
}

如你所见,Vu定义了一个通用的初始化例程,我可以通过它来传递一个填充器和一个容器视图。它也有一个方法可以获得一个View的实例,每一个presenter将会和它自己的Vu关联,这个presenter将会实现这个接口(直接或间接地去实现一个继承自Vu的接口)。

2、创建Presenter基类

现在我有了抽象的View的基础,我可以着手定义一个Activity或者Fragment基类来充分利用Vu从而实现View的实例化。我是通过利用普通类型和抽象方法来实现的,它定义了一个特殊的表示Presenter的Vu类。这是实现中最单调乏味的部分,因为我需要重新实现想要的相似逻辑或者每一个Presente基类。

下面是我实现的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
public abstract class BasePresenterActivity<V extends Vu> extends Activity {
 
    protected V vu;
 
    @Override
    protected final void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        try {
            vu = getVuClass().newInstance();
            vu.init(getLayoutInflater(), null);
            setContentView(vu.getView());
            onBindVu();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
 
    @Override
    protected final void onDestroy() {
        onDestroyVu();
        vu = null;
        super.onDestroy();
    }
 
    protected abstract Class<V> getVuClass();
 
    protected void onBindVu(){};
 
    protected void onDestroyVu() {};
 
}

下面是我实现的Fragment例子:

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
public abstract class BasePresenterFragment<V extends Vu> extends Fragment {
 
    protected V vu;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
 
    @Override
    public final View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = null;
        try {
            vu = getVuClass().newInstance();
            vu.init(inflater, container);
            onBindVu();
            view = vu.getView();
        } catch (java.lang.InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return view;
    }
 
    @Override
    public final void onDestroyView() {
        onDestroyVu();
        vu = null;
        super.onDestroyView();
    }
 
    protected void onDestroyVu() {};
 
    protected void onBindVu(){};
 
    protected abstract Class<V> getVuClass();
 
}

相同的逻辑可以用在Activity和Fragment类型上,比如支持库中Activity和Fragment等等。

可以看到,我重写了创建视图view的方法(onCreate、onCreateView)和销毁视图view的方法(onDestroy、onDestroyView)。我选择重写这些方法目的是强制使用抽象实例Vu。一旦它们被重写,我就可以创建新的生命周期方法,来精确控制对其初始化和销毁,即onBindVu和onDestroyVu。这样做的好处就是,两种类型的presenter都可以利用同样的生命周期事件签名来实现。这也消除了Activity和Fragemnt生命周期差异的影响,使得两者之间的转换更加容易。 (你也可能会注意到,我并没有真正的利用InstantiationException 或者IllegalAccessException做一些异常处理。这仅仅是我比较懒罢了,因为如果我正确地使用这些类就不会抛出这些异常。)

3、写一个可以工作的例子

现在,我们可以使用刚才构建的框架。简单起见,我写一个“Hello World”的例子。我会从创建一个实现了Vu接口的类开始写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HelloVu implements Vu {
 
    View view;
    TextView helloView;
 
    @Override
    public void init(LayoutInflater inflater, ViewGroup container) {
        view = inflater.inflate(R.layout.hello, container, false);
        helloView = (TextView) view.findViewById(R.id.hello);
    }
 
    @Override
    public View getView() {
        return view;
    }
 
    public void setHelloMessage(String msg){
        helloView.setText(msg);
    }
 
}

下一步,我会创建一个Presenter来操作这个view:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HelloActivity extends BasePresenterActivity<HelloVu> {
 
    @Override
    protected void onBindVu() {
        vu.setHelloMessage("Hello World!");
    }
 
    @Override
    protected Class<MainVu> getVuClass() {
        return HelloVu.class;
    }
 
}

等等……有耦合警告!

你可能注意到了,HelloVu类直接实现了Vu接口,Presenter的getVuClass()方法直接引用了实现类。常规的MVP模式中,Presenter要通过接口与他们的View解耦。当然,你也可以这么做。为了避免直接实现Vu接口,我们可以创建一个扩展了Vu的IHelloView接口,然后使用这个接口作为Presenter的泛型类型。那么Presenter看起来应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HelloActivity extends BasePresenterActivity<IHelloVu> {
 
    @Override
    protected void onBindVu() {
        vu.setHelloMessage("Hello World!");
    }
 
    @Override
    protected Class<MainVu> getVuClass() {
        return HelloVuImpl.class;
    }
 
}

在我使用强大的模拟工具过程中,并没有看到一个接口下面实现Vu所带来的好处。但是对于我来说一个好的方面是,即使没有定义Vu接口它也能够工作,唯一的需求就是你最终还要实现Vu。

4、测试

通过以上几步我们可以发现,在去除了UI逻辑之后Activity变得非常简洁。同时,相关的测试也变的异常简单。请看如下单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HelloActivityTest {
 
    HelloActivity activity;
    HelloVu vu;
 
    @Before
    public void setup() throws Exception {
        activity = new HelloActivity();
        vu = Mockito.mock(HelloVu.class);
        activity.vu = vu;
    }
 
    <a href="http://www.jobbole.com/members/test/" rel="nofollow">@Test</a>
    public void testOnBindVu(){
        activity.onBindVu();
        verify(vu).setHelloMessage("Hello World!");
    }
 
}

以上代码是一段标准的JUnit单元测试的代码,不需要在Android设备中部署运行。当然我们测试的Activity要足够简单。特殊情况下,在测试需要某些硬件支持的方法的时候,你可能需要使用Android设备。例如当你想测试Activity生命周期中的onResume()方法。在缺乏硬件设备支持环境的时候,super.onResume()会报错。还好我们可以使用一些工具,例如Robolectric、还有Android Studio 中的Gradle 1.1 插件中内置的testOptions { unitTests.returnDefaultValues = true }选项。此外,你仍然可以将这些生命周期按照下面的方式抽离出来:

1
2
3
4
5
6
7
8
9
10
11
...
 
    @Override
    protected final void onResume() {
        super.onResume();
        afterResume();
    }
 
    protected void afterResume(){}
 
...

现在,你可以把应用程序中特定的逻辑代码转移到生命周期事件中,并且在没有Android设备的情况下运行测试了。

意外收获:使用Adapter作为Presenter

将Activity作为Presenter已经足够巧妙了吧,如果是adapter,情况会更复杂。它们可以是View或者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
public abstract class BasePresenterAdapter<V extends Vu> extends BaseAdapter {
 
    protected V vu;
 
    @Override
    public final View getView(int position, View convertView, ViewGroup parent) {
        if(convertView == null) {
            LayoutInflater inflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            try {
                vu = (V) getVuClass().newInstance();
                vu.init(inflater, parent);
                convertView = vu.getView();
                convertView.setTag(vu);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        } else {
            vu = (V) convertView.getTag();
        }
        if(convertView!=null) {
            onBindListItemVu(position);
        }
        return convertView;
    }
 
    protected abstract void onBindListItemVu(int position);
 
    protected abstract Class<V> getVuClass();
 
}

正如你看到的,实现方式和Activity和Fragment的Presenter是一样的。然而,我不是用空的onBindVu方法,而是用参数为整型的position的onBindListItemVu方法。同时,我仍然沿用了View Holder模式。

总结和Demo项目

这篇文章介绍了一种实现MVP模式的方法。从中我发现唯一的途径就是网上寻找答案。我非常期待其他Android开发者的反馈,是否有人在用这个方法?你发现它有用么?我是否过于大胆(疯狂)?如果是的话,这是一个好办法吗?

我已经把这套方法(和一些其他的比如Dagger开源库)集成在一个开源框架上,并且即将公布。与此同时,我在Github上面有一个demo项目,望各位不吝赐教。

Android上实现MVP模式的途径的更多相关文章

  1. Android上的MVP:如何组织显示层的内容

    MVP(Model View Presenter)模式是著名的MVC(Model View Controller)模式的一个演化版本,目前它在Android应用开发中越来越重要了,大家也都在讨论关于M ...

  2. Android 中的MVP 模式

    MVP模式的核心思想: MVP把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成功接口,Model类还是原来的Model. MVC 其中View层其实就是程序的UI界面,用于向用户展示 ...

  3. [译]Google官方关于Android架构中MVP模式的示例

    概述 该示例(TODO-MVP)是后续各种示例演变的基础,它主要演示了在不带架构性框架的情况下实现M-V-P模式.其采用手动依赖注入的方式来提供本地数据源和远程数据源仓库.异步任务通过回调处理. 注意 ...

  4. Google官方关于Android架构中MVP模式的示例续-DataBinding

    基于前面的TODO示例,使用Data Binding库来显示数据并绑定UI元素的响应动作. 这个示例并未严格遵循 Model-View-ViewModel 或 Model-View-Presenter ...

  5. Android开发之MVP模式的使用

    前几天发现,在Android项目代码里有一个Activity类行数居然有1000多行,而600行左右都是逻辑控制,真正和页面控件处理相关的代码不多,虽然可以用#region <>...#e ...

  6. 在Android上启用Kiosk模式

    我们的云帆机器人(上面运行的安卓程序)有一个线下场景是商场,由于商场人多,总会遇到一些用户在我们的app里乱点,然后会跳出程序进入到系统设置的一些界面,这样很不友好. 比如程序中有一些需要输入文字的地 ...

  7. android中的MVP模式

    1.建立bean public class UserBean { private String mFirstName; private String mLastName; public UserBea ...

  8. Android开发学习--MVP模式入门

    1.模型与视图完全分离,我们可以修改视图而不影响模型2.可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部3.我们可以将一个Presenter用于多个视图,而不需要改变Pr ...

  9. Android开发MVP模式解析

    http://www.cnblogs.com/bravestarrhu/archive/2012/05/02/2479461.html 在开发Android应用时,相信很多同学遇到和我一样的情况,虽然 ...

随机推荐

  1. 四轴自适应控制算法的一些尝试开源我的山猫飞控和梯度在线辨识自适应等算法—(转)

    本文的最主要目的在于抛砖引玉,阿莫论坛真的是非常好的一个论坛,没有这个论坛,没有那么多这个论坛上的前人无私的奉献和热烈的讨论,我想我是怎么也无法入门四轴的控制的.只是论坛上已经很多年都没有看到过新东西 ...

  2. Springmvc 服务器端文件下载

    转自:http://blog.csdn.net/boneix/article/details/51303280 业务场景:点击下载后直接保存而不是打开 解决代码:前端传入url /** * 返回流 * ...

  3. Android Studio 的 10 个你非常有可能不知道的技巧

    本文首发:http://prototypez.github.io/2016/04/19/about-10-things-you-probably-didn-t-know-you-could-do-in ...

  4. 用一条sql取得第10到第20条的记录

    因为id可能不是连续的,所以不能用取得10<id<20的记录的方法. 有三种方法可以实现:一.搜索前20条记录,指定不包括前10条语句:select top 20 * from tbl w ...

  5. [网摘][医学影像] DICOM 和 NIFTI 基础知识与区别

    查找DICOM基础知识时,看到这篇文章里面写了一些关于使用深度学习进行医疗影像分析:文件格式篇.下文摘自:https://www.jiqizhixin.com/articles/2017-07-31 ...

  6. Layer文件上传操作

    1:upload.html <!DOCTYPE html> <html lang="en"> <head> <meta charset=& ...

  7. python乱码问题之爬虫篇

    UnicodeDecodeError: 'gbk' codec can't decode byte 0xae in position 167: illegal multibyte sequence 使 ...

  8. python binascii模块详解

    ['Error', 'Incomplete', 'b2a_hex', 'hexlify' #Hexadecimal representation of binary data. 字符串转16进制'a2 ...

  9. SpringMVC Controller配置方法有哪几种

    第一种 URL对应Bean 如果要使用此类配置方式,需要在XML中做如下样式配置 <!-- 表示将请求的URL和Bean名字映射--> <bean class="org.s ...

  10. TortoiseGit使用指南

    http://www.360doc.com/content/13/0424/17/9171956_280649187.shtml