一种更清晰的 Android 架构

发布于 2024-09-08 18:16:44 字数 4839 浏览 8 评论 0

过去几个月以来,通过在 Tuenti 网站上与 @pedro_g_s 和 @flipper83(安卓开发两位大牛)进行友好讨论之后,我决定写这篇关于架构安卓应用的文章。

我写这篇文章的目的是想把我在过去几个月体悟到的小方法以及在调查和应用中学到的有用的东西分享给大家。

入门指南

大家都知道要写一款精品软件是有难度且很复杂的:不仅要满足特定要求,而且软件还必须具有稳健性,可维护、可测试性强,并且能够灵活适应各种发展与变化。这时候,“清晰架构”就应运而生了,这一架构在开发任何软件应用的时候用起来非常顺手。

这个思路很简单:简洁架构 意味着产品系统中遵循一系列的习惯原则:

  • 框架独立性
  • 可测试
  • UI 独立性
  • 数据库独立性
  • 任何外部代理模块的独立性

我们并不要求一定要用四环结构(如图所示),这只是一个示例图解,但是要考虑的是依赖项规则:源码依赖项只能向内指向,内环里的所有项不能了解外环所发生的东西。

以下是更好地理解和熟悉本方法的一些相关词汇:

  • Entities:是指一款应用的业务对象
  • Use cases:是指结合数据流和实体中的用例,也称为 Interactor
  • Interface Adapters: 这一组适配器,是负责以最合理的格式转换用例(use cases)和实体(entities)之间的数据,表现层(Presenters )和控制层(Controllers ),就属于这一块的。
  • Frameworks and Drivers: 这里是所有具体的实现了:比如:UI,工具类,基础框架,等等。

想要更具体,更生动丰富的解释,可以参考 这篇文章 或者 这个视频

场景

我会设置一个简单的场景来开始:创建一个简单的小 app,app 中显示从云端获取的一个朋友或用户列表。当点击其中任何一个时,会打开一个新的窗口,显示该用户的详细信息。这里我放了一段视频,大家看看 这个视频 (需翻墙) 大概就可以对我所描述的东西了解个大概了。

Android 应用架构

这一对象遵循关注分离原则,也就是通过业务规则让内环操作对外环事物一无所知,这样一来,在测试时它们就不会依赖任何的外部元素了。
要达到这个目的,我的建议就是把一个项目分成三个层次,每个层次拥有自己的目的并且各自独立于堆放运作。
值得一提的是,每一层次使用其自有的数据模型以达到独立性的目的(大家可以看到,在代码中需要一个数据映射器来完成数据转换。如果你不想把你的模型和整个应用交叉使用,这是你要付出的代价)。

注:我并没有使用任何的外部库(除了用于 json 数据句法分析的 gson 和用于测试的 junit, mockito, robolectric 和 espresso 以外)。原因是它可以使这个示例更清晰。总之,在存储磁盘数据时,记得加上 ORM、依赖注入框架或者你熟悉的任何工具或库,这些都会带来很大帮助。(记住:重复制造轮子可不是明智的选择)

表现层 (Presentation Layer)

表现层在此,表现的是与视图和动画相关的逻辑。这里仅用了一个 Model View Presenter(下文简称 MVP),但是大家也可以用 MVC 或 MVVM 等模式。这里我不再赘述细节,但是需要强调的是,这里的 fragment 和 activity 都是 View,其内部除了 UI 逻辑以外没有其他逻辑,这也是所有渲染的东西发生的地方。
本层次的 Presenter 由多个 interactor(用例)组成,Presenter 在 android UI 线程以外的新线程里工作,并通过回调将要渲染到 View 上的数据传递回来。

如果你需要一个使用 MVP 和 MVVM 的 Effective Android UI 典型案例,可以参考我朋友 Pedro Gómez 的文章。

领域层 (Domain Layer)

这里的业务规则是指所有在本层发生的逻辑。对于 Android 项目来说,大家还可以看到所有的 interactor(用例)实施。这一层是纯粹的 java 模块,没有任何的 Android 依赖性。当涉及到业务对象时,所有的外部组件都使用接口。

数据层 (Data Layer)

应用所需的所有数据都来自这一层中的 UserRepository 实现(接口在领域层)。这一实现采用了 Repository Pattern ,主要策略是通过一个工厂根据一定的条件选取不同的数据来源。
比如,通过 ID 获取一个用户时,如果这个用户在缓存中已经存在,则硬盘缓存数据源会被选中,否则会通过向云端发起请求获取数据,然后存储到硬盘缓存。

这一切背后的原理是由于原始数据对于客户端是透明的,客户端并不关心数据是来源于内存、硬盘还是云端,它需要关心的是数据可以正确地获取到。

注:在代码方面,出于学习目的,我通过文件系统和 Android preference 实现了一个简单、原始的硬盘缓存。请记住,如果已经存在了能够完成这些工作的库,就不要重复制造轮子。

错误处理

这是一个长期待解决的讨论话题,如果大家能够分享各自的解决方案,那真真是极好的。
我的策略是使用回调,这样的话,如果数据仓库发生了变化,回调有两个方法:onResponse() 和 onError(). onError 方法将异常信息封装到一个 ErrorBundle 对象中: 这种方法的难点在于这其中会存在一环扣一环的回调链,错误会沿着这条回调链到达展示层。因此会牺牲一点代码的可读性。另外,如果出现错误,我本来可以通过事件总线系统抛出事件,但是这种实现方式类似于使用 C 语言的 goto 语法。在我看来,当你订阅多个事件时,如果不能很好的控制,你可能会被弄得晕头转向。

测试

关于测试方面,我根据不同的层来选择不同的方法:

  • 展示层 ( Presentation Layer) : 使用 android instrumentation 和 espresso 进行集成和功能测试
  • 领域层 ( Domain Layer) : 使用 JUnit 和 Mockito 进行单元测试;
  • 数据层 ( Data Layer) : 使用 Robolectric ( 因为依赖于 Android SDK 中的类 )进行集成测试和单元测试。

代码展示

我猜你现在在想,扯了那么久的淡,代码究竟在哪里呢? 好吧,这就是你可以找到上述解决方案的 github 链接 。还要提一点,在文件夹结构方面,不同的层是通过以下不同的模块反应的:

  • presentation: 展示层的 Android 模块
  • domain: 一个没有 android 依赖的 java 模块
  • data: 一个数据获取来源的 android 模块。
  • data-test: 数据层测试,由于使用 Robolectric 存在一些限制,所以我得再独立的 java 模块中使用。

结论

正如 Bob 大叔 所说:“Architecture is About Intent, not Frameworks” ,我非常同意这个说法,当然了,有很多不同的方法做不同的事情(不同的实现方法),我很确定,你每天(像我一样)会面临很多挑战,但是遵循这些方法,可以确保你的应用会:

  • 易维护 Easy to maintain
  • 易测试 Easy to tes.
  • 高内聚 Very cohesive.
  • 低耦合 Decoupled.

最后,我强烈推荐你去实践一下,并且分享你的经验。也许你会找到更好的解决方案:我们都知道,不断提升自己是一件件非常好的事。我希望这篇文章对你有所帮助,欢迎拍砖。

参考资料

  1. Source code: https://github.com/android10/Android-CleanArchitecture
  2. The clean architecture by Uncle Bob
  3. Architecture is about Intent, not Frameworks
  4. Model View Presenter
  5. Repository Pattern by Martin Fowler
  6. Android Design Patterns Presentation

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

メ斷腸人バ

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

謌踐踏愛綪

文章 0 评论 0

开始看清了

文章 0 评论 0

高速公鹿

文章 0 评论 0

alipaysp_PLnULTzf66

文章 0 评论 0

热情消退

文章 0 评论 0

白色月光

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文