Architecture

Architecture Components

以前 Android Developers 网站没有特别提及过写 Android app 应该用甚麽 architecture,直至到近年 Google 因应社群的要求才建议使用 MVVM并推出了对应 MVVM 的 Architecture Components library。下图是 Android Developers 网站建议的 architecture:

https://ithelp.ithome.com.tw/upload/images/20210916/201396668d7RoPJMXC.png

它主要特色是:

  • Activity/Fragment 负责 UI 部分
  • Repository 是外露资料处理的 method 让 ViewModel call,使 ViewModel 无须得知资料的实质来源地
  • ViewModel 是把 Activity/Fragment 和 Repository 粘合在一起。它外露一些 method 触发资料的处理(例如一个按钮的 onClick listener 会 call ViewModel 的一个 method,ViewModel 会 call Repository 发送资料去 server),而 Repository 处理资料後的结果会透过 ViewModel 外露的 LiveData 然後在 Activity/Fragment observe 并按所收到的东西再更新 UI

如果 app 是一般 CRUD (Create, Read, Update, Delete) 类型的话这个架构应该足以应付。但如果 app 有很多 business logic 要处理的话(以计算地铁车费为例,buiness logic 就是把 SQLite database 取得的由 A 站到 B 站的原车费按不同的情景加工处理,然後输出折实价给 UI 显示),这些 business logic code 可能会塞去 ViewModel 或 Repository,结果令那些 class 塞了太多 code。所以就出现了我们常常听到的 Clean Architecture。在 Android app 一般的做法是把上图的 Repository 和 ViewModel 加插 Use Case 或 Interactor 来塞 business logic,那些 use case 或 interactor 就是 Domain layer;Activity/Fragment 至 ViewModel 是 presentation layer;repository 至 SQLite/Retrofit 是 Data layer。

Modularization

Clean Architecture 没有特定标准,所以有些比较讲究的人会开设不同的 Gradle module 放置不同部分的 code 而不是把所有的 code 都放到同一个 module 内。至於如何界开不同的 module(即是「modularization」)亦都是一个不会有结论的话题。

最简单的方法是:

  • Presentation(放 UI 相关的东西,即是 Activity/Fragment/ViewModel)
  • Domain(放 Use case 的 interface 和 concrete implementation、Repository interface)
  • Data(放 Repository 的 concrete implementation、供 Local 和 Remote implement 的 data source interface)
  • Local(处理本机资料来源,例如存取 Shared Preferences、Data Store、SQLite 之类并 implement data source interface 供 Data 使用)
  • Remote(处理远端资料来源,例如 HTTP request 和 Web socket 之类并 implement data source interface 供 Data 使用)

好处是可以尽量把 module 设定成纯 Kotlin/Java module,例如是 Domain、Data 和 Remote。那就减少乱写 code 的情况(比如说是在 HTTP response error 时直接弹出 Toast 而不是在在 Presentation layer 决定用甚麽形式显示 error)。还有是因为 Gradle module 之间的 dependency 能控制 module 内的 class 能否接触到另一 module 的 class,所以比单纯以 Java/Kotlin package 分隔不同 layer 更有保证。如果你打算做 Kotlin Multiplatform Mobile (KMM) 的话,刻意分割成纯 Kotlin module 是必要的,因为 KMM 共用的 code 必须要是纯 Kotlin 写的,连调用 Java Standard Libaray 都不可以。

缺点是如果要修改某一个 feature 的话,你需要到每一个 module 修改 code,如果初接触这种 modularization 方式的话很容易会被搞到头昏脑胀,同时亦做不了 Play Feature Delivery。

另一种 modularization 方式是单纯的按 feature 划分,一个 feature 一个 module。一个 module 包含了该 feature 全部 layer 的 class,通常都是会用 Java/Kotlin package 划分,这样就可以做到 Play Feature Delivery。但缺点是要决定那些东西是跨 feature 共用,例如 SQLite database 是不是共用同一个,如果不共用如何保证 ACID?或者是两个 feature 都要 call 同一个 API endpoint,那应该是共用同一套 code 还是把那些 code 抄到各自的 feature module 内?当出现了 common module 放这些跨 feature 的 code 後,common module 最终会不会变成另一个垃圾岗?另外,跨 module 的 navigation 亦是一大难题,即使出了 Navigation Component 仍未有完整方案,你不能在 feature module 的 project 使用 Navigation Component 的全部功能。这可能是你不能用到 safe args、IDE 会出现红字、要自己写额外 code 去封装 Navigation Component 之类。

如果想同时做到按 feature 及 layer 分 module,可以每个 feature 都有对应的 layer module。即是Feature A 会有 Feature A 的 presentation、domain 和 data module。但这样就会有太多 module。折中方案可以是 presentation 和 domain layer 就按 feature 划分,data layer 就所有 feature 共用同一套 module。

我觉得如果是打算把现有 project 做 modularization 的话,按 layer 划分或者是折中方案或许比较好。因为很多时候开始做的时候都是对全局没有太深入的了解,未能为意那些地方是好多个 feature 都会同时用到。当你把旧有的 code 改成新写法之後,对整个 app 有更深入的理解。那时候才决定最终用那款 modularization 还未算迟。因为新 code 都或多或少按 layer 和 feature 划分,而且应该有 interface 分隔 layer 之间的 dependency,那时候再把 code 搬去另一 module 应该不用再大改现有的 code。

还有另一样东西要留意是要不要每个 layer 都有对应的 data class 表达要处理的 data。相信大家都看过一些 project 是使用同一个 class 去接收 API response、表达 SQLite database table 的 row、加上 Parcelable 用来开启 Activity/Fragment。如果 API response 的式样跟你最後使用的需要是一致的话还好,但如果 API response 的式样需要特别处理才能用的话这类多用途 data class 会很难理解。

示范 App

这个示范 app 都是用 MVVM,但就不会做 modularization,Layer 都是用 package 区分,layer 之间都是会有 interface 分隔。而因为列车抵站时间是每分钟都在变,所以这个 app 不会做 local cache,只需要定时 call API 就可以了,那就把原本 data 和 remote layer 合并成 data layer。如果我们打算由一开到 Fragment 就 call backend API 并将 response 显示在 UI 上,大概会是这样:

https://ithelp.ithome.com.tw/upload/images/20210916/20139666GeRGOgnKkr.png

数字是步骤,橙色 Fragment 有个箭头指向 Layout XML 和 ViewModel 意思是 Fragment 会建立 Binding 和 ViewModel instance。


<<:  Day04 - Gem-activerecord-import 批次建立介绍与应用

>>:  [Day2] What is Pentest

Day1:Tensorflow?Keras?

  最初接触Tensorflow的时候,还是在1.x版本,那时候Keras支援性不高,因此和同学之间...

JavaScript入门 Day15阵列

今天终於没有要讲数字的语法了,要讲的是阵列 那J个是什麽呢,他是方便存放资料的资料型态 今天若是有多...

day 15 - 从执行时间开始优化

经过了前面几天的步骤, 已经算是走过一遍本机开发到交付的流程了, 接下来再依照团队推上k8s的流程新...

LeetCode 刷题的只是写好程序的第一哩路

什麽是 LeetCode? 「什麽是 LeetCode?」是整个铁人赛系列文章的第一个主题,你现在...

[Day02] Which one is better? Oh... I mean more suitable

其实任何技术上的选型都没有最好,就像选择程序语言一样,大家都有共识 PHP 是世界上最好的...咳,...