Android - MVVM 中 ViewModel 状态的最佳实践?

Posted

技术标签:

【中文标题】Android - MVVM 中 ViewModel 状态的最佳实践?【英文标题】:Android - Best Practices for ViewModel State in MVVM? 【发布时间】:2018-11-19 11:01:48 【问题描述】:

我正在使用 MVVM 模式沿着 LiveData(可能是转换)和 View 和 ViewModel 之间的 DataBinding 开发一个 android 应用程序。由于应用程序正在“增长”,现在 ViewModel 包含大量数据,而后者大部分都保存为 LiveData 以让 View 订阅它们(当然,UI 需要这些数据,无论是双向绑定每个 EditTexts 或单向绑定)。我听说(并在 Google 上搜索过)在 ViewModel 中保留代表 UI 状态的数据。但是,我发现的结果只是简单而通用。我想知道是否有人有提示或可以分享有关此案例最佳实践的一些知识。简而言之,考虑到 LiveData 和 DataBinding 可用,在 ViewModel 中存储 UI(视图)状态的最佳方法是什么?提前感谢您的任何回答!

【问题讨论】:

【参考方案1】:

Android 单向数据流 (UDF) 2.0

2019 年 12 月 18 日更新: Android Unidirectional Data Flow with LiveData — 2.0

我使用 KotlinLiveData 设计了一个基于 单向数据流 的模式。

UDF 1.0

查看完整的 Medium 帖子或 YouTube 讨论以获得深入的解释。

中等 - Android Unidirectional Data Flow with LiveData

YouTube - Unidirectional Data Flow - Adam Hurwitz - Medellín Android Meetup

代码概览

第 1 步(共 6 步) — 定义模型

ViewState.kt

// Immutable ViewState attributes.
data class ViewState(val contentList:LiveData<PagedList<Content>>, ...)

// View sends to business logic.
sealed class ViewEvent 
  data class ScreenLoad(...) : ViewEvent()
  ...


// Business logic sends to UI.
sealed class ViewEffect 
  class UpdateAds : ViewEffect() 
  ...

第 2 步(共 6 步) — 将事件传递给 ViewModel

Fragment.kt

private val viewEvent: LiveData<Event<ViewEvent>> get() = _viewEvent
private val _viewEvent = MutableLiveData<Event<ViewEvent>>()

override fun onCreate(savedInstanceState: Bundle?) 
    ...
    if (savedInstanceState == null)
      _viewEvent.value = Event(ScreenLoad(...))


override fun onResume() 
  super.onResume()
  viewEvent.observe(viewLifecycleOwner, EventObserver  event ->
    contentViewModel.processEvent(event)
  )

第 3 步,共 6 步 — 处理事件

ViewModel.kt

val viewState: LiveData<ViewState> get() = _viewState
val viewEffect: LiveData<Event<ViewEffect>> get() = _viewEffect

private val _viewState = MutableLiveData<ViewState>()
private val _viewEffect = MutableLiveData<Event<ViewEffect>>()

fun processEvent(event: ViewEvent) 
    when (event) 
        is ViewEvent.ScreenLoad -> 
          // Populate view state based on network request response.
          _viewState.value = ContentViewState(getMainFeed(...),...)
          _viewEffect.value = Event(UpdateAds())
        
        ...

第 4 步(共 6 步) — 使用 LCE 模式管理网络请求

LCE.kt

sealed class Lce<T> 
  class Loading<T> : Lce<T>()
  data class Content<T>(val packet: T) : Lce<T>()
  data class Error<T>(val packet: T) : Lce<T>()

结果.kt

sealed class Result 
  data class PagedListResult(
    val pagedList: LiveData<PagedList<Content>>?, 
    val errorMessage: String): ContentResult()
  ...

Repository.kt

fun getMainFeed(...)= MutableLiveData<Lce<Result.PagedListResult>>().also  lce ->
  lce.value = Lce.Loading()
  /* Firestore request here. */.addOnCompleteListener 
    // Save data.
    lce.value = Lce.Content(ContentResult.PagedListResult(...))
  .addOnFailureListener 
    lce.value = Lce.Error(ContentResult.PagedListResult(...))
  

第 5 步,共 6 步 — 处理 LCE 状态

ViewModel.kt

private fun getMainFeed(...) = Transformations.switchMap(repository.getFeed(...))  
  lce -> when (lce) 
    // SwitchMap must be observed for data to be emitted in ViewModel.
    is Lce.Loading -> Transformations.switchMap(/*Get data from Room Db.*/)  
      pagedList -> MutableLiveData<PagedList<Content>>().apply 
        this.value = pagedList
      
    
    is Lce.Content -> Transformations.switchMap(lce.packet.pagedList!!)  
      pagedList -> MutableLiveData<PagedList<Content>>().apply 
        this.value = pagedList
      
        
    is Lce.Error ->  
      _viewEffect.value = Event(SnackBar(...))
      Transformations.switchMap(/*Get data from Room Db.*/)  
        pagedList -> MutableLiveData<PagedList<Content>>().apply 
          this.value = pagedList 
        
    

第 6 步,共 6 步 — 观察状态变化!

Fragment.kt

contentViewModel.viewState.observe(viewLifecycleOwner, Observer  viewState ->
  viewState.contentList.observe(viewLifecycleOwner, Observer  contentList ->
    adapter.submitList(contentList)
  )
  ...

【讨论】:

【参考方案2】:

我在工作中遇到了同样的问题,可以分享对我们有用的方法。我们正在 100% 使用 Kotlin 进行开发,因此以下代码示例也将如此。

界面状态

为防止 ViewModel 因大量 LiveData 属性而变得臃肿,请公开单个 ViewState 以供查看(ActivityFragment)进行观察。它可能包含先前由多个LiveData 公开的数据以及视图可能需要正确显示的任何其他信息:

data class LoginViewState (
    val user: String = "",
    val password: String = "",
    val checking: Boolean = false
)

请注意,我正在使用具有不可变状态属性的 Data 类,并且故意不使用任何 Android 资源。这不是 MVVM 特有的,但不可变的视图状态可以防止 UI 不一致和线程问题。

ViewModel 内部创建一个LiveData 属性以公开状态并对其进行初始化:

class LoginViewModel : ViewModel() 
    private val _state = MutableLiveData<LoginViewState>()
    val state : LiveData<LoginViewState> get() = _state

    init 
        _state.value = LoginViewState()
    

然后要发出新状态,请在 ViewModel 内的任何位置使用 Kotlin 的 Data 类提供的 copy 函数:

_state.value = _state.value!!.copy(checking = true)

在视图中,像观察任何其他LiveData 一样观察状态并相应地更新布局。在视图层中,您可以将状态的属性转换为实际的视图可见性,并使用具有对Context 的完全访问权限的资源:

viewModel.state.observe(this, Observer 
    it?.let 
        userTextView.text = it.user
        passwordTextView.text = it.password
        checkingImageView.setImageResource(
            if (it.checking) R.drawable.checking else R.drawable.waiting
        )
    
)

合并多个数据源

由于您之前可能在ViewModel 中公开了来自数据库或网络调用的结果和数据,您可以使用MediatorLiveData 将它们合并为单一状态:

private val _state = MediatorLiveData<LoginViewState>()
val state : LiveData<LoginViewState> get() = _state

_state.addSource(databaseUserLiveData,  name ->
    _state.value = _state.value!!.copy(user = name)
)
...

数据绑定

由于统一的、不可变的ViewState 本质上破坏了数据绑定库的通知机制,我们使用一个扩展BaseObservable 的可变BindingState 来选择性地通知布局更改。它提供了一个refresh函数,接收对应的ViewState

更新:删除了检查更改值的 if 语句,因为数据绑定库已经负责仅呈现实际更改的值。感谢@CarsonH​​olzheimer

class LoginBindingState : BaseObservable() 
    @get:Bindable
    var user = ""
        private set(value) 
            field = value
            notifyPropertyChanged(BR.user)
        

    @get:Bindable
    var password = ""
        private set(value) 
            field = value
            notifyPropertyChanged(BR.password)
        

    @get:Bindable
    var checkingResId = R.drawable.waiting
        private set(value) 
            field = value
            notifyPropertyChanged(BR.checking)
        

    fun refresh(state: AngryCatViewState) 
        user = state.user
        password = state.password
        checking = if (it.checking) R.drawable.checking else R.drawable.waiting
    

在观察视图中为BindingState 创建一个属性并从Observer 调用refresh

private val state = LoginBindingState()

...

viewModel.state.observe(this, Observer  it?.let  state.refresh(it)  )
binding.state = state

然后,将状态用作布局中的任何其他变量:

<layout ...>

    <data>
        <variable name="state" type=".LoginBindingState"/>
    </data>

    ...

        <TextView
            ...
            android:text="@state.user"/>

        <TextView
            ...
            android:text="@state.password"/>

        <ImageView
            ...
            app:imageResource="@state.checkingResId"/>
    ...

</layout>

高级信息

某些样板文件肯定会受益于扩展功能和委托属性,例如更新ViewState 和通知BindingState 中的更改。

如果您想了解更多关于使用“干净”架构使用架构组件处理状态和状态的信息,您可以查看Eiffel on GitHub。

这是我专门创建的一个库,用于处理不可变视图状态和与ViewModelLiveData 的数据绑定,并将其与 Android 系统操作和业务用例粘合在一起。 文档比我在这里提供的更深入。

【讨论】:

@Giordano 除了尝试 Kotlin 之外,我没有任何好的建议? ;) 看起来这将是 Android 开发的未来。您可以使用复制构造函数、Cloneable 或 Builder 模式在 Java 中制作类似的东西,但没有那么简单。 @ArchieG.Quiñones 这取决于您认为“推荐”的内容。 Google 的示例通常会公开多个 LiveData,如果这符合您的需求,那就去做吧。一旦您使用具有高级业务逻辑的更复杂的应用程序,我个人会推荐一个 LiveData。这也是 Florina Muntenescu 在她的 talk on app architecture 中展示的内容。但同样,咨询建议然后使用适合您特定用例的任何方法总是好的。 @ArchieG.Quiñones 不太清楚您的意思?我只更新了“数据绑定”示例以删除 if (field != value) ... 语句。不过还是谢谢。 :) @DivyanshuNegi 双爆炸只是用作“快速失败”的方法。由于必须在执行任何合理的updateState() 之前调用state.value = LoginViewState(),因此!! 确保在您忘记设置初始状态时发生崩溃。 @DivyanshuNegi 如果您正在使用带有BaseObservable 的数据绑定,则包含的BindingAdapters 已经注意仅在相应值更改时才更新视图。当使用手动更新Observer 中的视图时,您必须自己跟踪更改。尽管我目前正在对 GitHub 上的链接库进行完全重写,这将允许观察特定状态属性的更改。如果您想查看它,它已经在 master 分支上可用,只是尚未发布。

以上是关于Android - MVVM 中 ViewModel 状态的最佳实践?的主要内容,如果未能解决你的问题,请参考以下文章

Android架构组件:ViewMode概述

vue

如何在MVVM中使用相同的ViewModel拥有多个视图?

为啥要避免 WPF MVVM 模式中的代码隐藏?

MVVM-light 中的清理与处置(布尔)

wpf MVVM Viewmodel之间传值