本系列学习教程笔记属于详细讲解Kotlin语法的教程,需要快速学习Kotlin语法的小伙伴可以查看“简洁” 系列的教程
快速入门请阅读如下简洁教程:
Kotlin学习教程(一)
Kotlin学习教程(二)
Kotlin学习教程(三)
Kotlin学习教程(四)
Kotlin学习教程(五)
Kotlin学习教程(六)
Kotlin学习教程(七)
Kotlin学习教程(八)
Kotlin学习教程(九)
Kotlin学习教程(十)
Kotlin教程笔记(80) - MVVM架构设计
在Android应用开发中,MVVM(Model-View-ViewModel)架构是非常常见的一种设计模式,它通过ViewModel来管理UI相关的数据,并将这些数据与UI层分离,避免了Activity/Fragment与业务逻辑的强耦合。使用Kotlin实现MVVM模式,并结合ViewModel,可以有效地避免内存泄漏。下面我们就从设计思路和代码实现两个方面进行详细说明。
设计思路
- Model 层:
- 负责数据的获取和处理,可能通过网络请求、数据库查询等。
- 不与UI层直接交互,只与ViewModel进行数据的交互。
- ViewModel 层:
- 持有Model层的数据并通过LiveData等可观察数据类型暴露给View层。
- 负责业务逻辑处理,不持有View的引用,避免内存泄漏。
- ViewModel的生命周期与Activity/Fragment绑定,能在配置变化(如旋转屏幕)时保持数据不丢失。
- View 层(Activity/Fragment):
- 负责展示UI和用户交互,但不直接处理业务逻辑。
- 通过观察ViewModel中的数据(LiveData或StateFlow)来更新UI。
Kotlin MVVM 实现代码
1. Model 层
Model 层可以是一个普通的 Kotlin 类,负责获取数据。
kotlin复制代码data class User(val id: Int, val name: String, val age: Int)
class UserRepository {
// 模拟从数据库或网络获取用户数据
fun getUser(userId: Int): User {
return User(userId, "John Doe", 25)
}
}
2. ViewModel 层
ViewModel 持有Model数据,并通过 LiveData
暴露给 View 层。LiveData
是一个生命周期感知型数据容器,它能自动管理观察者,避免内存泄漏。
kotlin复制代码import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
// 使用LiveData来保存用户数据
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
// 获取用户信息
fun fetchUser(userId: Int) {
viewModelScope.launch {
// 模拟耗时操作
val user = userRepository.getUser(userId)
_user.value = user
}
}
}
设计说明:
MutableLiveData
是可变的数据容器,内部使用,LiveData
是只读的数据容器,暴露给外部使用。viewModelScope
是ViewModel提供的协程作用域,能够与ViewModel的生命周期保持一致,自动在ViewModel销毁时取消协程,避免内存泄漏。
3. View 层(Activity/Fragment)
View 层通过观察 LiveData
来响应数据变化,并更新UI。
kotlin复制代码import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
class UserActivity : AppCompatActivity() {
// 使用viewModels扩展函数获取ViewModel实例
private val userViewModel: UserViewModel by viewModels {
UserViewModelFactory(UserRepository())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 观察用户数据的变化
userViewModel.user.observe(this, Observer { user ->
// 更新UI
// findViewById<TextView>(R.id.tvUserName).text = user.name
// findViewById<TextView>(R.id.tvUserAge).text = user.age.toString()
})
// 模拟获取用户数据
userViewModel.fetchUser(1)
}
}
4. ViewModelFactory
如果 ViewModel
构造函数中需要传递参数,则可以通过 ViewModelFactory
来创建 ViewModel
。
kotlin复制代码import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class UserViewModelFactory(private val userRepository: UserRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
return UserViewModel(userRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
内存泄漏防范
- ViewModel 的使用:
ViewModel
不持有Activity
或Fragment
的引用,只通过LiveData
进行数据的暴露和通知,这样可以有效避免ViewModel
内存泄漏。- 通过
viewModelScope
来启动协程,避免手动管理协程生命周期的问题。
- LiveData 的使用:
LiveData
是生命周期感知型组件,它只在Activity
或Fragment
处于活跃状态时通知观察者,从而避免了因生命周期变化导致的内存泄漏。
- 观察者的移除:
- 由于
LiveData
与Activity/Fragment
的生命周期绑定,当Activity/Fragment
销毁时,观察者会自动移除,无需手动清理。
- 由于
结论
这个Kotlin实现的MVVM架构通过ViewModel
来管理数据,避免了将业务逻辑和UI操作耦合在一起。同时,LiveData
和 viewModelScope
确保了内存管理的安全性。
MutableLiveData
是 LiveData
的一个子类,用于保存和管理可变的数据。它的作用是允许应用程序中的数据可以在运行时动态更改,并且通知所有观察它的数据变化的观察者(通常是UI层,比如Activity或Fragment)。LiveData
本身是只读的,而 MutableLiveData
提供了对数据的修改权限。
MutableLiveData
的主要作用:
- 数据存储和管理:
MutableLiveData
可以用来存储任意类型的数据(例如字符串、数字、对象等)。当数据发生变化时,LiveData
会通知所有相关的观察者进行更新。
- 数据的可变性:
- 与
LiveData
不同,MutableLiveData
允许通过setValue()
或postValue()
方法来改变内部的数据。通常,ViewModel 通过MutableLiveData
来修改数据,而暴露给外部的仍然是不可变的LiveData
,这样可以保证外部代码只能读取数据而无法修改,遵循数据封装的原则。
- 与
- 生命周期感知:
LiveData
是生命周期感知型的组件,只有当观察者处于活动状态(即Activity
或Fragment
处于STARTED
或RESUMED
状态)时,才会接收到数据更新。这样可以避免Activity
/Fragment
的生命周期变化(如屏幕旋转、销毁重建等)导致的内存泄漏和不必要的UI更新。
MutableLiveData
的常见用法
kotlin复制代码// 定义 ViewModel
class UserViewModel : ViewModel() {
// 私有的 MutableLiveData,只允许内部修改
private val _userName = MutableLiveData<String>()
// 只读的 LiveData,暴露给外部观察
val userName: LiveData<String> = _userName
// 更新数据
fun updateUserName(newName: String) {
_userName.value = newName
}
}
在 Activity
或 Fragment
中观察数据
kotlin复制代码class UserActivity : AppCompatActivity() {
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 观察 LiveData 的变化
userViewModel.userName.observe(this, Observer { name ->
// 当 userName 发生变化时,更新UI
findViewById<TextView>(R.id.userNameTextView).text = name
})
// 更新用户数据
userViewModel.updateUserName("John Doe")
}
}
MutableLiveData
与 LiveData
的区别
MutableLiveData
允许数据的修改,外部可以通过setValue()
或postValue()
来更新数据。LiveData
是只读的,通常通过暴露LiveData
给外部,从而保护内部数据不被意外修改。
更新数据的两种方式:
setValue(T value)
:在主线程中更新数据。postValue(T value)
:可以在任何线程中更新数据,适用于需要在工作线程更新UI时使用。
总结
MutableLiveData
主要用于在 ViewModel 内部修改和管理数据,同时保证外部只能读取 LiveData
的数据,从而实现数据的封装和状态管理。在Android开发中,它是实现MVVM架构的重要工具,特别是在配合ViewModel使用时,能有效防止内存泄漏和无效的UI更新。
MutableLiveData如何传递多个复杂的对象?
ChatGPT 说:
ChatGPT
MutableLiveData
可以存储和传递任意类型的数据,包括多个复杂的对象。你可以将这些复杂对象封装成一个数据类或者使用List
、Map
、Pair
等数据结构来管理多个对象。下面详细说明如何使用 MutableLiveData
来传递多个复杂对象的几种常见方式。
方法一:使用数据类封装多个对象
如果你有多个相关的复杂对象,最好的方法是定义一个数据类,将这些对象封装在一起传递。
1. 定义数据类
kotlin复制代码data class UserInfo(
val user: User,
val address: Address,
val preferences: Preferences
)
data class User(val id: Int, val name: String)
data class Address(val city: String, val street: String)
data class Preferences(val notificationsEnabled: Boolean)
2. 在ViewModel中使用 MutableLiveData
kotlin复制代码class UserViewModel : ViewModel() {
// 定义 MutableLiveData 保存 UserInfo 对象
private val _userInfo = MutableLiveData<UserInfo>()
val userInfo: LiveData<UserInfo> = _userInfo
// 更新数据
fun updateUserInfo(user: User, address: Address, preferences: Preferences) {
val newUserInfo = UserInfo(user, address, preferences)
_userInfo.value = newUserInfo
}
}
3. 在 Activity
或 Fragment
中观察和使用
kotlin复制代码class UserActivity : AppCompatActivity() {
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 观察 userInfo 数据的变化
userViewModel.userInfo.observe(this, Observer { userInfo ->
// 使用 userInfo 数据更新 UI
val user = userInfo.user
val address = userInfo.address
val preferences = userInfo.preferences
findViewById<TextView>(R.id.userNameTextView).text = user.name
findViewById<TextView>(R.id.userAddressTextView).text = "${address.street}, ${address.city}"
findViewById<TextView>(R.id.userPreferencesTextView).text =
if (preferences.notificationsEnabled) "Notifications ON" else "Notifications OFF"
})
// 更新数据
userViewModel.updateUserInfo(
User(1, "John Doe"),
Address("New York", "5th Avenue"),
Preferences(true)
)
}
}
方法二:使用 List
或 Map
传递多个对象
如果你需要传递一个动态数量的对象(如用户列表),可以使用 List
或 Map
等集合类型。
1. 使用 List
kotlin复制代码class UserViewModel : ViewModel() {
// 使用 MutableLiveData 保存用户列表
private val _userList = MutableLiveData<List<User>>()
val userList: LiveData<List<User>> = _userList
// 更新用户列表
fun updateUserList(users: List<User>) {
_userList.value = users
}
}
2. 使用 Map
kotlin复制代码class UserViewModel : ViewModel() {
// 使用 MutableLiveData 保存 ID 到 User 对象的映射
private val _userMap = MutableLiveData<Map<Int, User>>()
val userMap: LiveData<Map<Int, User>> = _userMap
// 更新用户映射
fun updateUserMap(userMap: Map<Int, User>) {
_userMap.value = userMap
}
}
方法三:使用 Pair
或 Triple
如果你只有两到三个对象需要传递,并且不想创建单独的数据类,可以使用 Pair
或 Triple
这样的容器。
1. 使用 Pair
kotlin复制代码class UserViewModel : ViewModel() {
// 使用 MutableLiveData 保存两个对象的 Pair
private val _userAndAddress = MutableLiveData<Pair<User, Address>>()
val userAndAddress: LiveData<Pair<User, Address>> = _userAndAddress
// 更新数据
fun updateUserAndAddress(user: User, address: Address) {
_userAndAddress.value = Pair(user, address)
}
}
2. 使用 Triple
kotlin复制代码class UserViewModel : ViewModel() {
// 使用 MutableLiveData 保存三个对象的 Triple
private val _userAddressPreferences = MutableLiveData<Triple<User, Address, Preferences>>()
val userAddressPreferences: LiveData<Triple<User, Address, Preferences>> = _userAddressPreferences
// 更新数据
fun updateUserAddressPreferences(user: User, address: Address, preferences: Preferences) {
_userAddressPreferences.value = Triple(user, address, preferences)
}
}
总结
- 如果对象之间有明确的关联,使用 数据类 封装多个对象是最好的选择,能增强代码的可读性和可维护性。
- 如果是需要传递多个同类对象(如多个用户),可以使用 List 或 Map。
- 当仅有少数对象(如2到3个)并且不需要定义数据类时,可以使用 Pair 或 Triple 来简化代码。
这些方法都可以通过 MutableLiveData
管理和传递复杂对象,并且可以通过 LiveData
的观察机制让 UI 及时响应数据变化。
上面的MVVM架构模式有什么缺点吗?平时写代码如何规避这些缺点?
ChatGPT
MVVM 架构模式虽然在 Android 开发中非常流行,能有效地将 UI 层与业务逻辑分离,但它并非完美无缺,存在一些潜在的缺点。了解这些缺点并在开发中尽量规避它们,可以提高项目的可维护性和性能。
MVVM 架构的常见缺点
ViewModel 过于臃肿:
- 在实际开发中,业务逻辑、数据处理和状态管理等都可能会集中在 ViewModel 中,导致 ViewModel 代码过于复杂和臃肿。这会使得 ViewModel 变得难以维护和测试。
解决方案:
- 拆分业务逻辑: 使用 UseCase 或 Interactor 类,将复杂的业务逻辑放到独立的模块中,而不是堆积在 ViewModel 内。
- 单一职责原则: 遵循单一职责原则,确保 ViewModel 只负责数据的管理和状态的维护,不要将所有的业务逻辑放入其中。
- 层次化设计: 可以将某些与数据处理相关的逻辑放入
Repository
或Service
中,避免 ViewModel 承担过多责任。
难以管理复杂的 UI 逻辑:
- 在 MVVM 中,UI 的状态通常通过
LiveData
或StateFlow
等可观察对象驱动。如果 UI 状态过于复杂,比如有多个不同的状态组合,可能会导致复杂的逻辑控制,甚至难以调试。
解决方案:
- 状态机模式:对于复杂的 UI 状态管理,可以引入状态机模式(State Machine),将不同的 UI 状态进行清晰的定义,减少状态间的混乱。
- View 层处理 UI 逻辑:虽然 MVVM 强调将逻辑与 UI 分离,但部分复杂的 UI 控制逻辑可以适当留在 View 层,避免过度复杂的状态传递。
- 在 MVVM 中,UI 的状态通常通过
LiveData 更新逻辑不明确:
- 在 MVVM 中,
LiveData
的更新逻辑分布在 ViewModel 的多个方法中,有时可能会导致难以追踪状态的变化。特别是在并发操作中,LiveData
的更新顺序和时机可能不太容易控制。
解决方案:
- 避免滥用
MutableLiveData
:严格限制MutableLiveData
的使用范围,确保只有特定的操作可以修改数据。 - 使用单一入口修改数据:可以通过定义统一的更新方法或事件处理方法,确保数据的修改和状态变化有清晰的触发点。
- 使用
StateFlow
或SharedFlow
:相比于LiveData
,StateFlow
提供了更强的线程安全性和对数据流的明确控制,特别适合并发环境。
- 在 MVVM 中,
过多的观察者导致性能问题:
- 在复杂的应用中,过多的观察者可能会导致性能问题,尤其是当观察的数据量很大时,频繁的 UI 更新可能会拖慢渲染性能。
解决方案:
- 谨慎添加观察者:只在必要的生命周期内添加观察者,避免在不必要的生命周期中对
LiveData
进行订阅。 - 使用
DistinctUntilChanged
:在LiveData
中使用DistinctUntilChanged
,可以避免多次相同数据的更新,从而减少不必要的 UI 刷新。 - 懒加载:对于不经常使用的 UI 数据,可以延迟订阅
LiveData
,避免过早创建大量的观察者。
ViewModel 的生命周期管理:
- ViewModel 的生命周期比
Activity
和Fragment
更长,当 Activity/Fragment 销毁时,ViewModel 仍然可能持有某些状态或逻辑,可能会导致意想不到的问题。例如,UI已经销毁但数据仍然在更新。
解决方案:
- 正确管理协程和异步任务:使用
viewModelScope
来确保协程与 ViewModel 的生命周期一致,避免 ViewModel 持有长期异步任务导致内存泄漏。 - 取消无效的观察:在
Activity
或Fragment
销毁时,确保观察者能够正确解除订阅,避免LiveData
仍然持有 UI 的引用。 - 考虑 ViewModel 的清理工作:对于需要手动清理的资源(如某些回调、订阅等),在
onCleared()
中进行清理,避免资源泄漏。
- ViewModel 的生命周期比
难以测试复杂的 UI 交互:
- MVVM 通过
LiveData
或其他可观察的数据流来驱动 UI,这有时会增加 UI 交互的测试难度。特别是当存在多个观察者或复杂的状态管理时,编写单元测试会变得复杂。
解决方案:
- Mock
LiveData
和协程:在测试中,使用Mockito
或类似的框架来模拟LiveData
和协程行为,简化 ViewModel 的测试。 - 使用
StateFlow
测试:相比于LiveData
,StateFlow
更容易测试,因为它可以在不依赖 Android 的环境中测试数据流和状态管理。
- MVVM 通过
数据绑定的复杂性:
- 如果结合
DataBinding
使用 MVVM,在处理复杂的绑定表达式时,代码可能会变得难以维护,特别是在XML文件中书写复杂逻辑时,容易引发调试和维护难题。
解决方案:
- 减少复杂绑定表达式:尽量避免在 XML 中写过于复杂的表达式,将逻辑尽可能地移到 ViewModel 或辅助类中。
- 使用
BindingAdapter
:对于重复和复杂的UI更新逻辑,使用BindingAdapter
自定义UI的绑定逻辑,避免在XML中堆砌大量表达式。
- 如果结合
总结
MVVM 模式虽然能很好地实现业务逻辑和 UI 层的解耦,但它在实际使用中也会带来一些复杂性,如 ViewModel 变得臃肿、状态管理混乱等问题。通过遵循以下实践可以有效规避这些缺点:
- 遵循单一职责原则,合理拆分业务逻辑。
- 使用合适的数据流工具(如
StateFlow
)替代LiveData
以更好管理状态。 - 对复杂的 UI 状态引入状态机等模式,简化状态管理。
- 确保 ViewModel 的生命周期管理得当,避免内存泄漏和状态不一致问题。