作者: madroid
根据 App 行为的不同,我们对其进行分离/分层并确定其职责,每层之间的通讯交互采用响应式方式。
App 有三层结构,分别为 UI Layer、Domain Layer、Data Layer,其依赖关系是单向的,上层可以依赖下层,下层却不能反过来依赖上层。大致如下,其中 Domain Layer 是可选层:
每层的主要职责分别为:
1、UI Layer: 使用 UI 元素展示 App 中数据
2、Domain Layer: 封装通用的业务逻辑
3、Data Layer: 封装统一的数据来源,提供单一可信来源
每层依赖关系是单向的,UI Layer 可以依赖 Domain Layer,但是 Domain Layer 却不能依赖 UI Layer。这种依赖方式可以使用简单的函数传递依赖事件,但是却不能处理结果的回调,即 UiState 的更新。想要处理结果的回调每层之间就可以采用数据驱动/响应式的方式来交互了。这种方式也被称为是单向数据流的方式,即 UI 事件从 UI 层流向数据层,UiState 从数据层流向 UI 层。
关于 UI Layer、Domain Layer、Data Layer 中更多详细内容可以查看官方文档应用架构指南:
https://developer.android.google.cn/jetpack/guide
MVI 的全称是 Model-View-Intent,这里的 Intent 并不是指 Android 中的 Intent 类,而是表示一种意图,可以简单理解为对用户 Event 的一种抽象。其交互图大致如下:
MVI 并不像 MVC、MVP、MVVM 一样,不论是 Controller、Presenter 还是 ViewModel 都是 View 与 Model 之间的桥接类,负责这两者之间的通信与交互 (虽然 MVC 可以跨过 Controller 直接进行交互)。而 Intent 并没有类似的职责,仅仅是约束了 View 的事件通过类似枚举的方式定义,这种方式更像是前端框架中的 Flux 或者是 Redux,更多内容可以查看 Reclaim the reactivity of your state management, say no to imperative MVI,实现 MVI 的主流框架有: Orbit、Mavericks、Uniflow-kt、Mobius。
https://zhuinden.medium.com/reclaim-the-reactivity-of-your-state-management-say-no-to-imperative-mvi-3b23ca6b8537
https://github.com/orbit-mvi/orbit-mvi/blob/06e9f759a87e7192767baeebc682fc92369a7eff/orbit-core/src/commonMain/kotlin/org/orbitmvi/orbit/internal/RealContainer.kt#L74-L75
https://github.com/airbnb/mavericks/blob/e8a631a19fc1b044da3ddff358712e129dc487a6/mvrx/src/main/kotlin/com/airbnb/mvrx/CoroutinesStateStore.kt#L57-L59
https://github.com/uniflow-kt/uniflow-kt/blob/a1fdbeb733a0b550a162227be3b1e03d03197023/uniflow-core/src/main/kotlin/io/uniflow/core/flow/ActionReducer.kt#L28-L32
https://github.com/spotify/mobius
有的 MVI 在实现还需要借助 ViewModel,仅仅是把 View 的事件定义成的对应的密封类。目的仅仅是为了强制实现单向数据流的方式,根据之前介绍实现单向数据流的方式还是比较简单的,上层只能依赖下层实现,下层的处理结果通过 LiveData、Flow 方式更新。
那再来聊一下 MVC、MVP、MVVM 与 Android 官方的推荐的 MAD Arch 之间的关系。其实经常提到的 MVVM 与 Android 官方的架构还是有本质区别的。MVX (对 MVC、MVP、MVVM 的统称) 的架构方式对 Model 这一层提到的非常少,留下的印象可能就是除了 VX 之外剩下的就是 Model 的部分。但是这部分在整个 App 的架构中也是非常重要的。我们还是有大量的业务逻辑是在 Model 层处理的。
而 Android 官方的架构中却包含了这部分的描述,新增了 Data Layer 与 Domain Layer。所以总结下来就是 MVX 处理的仅仅是 UI Layer 中的问题,描述的是状态管理的部分;官方文档中描述的确是整个 App 的架构,是一种包含的关系。
无论是在哪一层都要确保其在主线程安全的,即在主线程调用不会阻塞主线程或者是抛出异常。那应该是在哪一层进行处理呐?其可选项有 ViewModel、UseCase、Repository、DataSource,只要在任何一层处理耗时操作都可以确保其是主线程安全的。这里建议采用 “就近原则”,即谁产生数据谁就保持数据的安全性。
Data Layer 中 DataSource 是 “产生” 数据的地方,在这里直接切换到对应的子线程是可以的,代码大致如下:
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
/**
* 在 IO 线程中,获取网络数据,在主线程调用是安全的
*/
suspend fun fetchLatestNews(): List = withContext(ioDispatcher) {
// 将耗时操作移动到 IO 线程中
newsApi.fetchLatestNews()
}
}
如果 Repository 中需要整合很多的 DataSource 中的数据,在 Repository 中切换到对应的子线程也是可以的,这样可以减少频繁的线程调度。
同时也需要考虑响应业务的生命周期情况,如果当前业务跟随这页面进行的,那么使用 viewModelScope 或者是 lifecycleScope 即可;如果其业务是跟随 App 的什么周期的,那么则需要使用整个 App 生命周期的 CoroutineScope;如果在 App 被终止后,仍然希望可以执行任务,那么可以考虑使用 WorkManager:
https://developer.android.google.cn/topic/libraries/architecture/workmanager
各层之间的 Entity 根据其职责定义会有所不同,可以根据具体的使用场景自定义 Entity。如云端返回的 Entity 与数据库需要存储的 Entity 可能并不相同,使用相同的 Entity 会导致代码的可维护性下降,而且没有必要暴露过多的细节。如下:
@Entity(tableName = "user")
data class RemoteUser(
@PrimaryKey
@SerializedName("user_id")
val userId: String,
val username: String,
@Ignore
val token: String,
@Ignore
val inventory: RemoteInventory,
@Ignore
val profile: RemoteProfile,
)
这种场景下,我们就可以针对云端返回数据与数据库存储数据分别定义不同的 Entity,如下:
// 云端数据 Entity
data class RemoteUser(
@SerializedName("user_id")
val userId: String,
val username: String,
val token: String,
val inventory: RemoteInventory,
val profile: RemoteProfile,
)
// 数据库 Entity
@Entity(tableName = "user")
data class UserEntity(
@PrimaryKey
val userId: String,
val username: String,
)
对于不同页面直接传递数据的场景 (Intent),建议定义单独的 Entity,因为传递数据的大小是有限的。定义大致如下:
@Parcelize
data class Inventory(
val id: UUID,
val type: String
): Parcelable
对于 UI Layer 中的实体定义,要根据其业务类型进行细分,切记不要将一页面中的所有的 UiState 都定义在同一个 Entity 中。因为汇总型的定义在相关字段的更新频率不一致的时候会导致频繁的 UI element 重复绘制,同时不可变的 Entity 的字段增加也会导致不必要的内存开销。如果一个 UiState 中有超过 5 个状态,那就需要回过来看下 UiState 是否可以进行拆分了。
UiState 中经常遇到的一个场景就是添加 Loading 状态,这种情况添加封装统一的 Wrapper 类进行处理,如下:
sealed interface UiStateWrapper {
object Loading : UiStateWrapper
class Success(val uiState: T) : UiStateWrapper
class Failure(val exception: Throwable) : UiStateWrapper
}
这种处理方式,并不需要在 UiState Entity 新增一个 isLoading 字段,保持 UiState 的 “纯洁性”,同时也可以在 UI elements 中对 UiStateWrapper 做统一的处理,不必每个 UiState 中都出 Loading 的状态,当然,这是在 Loading 处理逻辑相同的前提下的。
整体而言,根据不同职责定义不同的 Entity 会让我们的代码逻辑相对合理,但是会增加一定的工作量以及会对要使用何种 Entity 产生混淆。所以还是需要根据自己的项目及团队情况决定是否需要精细化管理 Entity,大型团队建议采用这种方式。
代码建议按照业务模块方式进行组织,而非功能进行组织。大致如下:
# DO
- Project
- feature1
- ui
- domain
- data
- feature2
- ui
- domain
- data
- feature3
不要使用如下的方式:
# DO NOT
- Project
- ui
- feature1
- feature2
- feature3
- domain
- feature1
- feature2
- feature3
- data
采用 Feature 方式组织代码的优势大致有以下几点:
整理了一些关键知识点,可以保存图片定期回顾。
文章中的内容基本上都是参考官方文档以及 Youtube 上的 mad - arch 系列。都看到这里了建议您到官方文档中的 pathawy 地址中获取下现代 Android 应用架构徽章,只要阅读完下面的文档以及完成对应测试即可。
https://www.youtube.com/watch?v=TPWmfJq16rA&list=PLWz5rJ2EKKc8GZWCbUm3tBXKeqIi3rcVX&ab_channel=AndroidDevelopers
https://developer.android.google.cn/courses/pathways/android-architecture
https://developer.android.google.cn/jetpack/guide/ui-layer
https://developer.android.google.cn/jetpack/guide/ui-layer/events
https://developer.android.google.cn/jetpack/guide/domain-layer
https://developer.android.google.cn/jetpack/guide/data-layer
https://www.youtube.com/playlist?list=PLWz5rJ2EKKc8GZWCbUm3tBXKeqIi3rcVX
https://developer.android.google.cn/courses/quizzes/android-architecture/architecture-layers?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-architecture%23quiz-%2Fcourses%2Fquizzes%2Fandroid-architecture%2Farchitecture-layers
今年的 Google I/O 发布了一个最新的官方示例 Now in Android,这个示例的完整度比之前的 JetNews、Sunflower 要高,后面也将基于这个仓库做进一步的说明解析,从一个完整项目的角度来看 Android 新推出的架构指南。
https://github.com/android/nowinandroid
这里也分享一些珍藏资源,从面试简历模板到大厂面经汇总,从大厂内部技术资料到互联网高薪必读书单,以及Android面试核心知识点(844页)和Android面试题合集2022年最新版(354页)等等,这些资料整理给大家,希望踩过的坑不要再踩,遭遇的技术瓶颈一次性消灭。
如果需要的话,可以顺手帮我点赞评论一下,直接前往公号:Android开发之家免费领取
01.Android必备底层技术:
02.Framework:
03.Android常用组件:
04.高级UI:
05.Jetpack:
06.性能优化:
如果需要的话,可以顺手帮我点赞评论一下,直接前往公号:Android开发之家免费领取
07.音视频:
08.开源框架原理:
09.Gradle:
10.kotlin:
11.Flutter:
12.鸿蒙:
如果需要的话,可以顺手帮我点赞评论一下,直接前往公号:Android开发之家免费领取
Android路漫漫,共勉!