web-dev-qa-db-fra.com

Android - Meilleures pratiques pour l'état ViewModel dans MVVM?

Je travaille sur une application Android utilisant le modèle MVVM le long de LiveData (éventuellement des transformations) et de la liaison de données entre View et ViewModel. Puisque l'application "grandit", maintenant les ViewModels contiennent beaucoup de données, et la plupart de ces derniers sont conservés en tant que LiveData pour que des vues s'y abonnent (bien sûr, ces données sont nécessaires pour l'interface utilisateur, que ce soit une liaison bidirectionnelle selon EditTexts ou une liaison unidirectionnelle). J'ai entendu (et googlé) à propos de conserver les données qui représentent l'état de l'interface utilisateur dans le ViewModel. Cependant, les résultats que j'ai trouvés étaient simplement simples et génériques. J'aimerais savoir si quelqu'un a des indices ou pourrait partager certaines connaissances sur les meilleures pratiques dans ce cas. En termes simples, que être le meilleur moyen de stocker l'état d'une interface utilisateur (vue) dans un ViewModel compte tenu de LiveData et DataBinding disponibles? Merci d'avance pour toute réponse!

11
Giordano

J'ai lutté avec le même problème au travail et je peux partager ce qui fonctionne pour nous. Nous développons 100% dans Kotlin, les exemples de code suivants le seront également.

État de l'interface utilisateur

Pour éviter que ViewModel ne se gonfle de nombreuses propriétés LiveData, exposez un seul ViewState pour les vues (Activity ou Fragment) à observer. Il peut contenir les données précédemment exposées par les multiples LiveData et toute autre information dont la vue pourrait avoir besoin pour s'afficher correctement:

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

Notez que j'utilise une classe Data avec des propriétés immuables pour l'état et que je n'utilise délibérément aucune ressource Android. Ce n'est pas quelque chose de spécifique à MVVM, mais un état d'affichage immuable empêche l'interface utilisateur incohérences et problèmes de filetage.

À l'intérieur de ViewModel créez une propriété LiveData pour exposer l'état et l'initialiser:

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

    init {
        _state.value = LoginViewState()
    }
}

Pour ensuite émettre un nouvel état, utilisez la fonction copy fournie par la classe Data de Kotlin depuis n'importe où à l'intérieur du ViewModel:

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

Dans la vue, observez l'état comme vous le feriez pour tout autre LiveData et mettez à jour la disposition en conséquence. Dans la couche View, vous pouvez traduire les propriétés de l'état en visibilités réelles et utiliser les ressources avec un accès complet à 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
        )
    }
})

Conflit de plusieurs sources de données

Comme vous avez probablement déjà exposé les résultats et les données de la base de données ou des appels réseau dans le ViewModel, vous pouvez utiliser un MediatorLiveData pour les regrouper en un seul état:

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

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

Liaison de données

Étant donné qu'un ViewState unifié et immuable rompt essentiellement le mécanisme de notification de la bibliothèque de liaison de données, nous utilisons un BindingState mutable qui étend BaseObservable pour notifier sélectivement la disposition des modifications. Il fournit une fonction refresh qui reçoit le ViewState correspondant:

Mise à jour: Suppression des instructions if vérifiant les valeurs modifiées car la bibliothèque de liaison de données prend déjà en charge uniquement le rendu des valeurs réellement modifiées. Merci à @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
    }
}

Créez une propriété dans la vue d'observation pour le BindingState et appelez refresh à partir du Observer:

private val state = LoginBindingState()

...

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

Ensuite, utilisez l'état comme toute autre variable dans votre mise en page:

<layout ...>

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

    ...

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

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

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

</layout>

Infos avancées

Une partie du passe-partout bénéficierait certainement des fonctions d'extension et des propriétés déléguées comme la mise à jour du ViewState et la notification des changements dans le BindingState.

Si vous voulez plus d'informations sur la gestion des états et des états avec les composants d'architecture utilisant une architecture "propre", vous pouvez vérifier Eiffel sur GitHub .

C'est une bibliothèque que j'ai créée spécifiquement pour gérer les états de vue immuables et la liaison de données avec ViewModel et LiveData ainsi que les coller avec Android opérations système Android et utilisation commerciale) La documentation va plus en profondeur que ce que je peux fournir ici.

21
Etienne Lenhart

J'ai conçu un modèle basé sur le Flux de données unidirectionnel en utilisant Kotlin avec LiveData .

Consultez l'article complet Medium ou YouTube pour une explication approfondie.

Moyen - Flux de données unidirectionnel Android avec LiveData

YouTube - Flux de données unidirectionnel - Adam Hurwitz - Medellín Android Meetup

Présentation du code

Étape 1 sur 6 - Définir des modèles

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

Étape 2 sur 6 - Passer des événements à 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)
  })
}

Étape 3 sur 6 - Traiter les événements

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

Étape 4 sur 6 - Gérer les demandes réseau avec le modèle 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(...))
  }
}

Étape 5 sur 6 - Gérer les états 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 
        }
    }
}

Étape 6 sur 6 - Observez le changement d'état!

Fragment.kt

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