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

时间:2018-06-09 17:05:40

标签: android mvvm state viewmodel

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

2 个答案:

答案 0 :(得分:17)

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

UI状态

为防止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()
    }
}

要发出新状态,请在copy内部的任何地方使用Kotlin数据类提供的ViewModel函数:

_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本质上破坏了数据绑定库的通知机制,因此我们使用了可变的BindingState,该变量扩展了BaseObservable以选择性地通知更改的布局。它提供了一个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创建一个属性,然后从refresh调用Observer

private val state = LoginBindingState()

...

viewModel.state.observe(this, Observer { it?.let { state.refresh(it) } })
binding.state = 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>

高级信息

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

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

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

答案 1 :(得分:0)

我使用 Kotlin LiveData 设计了基于单向数据流的模式。

查看完整的 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 of-将事件传递给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>()
}

Result.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步-State观察状态变化!

Fragment.kt

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