Bootstrap

Android开发实战班 -应用架构 - MVVM 架构模式

随着 Android 应用复杂性的增加,采用良好的架构模式变得越来越重要。MVVM(Model-View-ViewModel) 是一种流行的架构模式,旨在将应用的 UI 逻辑、数据和业务逻辑分离,使代码更易于维护、测试和扩展。本章节将深入讲解 MVVM 架构模式的原理、组件、Jetpack 组件(如 ViewModel 和 LiveData)的使用,以及如何在 Android 项目中应用 MVVM 架构。

MVVM 架构模式简介

  • MVVM 的起源:

    • MVVM(Model-View-ViewModel)架构模式最初由 Microsoft 提出,用于构建 WPF(Windows Presentation Foundation)和 Silverlight 应用。
    • 随着 Android 开发的复杂化,MVVM 被引入到 Android 开发中,并成为主流架构模式之一。
  • MVVM 的核心思想:

    • 分离关注点: 将应用的 UI 逻辑、数据和业务逻辑分离,使代码更清晰、易于维护。
    • 数据驱动 UI: ViewModel 持有 UI 数据,并暴露数据给 View,View 通过观察数据的变化来更新 UI。
    • 可测试性: ViewModel 不依赖于 View,可以独立进行单元测试。
  • MVVM 的优点:

    • 代码清晰: 各层职责分明,代码更易于理解和维护。
    • 可测试性高: ViewModel 可以独立于 UI 进行单元测试,提高测试覆盖率。
    • 可复用性: ViewModel 可以被多个 View 复用,减少代码重复。
    • 数据绑定: 通过数据绑定机制,View 可以自动响应数据变化,无需手动更新 UI。

10.2 MVVM 架构模式的组成部分

MVVM 架构模式主要由以下三个部分组成:

  1. Model(模型):

    • 负责数据的获取、存储和业务逻辑处理。
    • 例如,数据库、网络请求、数据模型等。
  2. View(视图):

    • 负责 UI 的展示和用户交互。
    • 例如,Activity, Fragment, Composable 等。
  3. ViewModel(视图模型):

    • 充当 View 和 Model 之间的桥梁,负责处理 UI 相关的数据和逻辑。
    • ViewModel 不直接引用 View,可以独立于 View 进行单元测试。

10.3 Jetpack 组件在 MVVM 中的应用

Jetpack 提供了多个组件,可以帮助我们更好地实现 MVVM 架构:

  • ViewModel:

    • ViewModel 是 MVVM 架构中的核心组件,负责存储和管理 UI 相关的数据。
    • ViewModel 生命周期与 View 分离,可以在配置变化(例如屏幕旋转)时保留数据。
    class MyViewModel : ViewModel() {
        private val _data = MutableLiveData<String>()
        val data: LiveData<String> get() = _data
    
        fun loadData() {
            // 模拟网络请求
            _data.value = "Hello, MVVM!"
        }
    }
    
  • LiveData:

    • LiveData 是一种可观察的数据持有者,可以感知生命周期变化,避免内存泄漏。
    • View 可以观察 LiveData 的变化,并自动更新 UI。
    class MyActivity : AppCompatActivity() {
        private lateinit var viewModel: MyViewModel
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
            viewModel.data.observe(this) { data ->
                // 更新 UI
                findViewById<TextView>(R.id.textView).text = data
            }
    
            viewModel.loadData()
        }
    }
    
  • Data Binding:

    • Data Binding 允许将 UI 组件直接绑定到 ViewModel 的数据,减少样板代码。
    • 例如,将 TextView 的文本属性绑定到 ViewModel 的数据。
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable
                name="viewModel"
                type="com.example.app.MyViewModel" />
        </data>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.data}" />
    </layout>
    
  • Room:

    • Room 是 Android 官方提供的持久化库,提供了对 SQLite 数据库的抽象层。
    • 可以与 ViewModel 结合使用,实现数据的存储和查询。
    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val name: String
    )
    
    @Dao
    interface UserDao {
        @Query("SELECT * FROM user")
        fun getAllUsers(): LiveData<List<User>>
    }
    
    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }
    
    class MyViewModel(application: Application) : AndroidViewModel(application) {
        private val db = Room.databaseBuilder(
            application,
            AppDatabase::class.java, "database-name"
        ).build()
        val users: LiveData<List<User>> = db.userDao().getAllUsers()
    }
    

10.4 MVVM 架构模式的实现步骤

在前面的章节中,我们介绍了 MVVM 架构模式的基本概念和组成部分。接下来,我们将详细介绍如何在 Android 项目中实现 MVVM 架构,包括 ViewModel 的创建与使用、LiveData 的观察、数据绑定以及与 Repository 模式的结合。

10.4.1 创建 ViewModel

ViewModel 是 MVVM 架构的核心组件,负责存储和管理 UI 相关的数据和业务逻辑。ViewModel 不直接引用 View,因此可以独立于 View 进行单元测试。

  • 步骤:

    1. 创建一个继承自 ViewModel 的类。
    2. 在 ViewModel 中定义 UI 相关的数据和逻辑。
    3. 使用 LiveDataStateFlow 暴露数据给 View。
  • 示例:

    // UserRepository.kt
    class UserRepository {
        fun getUsers(): List<User> {
            // 模拟网络请求或数据库查询
            return listOf(User(1, "Alice"), User(2, "Bob"))
        }
    }
    
    // UserViewModel.kt
    class UserViewModel(private val repository: UserRepository) : ViewModel() {
        private val _users = MutableLiveData<List<User>>()
        val users: LiveData<List<User>> get() = _users
    
        fun loadUsers() {
            // 模拟数据加载
            val userList = repository.getUsers()
            _users.value = userList
        }
    }
    
    // User.kt
    data class User(val id: Int, val name: String)
    
10.4.2 创建 View

View 负责 UI 的展示和用户交互。在 Android 中,View 通常是 Activity 或 Fragment。在 Jetpack Compose 中,View 可以是 Composable 函数。

  • 步骤:

    1. 在 Activity 或 Fragment 中创建 ViewModel 实例。
    2. 使用 ViewModelProviderViewModel 的构造函数注入 ViewModel。
    3. 观察 ViewModel 中的 LiveData 数据,并更新 UI。
  • 示例:

    // UserActivity.kt
    class UserActivity : AppCompatActivity() {
        private lateinit var viewModel: UserViewModel
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_user)
    
            // 创建 ViewModel 实例
            viewModel = ViewModelProvider(this, ViewModelFactory(UserRepository()))
                .get(UserViewModel::class.java)
    
            // 观察 LiveData 数据
            viewModel.users.observe(this) { users ->
                // 更新 UI,例如使用 RecyclerView 显示用户列表
                val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
                recyclerView.adapter = UserAdapter(users)
            }
    
            // 加载数据
            viewModel.loadUsers()
        }
    }
    
    // UserAdapter.kt
    class UserAdapter(private val users: List<User>) : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false)
            return UserViewHolder(view)
        }
    
        override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
            val user = users[position]
            holder.bind(user)
        }
    
        override fun getItemCount(): Int = users.size
    
        class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            fun bind(user: User) {
                itemView.findViewById<TextView>(R.id.textViewName).text = user.name
            }
        }
    }
    
10.4.3 使用 Data Binding

Data Binding 允许将 UI 组件直接绑定到 ViewModel 的数据,减少样板代码,提高代码可读性。

  • 步骤:

    1. build.gradle 文件中启用 Data Binding。
      android {
          ...
          buildFeatures {
              dataBinding true
          }
      }
      
    2. 在布局文件中使用 <layout> 标签包裹 UI 组件,并定义 ViewModel 变量。
      <layout xmlns:android="http://schemas.android.com/apk/res/android">
          <data>
              <variable
                  name="viewModel"
                  type="com.example.app.UserViewModel" />
          </data>
          <RecyclerView
              android:id="@+id/recyclerView"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:adapter="@{viewModel.users}" />
      </layout>
      
    3. 在 Activity 或 Fragment 中设置布局和数据绑定。
      class UserActivity : AppCompatActivity() {
          private lateinit var viewModel: UserViewModel
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              val binding: ActivityUserBinding = DataBindingUtil.setContentView(this, R.layout.activity_user)
      
              viewModel = ViewModelProvider(this, ViewModelFactory(UserRepository()))
                  .get(UserViewModel::class.java)
      
              binding.viewModel = viewModel
              binding.lifecycleOwner = this
      
              viewModel.loadUsers()
          }
      }
      
  • 示例:

    <!-- activity_user.xml -->
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable
                name="viewModel"
                type="com.example.app.UserViewModel" />
        </data>
        <RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:adapter="@{viewModel.users}" />
    </layout>
    
    // UserViewModel.kt
    class UserViewModel(private val repository: UserRepository) : ViewModel() {
        val users: LiveData<List<User>> = MutableLiveData()
    
        fun loadUsers() {
            val userList = repository.getUsers()
            (users as MutableLiveData).value = userList
        }
    }
    

10.4.4 使用 Repository 模式

Repository 模式 是 MVVM 架构的重要组成部分,用于抽象数据源,提供统一的接口给 ViewModel。Repository 模式可以有效地管理数据来源,包括网络请求、数据库、文件存储等,使 ViewModel 更加专注于 UI 逻辑和数据处理,而无需关心数据的具体来源。

10.4.4.1 Repository 模式的优势
  • 数据来源抽象: Repository 模式将数据来源抽象出来,使 ViewModel 不需要关心数据的具体来源(如网络、数据库等)。
  • 数据缓存: 可以通过 Repository 实现数据的缓存机制,提高应用性能。
  • 单一职责: 每个 Repository 类只负责特定的数据源,遵循单一职责原则。
  • 可测试性: Repository 可以独立于 ViewModel 进行单元测试,提高代码的可测试性。
10.4.4.2 实现 Repository 模式

以下是一个典型的 Repository 模式实现步骤:

  1. 定义数据模型:

    • 定义数据模型类,例如 User
    data class User(val id: Int, val name: String)
    
  2. 定义数据源接口:

    • 定义数据源接口,例如 UserDataSource,用于获取用户数据。
    interface UserDataSource {
        suspend fun getUsers(): List<User>
    }
    
  3. 实现具体的数据源:

    • 实现网络数据源,例如 RemoteUserDataSource,通过 API 获取用户数据。
    class RemoteUserDataSource(private val apiService: ApiService) : UserDataSource {
        override suspend fun getUsers(): List<User> {
            return apiService.fetchUsers()
        }
    }
    
    • 实现本地数据源,例如 LocalUserDataSource,从数据库获取用户数据。
    class LocalUserDataSource(private val userDao: UserDao) : UserDataSource {
        override suspend fun getUsers(): List<User> {
            return userDao.getAllUsers()
        }
    }
    
  4. 实现 Repository 类:

    • 创建 UserRepository 类,负责管理数据源。
    • 可以根据需要实现缓存机制,例如先从网络获取数据,再保存到数据库。
    class UserRepository(private val remoteDataSource: RemoteUserDataSource, private val localDataSource: LocalUserDataSource) {
    
        suspend fun getUsers(forceRefresh: Boolean = false): List<User> {
            if (forceRefresh) {
                // 从网络获取数据
                val users = remoteDataSource.getUsers()
                // 保存到数据库
                localDataSource.saveUsers(users)
                return users
            } else {
                // 先从数据库获取数据
                val users = localDataSource.getUsers()
                if (users.isNotEmpty()) {
                    return users
                } else {
                    // 如果数据库为空,则从网络获取数据
                    val usersFromNetwork = remoteDataSource.getUsers()
                    // 保存到数据库
                    localDataSource.saveUsers(usersFromNetwork)
                    return usersFromNetwork
                }
            }
        }
    }
    
  5. 在 ViewModel 中使用 Repository:

    • 在 ViewModel 中注入 UserRepository 实例,并调用其方法获取数据。
    class UserViewModel(private val repository: UserRepository) : ViewModel() {
        private val _users = MutableLiveData<List<User>>()
        val users: LiveData<List<User>> get() = _users
    
        fun loadUsers(forceRefresh: Boolean = false) {
            viewModelScope.launch {
                try {
                    val users = repository.getUsers(forceRefresh)
                    _users.postValue(users)
                } catch (e: Exception) {
                    // 处理异常,例如显示错误信息
                }
            }
        }
    }
    
  6. 依赖注入:

    • 使用依赖注入框架(例如 Hilt)来管理 Repository 和数据源的依赖关系。
    @Module
    @InstallIn(SingletonComponent::class)
    object RepositoryModule {
    
        @Provides
        @Singleton
        fun provideUserRepository(
            remoteDataSource: RemoteUserDataSource,
            localDataSource: LocalUserDataSource
        ): UserRepository {
            return UserRepository(remoteDataSource, localDataSource)
        }
    
        @Provides
        @Singleton
        fun provideRemoteUserDataSource(apiService: ApiService): RemoteUserDataSource {
            return RemoteUserDataSource(apiService)
        }
    
        @Provides
        @Singleton
        fun provideLocalUserDataSource(userDao: UserDao): LocalUserDataSource {
            return LocalUserDataSource(userDao)
        }
    }
    

通过使用 Repository 模式,ViewModel 可以专注于 UI 逻辑和数据处理,而不需要关心数据的具体来源和实现细节。这种分离使得代码更加清晰、可维护,并且易于测试。

作者简介

前腾讯电子签的前端负责人,现 whentimes tech CTO,专注于前端技术的大咖一枚!一路走来,从小屏到大屏,从 Web 到移动,什么前端难题都见过。热衷于用技术打磨产品,带领团队把复杂的事情做到极简,体验做到极致。喜欢探索新技术,也爱分享一些实战经验,帮助大家少走弯路!

温馨提示:可搜老码小张公号联系导师

;