本系列学习教程笔记属于详细讲解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 实现代码
- 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)
}
}
- 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销毁时取消协程,避免内存泄漏。
- 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)
}
}
- ViewModelFactory
如果 ViewModel 构造函数中需要传递参数,则可以通过 ViewModelFactory 来创建 ViewModel。
kotlin复制代码import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class UserViewModelFactory(private val userRepository: UserRepository) : ViewModelProvider.Factory {
override fun create(modelClass: Class): 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 来传递多个复杂对象的几种常见方式。
方法一:使用数据类封装多个对象
如果你有多个相关的复杂对象,最好的方法是定义一个数据类,将这些对象封装在一起传递。
- 定义数据类
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)
在ViewModel中使用 MutableLiveData
kotlin复制代码class UserViewModel : ViewModel() {// 定义 MutableLiveData 保存 UserInfo 对象
private val _userInfo = MutableLiveData()
val userInfo: LiveData = _userInfo// 更新数据
fun updateUserInfo(user: User, address: Address, preferences: Preferences) {val newUserInfo = UserInfo(user, address, preferences) _userInfo.value = newUserInfo
}
}在 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 等集合类型。使用 List
kotlin复制代码class UserViewModel : ViewModel() {// 使用 MutableLiveData 保存用户列表
private val _userList = MutableLiveData>()
val userList: LiveData> = _userList// 更新用户列表
fun updateUserList(users: List) {_userList.value = users
}
}使用 Map
kotlin复制代码class UserViewModel : ViewModel() {// 使用 MutableLiveData 保存 ID 到 User 对象的映射
private val _userMap = MutableLiveData>()
val userMap: LiveData> = _userMap// 更新用户映射
fun updateUserMap(userMap: Map) {_userMap.value = userMap
}
}
方法三:使用 Pair 或 Triple
如果你只有两到三个对象需要传递,并且不想创建单独的数据类,可以使用 Pair 或 Triple 这样的容器。使用 Pair
kotlin复制代码class UserViewModel : ViewModel() {// 使用 MutableLiveData 保存两个对象的 Pair
private val _userAndAddress = MutableLiveData>()
val userAndAddress: LiveData> = _userAndAddress// 更新数据
fun updateUserAndAddress(user: User, address: Address) {_userAndAddress.value = Pair(user, address)
}
}使用 Triple
kotlin复制代码class UserViewModel : ViewModel() {// 使用 MutableLiveData 保存三个对象的 Triple
private val _userAddressPreferences = MutableLiveData>()
val userAddressPreferences: LiveData> = _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 层,避免过度复杂的状态传递。
LiveData 更新逻辑不明确:
在 MVVM 中,LiveData 的更新逻辑分布在 ViewModel 的多个方法中,有时可能会导致难以追踪状态的变化。特别是在并发操作中,LiveData 的更新顺序和时机可能不太容易控制。
解决方案:
避免滥用 MutableLiveData:严格限制 MutableLiveData 的使用范围,确保只有特定的操作可以修改数据。
使用单一入口修改数据:可以通过定义统一的更新方法或事件处理方法,确保数据的修改和状态变化有清晰的触发点。
使用 StateFlow 或 SharedFlow:相比于 LiveData,StateFlow 提供了更强的线程安全性和对数据流的明确控制,特别适合并发环境。
过多的观察者导致性能问题:
在复杂的应用中,过多的观察者可能会导致性能问题,尤其是当观察的数据量很大时,频繁的 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() 中进行清理,避免资源泄漏。
难以测试复杂的 UI 交互:
MVVM 通过 LiveData 或其他可观察的数据流来驱动 UI,这有时会增加 UI 交互的测试难度。特别是当存在多个观察者或复杂的状态管理时,编写单元测试会变得复杂。
解决方案:
Mock LiveData 和协程:在测试中,使用 Mockito 或类似的框架来模拟 LiveData 和协程行为,简化 ViewModel 的测试。
使用 StateFlow 测试:相比于 LiveData,StateFlow 更容易测试,因为它可以在不依赖 Android 的环境中测试数据流和状态管理。
数据绑定的复杂性:
如果结合 DataBinding 使用 MVVM,在处理复杂的绑定表达式时,代码可能会变得难以维护,特别是在XML文件中书写复杂逻辑时,容易引发调试和维护难题。
解决方案:
减少复杂绑定表达式:尽量避免在 XML 中写过于复杂的表达式,将逻辑尽可能地移到 ViewModel 或辅助类中。
使用 BindingAdapter:对于重复和复杂的UI更新逻辑,使用 BindingAdapter 自定义UI的绑定逻辑,避免在XML中堆砌大量表达式。
总结
MVVM 模式虽然能很好地实现业务逻辑和 UI 层的解耦,但它在实际使用中也会带来一些复杂性,如 ViewModel 变得臃肿、状态管理混乱等问题。通过遵循以下实践可以有效规避这些缺点:
遵循单一职责原则,合理拆分业务逻辑。
使用合适的数据流工具(如 StateFlow)替代 LiveData 以更好管理状态。
对复杂的 UI 状态引入状态机等模式,简化状态管理。
确保 ViewModel 的生命周期管理得当,避免内存泄漏和状态不一致问题。