软件为什么“软”——从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 使用。

相关推荐
网安Ruler16 分钟前
Web开发-PHP应用&文件操作安全&上传下载&任意读取删除&目录遍历&文件包含
android
aningxiaoxixi30 分钟前
android audio 之 Engine
android·前端·javascript
教程分享大师36 分钟前
带root_兆能ZN802及兆能ZNM802融合终端安卓9系统线刷机包 当贝纯净版
android·电脑
tbit1 小时前
Flutter Provider 用法总结(更新中...)
android·flutter
whysqwhw1 小时前
Android硬件加速全景解析与深度优化指南
android
whysqwhw1 小时前
RecyclerView 快速滑动场景优化 Bitmap 加载
android
whysqwhw1 小时前
DRouter IPC简化AIDL
android
旭宇1 小时前
PDF注释的加载和保存功能的实现
android·kotlin
Yang-Never2 小时前
Kotlin协程 ->launch构建协程以及调度源码详解
android·java·开发语言·kotlin·android studio
用户2018792831672 小时前
浅谈ClassNotFoundException 和 NoClassDefFoundError
android