Bonjour à tous! Dans cet article, je veux parler d'une nouvelle bibliothèque qui apporte le modèle de conception MVI à Android. Cette bibliothèque est appelée MVIDroid, écrite à 100% en Kotlin, légère et utilise RxJava 2.x. Personnellement, je suis l'auteur de la bibliothèque, son code source est disponible sur GitHub, et vous pouvez le connecter via JitPack (lien vers le référentiel à la fin de l'article). Cet article se compose de deux parties: une description générale de la bibliothèque et un exemple de son utilisation.
MVI
Et donc, en préface, permettez-moi de vous rappeler ce qu'est MVI. Modèle - Vue - Intention ou, si en russe, Modèle - Vue - Intention. Il s'agit d'un modèle de conception dans lequel le modèle est un composant actif qui accepte les intentions et génère un état. La présentation (vue) accepte à son tour les modèles de représentation (voir le modèle) et produit ces mêmes intentions. L'état est converti en un modèle de vue à l'aide d'une fonction de transformation (View Model Mapper). Schématiquement, le modèle MVI peut être représenté comme suit:

Dans MVIDroid, la représentation ne produit pas directement des intentions. Au lieu de cela, il produit des événements d'interface utilisateur, qui sont ensuite convertis en intention à l'aide d'une fonction de transformation.

Composants principaux de MVIDroid
Modèle
Commençons par le modèle. Dans la bibliothèque, le concept de modèle est légèrement développé, ici il produit non seulement des états mais aussi des étiquettes. Les étiquettes sont utilisées pour communiquer les modèles entre eux. Les étiquettes de certains modèles peuvent être converties en intentions d'autres modèles à l'aide de fonctions de transformation. Schématiquement, le modèle peut être représenté comme suit:

Dans MVIDroid, le modèle est représenté par l'interface MviStore (le nom du magasin est emprunté à Redux):
interface MviStore<State : Any, in Intent : Any, Label : Any> : (Intent) -> Unit, Disposable { @get:MainThread val state: State val states: Observable<State> val labels: Observable<Label> @MainThread override fun invoke(intent: Intent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
Et pour que nous ayons:
- L'interface possède trois paramètres génériques: État - type d'état, Intention - type d'intention et Label - type d'étiquettes
- Il contient trois champs: état - l'état actuel du modèle, états - États observables et étiquettes - Étiquettes observables. Les deux derniers champs permettent de s'abonner aux modifications de l'état et des balises, respectivement.
- Intention du consommateur
- Il est jetable, ce qui permet de détruire le modèle et d'arrêter tous les processus qui s'y produisent.
Notez que toutes les méthodes Model doivent être exécutées sur le thread principal. Il en va de même pour tout autre composant. Bien sûr, vous pouvez effectuer des tâches d'arrière-plan à l'aide des outils RxJava standard.
Composant
Un composant de MVIDroid est un groupe de modèles unis par un objectif commun. Par exemple, vous pouvez sélectionner tous les modèles d'un écran dans le composant. En d'autres termes, le composant est la façade des modèles qu'il contient et vous permet de masquer les détails d'implémentation (modèles, fonctions de transformation et leurs relations). Regardons le diagramme des composants:

Comme vous pouvez le voir sur le diagramme, le composant a une fonction importante de transformation et de redirection des événements.
La liste complète de la fonction Composant est la suivante:
- Associe les événements et balises de représentation entrants à chaque modèle à l'aide des fonctions de transformation fournies
- Apportez les étiquettes de modèle sortantes à l'extérieur
- Détruit tous les modèles et rompt tous les liens lorsqu'un composant est détruit
Le composant possède également sa propre interface:
interface MviComponent<in UiEvent : Any, out States : Any> : (UiEvent) -> Unit, Disposable { @get:MainThread val states: States @MainThread override fun invoke(event: UiEvent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
Considérez l'interface du composant plus en détail:
- Contient deux paramètres génériques: UiEvent - type d'événements de vue et états - type de statuts des modèles
- Contient le champ États donnant accès au groupe États du modèle (par exemple, en tant qu'interface ou classe de données)
- Événements des consommateurs
- Il est jetable, ce qui permet de détruire le composant et tous ses modèles
Afficher
Comme vous pouvez le deviner, une vue est nécessaire pour afficher les données. Les données de chaque vue sont regroupées dans un modèle de vue et sont généralement représentées sous la forme d'une classe de données (Kotlin). Considérez l'interface View:
interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable }
Ici, tout est un peu plus facile. Deux paramètres génériques: ViewModel - type de modèle de vue et UiEvent - type d'événements de vue. Un champ uiEvents est l'Observable View Event, qui permet aux clients de s'abonner à ces mêmes événements. Et une méthode subscribe () qui vous permet de vous abonner à View Models.
Exemple d'utilisation
Il est maintenant temps d'essayer quelque chose dans la pratique. Je propose de faire quelque chose de très simple. Quelque chose qui ne nécessite pas beaucoup d'efforts pour comprendre, et qui donne en même temps une idée de la façon d'utiliser tout cela et dans quelle direction aller. Que ce soit un générateur d'UUID: au toucher d'un bouton, nous allons générer un UUID et l'afficher à l'écran.
Soumission
Tout d'abord, nous décrivons le modèle d'affichage:
data class ViewModel(val text: String)
Et voir les événements:
sealed class UiEvent { object OnGenerateClick: UiEvent() }
Maintenant, nous implémentons la vue elle-même, pour cela, nous avons besoin de l'héritage de la classe abstraite MviAbstractView:
class View(activity: Activity) : MviAbstractView<ViewModel, UiEvent>() { private val textView = activity.findViewById<TextView>(R.id.text) init { activity.findViewById<Button>(R.id.button).setOnClickListener { dispatch(UiEvent.OnGenerateClick) } } override fun subscribe(models: Observable<ViewModel>): Disposable = models.map(ViewModel::text).distinctUntilChanged().subscribe { textView.text = it } }
Tout est extrêmement simple: nous nous abonnons aux modifications de l'UUID et mettons à jour TextView lorsque nous recevons un nouvel UUID, et lorsque le bouton est cliqué, nous envoyons l'événement OnGenerateClick.
Modèle
Le modèle comprendra deux parties: l'interface et la mise en œuvre.
Interface:
interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } }
Tout est simple ici: notre interface étend l'interface MviStore, en indiquant les types d'état (State) et d'intention (Intent). Type d'étiquettes - Rien, car notre modèle ne les produit pas. L'interface contient également les classes State et Intent.
Afin de mettre en œuvre le modèle, vous devez comprendre comment il fonctionne. À l'entrée du modèle, des intentions sont reçues, qui sont converties en actions à l'aide de la fonction spéciale IntentToAction. Les actions sont entrées dans l'exécuteur, qui les exécute et produit le résultat et l'étiquette. Les résultats vont ensuite au réducteur, qui convertit l'état actuel en un nouvel état.
Les quatre modèles de composition:
- IntentToAction - une fonction qui convertit l'intention en action
- MviExecutor - Exécute des actions et produit des résultats et des balises
- MviReducer - convertit les paires (état, résultat) en nouveaux états
- MviBootstrapper est un composant spécial qui vous permet d'initialiser le modèle. Donne toutes les mêmes actions qui vont également à l'exécuteur. Vous pouvez effectuer une action unique ou vous abonner à une source de données et effectuer des actions sur certains événements. Bootstrapper démarre automatiquement lorsque vous créez un modèle.
Pour créer le modèle lui-même, vous devez utiliser une fabrique spéciale de modèles. Il est représenté par l'interface MviStoreFactory et son implémentation de MviDefaultStoreFactory. L'usine accepte les composants du modèle et délivre un modèle prêt à l'emploi.
L'usine de notre modèle ressemblera à ceci:
class UuidStoreFactory(private val factory: MviStoreFactory) { fun create(factory: MviStoreFactory): UuidStore = object : UuidStore, MviStore<State, Intent, Nothing> by factory.create( initialState = State(), bootstrapper = Bootstrapper, intentToAction = { when (it) { Intent.Generate -> Action.Generate } }, executor = Executor(), reducer = Reducer ) { } private sealed class Action { object Generate : Action() } private sealed class Result { class Uuid(val uuid: String) : Result() } private object Bootstrapper : MviBootstrapper<Action> { override fun bootstrap(dispatch: (Action) -> Unit): Disposable? { dispatch(Action.Generate) return null } } private class Executor : MviExecutor<State, Action, Result, Nothing>() { override fun invoke(action: Action): Disposable? { dispatch(Result.Uuid(UUID.randomUUID().toString())) return null } } private object Reducer : MviReducer<State, Result> { override fun State.reduce(result: Result): State = when (result) { is Result.Uuid -> copy(uuid = result.uuid) } } }
Cet exemple présente les quatre composants du modèle. D'abord, la méthode de création de l'usine, puis les actions et les résultats, suivie par l'entrepreneur et, à la fin, le réducteur.
Composant
Les états des composants (groupe d'états) sont décrits par la classe de données:
data class States(val uuidStates: Observable<UuidStore.State>)
Lors de l'ajout de nouveaux modèles à un composant, leur état doit également être ajouté au groupe.
Et, en fait, la mise en œuvre elle-même:
class Component(uuidStore: UuidStore) : MviAbstractComponent<UiEvent, States>( stores = listOf( MviStoreBundle( store = uuidStore, uiEventTransformer = UuidStoreUiEventTransformer ) ) ) { override val states: States = States(uuidStore.states) private object UuidStoreUiEventTransformer : (UiEvent) -> UuidStore.Intent? { override fun invoke(event: UiEvent): UuidStore.Intent? = when (event) { UiEvent.OnGenerateClick -> UuidStore.Intent.Generate } } }
Nous avons hérité de la classe abstraite MviAbstractComponent, spécifié les types d'états et d'événements de vue, passé notre modèle à la super classe et implémenté le champ States. De plus, nous avons créé une fonction de transformation qui transformera les événements de vue en intentions de notre modèle.
Cartographie des modèles de vue
Nous avons des conditions et des modèles de présentation, il est temps de les convertir l'un en l'autre. Pour ce faire, nous implémentons l'interface MviViewModelMapper:
object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } }
Reliure
La seule présence du composant et de la présentation ne suffit pas. Pour que tout commence à fonctionner, ils doivent être connectés. Il est temps de créer une activité:
class UuidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uuid) bind( Component(UuidStoreFactory(MviDefaultStoreFactory).create()), View(this) using ViewModelMapper ) } }
Nous avons utilisé la méthode bind (), qui prend un composant et un tableau de vues avec les mappeurs de leurs modèles. Cette méthode est une méthode d'extension sur LifecycleOwner (qui sont Activity et Fragment) et utilise le DefaultLifecycleObserver du package Arch, qui nécessite la compatibilité avec la source Java 8. Si, pour une raison quelconque, vous ne pouvez pas utiliser Java 8, la deuxième méthode bind () vous convient, qui n'est pas une méthode d'extension et renvoie MviLifecyleObserver. Dans ce cas, vous devrez appeler vous-même les méthodes du cycle de vie.
Les références
Le code source de la bibliothèque, ainsi que des instructions détaillées pour la connexion et l'utilisation peuvent être trouvés sur GitHub .