【译】Android应用架构

作者: 小鄧子 | 来源:发表于2015-12-14 13:10 被阅读27408次

Android开发生态圈的节奏非常之快。每周都会有新的工具诞生,类库的更新,博客的发表以及技术探讨。如果你外出度假一个月,当你回来的时候可能已经发布了新版本的Support Library或者Play Services

我与Ribot Team一起做Android应用已经超过三年了。这段时间,我们所构建的Android应用架构和技术也在不断地演变。本文将向您阐述我们的经验,错误以及架构变化背后的原因。

曾经的架构

追溯到2012年我们的代码库使用的是基本结构,那个时候我们没有使用任何第三方网络类库,而且AsyncTask也是我们的好朋友。当时的架构可以大致表示为下图。

代码被划分为两层结构:Data Layer(数据层)负责从REST API或者持久数据存储区检索和存储数据;View Layer(视图层)的职责是处理并将数据展示在UI上。

APIProvider提供了一些方法,使Activity和Fragment能够很容易的实现与REST API的数据交互。这些方法使用URLConnection和AsyncTask在一个单独的线程内执行网络请求,然后通过回调将结果返回给Activity。

按照同样的方式,CacheProvider 所包含的方法负责从SharedPreferences和SQLite数据库检索和存储数据。同样使用回调的方式,将结果传回Activity。

存在的问题:

使用这种结构,最主要的问题在于View Layer持有太多的职责。想象一个简单且常见的场景,应用需要加载一个博客文章列表,然后缓存这些条目到SQLite数据库,最后将他们展示到ListView等列表视图上。Activity要做到以下几个步骤:

  1. 通过APIProvider调用loadPosts方法(回调)

  2. 等待APIProvider的回调结果,然后调用CacheProvider中的savePosts方法(回调)

  3. 等待CacheProvider的回调结果,然后将这些文章展示到ListView等列表视图上

  4. 分别处理APIProvider和CacheProvider回调中潜在的异常。

这是一个非常简单的例子,在实际开发环境中REST API返回的数据可能并不是View直接需要的。因此,Activity在进行展示之前不得不通过某种方式将数据进行转换或过滤。另一个常见的情况是,调用loadPosts( )所需要的参数,需要事先从其他地方获取到,比如,需要Play Services SDK提供一个Email地址参数。就像SDK通过异步回调的方式返回Email地址,这就意味着现在我们至少有三层嵌套的回调。如果继续添加复杂的业务逻辑,这种架构就会陷入众所周知的Callback Hell(回调地狱)

总结:

  • Activitty和Fragment变得非常庞大并且难以维护。

  • 太多的回调嵌套意味着丑陋的代码结构而且不易读懂和理解。如果在这个基础上做更改或者添加新特性会感到很痛苦。

  • 单元测试变得非常有挑战性,如果有可能的话,因为很多逻辑都留在了Activity或者Fragment中,这样进行单元测试是很艰难的。

RxJava驱动的新型架构

我们使用上文提到的组织架构差不多两年的时间。在那段时间内,我们做了一些改进,稍微缓解了上述问题。例如,我们添加了一些Helper Class(帮助类)用来减少Activity和Fragment中的代码,在APIProvider中使用了Volley。尽管做出了这些改变,我们应用程序的代码还是不能进行友好的测试,并且Callback Hell(回调地狱)的问题还是经常发生。

直到2014年我们开始了解RxJava。在尝试了几个示例项目之后,我们意识到她可能最终帮助我们解决掉嵌套回调的问题。如果你还不熟悉响应式编程,可以阅读本文(译者注:译文点这里那些年我们错过的响应式编程)。简而言之,RxJava允许通过异步流的方式处理数据,并且提供了很多操作符,你可以将这些操作符作用于流上从而实现转换,过滤或者合并数据等操作

考虑到经历了前几年的痛苦,我们开始考虑,一个新的应用程序体系架构看起来会是怎样的。因此,我们想出了这个。

类似于第一种架构,这种体系架构同样被划分为Data LayerView LayerData Layer持有DataManager和一系列的Helper classeView Layer由Android的Framework组件组成,例如,Fragment,Activity,ViewGroup等。

Helper classes(图标中的第三列)有着非常特殊的职责以及简洁的实现方式。例如,很多项目需要一些帮助类对REST API进行访问,从数据库读取数据,或者与三方SDK进行交互等。不同的应用拥有不同数量的帮助类,但也存在着一些共性:

  • PreferencesHelper:从SharedPreferences读取和存储数据。

  • DatabaseHelper:处理操作SQLite数据库。

  • Retrofit services:执行访问REST API,我们现在使用Retrofit来代替Volley,因为它天生支持RxJava。而且也更好用。

帮助类里面的大多数public方法都会返回RxJava的Observable

DataManager是整个架构中的大脑。它广泛的使用了RxJava的操作符用来合并,过滤和转换从帮助类中返回的数据。DataManager旨在减少Activity和Fragment的工作量,它们(译者注:指Activity和Fragment)要做的就是展示已经准备好的数据而不需要再进行转换了。

下面这段代码展示了一个DataManager方法可能的样子。这个简单的示例方法如下:

  1. 调用Retrofit service从REST API加载一个博客文章列表

  2. 使用DatabaseHelper保存文章到本地数据库,达到缓存的目的

  3. 筛选出今天发表的博客,因为那才是View Layer想要展示的。

public Observable<Post> loadTodayPosts() {
            return mRetrofitService.loadPosts()
                    .concatMap(new Func1<List<Post>, Observable<Post>>() {
                        @Override
                        public Observable<Post> call(List<Post> apiPosts) {
                            return mDatabaseHelper.savePosts(apiPosts);
                        }
                    })
                    .filter(new Func1<Post, Boolean>() {
                        @Override
                        public Boolean call(Post post) {
                            return isToday(post.date);
                        }
                    });
    }

View Layer中诸如Activity或者Fragment等组件只需调用这个方法,然后订阅返回的Observable即可。一旦订阅完成,通过Observable发送的不同博客,就能够立即被添加进Adapter从而展示到RecyclerView或其他类似控件上。

这个架构的最后元素就是Event Bus(事件总线)。它允许我们在Data Layer中发送事件,以便View Layer中的多个组件都能够订阅到这些事件。比如DataManager中的退出登录方法可以发送一个事件,订阅这个事件的多个Activity在接收到该事件后就能够更改它们的UI视图,从而显示一个登出状态。

为什么这种架构更好?

  • RxJava的Observable和操作符避免了嵌套回调的出现。
  • DataManager接管了以前View Layer的部分职责。因此,它使Activity和Fragment变得更轻量了。

  • 将代码从Activity和Fragment转移到了DataManager和帮助类中,就意味着使写单元测试变得更简单。

  • 明确的职责分离和DataManager作为唯一与Data Layer进行交互的点,使这个架构变得Test-Friendly。帮助类和DataManager能够很容易的被模拟出来。

我们还存在什么问题?

  • 对于庞大和复杂的项目来讲,DataManager会变得非常的臃肿和难以维护。

  • 尽管View Layer诸如Activity和Fragment等组件变得更轻量,它们让然要处理大量的逻辑,如管理RxJava的订阅,解析错误等方面。

集成MVP

在过去的一年中,几个架构设计模式,如MVP或者MVVM在Android社区内已经越来越受欢迎了。通过在[示例工程]( sample project)和文章中进行探索后,我们发现MVP,可能给我们现有的架构带来非常价值的改进。因为当前我们的架构已经被划分为两个层(视图层和数据层),添加MVP会更自然些。我们只需要添加一个新的presenter层,然后将View中的部分代码转移到presenter就行了。

留下的Data Layer保持不变,只不过为了与这种模式保持一致性,它现在被叫做Model

Presenter负责从Model中加载数据,然后当数据准备好之后调用View中相对应的方法。还负责订阅DataManager返回的Observable。所以,他们还需要处理schedulerssubscriptions。此外,它们还能分析错误代码或者在需要的情况下为数据流提供额外的操作。例如,如果我们需要过滤一些数据而且这个相同的过滤器是不可能被重用在其他地方的,这样的话在Presenter中实现比在DataManager中或许更有意义。

下面你将看到在Presenter中一个public方法将是什么样子。这段代码订阅我们在前一节中定义的dataManager.loadTodayPosts( )所返回的Observable。

public void loadTodayPosts() {
    mMvpView.showProgressIndicator(true);
    mSubscription = mDataManager.loadTodayPosts().toList()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe(new Subscriber<List<Post>>() {
                @Override
                public void onCompleted() {
                    mMvpView.showProgressIndicator(false);
                }

                @Override
                public void onError(Throwable e) {
                    mMvpView.showProgressIndicator(false);
                    mMvpView.showError();
                }

                @Override
                public void onNext(List<Post> postsList) {
                    mMvpView.showPosts(postsList);
                }
            });
    }

mMvpView是与Presenter一起协助的View组件。通常情况下是一个ActivityFragment或者ViewGroup的实例。

像之前的架构,View Layer持有标准的Framework组件,如ViewGroup,Fragment或者Activity。最主要的不同在于这些组件不再直接订阅Observable。取而代之的是通过实现MvpView接口,然后提供一些列简洁的方法函数,比如showError( )或者showProgressIndicator( )。这个View组件也负责处理用户交互,如点击事件和调用相应Presenter中的正确方法。例如,我有一个按钮用来加载博客列表,Activity将会在点击事件的监听中调用presenter.loadTodayPosts( )

如果你想看到一个完整的运用MVP基本架构的工作示例,可以从Github检出我们的Android Boilerplate project。也可以从这里阅读关于它的更多信息Ribot的架构指导

为什么这种架构更好?

  • Activity和Fragment变得非常轻量。他们唯一的职责就是建立/更新UI和处理用户事件。因此,他们变得更容易维护。

  • 现在我们通过模拟View Layer可以很容易的编写出单元测试。之前这些代码是View Layer的一部分,所以我们很难对它进行单元测试。整个架构变得测试友好。

  • 如果DataManager变得臃肿,我们可以通过转移一些代码到Presenter来缓解这个问题。

我们依然存在哪些问题?

  • 当代码库变得非常庞大和复杂时,单一的DataManager依然是一个问题。虽然我们还没有走到这一步,但这是一个真正值得注意的问题,我们已经意识到了这一点,它可能发生。

值得一提的是它并不是一个完美的架构。事实上,不要天真的认为这是一个独特且完美的方案,能够解决你所有的问题。Android生态系统将保持快速发展的步伐,我们必须继续探索。不断地阅读和尝试,这样我们才能找到更好的办法来继续构建优秀的Android应用程序。

相关文章

网友评论

  • 肥哥竹竿:准备适用一下
  • 性冷淡_:MVP架构真的清晰
  • 958f33bbf444:特意用QQ注册了个号来感谢下楼主,最近写项目写到头痛,公司原来没有架构,从0开始,不停的重构重构重构,功能写完,感觉不对劲又上网到处找资料看,小邓子这篇文章对我启迪很大。感谢。
    小鄧子:@chinalin 能够帮助你,是我的荣幸,同时也要感谢原作者,我只是一个翻译的搬运工
  • Broncho:个人感觉,如果项目中用到了rxjava,就可以用rxbus替换掉过时的eventbus了:joy::joy:
  • 黑丫山上小旋风:看完凳子哥这篇 启发很大啊
  • zk0301:老实说,你能遇到几个规范的大型项目? 除非你在大公司咯.很多小公司急功近利,项目开发追求速度快,哪管什么封装架构.
    小鄧子:@zk0301 很多小公司也是非常的优秀的
  • 接地气的二呆:还是不理解 为什么要有这个dataManager 这一层, Presenter直接调用后面的 Dbhelper 层 和其他的 helper 层不好么~ 求解答~ :no_mouth:
    Broncho:@接地气的二呆 datamanager方便管理所有model层逻辑,也方便测试,dbhelper只能算datamanager中的一个小分支吧!
  • LITTLEDREAM:能结合dagger再来一篇架构的东西不!嘿嘿!
  • 满天丿星:有没有人知道那个架构图是用什么工具画的
  • 忽来:此博文前前后后读了三次,今天才真正闹明白,谢谢博主的翻译,受益匪浅!
    文中提到的DataManager爆炸的问题,其实用多个DataManager类是可以解决的。比如用户相关的放在UserDataManager中,日志相关的放在JournalDataManager中。
    小鄧子:@leege100 非常高兴能对你有所帮助,架构的演变是迅速的,几个月过去了,我也在这个基础上进行了很多的实践与改变。
  • 5745652955f0:博主老牛逼了
  • 隔壁_老王:牛,MVP和EventBus和Retrofit(Callback方式)已经用1年半了,现在正在尝试RxJava,不说RxJava,只是MVP就非常棒,重用、可维护、可测试性非常吊~哈哈,多谢你的翻译~
  • 田野光:感觉MVP示例代码中View接口的定义很奇怪,
    @Override
    public void onError(Throwable e) {
    mMvpView.showProgressIndicator(false);
    mMvpView.showError();
    }
    其实View只需要定义一个showError()方法,停止ProgressIndicator应该是View层内部的逻辑,p层显然不应该操心具体UI显示什么,不显示什么
    青岛大桥_Android到后端: @田野光 showError如果只看表面语意,容易忘了关掉进度条。所以作者设计也有道理。其实怎样都对,看团队习惯
  • 晒太阳的Mozzie:我的天啊,我实践了一下,DataManager爆炸辣。。。
  • 7a8e193e04b7:有你上面说的那些知识的参考项目么?
  • 7a8e193e04b7:安卓菜鸟,求指导
  • 裸奔的凯子哥:我昨天还在微博发过,原来是橙子兄发的:+1:
    裸奔的凯子哥:@小鄧子 哦哦,这样啊,这篇文章确实不错,技术架构很优秀,虽然我没实践过😅
    小鄧子:@裸奔的凯子哥 不过,你发的那个我也有看,不是我翻译的:relieved:
    小鄧子:@裸奔的凯子哥 哈哈哈,凯子哥,调皮了:stuck_out_tongue_closed_eyes:
  • 7b678a644127:很不错的文章
  • android_Joe:路过,学习了.......
  • smallSohoSolo:准备实践一下了
  • 7650769b4b02:非常棒的小邓子 赞
  • SongNick:看来我要在项目中实践一下了
  • 司空长卿:哎呀,我之前评论的那篇是你写的呀,完全没看作者,搜瑞,哈哈
    小鄧子:@司空长卿 没事,谁写的都能评论啊,这只能代表原作者的观点,而且他也表明这不是一个完美的方案,所以无论是他还是我们都要不断地进行尝试和探索。:relaxed:
  • 20e5ae09c353:。。。。不错
  • 司空长卿:论架构,这种架构不如androidannotations 结合retrofit
    ailurus:@司空长卿 这只能算一个工具吧...离架构还差很远呢...
    司空长卿:@小鄧子 androidannotations 我个人叫它注解静态编译框架,在编辑器编写代码时期根据注解配置生成安卓原生代码,代替一般框架注解加反射引起的性能问题,另外一个更重要的是可以解决网络异步回调接口修改UI的问题,在使用者看起来完全是同步操作,非常简洁。
    小鄧子:那是什么?科普一下吧,我没用过
  • MrSimp1e0:小鄧子 :) 欢迎提交到 开发技术前线 https://github.com/bboyfeiyu/android-tech-frontier/i ,我这边翻译了这篇文章的一半,刚好看到朋友圈有人分享你的文章。谢谢哈
  • 0d1da96e9467:正是需要
  • ailurus:文章写得好棒!感觉做项目的思路更清晰了。
    小鄧子:@ailurus 我只负责翻译。。。
  • 我是Asha:把数据处理逻辑全放到DataManager里...这太要命了...还是各自接口负责处理各自逻辑比较好,不然全是conflict
    小鄧子:@我是Asha 如果那样的话也会造成接口爆炸。很显然真的没有一个十全十美的方法。
  • 113aa3aaae7e:赞一个
  • 码上新视界:越来越有范了
    码上新视界: @小鄧子 看着你的帖子长大的 😄
    小鄧子:@cyning 什么意思:flushed:
  • 296d6ebc6cd2:层级可以尝试再多几层:smiley://纵横两向。
  • 10Buns:最后一句好评,没有完美的架构,只有目前最适合的
    0dcc0e483842:我经常纠结,有些内容写在P层好还是写在M层好,最后总结了:在有规矩的情况下,怎么爽怎么写
    soaringEveryday:@YoKey 比如RxJava也并不一定是合适的。有的项目嵌套回调几乎没有,接口端设计已经避免,而且项目组成员基本都没有接触过响应式编程,那么就不合适引入RxJava这玩意,反而把自己弄死了。任何技术,除了别人所宣称的先进性,还要看下是否适合自己的项目实际
    YoKey:@10Buns 赞同 很难说某个架构好或不好 毕竟每个人的代码风格 写代码思路 所处的项目环境并不一样 别人的架构作为参考是最合适的
  • hmz:♥ω♥

本文标题:【译】Android应用架构

本文链接:https://www.haomeiwen.com/subject/lajyhttx.html