Kotlin教程笔记(80) - MVVM架构设计

简介: 本系列学习教程笔记详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。对于希望快速学习Kotlin语法的读者,建议参考“简洁”系列教程。本文重点介绍了Kotlin实现MVVM架构的设计思路和代码实现,包括Model、ViewModel和View层的具体实现,以及如何通过LiveData和viewModelScope有效管理数据和内存,避免内存泄漏。此外,还讨论了MVVM架构的常见缺点及应对策略,帮助开发者在实际项目中更好地应用这一设计模式。

本系列学习教程笔记属于详细讲解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)
}
}

  1. 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销毁时取消协程,避免内存泄漏。

  1. 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)
}

}

  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 来传递多个复杂对象的几种常见方式。

方法一:使用数据类封装多个对象
如果你有多个相关的复杂对象,最好的方法是定义一个数据类,将这些对象封装在一起传递。

  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)

  1. 在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
    

    }
    }

  2. 在 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 等集合类型。

  3. 使用 List
    kotlin复制代码class UserViewModel : ViewModel() {

    // 使用 MutableLiveData 保存用户列表
    private val _userList = MutableLiveData>()
    val userList: LiveData> = _userList

    // 更新用户列表
    fun updateUserList(users: List) {

     _userList.value = users
    

    }
    }

  4. 使用 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 这样的容器。

  5. 使用 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)
    

    }
    }

  6. 使用 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 的生命周期管理得当,避免内存泄漏和状态不一致问题。

相关文章
|
7天前
|
Kotlin 索引
Kotlin教程笔记(22) -常见高阶函数
Kotlin教程笔记(22) -常见高阶函数
|
7天前
|
Kotlin
Kotlin教程笔记(21) -高阶函数与函数引用
Kotlin教程笔记(21) -高阶函数与函数引用
|
7天前
|
Kotlin
Kotlin教程笔记(20) - 枚举与密封类
Kotlin教程笔记(20) - 枚举与密封类
19 3
|
7天前
|
Java Kotlin
Kotlin教程笔记(19) - 内部类
Kotlin教程笔记(19) - 内部类
19 3
|
7天前
|
安全 Kotlin
Kotlin教程笔记(23) -作用域函数
Kotlin教程笔记(23) -作用域函数
|
1月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
20 1
|
2月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
76 1
|
3月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
61 4
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
151 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
59 8