zoukankan      html  css  js  c++  java
  • Android App 架构演变

    文:https://www.jianshu.com/p/ce26e7960926

    最近App项目(MVC架构)越做越大,协同开发效率较低,维护困难,所以产生了调整架构的想法,在 简书、csdn、知乎上看了不少文章,感觉知乎用户 0x8421bcd 对于“Android项目开发如何设计整体架构?”的回答颇为精彩,在此引用,鞠躬感谢!

    0. 前言

    想要设计App的整体框架,首先要清楚我们做的是什么。一般我们与网络交互数据的方式有两种:主动请求(http)和长连接推送。
    结合网络交互数据的方式来说一下我们开发的App的类型和特点:

    • 数据展示类型的App
      特点是页面多,需要频繁调用后端接口进行数据交互,以http请求为主;推送模块,IM类型App的IM核心功能以长连接为主,比较看重电量、流量消耗。
    • 手机助手类App
      主要着眼于系统API的调用,达到辅助管理系统的目的,网络调用的方式以http为主。
    • 游戏
      一般分为游戏引擎和业务逻辑,业务脚本化编写,网络以长连接为主,http为辅。

    一般我们做的App都是类型1,简要来说这类app的主要工作就是把服务端的数据拉下来给用户展示,把用户在客户端修改的数据上传给服务端处理,所以这类App的网络调用相当频繁,而且需要考虑到网络差、没网络等情况下App能够正常运行。

    成熟的商业应用的网络调用一般是如下流程:UI发起请求 -> 检查缓存 -> 调用网络模块 -> 解析返回JSON / 统一处理异常 -> JSON对象映射为Java对象 -> 缓存 -> UI获取数据并展示,这之中可以看到很明显职责划分,即:数据获取;数据管理;数据展示。确定了职责,就可以进入正题了。

    1. MVC架构

    Android最原生也是最基础的架构,可以理解为MVC(Model-View-Controller),Controller即是Activity和Fragment,但是这两者掌握了Android系统中绝大多数的资源,并且在内部直接控制View,因此MVC架构一般是以Activity和Fragment为核心,将网络模块,数据库管理模块,文件管理模块,常用工具类等分离成若干工具类包,供Activity和Fragment使用。

     
    MVC Frame.png

    这是比较基础的Android项目架构,市面上大部分App都是这种造型。

    • 优点
      开发简单,以页面为导向;如果构建水平可以,项目就已经基本实现模块化,基于Activity、Fragment这两个上帝般的存在,很多事情直接就妥了,不用绕。
    • 缺点
      维护难,因为是以页面为导向的,有些需要共用的业务逻辑就会很烦,don't repeat your self, 你要不要repeat ?不想repeat就要写模块,慢慢的项目就会多出一堆乱七八糟的小模块。另一方面,测试很困难,因为所有的数据处理都在Activity和Fragment,假如现在想先用假数据显示,就要直接改Activity和Fragment的数据控制逻辑。还有个最恼火的问题,那就是业务复杂起来后Activity和Fragment的代码量激增,举一个例子,电商App的购物车,如果只是管理一下购物车中的商品,无非就是查、删、改调用,列表管理,300多行代码应该就搞定了,假如现在加了个优惠券提示呢?光优惠券不够,还有满减,还有凑单,要计算运费。还要能领取优惠券…… 噢,忘了一般来说还有一个商品推荐,好了现在有两个列表要管理了,你觉得CartActivity 2000行代码能止住么?在上面这些缺点的描述中,可以看到一个很大的痛点在于:Activity和Fragment不应该管这么多数据处理逻辑

    2. 分层架构(在MVC的基础上分层)

    如果仔细看自己的项目,可以发现绝大多数数据处理的代码是不需要使用Activity和Fragment持有的资源的(比如Context),而很多时候我们需要多个页面共用一套数据和请求逻辑,很经典的例子是应用中的User对象,一般来说都是全局单例。这些全局的数据源写多了,很容易就能想到将数据处理统一抽出来形成一层,向上层提供数据接口,而上层并不关心数据的来源(内存,缓存,网络),因为不用从Activity和Fragment拿资源而且主要工作是数据处理,所以这一层是UI无关的,大幅提升了复用性,我把这一层称为DataManager层。
    这是我一个项目的包结构: 


    MVC-Hierarchy.png

    Activity和Fragment剥离了数据处理的责任后,持有DataManager的引用,负责获取数据并展示,向DataManager传递数据,绝不进行网络请求和缓存读写。
     
    image.png

    举个栗子,分页加载。一般来说分页加载接口返回的数据是这样的:
    {
        "code":0,
        "message":"success",
        "data":{
            "page":1,
            "totalPage":10,
            "pageSize":20,
            "total":200,
            "list":[......]
        }   
    }
    

    在传统的写法中,一般在Activity/Fragment中缓存page,totalPage,pageSize去进行分页请求,根据请求结果刷新数据并判断是否还有更多;每一个分页接口都要写一遍,假如把这段逻辑放到DataManager会怎么样?我是这么写的:

    //定义回调接口
    public interface ActionCallback<T> {
        void onSuccess(T data);
    
        void onFailure(String message, Throwable e);
    }
    

    分页加载DataManager实现

    public class PageLoadDataManager extends BaseDataManager {
        private static final int PAGE_COUNT = 20;
    
        private List<Data> mDataList = new ArrayList<>();
        private int currentPage = 0;
        private int totalPage = 0;
    
        public PageLoadDataManager() {
            // init something......
        }
    
        public void loadData(final boolean refresh, ActionListener<Boolean> listener) {
            if (refresh) {
                currentPage = 0;
            }
            currentPage++;
            RequestParams params = new RequestParams();
            params.put("page", currentPage);
            Request request = new Request(url, params);
            request.request(new RequestCallback(){
               @Override 
               public void onSuccess(JSONObject data) {
                   if (refresh) {
                        mDataList.clear();
                   }
                   totalPage = response.optInt("total_page");
                   // 返回数据添加到 mDataList ......
                   if (listener != null) {
                       boolean hasMore = currentPage <= totalPage
                       listener.onSuccess(hasMore);
                   }
               }
               @Override 
               public void onFailure(String message, Throwable e) {
                   if (listener != null) {
                       listener.onFailure(message, e);
                   }
               }
            });
        }
        public List<Data> getDataList() {
            return mDataList;
        }
    }
    

    Activity/Fragment初始化DataManager之后,只需要将数据源绑定到Adapter,loadData设置的回调告诉上层还有没有更多数据,UI层调用adapter.notifyDataSetChanged( );至于数据从哪来,分页逻辑,根本不需要UI层管理。UI层只需要通过loadData(refresh),告诉DataManager是否需要重新加载分页,与下拉刷新的逻辑完美契合。

    当然,在此基础上实现数据库缓存读写,也毫无压力。DataManager也很容易实现对某一数据的多个接口的统一管理,通过单例模式或者其他管理方法,将数据配发给多个页面。

    • 优点:大幅减轻Activity/Fragment的压力,实现数据统一管理,DataManager层成为了一个UI无关的AppSDK层.
    • 缺点:需要添加嵌套回调,这个问题在引入RxJava之后被完美处理。

    其实到了这一步,已经能满足大多数几万行代码规模中小App的框架需求了,而且分层架构统一处理数据以及代码复用度高的特点,使得项目中按照框架思路实现业务成为最快速可靠的开发方法。我认为一个优秀的框架,很重要的特性就是方便业务开发而不是给开发找麻烦,比如在分层设计过后,就算开发时间再紧张,依托分层框架依然是最快最保险的开发方法,假如某个接口直接在UI中写了,就意味着数据管理层提供的一切便利都无法直接使用,而且假如其他UI用到这个接口,还得再复制粘贴一遍改来改去,相反,依托框架,网络调用只实现一遍,上层即可重复使用这一业务接口(比较典型的:关注、收藏等),即便如此,项目规模进一步往上之后,DataManager,Activity/Fragment的压力仍然会增大,更高的测试需求,要求进一步分离Activity/Fragment的代码。这时候就可以看看MVP和MVVM了。

    3. MVP架构

    MVC的C是即持有具体Model,又持有具体View,所以C很臃肿,分层架构就算抽出了DataManager,实质上仍然是一个MVC架构,而MVP和MVVM则是C持有具体View这个问题做了点文章,其中MVP就是将大量的View <-> Model 交互剥离出来交由Presenter,Presenter持有抽象的View。

    在去年写这个回答的时候,我曾经写过这么一段:看上去很美好,但是网上很多博客的那种Demo写法我在尝试应用中发现并不实用,就是抽象出很多View接口,然后建立Presenter类来作为Presenter,这样做写些简单的列表获取,登录之类看起来很漂亮,好像做到了代码分离,但是业务场景一复杂就有点蛋疼

    那个时候我还仅仅只是尝试,不实用是一个很感性的认识,也没有多说,那时候是在做一个商城应用,使用MVP编写诸如购物车之类复杂场景的时候遇到了很大的困难,以至于让我怀疑我是不是在用MVP给自己找麻烦,写登录这些还好,写到购物车的时候我就开始怀疑人生了。一个ICartView,我要写多少接口?购物车查删改、优惠券满减查、凑单、价格计算、运费……二十个接口少不了吧?那么这个抽象的View除了给CartActivity用,还有其它什么卵用吗?假如我写成ICartView,IBonusView,IXXXView……可是有的界面并不需要删改购物车列表啊,难道我还要再细分?然后让Activity实现一堆接口?搞成这个样子,假如哪天需求变了怎么办……Presenter听起来很吊,主导者啊,但是没有Activity和Fragment的资源啊,我要怎么才能让它主导?需要获取系统的一些信息(需要Context)的时候怎么办?不持有Context难道再开接口吗?写这么多接口,接口实现,Presenter,多写了几百行代码n个类,就为了把1~200行代码从Activity移出去?还是放弃吧……

    后来Google出了TODO-MVP,但是发现跟上面那种Demo写法一样很麻烦,我也没有实际运用。后来反编译了某个大型App,发现其正好是MVP架构,于是仔细看了一下代码,就如同我最开始的想法,一个IXXXView有多少功能就写多少接口。再看看Presenter的实现,我忽然就明白我为什么会感觉不实用了:

    任何想要构建一个其他什么东西取代Activity/Fragment地位的尝试都是自找麻烦

    MVP正是一个典型。既然MVP把Activity/Fragment抽象为View,那么就意味着当它作为一个抽象View去使用的时候,生命周期,Context这些极其重要的资源Presenter是看不到的,但是这些东西是不可能不使用的。为了能让Presenter使用到这些,Presenter就必须持有Context,绑定Activity、Fragment的生命周期,就算如此,在一些需要确定使用Activity、Fragment的场合,仍需要使用强制转型。正因为Presenter这个“主导”,导致Presenter和Activity/Fragment高度绑定,Presenter和IXXXView,没有什么复用性。这是我对目前Android MVP的一点看法,如果有小伙伴有比较好的实践经验,可以在评论告诉我。

    4. MVVM(Model-View-ViewModel)架构

    在我研究MVP的时间点,MVVM也是一个很火的概念,基于data-binding框架的demo也很多,但是我看过之后立刻否决了这个方案,大部分应用在从接口获取数据后都会进行数据变换,哪怕拿到一个图片URL都会在Java层添加后缀获取缩略图,有的要根据数据源控制View大小,显隐,XML能做的事情太少了,如果将Model绑定到XML,大规模应用将会面临多少坑……

    MVVM相比于MVP,最重要的一个概念就是“数据绑定”!Presenter还持有抽象的View,ViewModel连这个都不需要,View通过ViewModel订阅其所需的数据源,ViewModel向View提供改变数据的接口,当View的操作引起数据改变或者数据源发生改变时,ViewModel通过订阅告知View,View进行视图更新。这就是MVVM吸引人的地方,ViewModel只提供数据订阅和数据接口,做到了与UI分离,ViewModel体量比Presenter小,复用性要比Presenter强太多,而且基于分层架构可以做到小幅修改就能实现。唯一的痛点在于:如何实现数据绑定?

    android-architecture-components

    之前提到的data-binding,并不是那么如意,而这次Google I/O 2017放出的android-architecture-components则很好的解决了这个问题。

    • ViewModel组件
      规范了ViewModel的所处地位,生命周期,生成方式,以及一个Activity下多个Fragment共享ViewModel数据的问题
    • LiveData组件
      提供了在Java层面View订阅ViewModel数据源的实现方案,很轻量。

    ViewModel的引入能够很好应对Activity销毁重建时大规模数据的恢复问题,以及多个界面依赖一个接口返回数据的场景,在这两个组件的规范下实现MVVM架构会十分容易,而且十分有意义。

    由于我已经在项目中大规模使用了RxJava,因此数据绑定我是采用RxJava方案实现的

    关于使用 android-architecture-components 组件实现MVVM的方案可以参考:
    googlesamples/android-architecture-components
    关于 新型MVVM结构的思路,推荐这三篇文章
    Android官方架构组件指南
    Android官方架构组件介绍之ViewModel
    Android官方架构组件介绍之LiveData

    5. 组件化和插件化

    这两年来这两个概念很火,但需要注意的一点是,这两个概念和上面的东西并不是一个层级的,组件化和插件化是比上面说的那一堆乱七八糟更上层的东西,是针对整个大工程下的若干小模块来说的,而这些小模块怎样搭建,则还是上面那些内容:)

    6. 一点总结

    一般来说我们做App,比如小外包,其实是用不到MVP,MVVM这样的架构的,一个分层架构就足以让我们快速高效的开发出App,选用什么框架,不仅要看你的应用类型,也要看你的应用规模,在分层架构的基础上,只要接口实现的足够好,代码够规范,切换到MVVM这样的架构也不是什么很难的事情。

    如果你有现成成熟的框架那无需多言,但如果你的应用只有几千行代码,为了追求MVVM,写了十几个类,踩了若干坑,只为了把一个Activity中的几十行代码抽到ViewModel里面,岂不是南辕北辙?

    最后分享一个我自己的代码库和基础框架工程,有没集成RxJava的基础分支、集成了RxJava的分层框架分支,还有一个使用android-arch-components的mvvm-rx分支,目前网络调用这块还不怎么完善,后面会逐步完善演示示例,希望能帮助到大家。

    ShonLin/QuickDevFramework

    最后鸣谢:知乎 0x8421bcd

  • 相关阅读:
    北京燃气IC卡充值笔记
    随机分析、随机控制等科目在量化投资、计算金融方向有哪些应用?
    量化交易平台大全
    Doctor of Philosophy in Computational and Mathematical Engineering
    Institute for Computational and Mathematical Engineering
    Requirements for the Master of Science in Computational and Mathematical Engineering
    MSc in Mathematical and Computational Finance
    万字长文:详解多智能体强化学习的基础和应用
    数据处理思想和程序架构: 使用Mbedtls包中的SSL,和服务器进行网络加密通信
    31-STM32+W5500+AIR202/302基本控制篇-功能优化-W5500移植mbedtls库以SSL方式连接MQTT服务器(单向忽略认证)
  • 原文地址:https://www.cnblogs.com/wytiger/p/11346091.html
Copyright © 2011-2022 走看看