软件为什么“软”——从Android架构史说起

软件为什么是"软"?

因为软件能够适应变化。

而安卓的架构历史,正好体现了这一点。

现代安卓开发建立在众多架构模式的基础之上。每种架构模式的出现都是为了解决前一种方法存在的问题,尤其是围绕用户界面(UI)与逻辑之间的耦合,以及在生命周期事件中管理状态的问题。

本文将逐步追溯这一演进过程,展示安卓开发者是如何从 "将所有内容都放在Activity中" 转变为采用更解耦、更便于测试的模式的。

这正是软件的"软"核心所在------通过解耦让软件具有更高的扩展性。

相对于硬件的"硬"------一旦产生就无法扩展和修改功能,软件能够不断的优化和扩展功能。

God Activity:All In One

早期的安卓应用常常将所有逻辑------业务逻辑、用户界面更新以及状态管理------都放入一个 ActivityFragment 中。这有时会被错误地称为 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 端的 MVCModel 负责业务逻辑和数据逻辑;View 负责生成 HTML 供浏览器展示;Controller 负责接收用户请求(如点击按钮、提交表单),调用模型处理业务逻辑,并将结果返回给 View

MVP:通过视图接口打破直接耦合

MVP (模型-视图-Persenter)通过引入视图接口解决了经典 MVC 中的双向耦合问题。Presenter 只引用这个接口,而不是具体的 ActivityFragment。同时,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)以响应式方式(LiveDataFlowRxJava 可观察对象)持有数据,而视图则观察这些数据。这意味着视图模型完全与用户界面无关,它不引用 ActivityFragment

这里实际上也有 Presenter,只不过变成了 VMViewModel)。

最重要的是,随着架构组件中视图模型(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),但视图模型对用户界面一无所知。
  • 通过响应式编程,视图模型通过可观察对象(如 LiveDataFlowRxJava)与视图进行通信。
  • 与 iOS 不同,在 iOS 中视图模型通常只是一个普通类,而在 Android 中,视图模型继承自 ViewModel 基类,将其与 ActivityFragment 的生命周期绑定,这意味着它会自动在配置更改后依然能够存活,对于开发者来讲,太省心了。

通过将生命周期感知和响应式更新相结合,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")
        }
    }
}

processIntentViewModel 的核心函数。

MVIViewModel 为了清晰起见,大部分情况下会将这部分逻辑提取到类似 processIntent 这个单独的函数中,原则就是:所有的更改都通过一个单一的步骤来进行。

MVI 的究极进化:归约(Redux)

什么是归约?

将复杂的事物、数据、问题等简化、归纳为更简单形式的意思。它是一种把多个元素、步骤或者复杂的状态逐步合并、精简,最终得到一个相对简洁结果的过程。

如果对照设计模式,有点像模板设计模式。只不过归约更加强调对于流程、数据处理的过程。

如果我们仔细思考 MVVM 或者 MVI,我们会总结出这样一个统一的业务逻辑:

用户操作 -> 处理用户操作 -> 转换成状态或者对 UI 的事件

上述处理用户操作到转化状态的流程,在上面的轻量级 MVI 示例中,是一个函数完成的,processIntent 既要负责处理用户操作,也需要发送 UI 的状态或者事件。

现在,我们研究下基于 Redux 风格的 MVI

为什么要采用 Redux 方式?

  • 显式意图处理:每个用户操作都是一种特定类型的意图,便于追踪。
  • 单一事实来源:状态对象是屏幕数据唯一的权威表示。
  • 可预测性:通过单一的归约器来处理变化,你可以系统地追踪状态随时间的变化。
  • 可扩展性:随着应用程序的增长,添加高级功能(例如事件、异步数据加载)会更容易,因为模式保持一致。

在更复杂的应用程序中,每个屏幕都可以定义自己的状态(State )、意图(Intent )和事件(Event)。

为了避免在流程和处理程序方面出现代码重复,你可以创建一个 BaseMviViewModel,其中包含:

  • 用于表示屏幕状态的 StateFlow
  • 用于事件(如 Toast、导航、弹窗)的 SharedFlow
  • 一个 reduce 函数,每个子视图模型都要重写此函数。

如果你不知道 StateFlowSharedFlow 有什么区别,可以花点时间参考这篇文章。这里简而言之: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 因其清晰性和单向数据流而备受瞩目。ReduxMVI 模式则更进一层,它围绕单个 Reducer 管道来组织代码,使状态更新具有可预测性且便于测试。通过将逻辑集中到 BaseMviViewModel 中,每个屏幕定义状态(State )、意图(Intent )以及可选的事件(Effect ),然后实现一个简洁明了的 reduce 方法。

尽管 ReduxMVI 模式在初始阶段可能会更加繁琐,但在可维护性、可扩展性和可预测性方面往往会收获回报,对于具有多步骤用户交互的复杂应用而言尤为如此。与 Jetpack Compose 相结合,便形成了一个现代的、响应式且可追踪的用户界面架构,它能与生命周期感知组件无缝集成。

无论你青睐简单的 MVI 模式还是结构更严谨的 Redux 风格,其原则始终不变:保持所有逻辑单向流动,集中管理状态,并将交互明确化。这一基础能够减少意外状况的出现,简化调试过程,并且让代码库能够稳定地发展壮大。

总结

安卓架构的发展历程体现了其在追求松散耦合、可测试性和强大状态管理方面的持续努力,这种努力,真正的体现了软件的"软"之所在:

  • 以用户界面为中心的架构将所有内容都集中在一处(即"God Activity"),这使得代码脆弱且难以测试。
  • 经典的 MVC 架构将逻辑与用户界面分离,但控制器仍直接引用 Activity
  • MVP 引入了视图接口,使 Presenter 与具体的用户界面类解耦。
  • MVVM 利用生命周期感知型的 ViewModel 自然地保留状态。用户界面观察响应式数据,将业务逻辑与用户界面代码隔离开来。
  • MVI 通过"意图→状态"管道锁定单向状态更改,对于需要可预测流程的复杂应用程序而言是理想之选。

MVI 可以是轻量级的,也可以是 Redux 风格的,但它始终强调单一的"事实来源"以及明确的状态更改路径。

在现代项目中,越来越多的团队出于代码清晰性、可测试性以及对生命周期友好的结构等方面的考虑,倾向于选择 MVVMMVI ,并结合 Jetpack Compose 使用。

相关推荐
阿巴斯甜15 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker15 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952716 小时前
Andorid Google 登录接入文档
android
黄林晴17 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android