4

个人偏爱的Android应用架构

 3 years ago
source link: https://aprildown.xyz/2021/04/18/android-opinoinated-arch/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

根据几年来的开发经验和阅读过的代码,总结出了个人觉得还算稳定可持续的Android应用架构。它以MVVM为基础,融合了Clean Architecture,参考了iosched,尽可能地照顾到测试和扩展。

不定期更新新的想法。

应用细分了许多Gradle Module,且除顶层app外,每个Module都是Android Library(com.android.library)。

应用的调用逻辑为Activity -> Fragment -> ViewModel -> UseCase -> Repository。我们按照Moule依赖顺序,从UseCase开始。

这里重度使用依赖注入,并尽可能使用了@Reusable

domain

最底层的基础Module,之后的所有Module都会隐式地依赖它。定义了:

  • Entity:用到的所有Java Bean或者Kotlin data class
  • Repository:全部为interface。提供各种底层操作,每个操作尽可能只完成简单的一件事,而将具体的整合留给下一位。
  • UseCase:一个抽象类为CoroutineUseCase。继承时,依赖注入CoroutineDispatcher和将使用到的Repository。定义了所有用到的操作(一个简单的例子)。

    • 为了照顾单元测试,避免使用Android专用代码(例如使用Kotlin Flow替代LiveData)。
    • 关于在应用的哪一层捕获异常,是一个见仁见智的问题。我个人偏向在UseCase中捕获异常。有异常就封装到一个Result<Xxx>(和Kotlin中的Result类似,为了避免烦人的警告而手动定义)中,没异常就直接返回。
  • 常量、日志、异常、Feature Flag等基础配置

  • 统计、崩溃收集的接口

data

api project(":domain")

实现了domDaain中的Repository,并用Dagger的@Binds绑定到对应的Repository。定义了并实现了数据库、网络、本地储存、WorkManager等来完成Repository的任务。

  • 使用Room时,数据在本Module定义为XxData(包含了数据的字段定义),而Repository中要返回XxEntity,所以在返回前要手动或定义专门的Mapper类进行转换。

    这么做可以将具体实现和应用逻辑分割。UI层直接处理简单的data class,而无需担心JSON、数据库等配置。

  • 得益于Hilt,绑定Repository的类可以定义为私有类。

  • 不在本Module中存放资源。非要不可的话,要么依赖注入,要么在res/values/ids.xml定义一个同名资源来引用。
  • 因为基本全是内部实现,所以几乎都是internal。配合Hilt应该可以全部为internal

presentation

在有了Hilt之后,本Module可以被删除,而将ViewModel移动到各个功能Module中。

api project(":domain")

定义了所有用到的ViewModel,ViewModel中被注入了各个需要的UseCase。

  • 业务逻辑应该在UseCase中,所以ViewModel中尽可能保持简单。
  • 虽然Google官方建议在Kotlin-only的项目中用Flow替代LiveData,但我习惯在ViweModel中将Flow调用asLiveData()转换后给UI层使用,因为在UI层使用Flow坑太多。

app-base

app-开头的Module将会按应用的不同功能分类,而本Module是它们的基础Module。

api project(":domain")
  • 各种UI层(Android层)的基础UI类、通用View、通用工具类。
  • 除了各个功能Module的特有资源,其他资源都放在这里。例如应用图标、通用图标、颜色、主题、翻译等。

app-xxx

api project(":app-base")
// api project(":prentation") // Hilt之前

将应用按功能分类,包含了功能的界面、Activity、Framgnet等。用了Hilt之后,也可以把ViewModel放进来。

app

implementation project(":domain")
implementation project(":data")
// implementation project(":prentation") // Hilt之前
implementation project(":app-base")
implementation project(":app-xxx")

compileOnly ... // Dagger可能会抱怨找不到data中使用的一些类,所以加进来就可以。

应用入口,也是仅有依赖了data Module的一处。这样将应用实现细节通过依赖注入抽象出来,可以达到代码不互相污染、提升编译速度、方便测试、提高逆向难度、提高可扩展性等的效果。

  • Navigation Component配置如下:

    • app-baseres/values/ids.xml定义了一群id,代表各个目的地或操作。
    • app-xxx中实现各个Fragment,并按需使用其他id。
    • app的Navigation graph中,将id和Fragment或操作对应起来。

    这么做的缺点是:

    • 无法保证编译时安全,少配置了东西只有在运行才会崩溃,但我还没找到更好的办法。
    • 不能使用生成的参数类。解决办法是乖乖bundleOf

各种使用场景

  • 单元测试主要是UseCase,可以新建一个测试Module,也可以在domain中就地解决。
  • Instrumented Test可以新建一个测试Module,也可以在app中就地解决。
  • 国产SDK

    因为它们代码混乱、污染名称空间,最好把它们丢到一个专门的Module(例如app-trashapp-third)中养蛊。如果它们只提供JAR或AAR,就把文件引用为一个新Module(New -> New Module -> Import .JAR/.AAR Package),然后再在我们的养蛊Module中,引用它们。之后在app-base中定义interfaceapp-trash中实现,app中依赖绑定,app-xxx注入使用。

    上述方法看起来很麻烦,但因为国产SDK太过恶心、给人造成的麻烦太多,怎么加抽象都不为过。另外,有了Lifecycle、新的Activity Result API等,上述方法也容易了些。

    个人经验:永远不要直接引用它们,哪怕多加几个抽象Module。

  • 因为现有的MVI框架还不够成气候,所以现在还是MVVM。但MVI最重要的是思想,Jetpack Compose也是有类似的思想。在将ViewModel精简、将逻辑放到UseCase后,实现起来还是容易的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK