Android 架构演进之路:从 MVC 到 MVI,拥抱单向数据流的革命

在移动应用开发的世界里,架构模式的演进从未停歇。从早期的 MVC 到后来的 MVP、MVVM,每一次变革都在尝试解决前一代架构的痛点。而今天,我们将探讨一种全新的架构模式 ——MVI(Model-View-Intent),它借鉴了前端 React 的单向数据流思想,并结合 Android 开发的最佳实践,为我们带来了一种更加清晰、可测试、可维护的代码结构。

一、架构模式的演进历程

1.1 MVC:原始的分层尝试

MVC(Model-View-Controller)是最早被广泛应用的架构模式之一,它将应用分为三个主要部分:

  • Model:负责数据和业务逻辑
  • View:负责 UI 展示
  • Controller:作为 View 和 Model 之间的桥梁,处理用户输入和状态更新

在 Android 中,Activity 通常扮演 Controller 的角色,但随着 UI 逻辑的复杂化,Activity 变得越来越臃肿,违反了单一职责原则。

1.2 MVP:引入 Presenter 层解耦

为了解决 MVC 的问题,MVP(Model-View-Presenter)应运而生:

  • Model:数据和业务逻辑
  • View:负责 UI 展示,通常是一个接口
  • Presenter:处理 UI 逻辑,与 Model 交互

MVP 通过接口将 View 和 Presenter 解耦,使得单元测试更加容易。但随着项目规模增大,Presenter 层可能会变得非常庞大。

1.3 MVVM:数据绑定的力量

MVVM(Model-View-ViewModel)在 MVP 的基础上引入了数据绑定:

  • Model:数据和业务逻辑
  • View:负责 UI 展示
  • ViewModel:暴露数据流,通过数据绑定与 View 交互

MVVM 通过 LiveData、Data Binding 等技术减少了 View 和 ViewModel 之间的耦合,但它仍然存在一些问题,例如双向数据流可能导致的复杂性。

1.4 MVI:单向数据流的革命

MVI(Model-View-Intent)是架构模式的最新演进,它借鉴了 React 的单向数据流思想:

  • Model:不可变的状态容器
  • View:订阅状态并渲染 UI
  • Intent:用户操作转换为 Intent,触发状态更新

MVI 的核心思想是单向数据流和状态不可变性,所有的状态变化都遵循一个可预测的流程,使得代码更加易于理解和调试。

二、MVI 架构的核心概念

2.1 单向数据流

MVI 的核心是单向数据流,它遵循以下流程:

  1. 用户操作产生 Intent
  2. Intent 被 ViewModel 处理
  3. ViewModel 根据 Intent 更新状态
  4. View 订阅状态变化并更新 UI

这种单向流动使得数据的流向变得清晰可追溯,大大降低了代码的复杂性。

2.2 不可变状态

在 MVI 中,状态是不可变的。每当有新的状态产生时,不会修改原有状态,而是创建一个新的状态对象。这样可以确保状态的变化是可追踪的,并且简化了状态管理。

2.3 状态容器

ViewModel 作为状态容器,负责管理和更新应用状态。它接收 Intent 并根据这些 Intent 生成新的状态,然后将新状态发送给 View。

三、MVI 架构的实现

3.1 基础架构实现

首先,我们需要定义一些基础接口和类来构建 MVI 架构:

kotlin

// BaseIntent.kt
interface BaseIntent {
}

// BaseState.kt
interface BaseState {
    data object Loading : BaseState  // 加载中状态
    data object Complete: BaseState // 处理完成
    data class Error(val message: String = "", val updateId: Long = System.currentTimeMillis()): BaseState // 异常
}

// BaseViewModel.kt
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

abstract class BaseViewModel : ViewModel() {

    /**
     * 获取上下文
     */
    protected fun getContext(): Context = BaseApplication.instance.applicationContext

    // 接收来自视图层的意图(Intent)
    val intentsFlow = MutableSharedFlow<BaseIntent>()

    // 用于向视图层发送状态更新,类型指定为BaseState
    val stateFlow = MutableStateFlow<BaseState?>(null)

    init {
        consumption()
    }

    /**
     * 分发消息
     */
    private fun consumption() {
        viewModelScope.launch {
            intentsFlow.collect { intent ->
                handleIntents(intent)
            }
        }
    }

    abstract fun handleIntents(intent: BaseIntent)

    // 方法用于向视图层发送新的BaseState类型的状态
    protected fun updateState(newState: BaseState) {
        viewModelScope.launch {
            delay(100) // 防止消息太快,丢失
            stateFlow.value = newState
        }
    }
}

// BaseActivity.kt
import android.util.Log
import android.view.LayoutInflater
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.launch
import kotlin.reflect.KClass

abstract class BaseActivity<VM : BaseViewModel, VB : ViewBinding>(
    override val vbInflater: (LayoutInflater) -> VB
) : AppCompatActivity() {

    val tag = this::class.simpleName

    private val _binding by lazy { vbInflater(layoutInflater) }
    val binding get() = _binding

    open val viewModel: VM by lazy {
        initializeViewModel()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        setupObservers()
        setupIntents()
        bindClickListener()
    }

    /**
     * 初始化ViewModel
     */
    private fun initializeViewModel(): VM {
        val classifier = this::class.supertypes[0].arguments[0].type?.classifier
        Log.d(this::class.simpleName, "initializeViewModel()----->${classifier}")
        val viewModelClass = (classifier as? KClass<VM>)?.java
            ?: throw IllegalArgumentException("Invalid ViewModel class")
        return ViewModelProvider(this)[viewModelClass]
    }

    /**
     * 分发状态
     */
    private fun setupObservers() {
        lifecycleScope.launch {
            viewModel.stateFlow.collect { state ->
                Log.d(tag, "setupObservers()---->$state")
                render(state)
            }
        }
    }

    // 抽象方法,由子类实现来根据不同的BaseState状态渲染UI
    abstract fun render(state: BaseState?)

    // 辅助方法,用于发送BaseIntent类型的意图到ViewModel
    protected fun sendIntent(intent: BaseIntent) {
        lifecycleScope.launch {
            viewModel.intentsFlow.emit(intent)
        }
    }

    // 设置意图,由子类实现
    open fun setupIntents() {}

    // 绑定点击事件,由子类实现
    open fun bindClickListener() {}
}

3.2 登录功能的 MVI 实现

接下来,我们以登录功能为例,展示如何使用 MVI 架构实现一个完整的功能模块。

首先是布局文件:

xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.appcompat.widget.AppCompatEditText
        android:id="@+id/et_username"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/et_password"
        app:layout_constraintVertical_chainStyle="packed"
        android:layout_marginStart="12dp"
        android:layout_marginEnd="12dp"
        android:layout_marginBottom="12dp"
        android:layout_width="match_parent"
        android:hint="手机号"
        android:layout_height="wrap_content"/>

    <androidx.appcompat.widget.AppCompatEditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        app:layout_constraintTop_toBottomOf="@+id/et_username"
        app:layout_constraintBottom_toTopOf="@+id/btn_login"
        android:hint="密码"
        android:layout_margin="12dp"
        app:layout_constraintVertical_chainStyle="packed"
        android:layout_height="wrap_content"/>

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:layout_marginStart="12dp"
        android:layout_marginEnd="12dp"
        android:textColor="@color/white"
        android:text="Login"
        android:background="@drawable/selector_rounded_button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_password"/>

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/progressBar"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:indeterminateTint="@color/main_color"
        app:indicatorColor="@color/main_color"
        app:trackColor="@color/main_color"
        android:layout_width="40dp"
        android:visibility="gone"
        android:layout_height="40dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

然后定义登录相关的 Intent、State 和 ViewModel:

kotlin

// LoginIntent.kt
sealed class LoginIntent: BaseIntent {
    data class EntryPhone(val phone: String): LoginIntent() // 手机号的意图
    data class EntryPassword(val password: String): LoginIntent() // 密码的意图
    data object Login: LoginIntent() // 点击登录
}

// LoginViewState.kt
sealed class LoginViewState: BaseState {
    data class LoginStatus(val highlight: Boolean) : LoginViewState()// 高亮状态
    data object Loading : LoginViewState()                   // 加载中状态
    data class Success(val user: UserBean?) : LoginViewState() // 成功登录状态
    data class Error(val message: String) : LoginViewState() // 登录错误状态
}

// LoginViewModel.kt
@HiltViewModel
class LoginViewModel @Inject constructor (
    private val repository: LoginRepository,
) : BaseViewModel() {

    private var phone: String = ""
    private var password: String = ""

    // 设置登录按钮是否可用
    private fun setLoginButtonEnabled(enabled: Boolean) {
        updateState(LoginViewState.LoginStatus(enabled))
    }

    /**
     * 调用登录
     */
    private fun login() {
        updateState(LoginViewState.Loading)
        viewModelScope.launch {
            repository.login(phone, password)
                .onSuccess {
                    updateState(LoginViewState.Success(it))
                }.onFailure {
                    updateState(LoginViewState.Error(it.message.toString()))
                }
        }
    }

    /**
     * 处理意图
     */
    override fun handleIntents(intent: BaseIntent) {
        when(intent) {
            is LoginIntent.EntryPhone -> {
                phone = intent.phone
                setLoginButtonEnabled(phone.length >= 6 && password.length >= 6)
            }
            is LoginIntent.EntryPassword -> {
                password = intent.password
                setLoginButtonEnabled(password.length >= 6 && phone.length >= 6)
            }
            is LoginIntent.Login -> {
                login()
            }
        }
    }
}

最后是 Activity 的实现:

kotlin

// LoginActivity.kt
@AndroidEntryPoint
class LoginActivity : BaseActivity<LoginViewModel, ActivityLoginBinding>(
    ActivityLoginBinding::inflate
) {

    /**
     * 监听状态
     */
    override fun render(state: BaseState?) {
        when(state) {
            is LoginViewState.LoginStatus -> {
                setLoginButtonEnabled(state.highlight)
            }
            is LoginViewState.Loading -> {
                showLoading()
            }
            is LoginViewState.Success -> {
                if (state.user != null) {
                    showSuccess(state.user)
                }
            }
            is LoginViewState.Error -> {
                showError(state.message)
            }
        }
    }

    /**
     * View视图与Intent意图进行绑定
     */
    override fun setupIntents() {
        binding.etUsername.addTextChangedListener { text ->
            sendIntent(LoginIntent.EntryPhone(text.toString()))
        }
        binding.etPassword.addTextChangedListener { text ->
            sendIntent(LoginIntent.EntryPassword(text.toString()))
        }
        binding.etUsername.setText("1234567890")
        binding.etPassword.setText("54321#")
    }

    /**
     * 设置点击事件
     */
    override fun bindClickListener() {
        super.bindClickListener()
        binding.btnLogin.setOnClickListener {
            sendIntent(LoginIntent.Login)
        }
    }

    /**
     * 登录成功
     */
    private fun showSuccess(user: UserBean) {
        binding.progressBar.visibility = View.GONE
        Toast.makeText(this, "Login successful: ${user.userName}", Toast.LENGTH_SHORT).show()
    }

    /**
     * 显示错误信息
     */
    override fun showError(errorMessage: String?) {
        super.showError(errorMessage)
        binding.progressBar.visibility = View.GONE
        Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()
    }

    // 登录按钮是否可用
    private fun setLoginButtonEnabled(enabled: Boolean) {
        binding.btnLogin.isEnabled = enabled
    }
}

四、MVI 架构的优势与挑战

4.1 优势

  1. 单向数据流:数据流动方向明确,易于理解和调试
  2. 可测试性:ViewModel 不依赖 View,单元测试更加简单
  3. 状态可追溯:所有状态变化都可以追踪,便于定位问题
  4. 解耦彻底:View 和 Model 完全解耦,职责更加清晰
  5. 一致性:所有状态更新都遵循相同的模式,代码风格统一

4.2 挑战

  1. 学习曲线:相较于 MVVM,MVI 的概念更加抽象,需要一定的时间来理解
  2. 初期开发效率:需要编写更多的代码(Intent 和 State),初期开发效率可能较低
  3. 状态管理复杂度:在复杂场景下,状态管理可能变得复杂,需要合理设计状态结构

五、总结

MVI 架构通过引入单向数据流和不可变状态,为 Android 开发带来了一种更加清晰、可测试、可维护的代码结构。虽然初期学习和开发成本较高,但在大型项目中,MVI 的优势将得到充分体现。

如果你正在寻找一种能够彻底解决 View 和 Model 耦合问题的架构模式,那么 MVI 绝对值得一试。通过遵循 MVI 的设计原则,你可以构建出更加健壮、易于维护的 Android 应用。

希望本文能够帮助你理解 MVI 架构的核心概念和实现方式,让你在 Android 开发的道路上更进一步。

机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集,个人经导师指导并认可通过的高分设计项目,评审分99分,代码完整确保可以运行,小白也可以亲自搞定,主要针对计算机相关专业的正在做大作业的学生和需要项目实战练习的学习者,可作为毕业设计、课程设计、期末大作业。 机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+文档报告+数据集机器学习作业,机器学习和深度学习方法实现的入侵检测源代码+
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值