MVI 是 Model-View-Intent 的缩写,它是一种架构模式,通常用于构建用户界面,尤其在 Android 开发中越来越受欢迎。它强调单向数据流 和不可变性,这使得应用的状态变化更加可预测和易于调试。
核心思想
MVI 的核心思想是将应用的状态和用户交互分离开来,并通过一个明确的循环来管理它们。
-
Model (模型):
- 是什么? Model 代表了应用的状态 (State) 和业务逻辑。
- 状态 (State) : 这是 MVI 中最核心的概念。UI 的每一个可能的表现都应该被一个不可变的 State 对象所描述。例如,一个列表界面的 State 可能包含
isLoading(是否正在加载)、data(列表数据)、error(错误信息) 等字段。 - 不可变性: 状态对象一旦创建,就不能被修改。当需要更新 UI 时,必须创建一个包含新状态的全新对象。这使得状态变化轨迹非常清晰。
- 业务逻辑: Model 也包含了处理数据、执行操作(如网络请求、数据库读写)的逻辑。
-
View (视图):
- 是什么? View 就是用户界面,负责展示 Model 中的状态 并将用户的交互意图 (Intent) 传递出去。
- 被动性: View 本身不包含任何业务逻辑。它就像一个 "渲染器",收到什么 State 就展示什么。
- 观察状态: View 会订阅(观察)Model 提供的状态流。当状态发生变化时,View 会自动收到通知并更新自己。
-
Intent (意图):
- 是什么? Intent 代表了用户的一次交互行为 或一个操作请求。它是一个不可变的数据类,封装了用户想要做什么。
- 例子 : 点击按钮、输入文本、下拉刷新等。这些都可以被封装成一个个 Intent 对象,如
ClickLoginButtonIntent、UpdateUsernameIntent、PullToRefreshIntent。 - 单向触发: 用户操作产生 Intent,Intent 被发送出去,触发后续的状态变化。
MVI 的工作流程 (单向数据流)
MVI 的数据流是单向的,形成一个闭环,非常容易理解和追踪:
- 用户交互: 用户在 View 上进行操作,比如点击一个 "加载数据" 的按钮。
- 触发 Intent : View 捕获到这个点击事件,将其封装成一个特定的 Intent(例如
LoadDataIntent),然后发送给ViewModel(或类似的中间层)。 - 处理 Intent :
ViewModel接收 Intent。它会根据 Intent 的类型,调用相应的业务逻辑(可能在 Model 层中)。例如,LoadDataIntent会触发一个网络请求去获取数据。 - 产生新 State : 业务逻辑执行后(可能是同步的也可能是异步的),会产生一个新的应用状态(State)。例如,网络请求开始时,会产生一个
LoadingState;请求成功后,会产生一个包含数据的DataLoadedState;请求失败则会产生一个ErrorState。 - 发送 State 到 View :
ViewModel将这个新的 State 发送给 View。 - View 渲染 : View 接收到新的 State,根据 State 中的数据和标志位来更新 UI。例如,如果是
LoadingState就显示进度条,如果是DataLoadedState就显示数据列表,如果是ErrorState就显示错误提示。
这个流程可以简化为: View --(用户操作)--> Intent --(触发)--> ViewModel --(处理 / 调用 Model)--> 新State --(通知)--> View --(渲染)--> ... (循环)
MVI 的优势
- 高度可预测性: 由于状态是不可变的且数据流是单向的,应用的行为变得非常可预测。任何状态的变化都可以追溯到一个具体的 Intent。
- 易于调试: 因为所有状态变化都遵循一个清晰的路径,你可以轻松地打印出每一个 Intent 和每一次 State 的变化,从而快速定位问题。时间旅行调试(Time Travel Debugging)在 MVI 中也更容易实现。
- 关注点分离 (Separation of Concerns): View 只负责展示,ViewModel 只负责处理逻辑和管理状态,Model 负责数据和业务规则。职责清晰,代码结构更清晰。
- UI 与逻辑解耦: View 不依赖于任何具体的业务逻辑实现,它只关心如何根据 State 来绘制界面。这使得 UI 测试变得非常简单,你可以模拟各种 State 来测试 View 的表现。
- 状态集中管理: 应用的所有状态都集中在 State 对象中,避免了状态分散在各个 View 或控件中的情况。
MVI 的挑战
- 样板代码: 相比其他模式(如 MVP),MVI 可能需要编写更多的模板代码,特别是大量的 Intent 和 State 类。
- 学习曲线: 对于习惯了双向绑定或更松散架构的开发者来说,理解和适应单向数据流和不可变性需要一定的学习成本。
- 状态设计: 设计一个好的、细粒度的 State 模型是关键。如果 State 设计得过于粗糙,可能会导致不必要的 UI 重绘;如果过于精细,又会增加复杂性。
总结
MVI 是一种非常强大和优雅的架构模式,特别适合构建复杂且状态多变的应用。它通过强制单向数据流和不可变性,让应用的状态管理变得简单、可预测和易于测试。虽然有一些初始的学习和代码成本,但从长远来看,它能极大地提高代码的质量和可维护性。
如何在 MVI 模式中进行数据持久化?
在 MVI 模式中进行数据持久化,核心原则是将数据持久化操作封装在 Model 层,确保数据流的单向性和可预测性。以下是具体的实现思路和步骤:
1. 数据持久化的核心原则
- 单一数据源:Model 层是唯一的数据来源,View 层不直接访问持久化存储。
- 单向数据流 :
- View 发送 Intent → ViewModel 处理 → Model 层执行持久化操作 → Model 更新状态 → View 渲染新状态。
- 可测试性:持久化逻辑应易于模拟和测试,避免直接在 ViewModel 或 View 中操作数据库 / 文件。
2. 分层架构设计
建议采用以下分层结构,清晰分离关注点:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ View │ │ ViewModel │ │ Model │ │ Persistence │
│ (UI 组件) │────▶│ (状态管理) │────▶│ (业务逻辑) │────▶│ (数据库/文件) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
▲ ▲ ▲ │
│ │ │ │
└──────────────────┴──────────────────┴──────────────────┘
(状态反馈,单向数据流)
3. 具体实现步骤
步骤 1:定义数据模型(Entity)
创建数据类,代表需要持久化的数据。
// 数据模型
data class User(
val id: String,
val name: String,
val email: String
)
步骤 2:定义持久化接口(Repository)
在 Model 层定义一个 Repository 接口,封装持久化操作(如增删改查)。这样可以隔离具体的持久化实现(如 Room、SharedPreferences、Retrofit)。
// 持久化仓库接口
interface UserRepository {
suspend fun saveUser(user: User)
suspend fun getUserById(id: String): User?
suspend fun deleteUser(id: String)
suspend fun getAllUsers(): List<User>
}
步骤 3:实现 Repository
根据需求选择持久化方案(Room、DataStore、文件等),实现 UserRepository 接口。
示例:使用 Room 数据库
// Room 实体
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
val name: String,
val email: String
)
// Room DAO
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: UserEntity)
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: String): UserEntity?
@Delete
suspend fun delete(user: UserEntity)
@Query("SELECT * FROM users")
suspend fun getAllUsers(): List<UserEntity>
}
// Room 数据库
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Repository 实现
class UserRepositoryImpl(
private val userDao: UserDao
) : UserRepository {
override suspend fun saveUser(user: User) {
userDao.insert(user.toEntity())
}
override suspend fun getUserById(id: String): User? {
return userDao.getUserById(id)?.toDomain()
}
override suspend fun deleteUser(id: String) {
userDao.getUserById(id)?.let { userDao.delete(it) }
}
override suspend fun getAllUsers(): List<User> {
return userDao.getAllUsers().map { it.toDomain() }
}
// 转换函数:Domain → Entity
private fun User.toEntity(): UserEntity {
return UserEntity(id, name, email)
}
// 转换函数:Entity → Domain
private fun UserEntity.toDomain(): User {
return User(id, name, email)
}
}
步骤 4:在 Model 层集成 Repository
Model 层负责调用 Repository 进行数据持久化,并更新应用状态。
// 应用状态
data class AppState(
val users: List<User> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
// Model 层(业务逻辑)
class UserModel(
private val userRepository: UserRepository
) {
// 状态流
private val _state = MutableStateFlow(AppState())
val state: StateFlow<AppState> = _state.asStateFlow()
// 处理 Intent
suspend fun handleIntent(intent: UserIntent) {
when (intent) {
is UserIntent.SaveUser -> saveUser(intent.user)
is UserIntent.LoadUsers -> loadUsers()
is UserIntent.DeleteUser -> deleteUser(intent.userId)
}
}
private suspend fun saveUser(user: User) {
_state.update { it.copy(isLoading = true) }
try {
userRepository.saveUser(user)
// 保存成功后,重新加载用户列表
loadUsers()
} catch (e: Exception) {
_state.update { it.copy(error = e.message, isLoading = false) }
}
}
private suspend fun loadUsers() {
_state.update { it.copy(isLoading = true) }
try {
val users = userRepository.getAllUsers()
_state.update { it.copy(users = users, isLoading = false, error = null) }
} catch (e: Exception) {
_state.update { it.copy(error = e.message, isLoading = false) }
}
}
private suspend fun deleteUser(userId: String) {
_state.update { it.copy(isLoading = true) }
try {
userRepository.deleteUser(userId)
loadUsers()
} catch (e: Exception) {
_state.update { it.copy(error = e.message, isLoading = false) }
}
}
}
// Intent 定义
sealed class UserIntent {
data class SaveUser(val user: User) : UserIntent()
object LoadUsers : UserIntent()
data class DeleteUser(val userId: String) : UserIntent()
}
步骤 5:ViewModel 调用 Model 层
ViewModel 接收 View 发送的 Intent,调用 Model 层处理,并将状态暴露给 View。
class UserViewModel(
private val userModel: UserModel
) : ViewModel() {
// 状态流(暴露给 View)
val state: StateFlow<AppState> = userModel.state
// 处理 View 发送的 Intent
fun onIntent(intent: UserIntent) {
viewModelScope.launch {
userModel.handleIntent(intent)
}
}
}
步骤 6:View 层触发 Intent 并渲染状态
View 层通过 ViewModel 发送 Intent,并观察状态变化以更新 UI。
// Activity/Fragment 中
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 观察状态变化
lifecycleScope.launchWhenStarted {
viewModel.state.collect { state ->
if (state.isLoading) {
// 显示加载中
progressBar.visibility = View.VISIBLE
} else {
progressBar.visibility = View.GONE
if (state.error != null) {
// 显示错误信息
Toast.makeText(this@UserActivity, state.error, Toast.LENGTH_SHORT).show()
} else {
// 更新 UI 列表
updateUserList(state.users)
}
}
}
}
// 按钮点击:保存用户
saveButton.setOnClickListener {
val user = User(
id = UUID.randomUUID().toString(),
name = nameInput.text.toString(),
email = emailInput.text.toString()
)
viewModel.onIntent(UserIntent.SaveUser(user))
}
// 页面加载时:加载用户列表
viewModel.onIntent(UserIntent.LoadUsers)
}
private fun updateUserList(users: List<User>) {
// 更新 RecyclerView 适配器
userAdapter.submitList(users)
}
}
4. 其他持久化方案
除了 Room,还可以根据需求选择:
- DataStore:用于存储键值对(替代 SharedPreferences)。
- 文件存储:适用于存储大文件或复杂数据结构。
- 远程 API + 本地缓存:结合 Retrofit 和 Room,实现数据同步。
5. 总结
在 MVI 中进行数据持久化的关键是:
- 封装:将持久化操作封装在 Model 层的 Repository 中。
- 单向数据流:确保所有数据操作都通过 Intent 触发,状态变化通过 State 反馈。
- 可测试性:通过接口隔离具体实现,便于单元测试。
通过以上步骤,可以在 MVI 模式中优雅地实现数据持久化,同时保持代码的清晰和可维护性。
如何在Model层实现数据持久化?
在 MVI 模式中,Model 层 是负责数据管理、业务逻辑和持久化操作的核心。数据持久化的实现主要依赖于 Repository 模式 和 数据源封装,确保数据操作的一致性、可测试性和可扩展性。以下是具体的实现步骤和最佳实践:
1. Model 层的核心组件
在 MVI 中,Model 层通常包含以下组件:
- 数据模型(Data Class/Entity):定义数据结构(如用户、商品等)。
- 仓库(Repository):封装数据操作逻辑,作为数据源的统一入口。
- 数据源(Data Source):负责实际的持久化操作(如数据库、文件、网络)。
- 状态管理(State):维护应用状态(如加载中、成功、错误)。
2. 数据持久化的实现步骤
步骤 1:定义数据模型
首先,创建数据类来表示需要持久化的数据。这些类通常是 不可变的 (使用 data class 和 val)。
// 领域模型(Domain Model)
data class User(
val id: String,
val name: String,
val email: String,
val createdAt: Long = System.currentTimeMillis()
)
步骤 2:定义数据源接口
为了隔离持久化实现(如 Room、DataStore、文件等),定义 数据源接口。每个数据源负责一种数据存储方式(本地或远程)。
// 本地数据源接口
interface LocalDataSource {
suspend fun saveUser(user: User)
suspend fun getUserById(id: String): User?
suspend fun deleteUser(id: String)
suspend fun getAllUsers(): List<User>
}
// 远程数据源接口(如果需要从网络获取数据)
interface RemoteDataSource {
suspend fun fetchUserFromApi(id: String): User
}
步骤 3:实现数据源
根据实际需求,实现本地或远程数据源。以下是常见的持久化方案:
方案 A:使用 Room 数据库(推荐)
Room 是 Android 官方推荐的本地数据库解决方案,适合结构化数据存储。
1. 添加依赖
// build.gradle (Module level)
dependencies {
def room_version = "2.5.2"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version" // 协程支持
}
2. 定义实体(Entity)
将领域模型转换为 Room 实体(如果需要)。
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
val name: String,
val email: String,
val createdAt: Long
)
// 扩展函数:Domain Model ↔ Room Entity
fun User.toEntity(): UserEntity {
return UserEntity(id, name, email, createdAt)
}
fun UserEntity.toDomain(): User {
return User(id, name, email, createdAt)
}
3. 定义 DAO(Data Access Object)
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: UserEntity)
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: String): UserEntity?
@Delete
suspend fun delete(user: UserEntity)
@Query("SELECT * FROM users")
suspend fun getAllUsers(): List<UserEntity>
}
4. 实现 Room 数据库
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
// 单例模式
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
5. 实现 LocalDataSource
class LocalDataSourceImpl(
private val userDao: UserDao
) : LocalDataSource {
override suspend fun saveUser(user: User) {
userDao.insert(user.toEntity())
}
override suspend fun getUserById(id: String): User? {
return userDao.getUserById(id)?.toDomain()
}
override suspend fun deleteUser(id: String) {
userDao.getUserById(id)?.let { userDao.delete(it) }
}
override suspend fun getAllUsers(): List<User> {
return userDao.getAllUsers().map { it.toDomain() }
}
}
方案 B:使用 DataStore(替代 SharedPreferences)
DataStore 是 Google 推出的用于存储键值对或类型化数据的现代解决方案。
1. 添加依赖
dependencies {
implementation "androidx.datastore:datastore-preferences:1.0.0"
}
2. 实现 LocalDataSource
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class LocalDataSourceImpl(
private val dataStore: DataStore<Preferences>
) : LocalDataSource {
private companion object {
val USER_KEY = stringPreferencesKey("user")
}
override suspend fun saveUser(user: User) {
dataStore.edit { preferences ->
preferences[USER_KEY] = Json.encodeToString(user) // 使用 JSON 序列化
}
}
override suspend fun getUserById(id: String): User? {
// 假设 DataStore 中存储单个用户,实际使用时可能需要调整
return dataStore.data.map { preferences ->
preferences[USER_KEY]?.let { Json.decodeFromString<User>(it) }
}.firstOrNull()
}
override suspend fun deleteUser(id: String) {
dataStore.edit { preferences ->
preferences.remove(USER_KEY)
}
}
override suspend fun getAllUsers(): List<User> {
// 示例:返回单个用户
return getUserById("")?.let { listOf(it) } ?: emptyList()
}
}
步骤 4:实现 Repository
Repository 是 Model 层的核心,负责协调本地和远程数据源,并提供统一的数据访问接口。
class UserRepositoryImpl(
private val localDataSource: LocalDataSource,
private val remoteDataSource: RemoteDataSource? = null // 可选:远程数据源
) : UserRepository {
override suspend fun saveUser(user: User) {
localDataSource.saveUser(user)
}
override suspend fun getUserById(id: String): User? {
// 策略:先从本地获取,本地没有再从远程获取
return localDataSource.getUserById(id)
?: remoteDataSource?.fetchUserFromApi(id)?.also {
localDataSource.saveUser(it) // 缓存到本地
}
}
override suspend fun deleteUser(id: String) {
localDataSource.deleteUser(id)
}
override suspend fun getAllUsers(): List<User> {
return localDataSource.getAllUsers()
}
}
// Repository 接口
interface UserRepository {
suspend fun saveUser(user: User)
suspend fun getUserById(id: String): User?
suspend fun deleteUser(id: String)
suspend fun getAllUsers(): List<User>
}
步骤 5:集成到 Model 层
在 MVI 的 Model 层中,使用 Repository 来处理数据持久化,并更新应用状态。
// 应用状态
data class AppState(
val users: List<User> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
// Model 层(业务逻辑 + 状态管理)
class UserModel(
private val userRepository: UserRepository
) {
private val _state = MutableStateFlow(AppState())
val state: StateFlow<AppState> = _state.asStateFlow()
suspend fun handleIntent(intent: UserIntent) {
when (intent) {
is UserIntent.SaveUser -> saveUser(intent.user)
is UserIntent.LoadUsers -> loadUsers()
is UserIntent.DeleteUser -> deleteUser(intent.userId)
}
}
private suspend fun saveUser(user: User) {
_state.update { it.copy(isLoading = true) }
try {
userRepository.saveUser(user)
loadUsers() // 保存后重新加载列表
} catch (e: Exception) {
_state.update { it.copy(error = e.message, isLoading = false) }
}
}
private suspend fun loadUsers() {
_state.update { it.copy(isLoading = true) }
try {
val users = userRepository.getAllUsers()
_state.update { it.copy(users = users, isLoading = false, error = null) }
} catch (e: Exception) {
_state.update { it.copy(error = e.message, isLoading = false) }
}
}
private suspend fun deleteUser(userId: String) {
_state.update { it.copy(isLoading = true) }
try {
userRepository.deleteUser(userId)
loadUsers() // 删除后重新加载列表
} catch (e: Exception) {
_state.update { it.copy(error = e.message, isLoading = false) }
}
}
}
// Intent 定义
sealed class UserIntent {
data class SaveUser(val user: User) : UserIntent()
object LoadUsers : UserIntent()
data class DeleteUser(val userId: String) : UserIntent()
}
3. 依赖注入(推荐)
为了简化组件间的依赖关系,推荐使用 依赖注入框架(如 Hilt)。
示例:使用 Hilt 注入 Repository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return AppDatabase.getInstance(context)
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
@Provides
fun provideLocalDataSource(userDao: UserDao): LocalDataSource {
return LocalDataSourceImpl(userDao)
}
@Provides
@Singleton
fun provideUserRepository(localDataSource: LocalDataSource): UserRepository {
return UserRepositoryImpl(localDataSource)
}
}
在 ViewModel 中注入 Repository:
import javax.inject.Inject
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
// ...
}
4. 总结
在 MVI 模式的 Model 层实现数据持久化的关键是:
- 分层设计:通过 Repository 隔离数据源,封装业务逻辑。
- 接口驱动:定义数据源接口,便于替换不同的持久化方案。
- 不可变性:确保数据模型和状态对象不可变,避免副作用。
- 依赖注入:使用 Hilt 等框架简化组件间的依赖管理。
通过以上步骤,可以实现一个清晰、可测试且易于维护的数据持久化层,同时保持 MVI 模式的单向数据流特性。