编辑推荐: |
本文主要介绍
了Android 官方现代 App 架构指南相关知识。需要的朋友们下面来一起学习学习吧!
本文来自于稀土掘金,由Linda编辑、推荐。 |
|
回顾
在 Arch - 概述 中简述了开发高质量的 App 所面临的挑战,以及应对这种挑战我们需要关注点分离、数据驱动等设计原则为指导来帮助我们应对这种挑战。
根据 App 行为的不同,我们对其进行分离/分层并确定其职责,每层之间的通讯交互采用响应式方式。
App 有三层结构,分别为 UI Layer、Domain Layer、Data Layer,其依赖关系是单向的,上层可以依赖下层,下层却不能反过来依赖上层。大致如下,其中
Domain Layer 是可选层:
每层的主要职责分别为:
UI Layer:使用 UI 元素展示 App 中数据
将底层数据处理成容易被 UI elements 使用的 UiState 数据;
根据 UiState 绘制对应的 UI elements;
响应用户的操作事件,根据需求对其进行分发;Domain Layer:封装通用的业务逻辑
封装复杂的业务逻辑,避免出现大型类;
封装多 ViewModel 通用的业务逻辑,避免代码重复;Data Layer:封装统一的数据来源,提供单一可信来源
定义不同的 DataSource 来封装 Framework 及三方 SDK 的 API;
定义 Repository 来整合相同业务的不同数据类型的 DataSource;
每层依赖关系是单向的,UI Layer 可以依赖 Domain Layer,但是 Domain Layer
却不能依赖 UI Layer 。这种依赖方式可以使用简单的函数传递依赖事件,但是却不能处理结果的回调,即
UiState 的更新。想要处理结果的回调每层之间就可以采用数据驱动/响应式的方式来交互了。这种方式也被称为是单向数据流的方式,及
UI 事件从 UI 层流向数据层,UiState 从数据层流向 UI 层。
关于 UI Layer、Domain Layer、Data Layer 中更多详细内容可查看之前文章介绍,也可以查看官方文档应用架构指南。
下面就聊下之前没有提到的并且和本次主题相关的一些内容。
与 MVI 的关系?
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 。
有的 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<ArticleHeadline>
= withContext(ioDispatcher) {
// 将耗时操作移动到 IO 线程中
newsApi.fetchLatestNews()
}
} |
如果 Repository 中需要整合很多的 DataSource 中的数据,在 Repository
中切换到对应的子线程也是可以的,这样可以减少频繁的线程调度。
同时也需要考虑响应业务的生命周期情况,如果当前业务跟随这页面进行的,那么使用 viewModelScope
或者是 lifecycleScope 即可;如果其业务是跟随 App 的什么周期的,那么则需要使用整个
App 生命周期的 CoroutineScope ;如果在 App 被终止后,仍然希望可以执行任务,那么可以考虑使用
WorkManager 。
如何处理实体类(Entity)?
各层之间的 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<T>(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 方式组织代码的优势大致有以下几点:
我们大概率都是在已有的项目中开发,而历史的项目中或多或少存在这一些历史技术债务,我们可以在开发性特性的时候引入新的技术,这样在不会对旧的目录结构产生过多影响;
后续可以很方便的对该特性进行改造,比如可以把这个文件夹移到一个单独的 module 中进行模块化相关的改造;
这方式在大型项目中的优势会更加明显;
速记手册
整理了一些关键知识点,可以保存图片定期回顾。
官方材料
文章中的内容基本上都是参考官方文档以及 Youtube 上的 mad - arch 系列。都看到这里了建议你到官方文档中的
pathawy 地址中获取下现代 Android 应用架构徽章,只要阅读完下面的文档以及完成对应测试即可。 ui-layer
ui-layer/events
domain-layer
data-layer
youtube playlist
Modern Android App Architecture quiz
最后
近几天随着 Google I/O 2022 的举行,也发布了一个最新的官方示例 Now in Android
,这个示例的完整度比之前的 JetNews、Sunflower 要高,后面也基于这个仓库做进一步的说明解析,从一个完整项目的角度来看
Android 新推出的架构指南。
|