浅谈 Android 编程思想和架构

我主要是想讲一讲自己对于 接口、模块化、MVP 的一些心得。

有这么一个场景,两个不同的页面,包含了看起来一模一样的界面内容(或者称 frame/UI),这种场景可能很常见,有时看到会说:“哈哈,我可以设计个复用!” 但是遇到一个问题是,这两个页面需要分别去请求不同的服务端 API,返回下来的数据结构也不一样(姑且不说去和服务端开发协商),这样就会导致具体的 view holder 或者适配器在绑定数据的时候无法复用,为何说无法复用或难以复用呢?举个例子,比如传进 Apdapter 的 list item 数据内容不一样,你总不能把 item 再拆了,分好几个 list 传进去吧?面向具体编程情况下,适配器得到不同的 items,得对 item 分发数据绑定到 UI,难免要写很多重复的代码。

这时候我们可以采取面向抽象编程,既然不同的数据对应一样的 UI,如果它们都实现了一样的接口,这个接口的设计取决于 UI 需要哪些数据块,然后不同的 Item model 去实现这个接口提供数据即可,这时适配器只要持有这个接口类型的 Item 即可,举个通俗的例子:数据模型1和2都实现了 IPost 接口,那么适配器就只要持有 List<IPost> 数据即可,List<数据模型1> 和 List<数据模型2> 都可以视作“一样的鸭子”传递给这个适配器。这样把数据模型的数据块抽取放到了数据模型本身实现,不仅不用写很多重复的分发代码,而且适配器本身都能复用了;当接口需要新的方法,也能驱动着实现者们去实现。

这就是抽象编程或者接口的好处,接口可以让不同的模型通过同样的方法提供内容,这样它们就可以一定程度上视为同类,有句话说的,如果一个动物走起来像鸭子,叫起来也像鸭子,我们就可以把它当作是鸭子

同样地,对于有相同、可复用的 UI 这个场景,我们可以更进一步去做,即把 View Holder 化为一个自定义 ViewGroup,或者包裹之。这样做的好处是,更多逻辑不同的地方也都能更好地去复用,ViewHolder 本身出了 Adapter 范围可能难以再复用,而且对于 Adapter,你不需要再纠结 Item UI 的内容点击事件 是要回调到 Adapter 外还是在 Adapter 内直接绑定数据响应。前者处理方式会导致 Adapter 得暴露很多接口,传递很多数据,经常要从数据集合中重新根据位置取绑定的数据,等等。后者,则会导致 Adapter 变得臃肿,处理过多不应该属于它的业务,而且同样存在着数据重复绑定问题。

这时候如果把 View Holder 化为一个自定义 ViewGroup,那么交互数据的时候就可以进行 UI 数据绑定和响应数据绑定,而监听器建议是在初始化的时候就进行设置,避免 View 被复用的时候,更新数据的同时重复 new 出无谓的监听器对象。

其中,交互给 ViewGroup 这个对象的数据模型也最好使用接口模型,比如同样接收 IPost 接口对象作为数据,这样凡是实现了同样的提供数据接口的类,都能传入这个 ViewGroup 中供其使用。这里提供一个我的常用写法,使用 ViewGroup 关联一个 item 布局:

public class PostView extends LinearLayout implements View.OnClickListener {

    private TextView mSummary;
    private ImageView mAvatar;
    private TextView mUsername;
    private TextView mCreateAt;

    protected IPost mData;


    public PostView(Context context) {
        this(context, null);
    }


    public PostView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }


    public PostView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        inflate(context, R.layout.view_post, this);
    }


    @Override protected void onFinishInflate() {
        super.onFinishInflate();
        mAvatar = (ImageView) findViewById(R.id.avatar);
        mUsername = (TextView) findViewById(R.id.username);
        mCreateAt = (TextView) findViewById(R.id.create_at);
        mSummary = (TextView) findViewById(R.id.summary);
        setOnClickListener(this);
    }


    public void setPost(IPost data) {
        mData = data;
        Glides.loadCircleImage(getContext(), data.getAvatar(), mAvatar);
        mUsername.setText(data.getUsername());
        mCreateAt.setText(data.getCreatedAtFromNow());
        mSummary.setText(data.getSummary());
    }
    

    @Override public void onClick(View v) {
        Intent intent = StreamActivity.newIntent(getContext(),
                mData.getAvatar(), mData.getUsername(), mData.getUserId(),
                mData.isCurrentUser());
        getContext().startActivity(intent);
    }
}

解释了接口的一些实际使用场景之后,我们接下来就很好谈 MVP 架构了,因为可能很多人会在使用 MVP 的时候产生困恼,为什么要搞那么多接口再实现,何不直接调用具体对象的方法?前面讲了那么多,现在应该有点清楚了吧。

今年 Android 开发的技术趋势,我觉得一是 RxJava 会继续被更多人接受进而开始使用,二是谷歌花了不少心思的 Data Binding 很可能会迎来正式版,data binding 是实现 MVVM 架构的重要组成部分,介于它还不够完善而且目前还无法提供双向绑定,目前很多人包括俺都还只能停留在个人项目玩一玩的阶段,所以我也是比较青睐于 MVP 架构。

MVP 逐渐流行起来了,必然是有一些好处,不然谁会去管这么一个新兴东西。首先它更加易于测试,Android 平台默认的应用架构导致单元测试变得异常的困难,和 SDK 纠缠在一起的代码,使你无法改变要测试单元的预备状态,无法进行单元测试的准备步骤,而且很多情况,你无法获得测试内容的结果或者状态,也就无法完成断言内容。而使用 MVP 的好处就是能够更易于做测试,同时也能够有更好的复用性和松耦合性。关于 MVP 的 mock & test,谷歌给出了非常好的示例:https://github.com/googlesamples/android-architecture/tree/todo-mvp/todoapp/app/src

MVP 即 Model – Presenter – View,各部分之间的通讯,都是双向的,Presenter 持有 View 和 Model 的抽象引用,作为中间人,处理业务逻辑,Model 角色用于调取数据,而 View 则用于展现和控制 UI,它们都由中间人 P 调度。抽象的好处前面说了,如果 M 或 V 有改变,只要换一个实现者就好了,对于 P,可以继续把它们当成提供固定可调用方法对象。

对于包的结构,如果项目比较小,可以把不同的 Presenter 置于同一个 package 下,而如果页面数很多项目模块很多,则可以每一个模块分一套 MVP packages/modules.

现在我们只要在 Activity 或 Fragment 中的生命周期简单做一些 UI 组件初始化等布置,然后一些业务逻辑工作就请求 Presenter 去完成,Presenter 内部持有 View 和 Model,Activity 是 View(MVP 中的 View) 的实现,于是 Presenter 调用 Model 去请求得到数据库或网络数据,完成之后再调用 View 改变 UI 的方法,或者把数据交给 View 去展现。如此,每个角色的代码都会变得很简洁、明确。而且,将业务逻辑放到 Presenter 中去,可以方便去驱动避免 Activity 退出而后台线程仍然引用着 Activity,致使资源无法被系统回收从而引起内存泄露。

对于 Presenter 的设计,或者说具体应该把哪些内容放到 Presenter 中,是一个关键。Model 并不是必须有的,Model 属于仓库层,如果使用 RxJava 和 Retrofit,可以很清晰地获取数据库内容和网络数据,则可以把 Model 的工作纳入到 Presenter 中。如果带有 Model,则 Presenter 要实现 Model 的回调,在回调中把数据传给 View 或响应。所以 Presenter 必须得有 View 的引用,但可以不必持有 Model.

一个 Activity 可以有多个 Presenter,要用到什么业务就加入什么 Presenter,并且实现这个 Presenter 所需要的 View 接口即可,这就是简单的复用逻辑。

最后,介绍一种我的重构技巧。对于原本不是 MVP 的项目,结合 Android Studio 进行重构也很容易,AS 有个好用的功能快捷键是 option + enter,可以用来自动解决错误,利用它,我们可以很方便的把原本都写到 Activity 中的业务抽出,并且不用手动去创建各种接口中的方法和实现方法,步骤大概是这样的:

首先建一个业务的 Presenter 和一个 View 接口,Presenter 中加入 View 接口变量,并写个构造方法用于 View 初始化。而 View 接口,只要先放空即可。然后回到 Activity,implements View 接口,初始化 Presenter,并把自己交给 Presenter,找到原本业务逻辑的地方,把相关代码剪切,然后输入比如 mPresenter.loadData(),这时候,Presenter 中并没有 loadData 这个方法,你只要对着错误按一下 option + enter 就可以出来 “自动在 Presenter 中创建这个方法” 的选项,然后自动创建了之后,再跳过去,粘贴刚才的代码,并且在回调的时候,调用 view.hideProgressBar() 方法,同样的,hideProgressBar 这个方法在 View 接口中并没有,于是 option + enter 自动在 View 接口中创建这个方法。这时候 Activity 就会报错,提示你必须实现 hideProgressBar 这个方法,然后你再对着 Activity 按 option + enter 自动生成需要实现的方法的框体,从而进行实现。这样就完成了整个循环驱动重构,是一条 step by step 很简单的套路。

另外,分享我的 Contract 模板(template),Contract 是一个 MVP 的接口统筹声明接口:

附:

 

  1. 我也在使用 RxJava 和 Retrofit 在一个 MVP 架构的项目中,一般还是需要 Model 层处理 RxJava 的 各种 map 变换、线程切换、doOnNext 之类等,在Presenter 中 subscibe 就好了

  2. MVP,M是不需要持有P的吧,那个图多了一条箭头。如果是Retrofit+RxJava的话我和楼上一个朋友的做法一样,仍然需要model层来处理数据,不过将subscribe留给P。
    另外,一些简单的逻辑我会直接在V去做,涉及到M的才重点放到P,虽然不是理想中的状态,但是感觉直观一点- –

    https://github.com/leelit/STUer-client

    • 简单可绕可不绕,不绕的话,等以后要做手脚的时候再修改绕就行了,但最好还是绕。总之,MVP 大致那样,很多人会根据需求和情况微调,都可以,反正只要知道大体这么个架构,要修改调整是很容易的,感觉不用太纠结。

  3. 也尝试mvp过,但感觉总不是很方便,业务的确是抽出来了,但是要多些2个接口和两个callback。

    • 多写两个文件是坏处吗?这就好比你把一堆内容挤在一个方法体内,你觉得把一部分代码分到新的方法中去多写了几个方法是不好的? … 应该不是这样的,而且我文末也提供了一种快速生成 MVP 文件内容的方法 …

  4. 当用户频繁的切换屏幕,这将会造成内存泄露,请求运行时,每一个回调将会持有MainActivity的引用,让其保存在内存中。因此引起的OOM和应用反应迟缓,会引发应用的Crash。
    private void unsubscribe() {
    if (subscription != null) {
    subscription.unsubscribe();
    subscription = null;
    }
    }
    MVP中给的链接文章中没有使用MVP时在destroy中解除了事件,为什么还会造成内存泄露呢,黑哥求解

  5. Pingback: Android开发技术周报 Issue#72 - 移动开发 - 阿里欧歌

  6. 能举一个例子嘛,有这么一个场景,两个不同的页面,包含了看起来一模一样的界面内容(或者称 frame/UI),这种场景可能很常见,有时看到会说:“哈哈,我可以设计个复用!” 但是遇到一个问题是,这两个页面需要分别去请求不同的服务端 API,返回下来的数据结构也不一样(姑且不说去和服务端开发协商),这样就会导致具体的 view holder 或者适配器在绑定数据的时候无法复用,为何说无法复用或难以复用呢

  7. Pingback: Android 开发技术周报 Issue#72 |

  8. Pingback: Android 开发技术周报 Issue#72 - Calm博客

  9. 同样跟楼主93年,大学同样稀里糊涂报了机械自动化专业,同样发现自己更感兴趣计算机方面,同样没转到专业,但是结果才知道差距好大。。。楼主的执行力真心令我佩服,向楼主学习了。

  10. Pingback: Android MVP 详解(上)-小新和小白

  11. 看完后,有了很多感悟,感谢分享。不过,还有个疑问,不知道是不是自己想错了,我感觉Adapter那个例子不是特别好,PostView其实就是ViewHolder的另一种形式。这样的话,文中提到的点击等事件的监听处理也完全可以放到ViewHolder中处理。相当于把ViewHolder单独写成一个类就好了。一个UI需要对应一个PostView,也可以一个UI对应一个ViewHolder。而写成ViewGroup的话,因为ViewGroup本身就包含了很多冗杂的处理,相对来说反而可能会比ViewHolder要繁重一些。不知道我这样的理解正确不正确,如果有空,请帮忙解答一下。谢谢。[嘻嘻]

    • 嗯,你的理解不错,但 ViewHolder 复用的时候没有 View 那么方便、自包含,比如你要在 Activity 中用这个 ViewHolder?比如你要在布局文件件中 写这个 ViewHolder(得用 include?)…

  12. Pingback: Android MVP 详解(上) | android学习贴士

  13. ViewGroup里面又inflate了另外一个布局,会不会使view层级多了一层呢?使用mvp模式之后,我发现需要在View里面暴露很多更新ui的方法,这个是不是和最小暴露原则有点相悖呢?还有一点,楼主我怎么知道你的博客更新了呢,有订阅功能吗?

    • 并没有影响,View 接口暴露出去并不会有影响就像你调试浏览器页面,你能修改支付宝余额显示,但你无法真正修改你的支付宝余额,它本该暴露出去,最小暴露原则不是一味地缩紧。另外,对于实体类,可以通过唯一工厂方法来提供缩紧接口,以此来在必要的时候,对外隐藏一些 public 接口

      • 哦哦,谢谢,最近在研究你的meizhi 项目,要是仔细研究一遍肯定能提高不少,太感谢你的分享了

  14. Pingback: spin rewriter bonus

  15. Pingback: web design services Melbourne

  16. Pingback: Roadside Assistance in Fairfield East , New South Wales , Australia

  17. Pingback: Surfers Paradise Shades & Blinds

  18. Pingback: ipl t20 live stream

  19. Pingback: New Artists

  20. Pingback: Best Magazine in India

  21. Pingback: Entertainment and Movie reviews with tips on how to get Website Traffic and Make Money Online.

  22. Pingback: para kazanmak

  23. Pingback: kryptodyne

  24. Pingback: phen375 buy

  25. Pingback: para para dinle

  26. Pingback: economics tuition

  27. Pingback: joseph shihara rukshan de saram

  28. Pingback: joseph de saram

  29. Pingback: joseph de saram

  30. Pingback: joe de saram

  31. Pingback: joseph s r de saram

  32. Pingback: joseph de saram

  33. 一个View里使用多个Presenter,一个Presenter可以被多个View使用,这种多对多的情况不会有问题吗?尤其是面向接口编程的时候,我也学习了Google的这套MVP的方案,但是发现View实现多个Contract.View的接口会出问题,编译不通过

    • 如果要使用多个 Presenter,就势必不能使用 Google MVP,凡事不要给自己限定死。另外,我现在已经不使用 MVP 了,这事有机会可以再写一篇文章…