Android - лучшие практики для ViewModel State в MVVM? - PullRequest
0 голосов
/ 09 июня 2018

Я работаю над приложением Android, используя шаблон MVVM вдоль LiveData (возможно, Transformations) и DataBinding между View и ViewModel.Поскольку приложение «растет», теперь ViewModels содержат много данных, и большинство из них хранятся в виде LiveData, чтобы подписки на них имели представления (конечно, эти данные необходимы для пользовательского интерфейса, будь то двусторонняя привязка какза EditTexts или одностороннюю привязку).Я слышал (и гуглил) о хранении данных, которые представляют состояние пользовательского интерфейса во ViewModel.Однако результаты, которые я нашел, были просто простыми и общими.Я хотел бы знать, есть ли у кого-нибудь намеки или могли бы поделиться некоторыми знаниями о лучших практиках для этого случая.Проще говоря, что может быть лучшим способом сохранить состояние пользовательского интерфейса (View) в ViewModel, учитывая наличие LiveData и DataBinding?Заранее спасибо за любой ответ!

Ответы [ 2 ]

0 голосов
/ 18 июня 2019

Я разработал шаблон на основе Однонаправленного потока данных , используя Kotlin с LiveData .

Проверьте полный Средняя запись или YouTube поговорите для подробного объяснения.

Средний - Однонаправленный поток данных Android с LiveData

YouTube - Однонаправленный поток данных - Адам Гурвиц - 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 Pattern

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

Репозиторий.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)
  })
  ...
}
0 голосов
/ 23 июня 2018

Я боролся с той же проблемой на работе и могу поделиться тем, что у нас работает.Мы разрабатываем 100% в Kotlin, поэтому следующие примеры кода будут также:

Состояние интерфейса

Чтобы предотвратить вздутие ViewModel с большим количеством свойств LiveData, выставьтеодин ViewState для просмотра (Activity или Fragment) для наблюдения.Он может содержать данные, ранее представленные кратным LiveData, и любую другую информацию, которая может потребоваться для правильного отображения представления:

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

Обратите внимание, что я использую класс Data с неизменяемыми свойствамидля государства и сознательно не использовать ресурсы Android.Это не является чем-то специфичным для MVVM, но неизменяемое состояние просмотра предотвращает несоответствия пользовательского интерфейса и проблемы с многопоточностью.

Внутри 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 и обновили макет соответственно.В слое 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)
})
...

Привязка данных

Поскольку унифицировано,immutable ViewState существенно нарушает механизм уведомления библиотеки привязки данных, мы используем изменяемый BindingState, который расширяет BaseObservable, чтобы выборочно уведомлять макет изменений.Он предоставляет функцию refresh, которая получает соответствующее ViewState:

Обновление: Удалены операторы if, проверяющие измененные значения, поскольку библиотека привязки данных уже заботится только о рендеринге фактически измененных значений. Благодаря @ CarsonHolzheimer

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.

Если вам нужна дополнительная информация об обработке состояний и состояний с помощью компонентов архитектуры с использованием «чистой» архитектуры, вы можете оформить заказ Eiffel на GitHub .

Это библиотека, которую я создал специально для обработки неизменяемых состояний просмотра и привязки данных с помощью ViewModel и LiveData, а также для склеивания их вместес операциями системы Android и бизнес-применениями.Документация идет глубже, чем я могу предоставить здесь.

...