从 MVC 到 MVI:Android 架构演进全景剖析与示例代码

一、为什么需要架构?

  • 业务膨胀: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,平滑过渡,持续交付。

相关推荐
whysqwhw3 小时前
安卓上WebRtc
android
青莲8433 小时前
内联函数 inline noinline crossinline reified
android
我又来搬代码了5 小时前
【Android】【bug】Json解析错误Expected BEGIN_OBJECT but was STRING...
android·json·bug
CANI_PLUS14 小时前
ESP32将DHT11温湿度传感器采集的数据上传到XAMPP的MySQL数据库
android·数据库·mysql
来来走走15 小时前
Flutter SharedPreferences存储数据基本使用
android·flutter
安卓开发者16 小时前
Android模块化架构深度解析:从设计到实践
android·架构
雨白17 小时前
HTTP协议详解(二):深入理解Header与Body
android·http
阿豪元代码17 小时前
深入理解 SurfaceFlinger —— 如何调试 SurfaceFlinger
android
阿豪元代码17 小时前
深入理解 SurfaceFlinger —— 概述
android