架构模式: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. 整体架构图

下面这张图展示了我们这一整套架构的协作关系。我习惯把它叫做「分层单向流」:

MVVM + Repository + UseCase + UDF 架构图 View (Composable) 订阅状态 · 发送事件 事件(Event) ViewModel 持有 StateFlow / SharedFlow 调用 UseCase 或 Repository 状态(State) UseCase 业务逻辑单元 Repository 数据统一入口 数据源 网络API · 本地数据库 · 文件 · 内存缓存 状态流向 事件流向 数据调用

从图中可以看到:

  • 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