一、为什么需要架构?
- 业务膨胀:Activity/Fragment 中混杂 UI、数据、网络、缓存,导致数千行"上帝类"。
- 维度冲突:UI 与状态同步困难,生命周期横插一刀,配置更改即崩溃。
- 测试困难:逻辑耦合在 Android 生命周期中,单元测试寸步难行。
架构模式通过关注点分离 (SoC)与单向数据流(UDF)缓解上述痛点。
二、四种主流架构速览
维度 | MVC(Model-View-Controller) | MVP(Model-View-Presenter) | MVVM(Model-View-ViewModel) | MVI(Model-View-Intent) |
---|---|---|---|---|
数据流方向 | 双向 | 双向 | 双向(基于观察者) | 严格单向环形 |
生命周期感知 | 无 | 手动 detach | ViewModel 自动感知 | StateFlow/Compose 自动感知 |
样板代码 | 极少 | 中等(接口爆炸) | 中等(DataBinding/ViewBinding) | 最少(Kotlin DSL) |
状态管理 | 分散 | Presenter 持有 | LiveData/StateFlow 集中 | 单一不可变 State |
适用场景 | 原型/小 Demo | 传统业务模块 | Jetpack 全家桶 | Compose/响应式新 UI |
三、MVC:最原始的双向绑定
3.1 角色划分
- Model:业务数据与持久化。
- View:XML + Activity 展示。
- Controller:Activity 自身,响应 View 事件并更新 Model,同时监听 Model 变化刷新 View。
3.2 代码示例
kotlin
// Model
object UserModel {
fun fetchUser(callback: (User) -> Unit) {
// 模拟网络
Handler(Looper.getMainLooper()).postDelayed({
callback(User("MVC User"))
}, 1000)
}
}
// Controller = Activity
class UserActivity : AppCompatActivity() {
private val model = UserModel
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
textView = findViewById(R.id.tvName)
model.fetchUser { user ->
textView.text = user.name // Controller 直接改 View
}
}
}
问题:Activity 既是 Controller 又是 View,配置更改后需手动保存数据,极易内存泄漏。
四、MVP:接口解耦,职责清晰
4.1 角色划分
- View:Activity/Fragment,通过接口抽象 UI 操作。
- Presenter:纯 Java/Kotlin 类,持有 View 接口引用,调度 Model。
- Model:与 MVC 相同。
4.2 关键改进
- 面向接口编程,单元测试可 mock View。
- 手动
attach/detach
避免内存泄漏。
4.3 代码示例(登录场景)
kotlin
// Contract
interface LoginContract {
interface View {
fun showLoading()
fun hideLoading()
fun showError(msg: String)
fun navigateHome(user: User)
}
interface Presenter {
fun attachView(view: View)
fun detachView()
fun login(user: String, pwd: String)
}
}
// Presenter
class LoginPresenter(
private val repo: UserRepository
) : LoginContract.Presenter {
private var view: LoginContract.View? = null
private var job: Job? = null
override fun attachView(view: LoginContract.View) {
this.view = view
}
override fun detachView() {
view = null
job?.cancel()
}
override fun login(user: String, pwd: String) {
view?.showLoading()
job = CoroutineScope(Dispatchers.IO).launch {
try {
val u = repo.login(user, pwd)
withContext(Dispatchers.Main) {
view?.hideLoading()
view?.navigateHome(u)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
view?.hideLoading()
view?.showError(e.message ?: "Unknown")
}
}
}
}
}
// View
class LoginActivity : AppCompatActivity(), LoginContract.View {
private lateinit var presenter: LoginContract.Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
presenter = LoginPresenter(UserRepository())
presenter.attachView(this)
btnLogin.setOnClickListener {
presenter.login(etUser.text.toString(), etPwd.text.toString())
}
}
override fun onDestroy() {
super.onDestroy()
presenter.detachView()
}
override fun showLoading() = progress.show()
override fun hideLoading() = progress.hide()
override fun showError(msg: String) = toast(msg)
override fun navigateHome(user: User) {
startActivity<HomeActivity>()
finish()
}
}
痛点:接口数量随 UI 复杂度急剧膨胀("接口地狱")。
五、MVVM:数据驱动 + 生命周期感知
5.1 角色划分
- View:Activity/Fragment + ViewBinding/DataBinding XML。
- ViewModel:存储与准备 UI 数据,不直接持有 View。
- Model:Repository + Remote/Locale DataSource。
5.2 关键改进
- 生命周期安全:ViewModel 在配置更改时自动保留。
- 数据驱动:LiveData/StateFlow 推送到 UI,降低手动刷新。
- 双向绑定(DataBinding):XML 表达式直接观察 ViewModel 字段。
5.3 代码示例(Jetpack 官方推荐写法)
kotlin
// ViewModel
class UserViewModel(
private val repo: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState
fun loadUser() = viewModelScope.launch {
_uiState.value = _uiState.value.copy(loading = true)
try {
val user = repo.fetchUser()
_uiState.value = UserUiState(user = user)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message)
}
}
}
// UI State
data class UserUiState(
val user: User? = null,
val loading: Boolean = false,
val error: String? = null
)
// View (Compose)
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
when {
state.loading -> CircularProgressIndicator()
state.error != null -> Text("Error: ${state.error}")
state.user != null -> Text("Hello, ${state.user.name}")
}
LaunchedEffect(Unit) { viewModel.loadUser() }
}
痛点:状态分散在多个 LiveData 中,复杂页面难以追踪。
六、MVI:单向数据流 + 不可变状态
6.1 核心思想
- Intent:用户事件、系统事件统一封装为"意图"。
- State:单一不可变数据类描述整个 UI 快照。
- Reducer :纯函数
(State, Intent) -> State
,确保可预测。 - Effect:一次性事件(导航、Toast)用 SideEffect 通道发送。
6.2 代码示例(Orbit-MVI + Compose)
kotlin
// Intent
sealed interface LoginIntent {
data class InputUser(val text: String) : LoginIntent
data class InputPwd(val text: String) : LoginIntent
object Submit : LoginIntent
}
// State
data class LoginState(
val user: String = "",
val pwd: String = "",
val loading: Boolean = false
)
// ViewModel
class LoginViewModel(
private val repo: UserRepository
) : ContainerHost<LoginState, LoginSideEffect>, ViewModel() {
override val container = container<LoginState, LoginSideEffect>(LoginState())
fun dispatch(intent: LoginIntent) = intent {
when (intent) {
is InputUser -> reduce { state.copy(user = intent.text) }
is InputPwd -> reduce { state.copy(pwd = intent.text) }
Submit -> {
reduce { state.copy(loading = true) }
val user = repo.login(state.user, state.pwd)
postSideEffect(LoginSideEffect.NavigateHome(user))
reduce { state.copy(loading = false) }
}
}
}
}
// View
@Composable
fun LoginScreen(vm: LoginViewModel = viewModel()) {
val state by vm.container.stateFlow.collectAsState()
val sideEffect = vm.container.sideEffectFlow.collectAsEffect()
Column {
TextField(state.user, onValueChange = { vm.dispatch(InputUser(it)) })
TextField(state.pwd, onValueChange = { vm.dispatch(InputPwd(it)) })
Button(onClick = { vm.dispatch(Submit) }, enabled = !state.loading) {
if (state.loading) CircularProgressIndicator() else Text("Login")
}
}
when (val effect = sideEffect.value) {
is LoginSideEffect.NavigateHome -> LaunchedEffect(effect) {
navController.navigate("home/${effect.user.id}")
}
null -> {}
}
}
优势:
- 状态集中,调试时直接打印单个 State 即可定位问题。
- Compose 天然适合 MVI,无需 DataBinding。
劣势:
- 学习曲线陡;需要额外库(Orbit、Mavericks)或自己封装基类。
七、如何选型?
场景 | 推荐架构 | 理由 |
---|---|---|
快速原型 | MVC | 零配置,最快出 Demo |
传统业务模块(Java) | MVP | 接口解耦,测试友好 |
Jetpack 全家桶(Kotlin) | MVVM | AAC 生命周期 + DataBinding/Compose |
Compose 新项目 | MVI | 单向数据流与声明式 UI 天生一对 |
八、总结
- MVC:简单直接,但易失控。
- MVP:接口隔离,适合老代码迁移。
- MVVM:Google 官方主推,生命周期安全,数据驱动。
- MVI:响应式终极形态,状态可预测,Compose 时代首选。
没有银弹,只有权衡。根据团队 Kotlin/Compose 熟练度、项目规模与测试诉求,选择最契合的架构,并保持渐进式演进------从 MVP 到 MVVM,再到 MVI,平滑过渡,持续交付。