一、概念
1.1 最佳实践
- 不要将数据在 Android 四大组件中处理,它们有自己的生命周期我们很难控制。
- 尽量减少对 Android 类(
Context、Activity等)的依赖,减少耦合,提高可测试性。 - 需要明确各个模块中的职责,在各自的分层中仅做自己职责范围内的事情,减少耦合。
- 每个模块仅仅只需暴露自己应该提供的功能,保持越简单越好。
- 应尽量设计离线模式,用户在网络不可用的情况下,仍然可以使用 App。
1.2 依赖管理
三层内容中会存在大量类的初始化及注入管理问题,如果处理不好将会导致一些不必要的麻烦。这种问题有两种解决方案:
- 依赖项注入:依赖项注入使类能够定义其依赖项而不构造它们。在运行时,另一个类负责提供这些依赖项。一般是使用注解方式,适合大中型项目,如 Hilt。
- 服务查找:服务查找的方式一般是维护一个注册表,所需的依赖都可以在这个注册表中查找;一般是使用相对简单,适合中小型项目,如 koin。
| UI Layer | 在屏幕上显示应用数据。 |
| Domain Layer (可选) | 重用界面层的业务逻辑或是拆分业务避免大型类。 |
| Data Layer | 包含应用的业务逻辑并公开数据访问。 |
二、界面层 UI Layer


| View Container | 界面的容器。Activity或Fragment。 |
| UI Elements | 界面元素。View或Compose。 |
| UI Events | 界面交互事件。生产和更新数据,代理给ViewModel处理。 |
| UI State | 数据状态。界面元素所展示的数据的状态,通过观察者方式处理。 |
| State Holder | 提供数据状态的类。包含处理对应任务所必须的逻辑,一般是ViewModel。 |
2.1 单向数据流
使用单项数据流方式(Unidirectional Data Flow)将职责进行分离,使得状态(UI State)的来源位置(Data,也就是数据层)、转换位置(State Holder,也就是ViewModel)、使用位置(UI Element,也就是Activity/Fragment)分散到不同类中。事件(UI Event)从界面层流向数据层,状态(UI State)从数据层流向界面层。
- 数据一致性:界面只有一个可信来源。
- 可测试性:状态来源是独立的,可独立进行界面测试。
- 可维护性:状态的更改遵循明确定义的模式,即用户事件及其数据拉取来源共同作用的结果。
2.1.1 初始化阶段
- 数据层返回数据给ViewModel(Data→State Holder)。
- ViewModel将数据转换为界面层需要的状态(State Holder→State)。
- 观察者模式响应状态变化,界面元素进行展示(State→Element)。
2.1.2 相应事件阶段
- Activity或Fragment将事件传递给ViewModel中(Event→State Holder)。
- ViewModel将事件传递给数据层(State Holder→Data)。
- 数据层根据业务逻辑对数据进行处理并更新给ViewModel(Data→State Holder)。
- 接再进行上面初始化阶段的 1 2 3 步。
2.2 状态 UI State
界面上展示的可变信息就是状态(数据状态)。通常会被定义为 data class(通过 copy() 方便更新属性),除了数据还包括一些动作的处理如 isUserLoggedIn 字段,需要处理页面跳转的逻辑。
- 不可变性: 要定义为常量,杜绝在数据传递的过程中有其它逻辑对其产生修改,确保只有数据源或数据所有者才负责更新。
- 使用统一命名:根据描述界面的功能命名,如显示新闻的屏幕状态可以称为 NewsUiState,列表中的条目状态可以称为 NewsItemUiState。
- 应处理彼此相关的状态:相关联的数据状态定义在同一个类中,防止定义在不同类中导致其中一处修改而另一处没有修改的数据不一致情况,并且可以对多个状态做派生处理(例如只有登录且订阅的用户才可以添加书签)。
- 合理使用单数据流与多数据流:使用单一数据流的最大优势是便捷性及数据一致性,但强行把不相关的数据捆绑在一个类中的代价会超过其优势,尤其是在刷新频率不一致的情况下,因为一个字段的变化会导致整个相关的 Element 都刷新一次,字段越多越可能出现。
data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = emptyList()
) {
val NewsUiState.canBookmarkNews: Boolean = isSignedIn && isPremium
}
2.3 状态持有者 State Holder
使用可观察数据容器提供状态(如LiveData、Flow、MutableState)给界面元素使用,当数据发生变化的时候 UI 能够及时刷新。
只要能提供状态并且能处理对应逻辑就行,可以是普通类,ViewModel是最常见的。
Compose声明式编码使得组合函数并不需要定义在统一的类中(View就需要在Activity或Fragment中统一管理)而是可以复用自由组合,使得有些情况下状态放在统一的ViewModel中会有些冲突。
ViewModel{
//LiveData
private var _xxState = MutableLiveData<Int>()
val xxState = _xxState as LiveData<Int>()
//MutableState
private var _xxState = MutableStateOf(-1)
}
2.4 使用 UI State
使用可观察容器时,需要考虑 View Container 的生命周期(Activity或Fragment),当界面不可见时不应该继续收集数据。在View体系中:使用 LiveData 时 LifecycleOwner 已经处理好了,使用 Flow 时需要使用 lifecycleScope() 和 repeatOnLifecycle() 来处理。
Activity {
private val viewModel by viewModels<MainViewModel>()
fun onCreate() {
initView()
observeData()
initData()
}
fun observeData() {
//使用LiveData
viewModel.userData.observe(this) {...}
}
fun initData(){
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
//使用Flow
viewModel.userDataFlow.collect {...}
}
}
}
}
@Composable
fun Demo(viewModel: MainViewModel = viewModel()) {
//使用MutableState
Text(text = viewModel.userState.value) {}
//使用LiveData
Text(text = viewModel.userData.observeAsState("默认值").value) {}
//使用Flow
Text(text = viewModel.userFlow.collectAsState(initial = "默认值").value) {}
Text(text = viewModel.userFlow.collectAsStateWithLifecycle(initialValue = "默认值")) {}
}
2.5 处理 UI Event
事件分为 UI Event(UI自己能够处理的事件,如页面跳转、列表展开、权限请求等与Contex相关的)和 User Event(与用户交互产生的事件需要执行的逻辑,如点击事件、数据更新)。 在界面中处理屏幕行为逻辑,在 ViewModel中处理业务逻辑并将结果转为状态。
用户事件的命名根据处理的操作以动词命名,如 addBookmark()、logIn()。

// 扩展部分的展示与否,与业务逻辑无关,直接在 UI 层处理
binding.expandButton.setOnClickListener {
binding.expandedSection.visibility = View.VISIBLE
}
// 刷新事件交由 ViewModel 来处理
binding.refreshButton.setOnClickListener {
viewModel.refreshNews()
}
三、网域层 Domain Layer(可选)
根据项目复杂程度决定是否选用。就是把 ViewModel 中的业务逻辑抽取出去(UseCase包含的业务并不是仅用于ViewModel,Service、Application也可以调用),封装复杂的业务逻辑避免出现大型类、封装多个通用的业务逻辑避免代码重复、增加了可读性和可测试性。之前是 ViewModel 的业务逻辑中调用 Repository,现在是 UseCase 调用 Repository,再由 ViewModel 调用 UseCase。
3.1 命名方式
每个用例都应仅负责单个功能,且不应包含可变数据。以其负责的单一操作命名,动词原形 + 名词/内容(可选)+ UseCase,如 LoginUseCAse、FormatDateUseCase。
3.2 依赖关系

通过构造传入仓库或其它用例,可对多个仓库数据进行合并,可调用其它用例的业务逻辑。
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val formatDateUseCase: FormatDateUseCase
) {...}
3.3 通过 invoke() 优化调用
使用 operator 修饰的 invoke() 函数,可以将实例作为函数名进行调用(可以用类名但这里是传入的实例,避免硬编码),还可以通过不同的参数列表提供重载。
class FormatDateUseCase(
userRepository: UserRepository
) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
class MyViewModel(
formatDateUseCase: FormatDateUseCase
) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today)
}
}
四、数据层 Data Layer

- 唯一可信源:向其它层提供获取数据的统一入口(外层不准直接访问数据,而是通过 Repository 作为入口),所有与数据相关的操作(如数据转换、缓存策略、错误处理)都集中在 Repository 中,避免了代码分散在多个地方造成的重复和混乱。
- 关注点分离:对外屏蔽了数据的获取和处理细节,内部的修改不会影响外层。这使得数据源的更换(如将 Retrofit 换成另一个库)变得非常容易,只需修改 Repository 内部实现,而无需触动外层的代码。
- 可测试性:可以非常轻松地为 ViewModel 编写单元测试。只需创建一个实现了相同接口的 Fake Repository 或使用 Mocking框架(如 Mockito) 来模拟 Repository 的行为,从而完全隔离网络和数据库等不可控因素。
| 仓库 Repository | 数据源 DataSource | |
| 概念 | 数据层包含一个或者多个仓库,根据不同的界面(购物车、消息)或场景(电影、音乐)创建,即下方命名方式中所指的“数据类型”。 | 每个仓库中包含零或多个数据源,根据不同的来源(本地、网络、文件)创建,即下方命名方式中所指的“来源类型”。 |
| 作用 | 整合同一数据的多个来源,集中处理数据的变化,对外暴露获取数据的统一入口。 | 从来源中获取数据,即封装系统及三方API(文件读写、位置信息、Retrofit、Room等)。 |
| 命名 | 数据类型+Repository。如 NewsRepository、UserRepository。 | 数据类型+来源类型+DataSource。如 UserLocalDataResource、UserRemoteDataResource。不要使用具体实现的技术来命名(既不需要知道,后期代码也会变动,如 |
4.1 数据源 DataSource
使用第三方库的时候(Retrofit网络请求、Room查询数据库),它们已经在内部处理好了IO调度,用它们自己的线程来管理,因此不要用 withContext(Dispatcher.IO) 去包裹,你只是把诸如准备请求和解析 json 这类占用 cpu 运算的工作交给了 IO 调度器,反而画蛇添足。
系统API及大部分三方SDK大都以callback形式返回数据,可以使用 suspendCancellableCoroutine 或者 callbackFlow 转换成挂起函数。
interface INewsDataSource {
suspend fun getData(): List<News>
}
class RemoteNewsDataSource(val apiService: ApiService) : IUserSource {
override suspend fun getData() = apiService.getData() //通过网络获取
}
class LocalNewsDataSource(val db: NewsDatabase) : IUserSource {
override suspend fun getData() = db.getData() //通过数据库获取
}
4.2 仓库 Repository
可以决定数据的来源和优先级(内存、本地缓存、网络),确保了UI始终有数据可显示。
class NewsRepositoryImpl(
private val localDataSource: LocalNewsSource,
private val remoteDataSource: RemoteNewsSource
) : INewsRepository {
//每次获取时,根据网络情况赋值为对应的数据源
private val dataResource: INewsSource
get() = if (NetUtils.checkNetwork()) localDataSource else remoteDataSource
override suspend fun getData(): String = dataResource.getData()
}
interface INewsRepository {
suspend fun getData(): List<News>
}
class NewsRepositoryImpl(
//通过构造引入DataSource作为依赖项
private val remoteDataSource: RemoteNewsDataSource,
private val localDataSource: LocalNewsDataSource
) : INewsRepository {
//添加同步锁,确保在多线程中安全
val mutex = Mutex()
//使用变量将最新数据缓存在内存中
var ram: List<News> = emptyList()
override suspend fun getData(): List<News> {
//先判断内存中是否有数据,没有的话从本地中读取
if (ram.isEmpty()) {
val local = localDataSource.getData()
//再判断Local中是否有数据,没有的话从网络读取
if (local.isEmpty()) {
val remote = remoteDataSource.getData()
//将网络中获取的数据保存到内存和本地
mutex.withLock {
newsLocalDataSource.saveData(remote)
ram = remote
}
}
//将本地中获取的数据保存到内存中
mutex.withLock { ram = local }
}
return mutex.withLock { ram }
}
}
4.3 一些数据处理的细节
4.3.1 线程处理
ViewModel、UseCase、Repository、DataSource无论哪一层处理都能保证最终在主线程(UI层)调用安全,采取就近原则,谁生产数据谁就保证安全性,因此选择在 DataSource 中切换到子线程中进行操作。当在 Repository 中需要整合多个数据源,可以再切到子线程中处理。
4.3.2 生命周期处理
| 面向UI的操作 | 这部分的业务会根据生命周期变化而变化,保证主线程调用安全即可。 | viewModelScope、lifecycleScope。 |
| 面向APP的操作 | 任务执行周期比UI更长,需要在UI销毁后仍可以执行,就需要在协程中传入一个全局的作用域对象。 | CoroutineScope()、withContext()....中传入自定义的Job。 |
| 面向业务的操作 | 业务在APP终止后仍希望进行。 | WorkManager。 |
4.3.3 异常处理

不要在生产数据的 DataSource 中内部消化,而是在提供数据的 Repository 中,这里能解决网络失败后从缓存中读取。处理了异常,返回的都是可空类型,外层只需要处理为null的情况)。
4.3.4 对外暴露API
如何定义 Repository 中对外提供获取数据的方法。
| 一般(不推荐这种Java式编程) | 接口回调 callback | ||
| 一次性操作(推荐) | 挂起函数 suspend | ||
| 数据流操作 | 分配 Channel | ||
| 共享 | 构建 Flow | ||
| 更新 | 历史 SharedFlow | ||
| 最新 StateFlow | |||
577

被折叠的 条评论
为什么被折叠?



