在移动应用开发的世界里,架构模式的演进从未停歇。从早期的 MVC 到后来的 MVP、MVVM,每一次变革都在尝试解决前一代架构的痛点。而今天,我们将探讨一种全新的架构模式 ——MVI(Model-View-Intent),它借鉴了前端 React 的单向数据流思想,并结合 Android 开发的最佳实践,为我们带来了一种更加清晰、可测试、可维护的代码结构。
MVC(Model-View-Controller)是最早被广泛应用的架构模式之一,它将应用分为三个主要部分:
在 Android 中,Activity 通常扮演 Controller 的角色,但随着 UI 逻辑的复杂化,Activity 变得越来越臃肿,违反了单一职责原则。
为了解决 MVC 的问题,MVP(Model-View-Presenter)应运而生:
MVP 通过接口将 View 和 Presenter 解耦,使得单元测试更加容易。但随着项目规模增大,Presenter 层可能会变得非常庞大。
MVVM(Model-View-ViewModel)在 MVP 的基础上引入了数据绑定:
MVVM 通过 LiveData、Data Binding 等技术减少了 View 和 ViewModel 之间的耦合,但它仍然存在一些问题,例如双向数据流可能导致的复杂性。
MVI(Model-View-Intent)是架构模式的最新演进,它借鉴了 React 的单向数据流思想:
MVI 的核心思想是单向数据流和状态不可变性,所有的状态变化都遵循一个可预测的流程,使得代码更加易于理解和调试。
MVI 的核心是单向数据流,它遵循以下流程:
这种单向流动使得数据的流向变得清晰可追溯,大大降低了代码的复杂性。
在 MVI 中,状态是不可变的。每当有新的状态产生时,不会修改原有状态,而是创建一个新的状态对象。这样可以确保状态的变化是可追踪的,并且简化了状态管理。
ViewModel 作为状态容器,负责管理和更新应用状态。它接收 Intent 并根据这些 Intent 生成新的状态,然后将新状态发送给 View。
首先,我们需要定义一些基础接口和类来构建 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()
// 用于向视图层发送状态更新,类型指定为BaseState
val stateFlow = MutableStateFlow(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(
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)?.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() {}
}
接下来,我们以登录功能为例,展示如何使用 MVI 架构实现一个完整的功能模块。
首先是布局文件:
xml
然后定义登录相关的 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(
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 架构通过引入单向数据流和不可变状态,为 Android 开发带来了一种更加清晰、可测试、可维护的代码结构。虽然初期学习和开发成本较高,但在大型项目中,MVI 的优势将得到充分体现。
如果你正在寻找一种能够彻底解决 View 和 Model 耦合问题的架构模式,那么 MVI 绝对值得一试。通过遵循 MVI 的设计原则,你可以构建出更加健壮、易于维护的 Android 应用。
希望本文能够帮助你理解 MVI 架构的核心概念和实现方式,让你在 Android 开发的道路上更进一步。