ViewModel 模式
学习如何在我们的项目中编写 ViewModel。
什么是 ViewModel?
ViewModel 用于存储和管理与 UI 相关的数据,并考虑 UI 控制器的生命周期。UI 控制器可以是 Activity 或 Fragment。
我们所说的 ViewModel 具备生命周期感知能力,意味着它能够在 Activity 或 Fragment 发生配置变更时保留数据,例如屏幕方向改变或键盘弹出等场景。得益于 ViewModel,当 Activity 被销毁并重建时,数据仍能被保留下来,用户即使在旋转屏幕后也能看到相同的数据。
ViewModel 负责处理所有 Activity 和 Fragment 的逻辑,而 Fragment 的职责仅仅是观察数据并将其展示给用户,从而使 Fragment 更轻量、更容易在布局中渲染。我们将在下一章中更详细地探讨这一点。
下图展示了 ViewModel 如何控制数据从模型到视图的流动过程。
使用 ViewModel 的规则
本节将回顾在项目中使用或计划使用 ViewModel 时应遵循的一些准则。
- ViewModel 不应持有任何对视图的引用,因为这会导致 ViewModel 中创建 Activity 或 Fragment 的实例,可能引发内存泄漏,甚至导致应用崩溃。
- ViewModel 不应了解视图,也不应包含对视图类的引用。
- 视图不应包含任何逻辑,所有逻辑都应由 ViewModel 处理。以下示例中,在 Activity 中直接设置视图可见性是错误的做法。
1 2fun View.bindVisibility(visible: Boolean?) { 3 isVisible = visible == true 4 // visibility = if (visible == true) View.VISIBLE else View.GONE 5}
ViewModel 应使用可观察对象(如
LiveData)与视图通信。可观察对象会监听数据,并在数据发生变化时收到通知。以下示例中,视图正在观察 ViewModel 通过LiveData暴露的文章列表,以便把数据展示给用户。
1 2viewModel.fetchSavedArticles().observe(viewLifecycleOwner, Observer { article -> 3 newsAdapter.differ.submitList(article) 4})
ViewModel 的优势
让我们来看看在项目中使用 ViewModel 作为 MVVM 架构组件所带来的好处。
- ViewModel 具备生命周期感知能力,能够应对配置变更和屏幕旋转。
- ViewModel 使使用
LiveData等可观察对象更新 UI 变得更加简单。它通过监听数据集的变化并通知视图,确保向用户提供最新数据。 - ViewModel 易于理解和测试。ViewModel 仅需使用 JUnit 进行测试即可。
使用 ViewModelProvider.Factory 接口
ViewModelProvider.Factory 是一个接口,用于实例化 ViewModel。在使用时,我们必须自行实现该接口来创建 ViewModel 实例。
默认情况下,ViewModelProviders.of() 方法内部会创建 ViewModelProvider.Factory,但该方法仅适用于无参数的 ViewModel。如果我们的 ViewModel 构造函数中包含参数,则必须自定义 ViewModelProvider.Factory 类,并将参数传入其中。
下面是一个示例:
1 2class NewsViewModel() : ViewModel() { 3 4 // 代码 5 6}
以上是 NewsViewModel 类的一个实现,其构造函数中没有参数。
1 2class MyActivity : Activity() { 3 fun onCreate() { 4 super.onCreate() 5 6 val viewModel: NewsViewModel = ViewModelProviders.of(context) 7 .get(NewsViewModel::class.java) 8 } 9}
此时,ViewModelProvider.Factory 会自动为我们创建 NewsViewModel 的实例并返回给 Activity。但当我们为 ViewModel 添加了构造参数时会发生什么?请看下面的例子。
1 2class NewsViewModel(val repo: Repository) : ViewModel() { 3 4 // 代码 5 6}
现在我们在 Activity 中调用它:
1 2class MyActivity : Activity() { 3 fun onCreate() { 4 super.onCreate() 5 6 val viewModel: NewsViewModel = ViewModelProviders.of(context) 7 .get(NewsViewModel::class.java) 8 } 9}
如果尝试运行这段代码,将会抛出运行时异常,因为 ViewModel 提供者无法根据提供的参数自动创建 ViewModel 实例。此时,我们必须手动创建自己的 ViewModelProvider.Factory 类,如下所示:
1 2class NewsViewModelProvider(val newsRepository: NewsRepository) : ViewModelProvider.Factory { 3 4 override fun <T : ViewModel> create(modelClass: Class<T>): T { 5 return NewsViewModel(newsRepository) as T 6 } 7}
该类的构造函数接收 NewsRepository 作为参数,并创建一个 ViewModel 实例,同时将参数传递给 ViewModel 类。下面的应用示例展示了一个带有 ViewModelProviderFactory 的 ViewModel,用于创建 ViewModel 实例。
No configuration found for ID: 3320daae2fc5e2c7a0b36b385a59487ee83ac1abc69727745cd5794991ef8e6a
1 2package com.example.booksapp.viewModel 3 4import androidx.lifecycle.MutableLiveData 5import androidx.lifecycle.ViewModel 6import androidx.lifecycle.viewModelScope 7import com.example.booksapp.model.Book 8import com.example.booksapp.model.BooksResponse 9import com.example.booksapp.repository.BookRepository 10import com.example.booksapp.util.Resource 11import kotlinx.coroutines.launch 12 13class BooksViewModel(private val bookRepository: BookRepository) : ViewModel() { 14 15 val books: MutableLiveData<Resource<BooksResponse>> = MutableLiveData() 16 17 fun getAllBooks() = viewModelScope.launch { 18 books.postValue(Resource.Loading()) 19 val response = bookRepository.getBooks() 20 } 21}
该 ViewModel 将 Repository 作为其构造参数之一,这意味着我们需要创建一个自定义的 ViewModelProvider 来实例化该 ViewModel。
我们使用 MutableLiveData 作为可观察对象,供 Activity 观察以更新 UI 并显示最新数据。
1 2package com.example.booksapp.viewModel 3 4import androidx.lifecycle.ViewModel 5import androidx.lifecycle.ViewModelProvider 6import com.example.booksapp.repository.BookRepository 7 8class BooksViewModelProviderFactory(val bookRepository: BookRepository) : ViewModelProvider.Factory { 9 override fun <T : ViewModel?> create(modelClass: Class<T>): T { 10 return BooksViewModel(bookRepository) as T 11 } 12}
我们在 BooksViewModelProviderFactory 中传入参数,并返回一个 ViewModel 实例,该实例随后被传递给 MainActivity。
1 2class MainActivity : AppCompatActivity() { 3 lateinit var booksViewModel: BooksViewModel 4 5 override fun onCreate(savedInstanceState: Bundle?) { 6 super.onCreate(savedInstanceState) 7 setContentView(R.layout.activity_main) 8 9 val bookRepository = BookRepository() 10 val viewModelFactory = BooksViewModelProviderFactory(bookRepository) 11 booksViewModel = ViewModelProvider(this, viewModelFactory).get(BooksViewModel::class.java) 12 } 13}
在 MainActivity 中,通过将 viewModelFactory 和 MainActivity 的引用作为参数传递给 ViewModelProvider,我们获取到了 booksViewModel 的实例。该实例后续用于访问 LiveData,并在 Activity 中进行观察以展示数据。