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!
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.
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
)
}
})
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)
})
...
É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>
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.
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
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()
...
}
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)
})
}
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())
}
...
}
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(...))
}
}
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
}
}
}
Fragment.kt
contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->
adapter.submitList(contentList)
})
...
}