1.官方应用架构指南
1.1架构的原则
应用架构定义了应用的各个部分之间的界限以及每个部分应承担的职责。谷歌建议按照以下原则设计应用架构。
- 分离关注点
- 通过数据模型驱动界面
- 单一数据源
- 单向数据流
1.2谷歌推荐的应用架构
每个应用应至少有两个层:
- 界面层 - 在屏幕上显示应用数据。
- 数据层 - 包含应用的业务逻辑并公开应用数据。
可以额外添加一个名为“网域层”的架构层,以简化和重复使用界面层与数据层之间的交互。
总结下来,我们的应用架构应该有三层:界面层、网域层、数据层。
其中网域层可选,即无论你的应用中有没有网域层,与你的应用架构是 MVVM 还是 MVI 无关。
1.2.1界面层架构设计指导
1.2.1.1界面层在架构中的作用
界面的作用是在屏幕上显示应用数据,并充当主要的用户互动点。
从数据层获取是业务数据,有时候需要界面层将业务数据转换成 UI 数据供界面元素显示。
1.2.1.2 界面层的组成
界面层由以下两部分组成:
界面元素:在屏幕上呈现数据的界面元素可以使用 View 或 Jetpack Compose 函数实现。
状态容器:用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 ViewModel 类)。
1.2.1.3界面层的架构设计遵循的原则
这里以一个常见的列表页面为案例进行讲解,这个列表页面有以下交互:
- 打开页面时,网络数据回来之前展示一个加载中 view。
- 首次打开页面,如果没有数据或者网络请求发生错误,展示一个错误 view。
- 具备下拉刷新能力,刷新后,如果有数据,则替换列表数据;如果无返回数据,则弹出一个 Toast。
1.2.1.4具体原则应用业务分析
接着我们用这个业务,按照以下原则进行分析:
1)定义界面状态
界面元素 加上 界面状态 才是用户看到的界面。
上面说的列表页面,根据它的业务需求,需要有以下界面状态
- 展示加载中 view 的界面状态
- 展示加载错误 view 的界面状态
- 列表数据 view 界面状态
- Toast view 界面状态
- 刷新完成 view 界面状态
无论采用 MVVM 还是 MVI,都需要这些界面状态,只是他们的实现细节不同。
2)定义状态容器
状态容器:就是存放我们定义的界面状态,并且包含执行相应任务所必需的逻辑的类。
ViewModel 类型是推荐的状态容器,用于管理屏幕级界面状态,具有数据层访问权限。但并不是只能用 ViewModel作为状态容器。
无论采用 MVVM 还是 MVI,都需要定义状态容器,来存放界面状态。
3)使用单向数据流管理状态
看看官方在界面层的架构指导图:
面状态数据流动是单向的,只能从 状态容器 到 界面元素。
界面发生的事件 events(如刷新、加载更多等事件)流动是单向的,只能从 界面元素 到 状态容器。
无论采用 MVVM 还是 MVI,都需要使用单向数据流管理状态。
4)唯一数据源
唯一数据源针对的是:定义的界面状态 和 界面发生的事件。
界面状态唯一数据源指的是将定义的多个界面状态,封装在一个类中,如上面的列表业务,不采用唯一数据源,界面状态的声明为:
/**
* 加载失败 UI 状态,显示失败图
* 首屏获取的数据为空、首屏请求数据失败时展示失败图
* 初始值:隐藏
*/
val loadingError: StateFlow<Boolean>
get() = _loadingError
private val _loadingError = MutableStateFlow<Boolean>(false)
/**
* 正在加载 UI 状态,显示加载中图
* 首屏时请求网络时展示加载中图
* 初始值:展示
*/
val isLoading: StateFlow<Boolean>
get() = _isLoading
private val _isLoading = MutableStateFlow<Boolean>(true)
/**
* 加载成功后回来的列表 UI 状态,将 list 数据展示到列表上
*/
val newsList: StateFlow<MutableList<News>>
get() = _newsList
private val _newsList = MutableStateFlow<MutableList<News>>(mutableListOf())
/**
* 加载完成 UI 状态
*/
val loadingFinish: StateFlow<Boolean>
get() = _loadingFinish
private val _loadingFinish = MutableStateFlow<Boolean>(false)
/**
* 界面 toast UI 状态
*/
val toastMessage: StateFlow<String>
get() = _toastMessage
private val _toastMessage = MutableStateFlow<String>("")
采用唯一数据源声明界面状态时,代码如下:
sealed interface NewsUiState {
object IsLoading: NewsUiState
object LoadingError: NewsUiState
object LoadingFinish: NewsUiState
data class Success(val newsList: MutableList<News>): NewsUiState
data class ToastMessage(val message: String = ""): NewsUiState
}
val newsUiState: StateFlow<NewsUiState>
get() = _newsUiState
private val _newsUiState: MutableStateFlow<NewsUiState> =
MutableStateFlow(NewsUiState.IsLoading)
界面发生的事件的唯一数据源指的是将界面发生的事件封装在一个类中,然后统一处理。比如上面描述的列表业务,它的界面事件有 初始化列表事件(首屏请求网络数据)、刷新事件、加载更多事件。
不采用唯一数据源,界面事件的调用实现逻辑为:在 activity 中直接调用 viewModel 提供的 initData、freshData 和 loadMoreData 方法;
采用唯一数据源,界面事件的调用实现逻辑为,先将事件中封装在一个 Intent 中,viewModel 中提供一个统一的事件入口处理方法 dispatchIntent,在 activity 中 各个场景下都调用 viewModel#dispatchIntent,代码如下:
sealed interface NewsActivityIntent {
data class InitDataIntent(val type: String = "init") : NewsActivityIntent
data class RefreshDataIntent(val type: String = "refresh") : NewsActivityIntent
data class LoadMoreDataIntent(val type: String = "loadMore") : NewsActivityIntent
}
fun dispatchIntent(intent: NewsActivityIntent) {
when (intent) {
is NewsActivityIntent.InitDataIntent -> {
//初始化逻辑
initNewsData()
}
is NewsActivityIntent.RefreshDataIntent -> {
//刷新逻辑
refreshNewsData()
}
is NewsActivityIntent.LoadMoreDataIntent -> {
//加载更多逻辑
loadMoreNewsData()
}
}
}
因为有了唯一数据源这一特点,才将最新的应用架构称为 MVI,MVVM 不具备这一特点。
5)向界面公开界面状态的方式
在状态容器中定义界面状态后,下一步思考的是如何将提供的状态发送给界面。
谷歌推荐使用 LiveData 或 StateFlow 等可观察数据容器中公开界面状态。这样做的优点有:
- 解耦界面元素(activity 或 fragment) 与 状态容器,如:activity 持有 viewModel 的引用,viewModel 不需要持有 activity 的引用。
无论采用 MVVM 还是 MVI,都需要向界面公开界面状态,公开的方式也可以是一样的。
6)使用界面状态
在界面中使用界面状态时,对于 LiveData,可以使用 observe() 方法;对于 Kotlin 数据流,您可以使用 collect() 方法或其变体。
注意:在界面中使用可观察数据容器时,需要考虑界面的生命周期。因为当未向用户显示视图时,界面不应观察界面状态。使用 LiveData 时,LifecycleOwner 会隐式处理生命周期问题。使用数据流时,最好通过适当的协程作用域和 repeatOnLifecycle API,如:
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
无论采用 MVVM 还是 MVI,都需要使用界面状态,使用的方式都是一样的。
1.2.2数据层架构设计指导
1.2.2.1数据层在架构中的作用
数据层包含应用数据和业务逻辑。业务逻辑决定应用的价值,它由现实世界的业务规则组成,这些规则决定着应用数据的创建、存储和更改方式。
1.2.2.2数据层的架构设计
数据层由多个仓库组成,其中每个仓库都可以包含零到多个数据源。您应该为应用中处理的每种不同类型的数据分别创建一个存储库类。例如,您可以为与电影相关的数据创建一个 MoviesRepository 类,或者为与付款相关的数据创建一个 PaymentsRepository 类。
每个数据源类应仅负责处理一个数据源,数据源可以是文件、网络来源或本地数据库。
层次结构中的其他层不能直接访问数据源;数据层的入口点始终是存储库类。
1.2.2.3公开 API
数据层中的类通常会公开函数,以执行一次性的创建、读取、更新和删除 (CRUD) 调用,或接收关于数据随时间变化的通知。对于每种情况,数据层都应公开以下内容:
一次性操作:在 Kotlin 中,数据层应公开挂起函数;对于 Java 编程语言,数据层应公开用于提供回调来通知操作结果的函数。
接收关于数据随时间变化的通知:在 Kotlin 中,数据层应公开数据流,对于 Java 编程语言,数据层应公开用于发出新数据的回调。
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
val data: Flow<Example> = ...
suspend fun modifyData(example: Example) { ... }
}
1.2.2.4多层存储库
在某些涉及更复杂业务要求的情况下,存储库可能需要依赖于其他存储库。这可能是因为所涉及的数据是来自多个数据源的数据聚合,或者是因为相应职责需要封装在其他存储库类中。
例如,负责处理用户身份验证数据的存储库 UserRepository 可以依赖于其他存储库(例如 LoginRepository 和 RegistrationRepository,以满足其要求。
注意:传统上,一些开发者将依赖于其他存储库类的存储库类称为 manager,例如称为 UserManager 而非 UserRepository。
1.2.2.5数据层生命周期
如果该类的职责作用于应用至关重要,可以将该类的实例的作用域限定为 Application 类。
如果只需要在应用内的特定流程(例如注册流程或登录流程)中重复使用同一实例,则应将该实例的作用域限定为负责相应流程的生命周期的类。例如,可以将包含内存中数据的 RegistrationRepository 的作用域限为 RegistrationActivity。
1.2.2.6数据层定位思考
数据层不应该是页面级别的(一个页面对应一个数据层),而应该是应用级别的(数据层有多个存储仓库,每种数据类型有一个对应的存储仓库,不同的界面层可以复用存储仓库)。
比如我做的应用是运动健康app,用户的睡眠相关的数据有一个 SleepResposity,用户体重相关的数据有一个 WeightReposity,由于应用中很多界面都可能需要展示用户的睡眠数据和体重数据,所以 SleepResposity 和 WeightReposity 可以供不同界面层使用。