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

简介: Kotlin教程笔记(80) - 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,可以有效地避免内存泄漏。下面我们就从设计思路和代码实现两个方面进行详细说明。

设计思路

  1. Model 层:
    • 负责数据的获取和处理,可能通过网络请求、数据库查询等。
    • 不与UI层直接交互,只与ViewModel进行数据的交互。
  2. ViewModel 层:
    • 持有Model层的数据并通过LiveData等可观察数据类型暴露给View层。
    • 负责业务逻辑处理,不持有View的引用,避免内存泄漏。
    • ViewModel的生命周期与Activity/Fragment绑定,能在配置变化(如旋转屏幕)时保持数据不丢失。
  3. 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")
    }
}

内存泄漏防范

  1. ViewModel 的使用:
    • ViewModel 不持有 ActivityFragment 的引用,只通过 LiveData 进行数据的暴露和通知,这样可以有效避免 ViewModel 内存泄漏。
    • 通过 viewModelScope 来启动协程,避免手动管理协程生命周期的问题。
  2. LiveData 的使用:
    • LiveData 是生命周期感知型组件,它只在 ActivityFragment 处于活跃状态时通知观察者,从而避免了因生命周期变化导致的内存泄漏。
  3. 观察者的移除:
    • 由于 LiveDataActivity/Fragment 的生命周期绑定,当 Activity/Fragment 销毁时,观察者会自动移除,无需手动清理。

结论

这个Kotlin实现的MVVM架构通过ViewModel来管理数据,避免了将业务逻辑和UI操作耦合在一起。同时,LiveDataviewModelScope 确保了内存管理的安全性。

MutableLiveDataLiveData 的一个子类,用于保存和管理可变的数据。它的作用是允许应用程序中的数据可以在运行时动态更改,并且通知所有观察它的数据变化的观察者(通常是UI层,比如Activity或Fragment)。LiveData 本身是只读的,而 MutableLiveData 提供了对数据的修改权限。

MutableLiveData 的主要作用:

  1. 数据存储和管理:
    • MutableLiveData 可以用来存储任意类型的数据(例如字符串、数字、对象等)。当数据发生变化时,LiveData 会通知所有相关的观察者进行更新。
  2. 数据的可变性:
    • LiveData 不同,MutableLiveData 允许通过 setValue()postValue() 方法来改变内部的数据。通常,ViewModel 通过 MutableLiveData 来修改数据,而暴露给外部的仍然是不可变的 LiveData,这样可以保证外部代码只能读取数据而无法修改,遵循数据封装的原则。
  3. 生命周期感知:
    • LiveData 是生命周期感知型的组件,只有当观察者处于活动状态(即 ActivityFragment 处于 STARTEDRESUMED 状态)时,才会接收到数据更新。这样可以避免 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
    }
}

ActivityFragment 中观察数据

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")
    }
}

MutableLiveDataLiveData 的区别

  • MutableLiveData 允许数据的修改,外部可以通过 setValue()postValue() 来更新数据。
  • LiveData 是只读的,通常通过暴露 LiveData 给外部,从而保护内部数据不被意外修改。

更新数据的两种方式:

  • setValue(T value):在主线程中更新数据。
  • postValue(T value):可以在任何线程中更新数据,适用于需要在工作线程更新UI时使用。

总结

MutableLiveData 主要用于在 ViewModel 内部修改和管理数据,同时保证外部只能读取 LiveData 的数据,从而实现数据的封装和状态管理。在Android开发中,它是实现MVVM架构的重要工具,特别是在配合ViewModel使用时,能有效防止内存泄漏和无效的UI更新。

MutableLiveData如何传递多个复杂的对象?

ChatGPT 说:

ChatGPT

MutableLiveData 可以存储和传递任意类型的数据,包括多个复杂的对象。你可以将这些复杂对象封装成一个数据类或者使用ListMapPair等数据结构来管理多个对象。下面详细说明如何使用 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. 在 ActivityFragment 中观察和使用

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

方法二:使用 ListMap 传递多个对象

如果你需要传递一个动态数量的对象(如用户列表),可以使用 ListMap 等集合类型。

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
    }
}

方法三:使用 PairTriple

如果你只有两到三个对象需要传递,并且不想创建单独的数据类,可以使用 PairTriple 这样的容器。

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

总结

  • 如果对象之间有明确的关联,使用 数据类 封装多个对象是最好的选择,能增强代码的可读性和可维护性。
  • 如果是需要传递多个同类对象(如多个用户),可以使用 ListMap
  • 当仅有少数对象(如2到3个)并且不需要定义数据类时,可以使用 PairTriple 来简化代码。

这些方法都可以通过 MutableLiveData 管理和传递复杂对象,并且可以通过 LiveData 的观察机制让 UI 及时响应数据变化。

上面的MVVM架构模式有什么缺点吗?平时写代码如何规避这些缺点?

ChatGPT

MVVM 架构模式虽然在 Android 开发中非常流行,能有效地将 UI 层与业务逻辑分离,但它并非完美无缺,存在一些潜在的缺点。了解这些缺点并在开发中尽量规避它们,可以提高项目的可维护性和性能。

MVVM 架构的常见缺点

  1. ViewModel 过于臃肿:

    • 在实际开发中,业务逻辑、数据处理和状态管理等都可能会集中在 ViewModel 中,导致 ViewModel 代码过于复杂和臃肿。这会使得 ViewModel 变得难以维护和测试。

    解决方案:

    • 拆分业务逻辑: 使用 UseCase 或 Interactor 类,将复杂的业务逻辑放到独立的模块中,而不是堆积在 ViewModel 内。
    • 单一职责原则: 遵循单一职责原则,确保 ViewModel 只负责数据的管理和状态的维护,不要将所有的业务逻辑放入其中。
    • 层次化设计: 可以将某些与数据处理相关的逻辑放入 RepositoryService 中,避免 ViewModel 承担过多责任。
  2. 难以管理复杂的 UI 逻辑:

    • 在 MVVM 中,UI 的状态通常通过 LiveDataStateFlow 等可观察对象驱动。如果 UI 状态过于复杂,比如有多个不同的状态组合,可能会导致复杂的逻辑控制,甚至难以调试。

    解决方案:

    • 状态机模式:对于复杂的 UI 状态管理,可以引入状态机模式(State Machine),将不同的 UI 状态进行清晰的定义,减少状态间的混乱。
    • View 层处理 UI 逻辑:虽然 MVVM 强调将逻辑与 UI 分离,但部分复杂的 UI 控制逻辑可以适当留在 View 层,避免过度复杂的状态传递。
  3. LiveData 更新逻辑不明确:

    • 在 MVVM 中,LiveData 的更新逻辑分布在 ViewModel 的多个方法中,有时可能会导致难以追踪状态的变化。特别是在并发操作中,LiveData 的更新顺序和时机可能不太容易控制。

    解决方案:

    • 避免滥用 MutableLiveData:严格限制 MutableLiveData 的使用范围,确保只有特定的操作可以修改数据。
    • 使用单一入口修改数据:可以通过定义统一的更新方法或事件处理方法,确保数据的修改和状态变化有清晰的触发点。
    • 使用 StateFlowSharedFlow:相比于 LiveDataStateFlow 提供了更强的线程安全性和对数据流的明确控制,特别适合并发环境。
  4. 过多的观察者导致性能问题:

    • 在复杂的应用中,过多的观察者可能会导致性能问题,尤其是当观察的数据量很大时,频繁的 UI 更新可能会拖慢渲染性能。

    解决方案:

    • 谨慎添加观察者:只在必要的生命周期内添加观察者,避免在不必要的生命周期中对 LiveData 进行订阅。
    • 使用 DistinctUntilChanged:在 LiveData 中使用 DistinctUntilChanged,可以避免多次相同数据的更新,从而减少不必要的 UI 刷新。
    • 懒加载:对于不经常使用的 UI 数据,可以延迟订阅 LiveData,避免过早创建大量的观察者。
  5. ViewModel 的生命周期管理:

    • ViewModel 的生命周期比 ActivityFragment 更长,当 Activity/Fragment 销毁时,ViewModel 仍然可能持有某些状态或逻辑,可能会导致意想不到的问题。例如,UI已经销毁但数据仍然在更新。

    解决方案:

    • 正确管理协程和异步任务:使用 viewModelScope 来确保协程与 ViewModel 的生命周期一致,避免 ViewModel 持有长期异步任务导致内存泄漏。
    • 取消无效的观察:在 ActivityFragment 销毁时,确保观察者能够正确解除订阅,避免 LiveData 仍然持有 UI 的引用。
    • 考虑 ViewModel 的清理工作:对于需要手动清理的资源(如某些回调、订阅等),在 onCleared() 中进行清理,避免资源泄漏。
  6. 难以测试复杂的 UI 交互:

    • MVVM 通过 LiveData 或其他可观察的数据流来驱动 UI,这有时会增加 UI 交互的测试难度。特别是当存在多个观察者或复杂的状态管理时,编写单元测试会变得复杂。

    解决方案:

    • Mock LiveData 和协程:在测试中,使用 Mockito 或类似的框架来模拟 LiveData 和协程行为,简化 ViewModel 的测试。
    • 使用 StateFlow 测试:相比于 LiveDataStateFlow 更容易测试,因为它可以在不依赖 Android 的环境中测试数据流和状态管理。
  7. 数据绑定的复杂性:

    • 如果结合 DataBinding 使用 MVVM,在处理复杂的绑定表达式时,代码可能会变得难以维护,特别是在XML文件中书写复杂逻辑时,容易引发调试和维护难题。

    解决方案:

    • 减少复杂绑定表达式:尽量避免在 XML 中写过于复杂的表达式,将逻辑尽可能地移到 ViewModel 或辅助类中。
    • 使用 BindingAdapter:对于重复和复杂的UI更新逻辑,使用 BindingAdapter 自定义UI的绑定逻辑,避免在XML中堆砌大量表达式。

总结

MVVM 模式虽然能很好地实现业务逻辑和 UI 层的解耦,但它在实际使用中也会带来一些复杂性,如 ViewModel 变得臃肿、状态管理混乱等问题。通过遵循以下实践可以有效规避这些缺点:

  • 遵循单一职责原则,合理拆分业务逻辑。
  • 使用合适的数据流工具(如 StateFlow)替代 LiveData 以更好管理状态。
  • 对复杂的 UI 状态引入状态机等模式,简化状态管理。
  • 确保 ViewModel 的生命周期管理得当,避免内存泄漏和状态不一致问题。
相关文章
|
12天前
|
Java 编译器 Kotlin
Kotlin入门笔记1 - 数据类型
Kotlin入门笔记1 - 数据类型
56 15
|
14天前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
14天前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
14天前
|
设计模式 Java Kotlin
Kotlin教程笔记(56) - 改良设计模式 - 装饰者模式
Kotlin教程笔记(56) - 改良设计模式 - 装饰者模式
30 2
|
14天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
20 2
|
14天前
|
Java 数据库连接 编译器
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
34 0
|
2月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
32 1
|
3月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
94 1
|
4月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
63 4
|
5月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
174 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?