
软件为什么是"软"?
因为软件能够适应变化。
而安卓的架构历史,正好体现了这一点。
现代安卓开发建立在众多架构模式的基础之上。每种架构模式的出现都是为了解决前一种方法存在的问题,尤其是围绕用户界面(UI)与逻辑之间的耦合,以及在生命周期事件中管理状态的问题。
本文将逐步追溯这一演进过程,展示安卓开发者是如何从 "将所有内容都放在Activity中" 转变为采用更解耦、更便于测试的模式的。
这正是软件的"软"核心所在------通过解耦让软件具有更高的扩展性。
相对于硬件的"硬"------一旦产生就无法扩展和修改功能,软件能够不断的优化和扩展功能。
God Activity:All In One

早期的安卓应用常常将所有逻辑------业务逻辑、用户界面更新以及状态管理------都放入一个 Activity
或 Fragment
中。这有时会被错误地称为 MVC ,但实际上,没有单独的控制器是不能称之为 MVC 的。所有内容都存在于同一个用户界面类中,从而导致了 "全能 Activity
" 的出现。
Kotlin
class MainActivity : AppCompatActivity() {
private var count = 0
private lateinit var countTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
countTextView = findViewById(R.id.countTextView)
findViewById<Button>(R.id.incrementButton).setOnClickListener {
// 业务逻辑,UI 逻辑全在一个地方
count++
updateView()
}
}
private fun updateView() {
countTextView.text = count.toString()
}
}
早期其实并不是Kotlin代码,这里只是为了说明架构风格,理解意思即可。
这样的架构有什么缺点呢:
- 紧密耦合:用户界面类掌控一切,难以进行扩展或测试。
- 测试困难:所有逻辑都在依赖安卓框架代码的
Activity
中。 - 状态丢失:屏幕旋转会重新创建
Activity
,除非手动处理,否则计数值会丢失。 - 职责不分:业务逻辑和用户界面代码交织在一起。
优点嘛,也是有的。如果你想写一段代码测试某个 API 具体的功能,这样写会很快,没有架构负担。
经典 MVC:引入独立的控制器

在安卓系统中,真正的或经典的 MVC 模式意味着我们要将模型(Model
)、视图(View
)和控制器(Controller
)在实际中分隔开。Activity
充当视图,而一个独立的控制器类则同时引用视图和模型。这比以用户界面为中心的方法有所进步:至少我们有一个单独的逻辑类。
这是代码上的一小步,却是整个架构历史的一大步!
也就说,MVC 第一次将业务逻辑与 UI 解耦:
Kotlin
// 模型
class CounterModel {
// 私有变量,用于存储计数器的值,初始值为0
private var count = 0
// 用于增加计数器值的方法
fun increment() {
count++
}
// 获取当前计数器值的方法
fun getCount(): Int = count
}
// "控制器",引用具体的Activity作为视图
class CounterController(
// 直接链接到Activity(视图)
private val view: MainActivity,
private val model: CounterModel
) {
// 当点击增加按钮时调用的方法
fun onIncrementClicked() {
// 调用模型的增加方法
model.increment()
// 调用视图的更新方法,传入当前计数器的值
view.updateCounter(model.getCount())
}
}
// 作为"视图"的Activity
class MainActivity : AppCompatActivity() {
private lateinit var controller: CounterController
private lateinit var model: CounterModel
private lateinit var countTextView: TextView
// Activity创建时调用的方法
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置Activity的布局为activity_main.xml
setContentView(R.layout.activity_main)
// 通过id找到布局中的TextView
countTextView = findViewById(R.id.countTextView)
// 创建CounterModel的实例
model = CounterModel()
// 创建CounterController的实例,传入当前Activity(视图)和模型实例
controller = CounterController(this, model)
// 为布局中的增加按钮设置点击监听器
findViewById<Button>(R.id.incrementButton).setOnClickListener {
// 点击按钮时,调用控制器的点击处理方法
controller.onIncrementClicked()
}
}
// 更新TextView显示的计数器值的方法
fun updateCounter(count: Int) {
// 将计数器的值转换为字符串并设置到TextView上
countTextView.text = count.toString()
}
}
同样,MVC 依然有他的局限性:
- 强耦合:控制器明确知道
MainActivity
。 - 测试难题:控制器依赖于一个真实的安卓类(
MainActivity
)。 - 模板代码:在
Activity
里面创建对应的Model
以及Controller
,仍需手动编写更多的、套路式的代码。
实际上,MVC 模式并不是只存在于 Android 应用开发。
它在上世纪 70 年代被发明,用于桌面 GUI 的开发。但是真正发扬光大它的,确是千禧年的 Web 端。
Web 端的 MVC ,
Model
负责业务逻辑和数据逻辑;View
负责生成 HTML 供浏览器展示;Controller
负责接收用户请求(如点击按钮、提交表单),调用模型处理业务逻辑,并将结果返回给View
。
MVP:通过视图接口打破直接耦合

MVP (模型-视图-Persenter
)通过引入视图接口解决了经典 MVC 中的双向耦合问题。Presenter
只引用这个接口,而不是具体的 Activity
或 Fragment
。同时,Activity
实现该接口。
一个典型的 MVP 架构如下所示:
Kotlin
// 模型
class CounterModel {
// 私有变量,用于存储计数器的值,初始值为0
private var count = 0
// 用于增加计数器值的方法
fun increment() {
count++
}
// 获取当前计数器值的方法
fun getCount(): Int = count
}
// 视图接口
interface CounterView {
// 更新计数的方法
fun updateCount(count: Int)
// 显示错误信息的方法
fun showError(message: String)
}
// 呈现器
class CounterPresenter(private val model: CounterModel) {
// 弱引用视图对象,初始值为null
private var view: CounterView? = null
// 附着视图的方法,将传入的视图赋值给类的视图属性,并更新计数显示
fun attachView(view: CounterView) {
this.view = view
view.updateCount(model.getCount())
}
// 分离视图的方法,将视图属性设为null
fun detachView() {
this.view = null
}
// 处理增加计数点击事件的方法
fun onIncrementClicked() {
model.increment()
view?.updateCount(model.getCount())
}
}
// 实现视图接口的Activity
class MainActivity : AppCompatActivity(), CounterView {
// 延迟初始化的呈现器对象
private lateinit var presenter: CounterPresenter
// Activity创建时调用的方法
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter = CounterPresenter(CounterModel())
presenter.attachView(this)
// 为布局中的增加按钮设置点击监听器
findViewById<Button>(R.id.incrementButton).setOnClickListener {
presenter.onIncrementClicked()
}
}
// 实现视图接口中更新计数的方法,在界面上显示当前计数值
override fun updateCount(count: Int) {
findViewById<TextView>(R.id.countTextView).text = count.toString()
}
// 实现视图接口中显示错误信息的方法,通过Toast显示错误消息
override fun showError(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
// Activity销毁时调用的方法
override fun onDestroy() {
super.onDestroy()
presenter.detachView()
}
}
为什么 MVP 比传统 MVC 更好:
- 松散耦合:
Presenter
并不直接引用MainActivity
,它只了解计数器视图(CounterView
)。 - 易于测试:你可以通过提供模拟的或假的计数器视图(
CounterView
)来测试计数器CounterPresenter
。 - 明确的契约:计数器视图(
CounterView
)接口明确声明了View
需要实现哪些用户界面接口。
MVVM:通过响应式流实现完全的用户界面隔离

MVVM (模型-视图-视图模型)更进一步,从 Presenter
中去除了 View
层的调用,即 Presenter
不主动调用 View
层的接口。取而代之的是,视图模型(ViewModel
)以响应式方式(LiveData
、Flow
或 RxJava
可观察对象)持有数据,而视图则观察这些数据。这意味着视图模型完全与用户界面无关,它不引用 Activity
或 Fragment
。
这里实际上也有 Presenter
,只不过变成了 VM (ViewModel
)。
最重要的是,随着架构组件中视图模型(
ViewModel
)的出现,无需再使用额外的方式来处理屏幕旋转问题等Activity
的重启问题。视图模型类天生就具备生命周期感知能力,能够自动在配置更改后依然存活。
Jetpack Compose 是一个现代化的声明式用户界面工具包,它与 MVVM 自然配合。用户界面收集或观察 StateFlow
,并且 Compose 会在数据发生变化时自动重新组合。这种协同作用有助于维持单向数据流:用户事件发送到 ViewModel
,状态更新后,Compose 会重新渲染。
同样,我们奉献上基于 StateFlow
+ Jetpack Compose 的代码:
Kotlin
// 视图模型
class CounterViewModel : ViewModel() {
// 可变状态流,初始值为0
private val _counter = MutableStateFlow(0)
// 公开的状态流
val counter: StateFlow<Int> = _counter.asStateFlow()
fun increment() {
// 增加计数器的值
_counter.value++
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// 收集状态流中的值,并转换为可组合函数能使用的状态
val count by viewModel.counter.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// 显示计数器的值
Text(text = count.toString())
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
}
}
MVVM 的关键点在于:
- 不直接引用用户界面:视图模型绝不能调用类似
activity?.updateCount()
这样的方法。 - 生命周期感知:默认情况下,视图模型的生命周期比活动更长,因此无需使用加载器或保留片段。
- 完全与用户界面无关:用户界面"获取"数据变化,这使得对视图模型进行单元测试极为简单。
- 单向数据流:在使用 Compose 或 基于 XML 的应用中使用
LiveData
/Flow
时,用户操作会输入到视图模型中,视图模型更新状态,用户界面则对这些状态进行观察。
为什么 MVVM 取代了 MVP?
根本原因是,MVVM 方法确保了单向数据流:
- 用户界面(UI)了解视图模型(
ViewModel
),但视图模型对用户界面一无所知。 - 通过响应式编程,视图模型通过可观察对象(如
LiveData
、Flow
或RxJava
)与视图进行通信。 - 与 iOS 不同,在 iOS 中视图模型通常只是一个普通类,而在 Android 中,视图模型继承自
ViewModel
基类,将其与Activity
或Fragment
的生命周期绑定,这意味着它会自动在配置更改后依然能够存活,对于开发者来讲,太省心了。
通过将生命周期感知和响应式更新相结合,MVVM 消除了处理状态所需的样板代码,VM 现在只需要关心自己内部的逻辑,准备好数据之后,放在 Flow
中即可,至于 View
层的接口,VM 一无所知,MVVM 使业务逻辑与用户界面完全解耦。
MVI:强制单向数据流

MVI (模型-视图-意图)是 MVVM 的一种特殊形式或扩展,它通过明确的用户意图来强制实现严格的单向数据流。
用户触发意图,视图模型将这些意图简化为单一状态。用户界面观察该状态,并重新渲染。
在更简单的形式中(如下方示例),MVI 看起来与 MVVM 相似,只有一个数据类和一个意图密封接口:
- 意图:用户的操作(例如,点击 "刷新" 按钮)。
- 动作:"要做什么" 的方面,通常用于统一或归类多种可能的意图。
- 结果:操作的结果,例如加载中、错误或成功。
- 视图状态:用户界面观察到的最终状态。
为什么选择 MVI?
- 单向数据流:每个用户操作都是一个意图,会产生一个新状态,不存在隐藏更新。
- 单一事实来源:用户界面从一个状态对象渲染,便于调试。
- 不仅仅是对 MVVM 的整理:MVI 明确地对每次交互进行建模,减少了对用户界面如何变化的猜测。
- 与 Compose 的协同:Jetpack Compose 具有响应式和无状态的特点;输入 MVI 状态与之自然契合。
以下是在单个视图模型类中实现的一个轻量级 MVI 示例:
Kotlin
// 1) 状态:用户界面的单一事实来源
data class CounterState(
val count: Int = 0,
val error: String? = null
)
// 2) 意图:所有可能的用户操作
sealed interface CounterIntent {
object Increment : CounterIntent
object Reset : CounterIntent
data class SetCount(val value: Int) : CounterIntent
}
// 3) MVI 风格的视图模型
class CounterMviViewModel : ViewModel() {
// 内部可变状态流
private val _state = MutableStateFlow(CounterState())
// 作为只读状态流暴露
val state: StateFlow<CounterState> = _state.asStateFlow()
/**
* 集中处理意图:我们可以在此内联定义它,也可以将其定义为一个单独的函数。
*
* 它接受当前状态并返回一个新状态。为了清晰起见,
*/
fun processIntent(intent: CounterIntent) {
_state.update { oldState ->
when (intent) {
is CounterIntent.Increment -> oldState.copy(count = oldState.count + 1, error = null)
is CounterIntent.Reset -> oldState.copy(count = 0, error = null)
is CounterIntent.SetCount -> {
if (intent.value >= 0) {
oldState.copy(count = intent.value, error = null)
} else {
oldState.copy(error = "Negative counts not allowed")
}
}
}
}
}
}
// 4) 观察状态流的可组合 "视图"
@Composable
fun CounterMviScreen(viewModel: CounterMviViewModel = viewModel()) {
// 从我们的MVI视图模型中收集当前状态
val uiState by viewModel.state.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = uiState.count.toString())
// 如果有错误则显示
uiState.error?.let { error ->
Text(
text = error,
color = Color.Red
)
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { viewModel.processIntent(CounterIntent.Increment) }) {
Text("Increment")
}
Button(onClick = { viewModel.processIntent(CounterIntent.Reset) }) {
Text("Reset")
}
}
Spacer(modifier = Modifier.height(16.dp))
// 示例:设置特定的计数
Button(onClick = { viewModel.processIntent(CounterIntent.SetCount(5)) }) {
Text("Set Count to 5")
}
}
}
processIntent
是 ViewModel
的核心函数。
MVI 的 ViewModel
为了清晰起见,大部分情况下会将这部分逻辑提取到类似 processIntent
这个单独的函数中,原则就是:所有的更改都通过一个单一的步骤来进行。
MVI 的究极进化:归约(Redux)
什么是归约?
将复杂的事物、数据、问题等简化、归纳为更简单形式的意思。它是一种把多个元素、步骤或者复杂的状态逐步合并、精简,最终得到一个相对简洁结果的过程。
如果对照设计模式,有点像模板设计模式。只不过归约更加强调对于流程、数据处理的过程。
如果我们仔细思考 MVVM 或者 MVI,我们会总结出这样一个统一的业务逻辑:
用户操作 -> 处理用户操作 -> 转换成状态或者对 UI 的事件
上述处理用户操作到转化状态的流程,在上面的轻量级 MVI 示例中,是一个函数完成的,processIntent
既要负责处理用户操作,也需要发送 UI 的状态或者事件。
现在,我们研究下基于 Redux 风格的 MVI。
为什么要采用 Redux 方式?
- 显式意图处理:每个用户操作都是一种特定类型的意图,便于追踪。
- 单一事实来源:状态对象是屏幕数据唯一的权威表示。
- 可预测性:通过单一的归约器来处理变化,你可以系统地追踪状态随时间的变化。
- 可扩展性:随着应用程序的增长,添加高级功能(例如事件、异步数据加载)会更容易,因为模式保持一致。
在更复杂的应用程序中,每个屏幕都可以定义自己的状态(State )、意图(Intent )和事件(Event)。
为了避免在流程和处理程序方面出现代码重复,你可以创建一个 BaseMviViewModel
,其中包含:
- 用于表示屏幕状态的
StateFlow
。 - 用于事件(如
Toast
、导航、弹窗)的SharedFlow
。 - 一个
reduce
函数,每个子视图模型都要重写此函数。
如果你不知道 StateFlow
和 SharedFlow
有什么区别,可以花点时间参考这篇文章。这里简而言之:StateFlow
用于表示表示 UI 的状态;SharedFlow
用于发送 UI 事件,例如弹出 Toast
Kotlin
/**
* Android 中 Redux 风格的 MVI 通用基类。
*
* @param S 屏幕状态的泛型
* @param I 屏幕意图的泛型
* @param E 一次性的事件,UI 仅处理一次
*/
abstract class BaseMviViewModel<S, I, E>(
initialState: S
) : ViewModel() {
// 当前不可变的状态
private val _viewState = MutableStateFlow(initialState)
val viewState: StateFlow<S> = _viewState.asStateFlow()
// 用于处理诸如 Toast 或导航之类的事件
private val _viewEffect = MutableSharedFlow<E>(replay = 0)
val viewEffect: SharedFlow<E> = _viewEffect
// 读取最新状态的方式
protected val currentState: S
get() = _viewState.value
/**
* 每当出现一个意图时,由 UI(或内部代码)调用。
* 1)使用(旧状态 + 意图)运行'reduce'函数。
* 2)用新状态更新 StateFlow。
* 3)如果有事件,将其发送出去供 UI 处理。
*/
fun processIntent(intent: I) {
val (newState, effect) = reduce(currentState, intent)
setState(newState)
effect?.let { postEffect(it) }
}
/**
* 子 ViewModel 需实现如何将旧状态与意图结合。
* 结果是(新状态,事件)。
*/
protected abstract fun reduce(
oldState: S,
intent: I
): Pair<S, E?>
/**
* 用新状态更新我们的StateFlow。
*/
protected fun setState(newState: S) {
_viewState.update { newState }
}
/**
* 发送一个事件。
*/
protected fun postEffect(effect: E) {
viewModelScope.launch {
_viewEffect.emit(effect)
}
}
}
Redux 风格的要点是:
viewState
:我们唯一的状态来源。viewEffect
:处理不属于持久状态的临时事件。processIntent(...)
:处理用户操作的唯一入口。reduce(...)
: 将(旧状态 + 意图)合并为(新状态,事件)。
接下来,让我们使用 BaseMviViewModel
将我们轻量级的 MVI 计数器改编成 Redux 的版本。我们将定义:
- 用于数据的
CounterState
- 针对用户操作的
CounterIntent
- 用于处理像吐司这类临时事件的
CounterEffect
Kotlin
// 1)状态
data class CounterState(
val count: Int = 0, // 计数,默认为0
val error: String? = null // 错误信息,可为空
)
// 2)意图
sealed class CounterIntent {
object Increment : CounterIntent() // 增加计数的意图
object Reset : CounterIntent() // 重置计数的意图
data class SetCount(val value: Int) : CounterIntent() // 设置特定计数值的意图
}
// 3)事件
sealed class CounterEffect {
data class ShowToast(val message: String) : CounterEffect() // 显示吐司消息的事件
// 或者,object NavigateToDetail : CounterEffect()(导航到详情页的事件示例)
}
Redux 风格的 ViewModel
:
Kotlin
class CounterReduxViewModel : BaseMviViewModel<
CounterState,
CounterIntent,
CounterEffect
>(
initialState = CounterState()
) {
override fun reduce(
oldState: CounterState,
intent: CounterIntent
): Pair<CounterState, CounterEffect?> {
return when (intent) {
is CounterIntent.Increment -> {
val newState = oldState.copy(
count = oldState.count + 1,
error = null
)
newState to null
}
is CounterIntent.Reset -> {
val newState = oldState.copy(
count = 0,
error = null
)
newState to null
}
is CounterIntent.SetCount -> {
if (intent.value < 0) {
val newState = oldState.copy(
error = "Negative counts not allowed"
)
newState to CounterEffect.ShowToast("Invalid count!")
} else {
val newState = oldState.copy(
count = intent.value,
error = null
)
newState to null
}
}
}
}
}
每当你使用 CounterIntent
调用 processIntent(...)
时,reduce
函数会计算出一个新的 CounterState
以及一个可选的 CounterEffect
。
在 Jetpack Compose 中,你需要收集状态(viewState
)和事件(viewEffect
):
Kotlin
@Composable
fun CounterReduxScreen(viewModel: CounterReduxViewModel = viewModel()) {
// 观察状态
val uiState by viewModel.viewState.collectAsStateWithLifecycle()
// 观察事件
val context = LocalContext.current
LaunchedEffect(Unit) {
viewModel.viewEffect.collect { effect ->
when (effect) {
is CounterEffect.ShowToast -> {
Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
}
}
}
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "计数: ${uiState.count}")
uiState.error?.let { errorMsg ->
Text(text = errorMsg, color = Color.Red)
}
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { viewModel.processIntent(CounterIntent.Increment) }) {
Text("Increment")
}
Button(onClick = { viewModel.processIntent(CounterIntent.Reset) }) {
Text("Reset")
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.processIntent(CounterIntent.SetCount(5)) }) {
Text("设置计数为5")
}
}
}
- 当状态发生变化时,
uiState
会自动重新组合。 processIntent(...)
是改变状态的唯一途径。- 像
ShowToast
这样的事件会在LaunchedEffect
块中启动,并且每一个新的事件只被消费一次。
为什么 Redux 风格的更好:
- 单一事实来源:通过引用
_viewState
获取数据,你的用户界面可以保持简单且具有响应性。 - 纯减速器:
reduce
函数具有确定性------易于测试(旧状态,意图) -> (新状态,事件)。 - 可扩展性:对于大型应用程序,可以通过引入新意图来处理高级事件(例如, API调用)。
- 团队开发的一致性:
BaseMviViewModel
可确保每个功能的状态管理都遵循相同的规则,简化代码审查和新成员入职流程。
MVI 之间的比较
在安卓架构领域,MVI 因其清晰性和单向数据流而备受瞩目。Redux 的 MVI 模式则更进一层,它围绕单个 Reducer 管道来组织代码,使状态更新具有可预测性且便于测试。通过将逻辑集中到 BaseMviViewModel
中,每个屏幕定义状态(State )、意图(Intent )以及可选的事件(Effect ),然后实现一个简洁明了的 reduce
方法。
尽管 Redux 的 MVI 模式在初始阶段可能会更加繁琐,但在可维护性、可扩展性和可预测性方面往往会收获回报,对于具有多步骤用户交互的复杂应用而言尤为如此。与 Jetpack Compose 相结合,便形成了一个现代的、响应式且可追踪的用户界面架构,它能与生命周期感知组件无缝集成。
无论你青睐简单的 MVI 模式还是结构更严谨的 Redux 风格,其原则始终不变:保持所有逻辑单向流动,集中管理状态,并将交互明确化。这一基础能够减少意外状况的出现,简化调试过程,并且让代码库能够稳定地发展壮大。
总结

安卓架构的发展历程体现了其在追求松散耦合、可测试性和强大状态管理方面的持续努力,这种努力,真正的体现了软件的"软"之所在:
- 以用户界面为中心的架构将所有内容都集中在一处(即"God Activity"),这使得代码脆弱且难以测试。
- 经典的 MVC 架构将逻辑与用户界面分离,但控制器仍直接引用
Activity
。 - MVP 引入了视图接口,使
Presenter
与具体的用户界面类解耦。 - MVVM 利用生命周期感知型的
ViewModel
自然地保留状态。用户界面观察响应式数据,将业务逻辑与用户界面代码隔离开来。 - MVI 通过"意图→状态"管道锁定单向状态更改,对于需要可预测流程的复杂应用程序而言是理想之选。
MVI 可以是轻量级的,也可以是 Redux 风格的,但它始终强调单一的"事实来源"以及明确的状态更改路径。
在现代项目中,越来越多的团队出于代码清晰性、可测试性以及对生命周期友好的结构等方面的考虑,倾向于选择 MVVM 或 MVI ,并结合 Jetpack Compose 使用。