当前位置:网站首页>Résoudre les points de douleur dans le développement Android avec kotlin Flow

Résoudre les points de douleur dans le développement Android avec kotlin Flow

2021-11-25 18:04:34 Bien.

Préface

Le but de cet article est d'illustrer comment utiliserKotlin FlowRésolutionAndroidProblèmes de points de douleur dans le développement,Pour étudier comment utiliser avec éléganceFlowEt corriger certaines erreurs d'utilisation typiques.ConcernantFlowPour une introduction et l'utilisation de l'opérateur, voir:Flux asynchrone - Kotlin Station de langue chinoise,Je ne vais pas le répéter ici..Basé surLiveData+ViewModelDeMVVMArchitecture dans certains scénarios(L'écran horizontal et Vertical est typique)Il y a des limites,Cet article sera présenté dans l'ordre approprié pourAndroidBase de développementFlow/ChannelDeMVIArchitecture.

Contexte

L'équipe de clients intelligents de Power s'efforce d'apprendre ensemble à l'extrémité de la tabletteAppLa profondeur supérieure correspond à la scène de l'écran horizontal et Vertical,Sera basé surRxjavaDeMVPLa reconfiguration de l'architecture est basée surLiveData+ViewModel+KotlinCo - ProcessMVVMArchitecture.Au fur et à mesure que la complexité du scénario d'affaires augmente,LiveDataEn tant que seul support de données, il semble peu à peu incapable d'assumer cette responsabilité,L'un des points douloureux est le flou“Statut”Et“Événements”Les limites de.LiveDataLe mécanisme visqueux de,Mais ce n'est pasLiveDataDéfauts de conception,C'est une surutilisation.

Kotlin FlowEst basé surkotlinUn ensemble de cadres de flux de données asynchrones pour le co - processus,Peut être utilisé pour renvoyer plusieurs valeurs asynchrones.kotlin 1.4.0Lancé lors de la sortie de la version officielleStateFlowEtSharedFlow, Les deux ont ChannelBeaucoup de caractéristiques de, On pourrait penser que Flow Poussez - vous vers la table ,Oui.Channel Opération importante dans les coulisses de la neige . Pour le nouveau cadre technologique , Nous n'avons pas un accès aveugle , Après une phase de recherche et d'essai ,DécouverteFlow Peut vraiment développer un effet analgésique pour l'entreprise , Le processus de cette exploration est décrit ci - dessous. .

Un peu de douleur: Un mauvais traitement ViewModelEtViewCommunication de niveau

Problèmes identifiés

Quand l'écran tourne ,LiveData Ça ne marche pas ?

Projet financé parMVPTransition versMVVMHeure, Un moyen typique de Refactoring est de Presenter L'écriture de rappel est réécrite dans ViewModelMoyenneLiveDataParView Abonnement au niveau ,Comme le scénario suivant:

Dans la salle d'étude , Lorsque l'enseignant passe en mode interactif , La page doit être modifiée et apparaît en même temps Toast Mode prompt commuté .

RoomViewModel.kt

class RoomViewModel : ViewModel() {

private val _modeLiveData = MutableLiveData<Int>(-1)
private val modeLiveData : LiveData<Int> = _mode

fun switchMode(modeSpec : Int) {
    _modeLiveData.postValue(modeSpec)
}

}
RoomActivity.kt

class RoomActivity : BaseActivity() {

...

override fun initObserver() {
    roomViewModel.modeLiveData.observe(this, Observer {
        updateUI()
        showToast(it)
    })
}

}
Ça n'a pas l'air mal à première vue. , Mais il n'est pas tenu compte du changement d'écran horizontal et Vertical si la page est détruite et reconstruite , Cela fait que chaque rotation de l'écran sur la page courante se produit à nouveau observe, Ce qui fait que chaque fois qu'il tourne, il rebondit. Toast.

LiveData Garantit que l'abonné peut toujours observer la valeur la plus récente lorsque la valeur change , Et chaque observateur qui s'abonne pour la première fois effectue une méthode de rappel . Une telle caractéristique est nécessaire pour maintenir UI Il n'y a aucun problème avec la cohérence des données , Mais pour observer LiveData Pour lancer un événement unique au - delà de ses capacités .

Bien sûr., Il y a une solution en garantissant LiveData La même valeur n'est déclenchée qu'une seule fois onChangedRappel,C'est encapsulé.MutableLiveDataDeSingleLiveEvent. Laisse tomber, il y a d'autres questions. , Mais c'est vrai LiveData La première chose que j'ai ressentie, c'est qu'il n'y avait pas de bon goût. ,Non.LiveDataDes idées de design, Ensuite, il n'y a pas d'autre problème? ?

ViewModelEtView La communication au niveau ne dépend que LiveData Est - ce suffisant? ?

En serviceMVVMTemps de construction,Changement de donnéesUIMise à jour.PourUI Il suffit de se soucier de l'état final , Mais pour certains événements , Ce n'est pas seulement l'espoir de suivre LiveData La politique de fusion pour supprime tous les événements précédents . Dans la plupart des cas, on s'attend à ce que chaque événement soit exécuté. ,EtLiveData Pas conçu pour ça .

Dans la salle d'étude , Le professeur va rendre hommage à ses camarades de classe. , Les élèves qui ont reçu des commentaires positifs afficheront différents styles de commentaires positifs selon le type de commentaires positifs . Afin d'éviter les POP - ups répétés dus à des changements de configuration ou d'écran horizontal ou vertical , En utilisant les SingleLiveEvent

RoomViewModel.kt

class RoomViewModel : ViewModel() {

private val praiseEvent = SingleLiveEvent<Int>()

fun recvPraise(praiseType : Int) {
    praiseEvent.postValue(praiseType)
}

}
RoomActivity.kt

class RoomActivity : BaseActivity() {

...

override fun initObserver() {
    roomViewModel.praiseEvent.observe(this, Observer {
        showPraiseDialog(it)
    })
}

}
Considérez ce qui suit:, Le professeur a donné à ses camarades de classe A“Asseyez - vous bien.”Et“ Interaction positive ” Deux sortes de choses , On s'attend à ce qu'il joue à la fenêtre deux fois. . Mais selon l'implémentation ci - dessus ,Si deux foisrecvPraiseDans unUI Appels consécutifs dans le cycle de rafraîchissement ,C'est - à - dire:liveData En très peu de temps postDeux fois, Finalement, les élèves n'ont qu'un deuxième pop - up positif. .

En général, Les deux problèmes mentionnés ci - dessus sont simplement qu'il n'y a pas de meilleur moyen de ViewModelEtView Communication au niveau , C'est comme ça que ça se passe LiveData L'utilisation extensive et l'absence de “Statut” Et “Événements” Faire la distinction

Analyse des problèmes

Sur la base du résumé ci - dessus ,LiveData C'est vraiment approprié pour indiquer “Statut”,Mais...“Événements” Ne doit pas être représenté par une seule valeur .Je voulaisView Les couches consomment chaque événement dans l'ordre , Sans affecter l'envoi de l'événement , Ma première réaction a été d'utiliser une file d'attente bloquée pour héberger les événements . Mais lors du choix du modèle, nous devons tenir compte des questions suivantes: ,C'est aussiLiveData Avantages recommandés :

Y a - t - il une fuite de mémoire , Le cycle de vie de l'observateur peut - il s'auto - nettoyer après avoir été détruit?
Si la commutation de fil est prise en charge ,Par exemple,LiveData S'assurer que le changement est perçu dans le fil principal et mis à jour UI
Ne consomme pas d'événements lorsque l'observateur est inactif ,Par exemple,LiveDataPrévention des causesActivity La consommation à l'arrêt provoque crash

Programme I:Bloquer la file d'attente

ViewModel Tenir la file d'attente bloquée ,View La couche lit le contenu de la file d'attente dans la boucle morte du fil principal .Besoin d'ajouter manuellementlifecycleObserver Pour s'assurer que les fils sont suspendus et restaurés , Et ne supporte pas l'orchestration .Envisager d'utiliserkotlinDans le processus de coChannelSubstitution.

Programme II: Kotlin Channel

Kotlin Channel C'est comme bloquer une file d'attente ,La différence est queChannel Avec un send L'opération remplace le blocage put, Avec un receive L'opération remplace le blocage take. Alors ouvre l'âme à trois questions :

Consommation dans les composantes du cycle de vie ChannelFuite de mémoire?

Ça ne va pas,Parce queChannel Ne tient pas de références aux composants du cycle de vie ,Ce n'est pas commeLiveDataEntréeObserver Utilisation de la formule .

Si la commutation de fil est prise en charge ?

Soutien,C'est exact.Channel La collecte nécessite un processus de collaboration ouvert , Le contexte du co - thread peut être commuté dans le co - thread pour réaliser le commutateur de thread .

Si l'observateur consomme encore des événements lorsqu'il n'est pas actif ?

Utiliserlifecycle-runtime-ktxDans la bibliothèquelaunchWhenXMéthodes,C'est exact.Channel Le processus de collecte de < X En attente ,Pour éviter les anomalies.Peut également être utilisérepeatOnLifecycle(State) Viens ici.UI Collection de couches , Quand le cycle de vie < StateHeure, L'accord sera annulé , Redémarrer le programme de collaboration lors de la récupération .

Il semble utiliser Channel L'hébergement d'événements est un bon choix , Et en général, la distribution des événements est individuelle , Il n'est donc pas nécessaire de soutenir un à plusieurs BroadcastChannel( Ce dernier a été progressivement abandonné ,ParSharedFlowSubstitution)

Comment créerChannel?RegardeChannel Méthodes de construction disponibles pour l'exposition externe , Envisager de passer les paramètres appropriés .

public fun <E> Channel(

//  Capacité tampon , Déclenche lorsque la capacité est dépassée onBufferOverflow Politique spécifiée 
capacity: Int = RENDEZVOUS,  

//  Politique de débordement du tampon ,Par défaut en attente,EtDROP_OLDESTEtDROP_LATEST
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,

//  Cas où l'élément de traitement n'a pas été livré avec succès au traitement , Si l'abonné est annulé ou lance une exception 
onUndeliveredElement: ((E) -> Unit)? = null

): Channel<E>
Tout d'abord,Channel C'est chaud , C'est - à - dire envoyer des éléments à tout moment Channel Exécuté même sans abonné . Donc, étant donné qu'il y a des cas où l'événement est envoyé lorsque l'Accord d'abonné est annulé ,C'est - à - dire qu'il existeChannel Réception de l'état de l'événement en période neutre sans abonné .Comme quandActivityUtiliserrepeatOnLifecycle Méthode démarrer le processus de co - consommation ViewModelDétenusChannel Message de l'événement dans ,En coursActivityParce que dansSTOPED Statut et annulation de l'accord .

Sur la base des demandes analysées précédemment , Les événements de la période neutre ne peuvent pas être supprimés ,Et devrait êtreActivity Consommer à tour de rôle lorsque vous retournez à l'état actif . Considérez donc que la politique est suspendue lorsque le tampon déborde , Capacité par défaut 0C'est tout., C'est - à - dire que la méthode de construction par défaut répond à nos besoins .

Comme nous l'avons mentionné précédemment,,BroadcastChannelA étéSharedFlowSubstitution,Alors on va utiliserFlowRemplacerChannelEst - ce possible??

Programme III:FréquentFlow(Courant froid)

Flow is cold, Channel is hot. Le Code d'un constructeur qui dit qu'un flux est un flux froid ne s'exécute pas tant que le flux n'est pas collecté , Voici un exemple très classique :

fun fibonacci(): Flow<BigInteger> = flow {

var x = BigInteger.ZERO
var y = BigInteger.ONE
while (true) {
    emit(x)
    x = y.also {
        y += x
    }
}

}

fibonacci().take(100).collect { println(it) }
Siflow Le Code dans le constructeur ne dépend pas de l'exécution indépendante de l'abonné , Au - dessus, il y a un cycle de mort direct , Et l'opération réelle a trouvé une sortie normale .

Revenons à notre question , Est - il possible d'utiliser un courant froid ici ? Ce n'est évidemment pas approprié , Parce que d'abord le flux froid intuitif ne peut pas transmettre de données en dehors du constructeur .

Mais la réponse n'est pas absolue. ,Enflow Utilisation interne du constructeur channel, Il est également possible d'effectuer des émissions dynamiques ,Par exemple:channelFlow.MaischannelFlow L'émission de valeurs en dehors du constructeur n'est pas prise en charge en soi ,AdoptionChannel.receiveAsFlowL'opérateur peutChannelConvertir enchannelFlow.Ça arrive.Flow“Froid extérieur et chaleur intérieure”, Utilisation des effets et collecte directe ChannelPresque aucune différence.

private val testChannel: Channel<Int> = Channel()

private val testChannelFlow = testChannel.receiveAsFlow ()
Programme IV:SharedFlow/StateFlow
D'abord, les deux flux de chaleur , Et prend en charge la transmission de données à l'extérieur du constructeur . Regardez simplement comment ils sont construits.

public fun <T> MutableSharedFlow(

//  Nombre de replays reçus lors de l'abonnement par nouvel abonné ,Par défaut0
replay: Int = 0,

// Sauf quereplay En plus du nombre ,Capacité du cache,Par défaut0
extraBufferCapacity: Int = 0,

//  Politique en cas de débordement de cache ,Par défaut en attente. Seulement s'il y a au moins un abonné ,onBufferOverflowPour entrer en vigueur. Quand il n'y a pas d'abonnés ,Seulement récemmentreplay La valeur du nombre est sauvegardée ,EtonBufferOverflowInvalide. 
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND

)
//MutableStateFlow équivalent à SharedFlow

MutableSharedFlow(

replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST

)
SharedFlowParPass Pour deux raisons principales :

SharedFlow Prise en charge de l'abonnement par plusieurs abonnés , Fait en sorte que le même événement soit consommé plus d'une fois , Pas comme prévu .
Si vous pensez1 Peut également être contrôlé par l'élaboration de spécifications ,SharedFlow La fonctionnalité qui élimine les données lorsqu'il n'y a pas d'abonné les empêche complètement d'être choisis pour héberger les événements qui doivent être exécutés
EtStateFlow Peut être compris comme spécial SharedFlow, De toute façon, il y a deux questions. .

Bien sûr.,Adapté à l'utilisationSharedFlow/StateFlow Il y a beaucoup de scènes , L'accent sera également mis ci - dessous sur .

Résumé
Pour ceux qui veulentViewModel Les émissions de calques doivent être effectuées et ne peuvent être effectuées qu'une seule fois View Exécution de la couche , Ne passe plus par LiveData postValueJeanView Mise en œuvre de l'écoute de niveau .RecommandéChannelOu parChannel.receiveAsFlowCréé par la méthodeChannelFlowPour réaliserViewModel Envoi d'événements pour la couche .

Résoudre le problème
RoomViewModel.kt

class RoomViewModel : ViewModel() {

private val _effect = Channel<Effect> = Channel ()
val effect = _effect. receiveAsFlow ()

private fun setEffect(builder: () -> Effect) {
    val newEffect = builder()
    viewModelScope.launch {
        _effect.send(newEffect)
    }
}

fun showToast(text : String) {
    setEffect {
        Effect.ShowToastEffect(text)
    }
}

}

sealed class Effect {

data class ShowToastEffect(val text: String) : Effect()

}
RoomActivity.kt

class RoomActivity : BaseActivity() {

...

override fun initObserver() {
    lifecycleScope.launchWhenStarted {
        viewModel.effect.collect {
            when (it) {
                is Effect.ShowToastEffect -> {
                    showToast(it.text)
                }
            }
        }
   }
}

}
Point de douleur 2:Activity/FragmentPar le partageViewModelProblèmes de communication
On laisse souvent ActivityEt lesFragment Copropriété par AcitivityEn tant queViewModelStoreOwnerConstruitViewModel,Pour réaliserActivityEtFragment、EtFragmentCommunication entre. Les scénarios typiques sont les suivants :

class MyActivity : BaseActivity() {

private val viewModel : MyViewModel by viewModels()

private fun initObserver() {
    viewModel.countLiveData.observe { it->
        updateUI(it)
    }
}

private fun initListener() {
    button.setOnClickListener {
        viewModel.increaseCount()
    }
}

}

class MyFragment : BaseFragment() {

private val activityVM : MyViewModel by activityViewModels()  

private fun initObserver() {
    activityVM.countLiveData.observe { it->
        updateUI(it)
    }
}

}

class MyViewModel : ViewModel() {

private val _countLiveData = MutableLiveData<Int>(0)

private val countLiveData : LiveData<Int> = _countLiveData

fun increaseCount() {
    _countLiveData.value = 1 + _countLiveData.value ?: 0
}

}
C'est simplement en faisant ActivityEtFragment Observez le même liveData,Assurer la cohérence.

Si c'est le cas FragmentAppel moyenActivityMéthode,Par le partageViewModelC'est possible??

Problèmes identifiés
DialogFragmentEtActivityCommunication de
Nous utilisons habituellementDialogFragment Pour réaliser le pop - up , Dans son hôte Activity Lorsque l'événement de clic de la fenêtre contextuelle est défini dans , Si la fonction de rappel fait référence à ActivityObjet, Il est facile de générer des erreurs de référence causées par la reconstruction de pages horizontales et verticales . Nous suggérons donc que ActivityInterface d'implémentation, À chaque fois que Attach Est actuellement attaché à Activity Forcer l'objet d'interface à définir la méthode de rappel .

class NoticeDialogFragment : DialogFragment() {

internal lateinit var listener: NoticeDialogListener    

interface NoticeDialogListener {
    fun onDialogPositiveClick(dialog: DialogFragment)
    fun onDialogNegativeClick(dialog: DialogFragment)
}

override fun onAttach(context: Context) {
    super.onAttach(context)
    try {
        listener = context as NoticeDialogListener
    } catch (e: ClassCastException) {
        throw ClassCastException((context.toString() +
                " must implement NoticeDialogListener"))
    }
}

}
class MainActivity : FragmentActivity(), NoticeDialogFragment.NoticeDialogListener {

fun showNoticeDialog() {
    val dialog = NoticeDialogFragment()
    dialog.show(supportFragmentManager, "NoticeDialogFragment")
}

override fun onDialogPositiveClick(dialog: DialogFragment) {
    // User touched the dialog's positive button
}

override fun onDialogNegativeClick(dialog: DialogFragment) {
    // User touched the dialog's negative button
}

}
Une telle formulation ne poserait pas les problèmes susmentionnés. , Mais avec plus de pop - ups pris en charge sur la page ,Activity De plus en plus d'interfaces doivent être mises en œuvre , Pas très convivial pour le codage ou la lecture de code . Y a - t - il une chance d'emprunter le partage? ViewModelFais un article?

Analyse des problèmes
Nous voulons ViewModelEnvoyer l'événement, Et que tous les composants qui en dépendent reçoivent des événements .Comme dansFragmentA Cliquez sur la touche pour déclencher l'événement A, Son hôte Activity、 Même hôte FragmentBEtFragmentA Il doit lui - même réagir aux événements. .

C'est un peu comme la radio , Et a deux caractéristiques :

Prise en charge d'un à plusieurs , C'est - à - dire qu'un support de message est consommé par plusieurs abonnés
Actualité, Les messages périmés n'ont aucun sens et ne doivent pas être consommés tardivement. .
On dirait queEventBus C'est une façon de réaliser ,Mais il y a déjàViewModel Réutiliser comme support est évidemment un gaspillage ,EventBus C'est encore mieux pour les pages croisées 、 Communication entre les composantes . Comparer l'utilisation de plusieurs modèles analysés précédemment ,DécouverteSharedFlow Très utile dans ce scénario .

SharedFlowSimilaireBroadcastChannel, Prise en charge de plusieurs abonnés , Envoyer plusieurs consommations à la fois .
SharedFlowConfiguration flexible, Comme la configuration par défaut capacity = 0, replay = 0, Cela signifie que les nouveaux abonnés ne recevront pas de réponse similaire LiveDataLecture de. Jetez - les directement lorsqu'il n'y a pas d'abonnés , Correspond exactement aux caractéristiques de l'événement temporel ci - dessus .
Résoudre le problème
class NoticeDialogFragment : DialogFragment() {

private val activityVM : MyViewModel by activityViewModels()

fun initListener() {
    posBtn.setOnClickListener {
        activityVM.sendEvent(NoticeDialogPosClickEvent(textField.text))
        dismiss()
    }

    negBtn.setOnClickListener {
        activityVM.sendEvent(NoticeDialogNegClickEvent)
        dismiss()
    }
}

}

class MainActivity : FragmentActivity() {

private val viewModel : MyViewModel by viewModels()

fun showNoticeDialog() {
    val dialog = NoticeDialogFragment()
    dialog.show(supportFragmentManager, "NoticeDialogFragment")
}

fun initObserver() {
    lifecycleScope.launchWhenStarted {
       viewModel.event.collect {
            when(it) {
                is NoticeDialogPosClickEvent -> {
                    handleNoticePosClicked(it.text)
                }

                NoticeDialogNegClickEvent -> {
                    handleNoticeNegClicked()
                }
            }
        }
    }
}

}

class MyViewModel : ViewModel() {

private val _event: MutableSharedFlow<Event> = MutableSharedFlow ()

val event = _event. asSharedFlow ()

fun sendEvent(event: Event) {
    viewModelScope.launch {
        _event.emit(event)
    }
}

}
Par icilifecycleScope.launchWhenX Le lancement d'un processus de collaboration n'est pas vraiment une pratique exemplaire ,Si tu veuxActivity Événements reçus rejetés directement dans un état inactif ,Il faut utiliserrepeatOnLifecycle Pour contrôler l'ouverture et l'annulation de l'accord au lieu de la suspension .Mais étant donné queDialogFragment Le cycle de vie est l'hôte ActivityUn sous - ensemble de, Donc il n'y a pas de gros problèmes ici .

Basé surFlow/ChannelDeMVIArchitecture

La question des points douloureux dont il a été question plus haut , En fait, c'est pour la prochaine présentation. MVI L'architecture jette des briques sur le jade .EtMVI Mise en oeuvre concrète de l'architecture , C'est - à - dire intégrer les solutions ci - dessus dans le Code modèle , Maximiser les avantages de l'architecture .

MVIQu'est - ce que c'est?

Ce qu'on appelleMVI, Les correspondances sont Model、View、Intent

Model: Non, pas du tout.MVC、MVP- Oui.M Niveau de données auquel il est fait référence , C'est une représentation UI Objet agrégé pour l'état .ModelEst immuable,Model Avec la présentation de UIEst une correspondance individuelle.

View:EtMVC、MVP C'est une référence VC'est pareil, Se réfère au rendu UIUnit é,C'est possible.ActivityOuView. Peut recevoir l'intention d'interaction de l'utilisateur , Selon le nouveau Model Dessiner de façon réactive UI.

Intent:Ce n'est pas traditionnel.Android Dans le design Intent, Se réfère généralement à l'utilisateur et UI L'intention de l'interaction , Si le bouton clique .IntentC'est le changementModel La seule source de .

ComparerMVVM Quelle est la principale différence entre ?

MVVMAvec les contraintesViewCouche etViewModelComment interagir,Plus précisément,ViewLes couches peuvent être appelées à volontéViewModelMéthodes,EtMVISous ArchitectureViewModelLa mise en œuvre deViewBlindage de la couche,Ne peut être envoyé que parIntentPour conduire l'événement.
MVVM L'architecture ne met pas l'accent sur la représentation UIStatutModel Convergence des valeurs , Et avoir un impact sur l'énergie UI Les modifications des valeurs de peuvent être dispersées à l'intérieur de chaque méthode directement appelable .EtMVISous Architecture,IntentC'est le moteur.UI La seule source de changement , Et caractériser UI La valeur de l'état converge dans une variable .
Basé surFlow/ChannelDeMVIComment réaliser
Abstraction de la classe de base BaseViewModel

UiState Est capable de caractériser UIDeModel,AvecStateFlowPorter(Peut également être utiliséLiveData)

UiEvent Est une représentation d'un événement interactif Intent,AvecSharedFlowPorter

UiEffect C'est l'événement qui a causé le changement UI Autres effets indésirables ,AvecchannelFlowPorter

BaseViewModel.kt

abstract class BaseViewModel<State : UiState, Event : UiEvent, Effect : UiEffect> : ViewModel() {

/**
 * État initial
 * stateFlowDifférent deLiveData Il doit y avoir une valeur initiale 
 */
private val initialState: State by lazy { createInitialState() }

abstract fun createInitialState(): State

/**
 * uiState Regrouper toutes les pages UI Statut
 */
private val _uiState: MutableStateFlow<State> = MutableStateFlow(initialState)

val uiState = _uiState.asStateFlow()

/**
 * event Contient les utilisateurs et uiL'interaction de( Comme cliquer sur l'opération ), Il y a aussi des messages des coulisses ( Comme changer le mode d'auto - étude )
 */
 private val _event: MutableSharedFlow<Event> = MutableSharedFlow()

 val event = _event.asSharedFlow()

/**
 * effectUtilisé comme  Effets secondaires de l'événement ,En général. Un événement unique Et  Une relation d'abonnement individuelle 
 * Par exemple:JouerToast、NavigationFragmentAttendez.
 */
 private val _effect: Channel<Effect> = Channel()

 val effect = _effect.receiveAsFlow()

init {
    subscribeEvents()
}

private fun subscribeEvents() {
    viewModelScope.launch {
        event.collect {
            handleEvent(it)
        }
    }
}

protected abstract fun handleEvent(event: Event)

fun sendEvent(event: Event) {
    viewModelScope.launch {
        _event.emit(event)
    }
 }

protected fun setState(reduce: State.() -> State) {
    val newState = currentState.reduce()
    _uiState.value = newState
}

protected fun setEffect(builder: () -> Effect) {
    val newEffect = builder()
    viewModelScope.launch {
        _effect.send(newEffect)
    }
 }

}

interface UiState

interface UiEvent

interface UiEffect
StateFlowEssentiellement équivalent àLiveData,La différence est queStateFlow Il doit y avoir une valeur initiale , Il est également plus logique que la page ait un état initial .Utilisation généraledata classRéalisationUiState, L'état de tous les éléments de la page est représenté par une variable membre .

Pour les événements d'interaction utilisateur SharedFlow, Actualité et prise en charge d'un à plusieurs abonnements , Il peut être utilisé pour résoudre le problème du point de douleur 2 mentionné ci - dessus .

Effets secondaires des événements de consommation ChannelFlowPorter, Pas de perte et abonnement individuel ,Une seule fois. Utilisez - le pour résoudre le problème du point de douleur mentionné ci - dessus .

Catégorie de protocole, Définir les besoins opérationnels spécifiques State、Event、EffectCatégorie

class NoteContract {

/**
* pageTitle: Titre de la page
* loadStatus:  État de la charge de traction 
* refreshStatus:  Drop - down Refresh Status 
* noteList :  Liste des mémos 
*/
data class State(
    val pageTitle: String,
    val loadStatus: LoadStatus,
    val refreshStatus: RefreshStatus,
    val noteList: MutableList<NoteItem>
) : UiState

sealed class Event : UiEvent {
    // Événement de rafraîchissement déroulant
    object RefreshNoteListEvent : Event()

    //  Pull up Load Event 
    object LoadMoreNoteListEvent: Event()

    //  Ajouter un événement de clic de clé 
    object AddingButtonClickEvent : Event()

    // ListeitemCliquez sur l'événement
    data class ListItemClickEvent(val item: NoteItem) : Event()

    //  Ajouter un événement de disparition de fenêtre 
    object AddingNoteDialogDismiss : Event()

    //  Ajouter un élément popup ajouter une confirmation cliquer sur l'événement 
    data class AddingNoteDialogConfirm(val title: String, val desc: String) : Event()

    //  Ajouter une fenêtre contextuelle annuler la confirmation cliquer sur l'événement 
    object AddingNoteDialogCanceled : Event()
}

sealed class Effect : UiEffect {

    //  Erreur de chargement des données d'éjection Toast
    data class ShowErrorToastEffect(val text: String) : Effect()

    //  Popup add item popup 
    object ShowAddNoteDialog : Effect()
}

sealed class LoadStatus {

    object LoadMoreInit : LoadStatus()

    object LoadMoreLoading : LoadStatus()

    data class LoadMoreSuccess(val hasMore: Boolean) : LoadStatus()

    data class LoadMoreError(val exception: Throwable) : LoadStatus()

    data class LoadMoreFailed(val errCode: Int) : LoadStatus()

}

sealed class RefreshStatus {

    object RefreshInit : RefreshStatus()

    object RefreshLoading : RefreshStatus()

    data class RefreshSuccess(val hasMore: Boolean) : RefreshStatus()

    data class RefreshError(val exception: Throwable) : RefreshStatus()

    data class RefreshFailed(val errCode: Int) : RefreshStatus()

}

}
Collecte des flux de changement d'état et des flux d'événements ponctuels dans les composantes du cycle de vie , Envoyer un événement d'interaction utilisateur

class NotePadActivity : BaseActivity() {

  ...

override fun initObserver() {
    super.initObserver()
    lifecycleScope.launchWhenStarted {
        viewModel.uiState.collect {
            when (it.loadStatus) {
                is NoteContract.LoadStatus.LoadMoreLoading -> {
                    adapter.loadMoreModule.loadMoreToLoading()
                }
                ...
            }

            when (it.refreshStatus) {
                is NoteContract.RefreshStatus.RefreshSuccess -> {
                    adapter.setDiffNewData(it.noteList)
                    refresh_layout.finishRefresh()
                    if (it.refreshStatus.hasMore) {
                        adapter.loadMoreModule.loadMoreComplete()
                    } else {
                        adapter.loadMoreModule.loadMoreEnd(false)
                    }
                }
                ...
            }

            txv_title.text = it.pageTitle
            txv_desc.text = "${it.noteList.size}Enregistrement (s)"
        }
    }

    lifecycleScope.launchWhenStarted {
        viewModel.effect.collect {
            when (it) {

                is NoteContract.Effect.ShowErrorToastEffect -> {
                    showToast(it.text)
                }

                is NoteContract.Effect.ShowAddNoteDialog -> {
                    showAddNoteDialog()
                }
            }
        }
    }
}

private fun initListener() {
    btn_floating.setOnClickListener {
        viewModel.sendEvent(NoteContract.Event.AddingButtonClickEvent)
    }
}

}

UtiliserMVIQuels sont les avantages?

Les deux points douloureux ci - dessus ont été résolus . C'est pourquoi j'ai passé beaucoup de temps à présenter le processus de résolution de deux problèmes . Ce n'est que lorsque vous avez vraiment mal que vous ressentez l'avantage de choisir la bonne architecture .
Flux unidirectionnel de données, Tout changement d'état provient d'un événement , Il est donc plus facile de repérer les problèmes .
Idéalement, oui. ViewCouche etViewModel La couche isole l'interface ,Plus découplé.
Statut、 Les événements sont clairement classés au niveau architectural , Il est facile de contraindre les développeurs à écrire de beaux codes .
Problèmes pratiques
Gonflé UiState, Lorsque la complexité de la page augmente ,ReprésentationUiStateDedata class Ça va gonfler , Et les caractéristiques du corps entier en raison de sa traction , Le coût de la mise à jour locale est élevé . Donc pour les pages complexes , Il est possible de diviser le module ,Que chacunFragment/View Chacun détient son propre ViewModel Pour démonter la complexité .
Pour la plupart des événements, il suffit d'appeler la méthode , Il y a plus d'encodage pour définir le type d'événement et la partie de transition que l'appel direct .
Conclusions
Dans le schéma SharedFlowEtchannelFlow L'utilisation de ,Même s'il n'est pas utiliséMVIArchitecture, La référence à la mise en œuvre ici peut également aider à résoudre de nombreux défis de développement , En particulier en ce qui concerne les écrans horizontaux et verticaux .

Possibilité d'utiliserStateFlow/LiveData Page de convergence tous les états , Peut également être divisé en plusieurs . Mais il est plus recommandé d'appuyer sur UI Convergence de la Division des modules de composants .

Sauter l'utilisation Intent,Appel directViewModel La méthode est également acceptable .

UtiliserFlowQu'est - ce que ça peut nous apporter d'autre?
QueRxjavaPlus simple.,QueLiveData Plus d'opérateurs

Si utiliséflowOn L'opérateur change le contexte de l'accord 、Utiliserbuffer、conflate L'opérateur gère la contre - pression 、Utiliserdebounce L'opérateur implémente l'anti - secousse 、UtilisercombineMise en œuvre de l'opérateurflow Et ainsi de suite .

Il est plus facile d'utiliser des coops basés sur des callbacks api écraser l'appel comme le Code de synchronisation

UtilisercallbackFlow, Transmettre les résultats des opérations asynchrones sous forme de suspension synchrone .

版权声明
本文为[Bien.]所创,转载请带上原文链接,感谢
https://chowdera.com/2021/11/20211125180118177j.html

随机推荐