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

问题描述 投票:8回答:2

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

android mvvm state viewmodel
2个回答
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的Data类提供的ViewModel函数:

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

在视图中,像观察任何其他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
        )
    }
})

合并多个数据源

由于您之前可能在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

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

<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中的更改。

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

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


0
投票

I've designed a pattern based on the Unidirectional Data Flow using Kotlin with LiveData.

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

中 - Android Unidirectional Data Flow with LiveData

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

Code Overview

第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>()
}

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

Fragment.kt

contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
  viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->
    adapter.submitList(contentList)
  })
  ...
}
© www.soinside.com 2019 - 2024. All rights reserved.