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

Gio*_*ano 12 android state mvvm viewmodel

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

Eti*_*art 25

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

UI状态

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

data class LoginViewState (
    val user: String = "",
    val password: String = "",
    val checking: Boolean = false
)
Run Code Online (Sandbox Code Playgroud)

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

ViewModel创建一个LiveData属性以暴露状态并初始化它:

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

    init {
        _state.value = LoginViewState()
    }
}
Run Code Online (Sandbox Code Playgroud)

然后发出一个新状态,copy从以下任何地方使用Kotlin的Data类提供的函数ViewModel:

_state.value = _state.value!!.copy(checking = true)
Run Code Online (Sandbox Code Playgroud)

在视图中,像观察其他任何一样观察状态LiveData并相应地更新布局.在View层中,您可以将州的属性转换为实际的视图可见性,并使用具有完全访问权限的资源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
        )
    }
})
Run Code Online (Sandbox Code Playgroud)

合并多个数据源

由于您之前可能已经从数据库或网络调用中公开了结果和数据ViewModel,因此您可以使用a MediatorLiveData将这些结果和数据混合为单个状态:

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

_state.addSource(databaseUserLiveData, { name ->
    _state.value = _state.value!!.copy(user = name)
})
...
Run Code Online (Sandbox Code Playgroud)

数据绑定

由于统一的,不可变的ViewState本质上打破了数据绑定库的通知机制,因此我们使用一个BindingState扩展的mutable 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
    }
}
Run Code Online (Sandbox Code Playgroud)

为创建在观察视图属性BindingState和调用refresh来自Observer:

private val state = LoginBindingState()

...

viewModel.state.observe(this, Observer { it?.let { state.refresh(it) } })
binding.state = state
Run Code Online (Sandbox Code Playgroud)

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

<layout ...>

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

    ...

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

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

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

</layout>
Run Code Online (Sandbox Code Playgroud)

高级信息

一些样板文件肯定会受益于扩展函数和委托属性,例如更新ViewState和通知更改BindingState.

如果您想了解有关使用"干净"架构的Architecture Components处理状态和状态的更多信息,您可以在GitHub上查看Eiffel.

这是我专为处理不可变的视图状态和数据绑定创建一个图书馆ViewModelLiveData以及采用Android系统的运营和业务用例一起胶合它.文档比我在这里提供的内容更深入.

  • @ArchieG.Quiñones 这取决于您认为“推荐”的内容。Google 的示例通常会公开多个 LiveData,如果这符合您的需求,那就去做吧。一旦您使用具有高级业务逻辑的更复杂的应用程序,我个人会推荐一个 LiveData。这也是 Florina Muntenescu 在她的 [应用架构演讲](https://www.youtube.com/watch?v=Sy6ZdgqrQp0) 中展示的内容。但同样,咨询建议然后使用适合您特定用例的任何方法总是好的。 (2认同)

Ada*_*itz 6

Android 单向数据流 (UDF) 2.0

2019 年12 月 18 日更新:Android 单向数据流与 LiveData — 2.0

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

UDF 1.0

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

中 -带有 LiveData 的 Android 单向数据流

YouTube -单向数据流 - Adam Hurwitz - Medellín Android Meetup

代码概览

第 1 步,共 6 步?—?定义模型

视图状态.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() 
  ...
}
Run Code Online (Sandbox Code Playgroud)

Step 2 of 6?—?将事件传递给 ViewModel

片段.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)
  })
}
Run Code Online (Sandbox Code Playgroud)

Step 3 of 6?—?处理事件

视图模型.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())
        }
        ...
}
Run Code Online (Sandbox Code Playgroud)

第 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>()
}
Run Code Online (Sandbox Code Playgroud)

结果.kt

sealed class Result {
  data class PagedListResult(
    val pagedList: LiveData<PagedList<Content>>?, 
    val errorMessage: String): ContentResult()
  ...
}
Run Code Online (Sandbox Code Playgroud)

存储库.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(...))
  }
}
Run Code Online (Sandbox Code Playgroud)

Step 5 of 6?—?处理 LCE 状态

视图模型.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 
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

片段.kt

contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
  viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->
    adapter.submitList(contentList)
  })
  ...
}
Run Code Online (Sandbox Code Playgroud)