架构模式:MVVM架构、Repository模式、UseCase模式、单向数据流(UDF)、StateFlow与SharedFlow
说实话,很多初学者一听到「架构」两个字就觉得头大。我当年刚入行时也一样,觉得能跑就行,搞那么复杂干嘛?直到我在一个中型项目里,看到Activity里塞了两千行代码,一个网络请求回调嵌套另一个,改一个需求要翻遍五个文件……嗯,从那以后,我再也不敢轻视架构了。
这一章,我们来聊聊Android开发中最主流的架构组合。说白了,就是一套让代码「好写、好改、好测」的规矩。我会结合Jetpack Compose,把MVVM、Repository、UseCase、UDF、StateFlow这些概念串起来讲。
1. MVVM架构:你的UI不该管业务
MVVM全称是Model-View-ViewModel。我习惯把它理解成三层:
- View:只管显示和用户交互。在Compose里就是各种Composable函数。
- ViewModel:管状态和业务逻辑。它持有数据,View观察它。
- Model:数据层,包括网络、数据库、本地文件等。
为什么非要拆开?你想想看,如果网络请求直接写在Composable里,测试怎么办?换数据源怎么办?我在项目中遇到过,一个同事把API调用直接写在Activity的onCreate里,后来要加缓存,几乎重写了整个页面。
核心原则:View只负责订阅状态,不负责产生状态。ViewModel只负责管理状态,不持有View引用。
在Compose中,ViewModel的用法很简单:
class MainViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value++
}
}
@Composable
fun CounterScreen(viewModel: MainViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Button(onClick = { viewModel.increment() }) {
Text("点击了 $count 次")
}
}
注意看,Composable里没有写任何业务逻辑。它只是把状态映射成UI。这就是MVVM的精髓。
2. Repository模式:统一的数据来源
Repository,说白了就是「数据仓库」。你的ViewModel不需要知道数据是从网络来的,还是从本地数据库来的。它只需要跟Repository要数据。
我个人的习惯是:每个业务模块对应一个Repository。比如用户模块有UserRepository,订单模块有OrderRepository。
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
fun getUser(id: String): Flow<Result<User>> = flow {
// 先发本地缓存
val cached = dao.getUser(id)
if (cached != null) emit(Result.success(cached))
// 再请求网络
try {
val remote = api.getUser(id)
dao.insert(remote)
emit(Result.success(remote))
} catch (e: Exception) {
emit(Result.failure(e))
}
}
}
这样做的好处是什么?如果以后要换网络库,或者改缓存策略,你只需要改Repository内部。ViewModel和UI完全不受影响。
小技巧:Repository的构造函数参数都写成接口,方便单元测试时注入Mock。
3. UseCase模式:把业务逻辑拆成小块
UseCase,也叫Interactor。它代表一个独立的业务操作。比如「登录」、「获取用户信息」、「提交订单」。
有人会问:ViewModel里直接调Repository不行吗?行,但项目大了以后,ViewModel会变得臃肿。UseCase的作用就是让每个业务操作独立成类,方便复用和测试。
class LoginUseCase(
private val userRepo: UserRepository
) {
suspend operator fun invoke(username: String, password: String): Result<User> {
// 可以在这里做参数校验
if (username.isBlank() || password.isBlank()) {
return Result.failure(IllegalArgumentException("用户名或密码不能为空"))
}
return userRepo.login(username, password)
}
}
// 在ViewModel中使用
class LoginViewModel(
private val loginUseCase: LoginUseCase
) : ViewModel() {
fun login(username: String, password: String) {
viewModelScope.launch {
val result = loginUseCase(username, password)
// 更新状态
}
}
}
我建议:如果业务逻辑只是简单的增删改查,可以不用UseCase。但如果涉及多个数据源组合、权限校验、缓存策略等,UseCase会让代码清晰很多。
4. 单向数据流(UDF):数据只朝一个方向走
单向数据流,英文叫Unidirectional Data Flow,简称UDF。它的规则很简单:
- 事件从View流向ViewModel
- 状态从ViewModel流向View
- 数据绝不反向流动
为什么会这样设计?因为双向绑定在复杂页面里容易出问题。A改了数据,B也改了,到底听谁的?我见过一个项目,因为数据流混乱,改一个输入框的值会触发三个回调,调试了一整天。
在Compose中,UDF的实现非常自然:
// ViewModel 暴露状态
data class SearchUiState(
val query: String = "",
val results: List<String> = emptyList(),
val isLoading: Boolean = false
)
class SearchViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SearchUiState())
val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow()
fun onQueryChanged(query: String) {
_uiState.update { it.copy(query = query) }
// 触发搜索
}
}
// Composable 只读状态,只发事件
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
val state by viewModel.uiState.collectAsState()
TextField(
value = state.query,
onValueChange = { viewModel.onQueryChanged(it) }
)
}
你看,数据永远是从ViewModel流向Composable。Composable不会直接修改数据,它只是告诉ViewModel「用户输入了新的内容」。这就是UDF。
注意:不要在Composable里直接修改ViewModel的MutableStateFlow。一定要通过ViewModel暴露的方法来修改。否则状态变化无法追踪,调试会非常痛苦。
5. StateFlow与SharedFlow:两种状态容器
StateFlow和SharedFlow都是Kotlin协程库里的冷流/热流概念。但在Android开发中,我们主要用它们来管理UI状态。
| 特性 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须有 | 可选 |
| 粘性事件 | 有(新订阅者会收到最新值) | 默认无,可配置 |
| 去重 | 自动(值不变不发送) | 需手动配置 |
| 典型用途 | UI状态 | 一次性事件(如导航、Snackbar) |
StateFlow:我习惯用它来保存UI状态。因为它有初始值,而且会自动去重。比如上面的SearchUiState,用StateFlow非常合适。
SharedFlow:适合处理「一次性事件」。比如显示一个Toast、跳转页面、弹出对话框。为什么不用StateFlow?因为StateFlow有粘性,如果你用StateFlow保存「显示Toast」的状态,屏幕旋转后Toast会再次弹出。
// 一次性事件用 SharedFlow
class MainViewModel : ViewModel() {
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onSaveClick() {
viewModelScope.launch {
// 保存逻辑...
_events.emit(UiEvent.ShowToast("保存成功"))
}
}
}
sealed class UiEvent {
data class ShowToast(val message: String) : UiEvent()
data class NavigateTo(val route: String) : UiEvent()
}
// 在Composable中收集一次性事件
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is UiEvent.ShowToast -> { /* 显示Toast */ }
is UiEvent.NavigateTo -> { /* 导航 */ }
}
}
}
}
我的经验:SharedFlow的replay参数设为0,extraBufferCapacity设为1,可以避免事件丢失,又不会重复消费。这是我在一个即时通讯项目里踩坑后总结出来的。
6. 整体架构图
下面这张图展示了我们这一整套架构的协作关系。我习惯把它叫做「分层单向流」:
从图中可以看到:
- View只跟ViewModel交互
- ViewModel可以调用UseCase或直接调Repository
- UseCase和Repository都依赖底层数据源
- 数据永远单向流动:数据源 → Repository → ViewModel → View
7. 避坑指南
最后,分享几个我踩过的坑:
- 不要滥用SharedFlow:我见过有人把所有状态都放在SharedFlow里,结果每次订阅都要处理粘性问题。记住:UI状态用StateFlow,一次性事件用SharedFlow。
- ViewModel不要持有View引用:在Compose中,ViewModel的生命周期比Composable长。如果你在ViewModel里持有Composable引用,会导致内存泄漏。
- UseCase不要太大:一个UseCase只做一件事。如果一个UseCase里既有登录又有注册,拆开。
- Repository不要直接暴露MutableStateFlow:对外暴露StateFlow,内部用MutableStateFlow。这是封装的基本要求。
我曾经犯过的错:在一个电商项目里,我把购物车的状态直接放在Repository里用MutableStateFlow暴露,结果多个ViewModel同时修改,导致数据不一致。后来改成Repository只提供查询方法,修改操作统一走UseCase,问题才解决。
好了,这一章的内容就到这里。架构这东西,光看没用,得在项目里用起来。下次你写一个新页面,试试先画一个UDF流程图,再动手写代码。你会发现,思路清晰了,代码自然就整洁了。
公众号:蓝海资料掘金营,微信deep3321