Salle de réunion L̶i̶t̶t̶l̶e̶ Helper v 2

Cet article décrit en détail les étapes de développement de l'application mobile Meeting Room Helper: de la création de l'idée à la sortie. L'application est écrite en Kotlin et construite sur une architecture MVVM simplifiée, sans utiliser de liaison de données. La partie UI est mise à jour à l'aide d'objets LiveData. Les raisons du refus de la liaison des données sont détaillées et expliquées. L'architecture utilise un certain nombre de solutions intéressantes qui permettent de diviser logiquement le programme en petits fichiers, ce qui simplifie finalement la prise en charge du code.



Description du projet


Il y a 3 ans, notre entreprise a eu l'idée de développer un petit projet de réservation instantanée de salles de réunion. La plupart des responsables RH et Arcadia préfèrent utiliser le calendrier Outlook à de telles fins, mais qu'en est-il du reste?

Je vais donner 2 exemples de la vie du développeur

  1. Toute équipe a périodiquement un désir spontané de tenir un échange rapide pendant 5 à 10 minutes. Ce désir peut dépasser les développeurs dans n'importe quel coin du bureau, et afin de ne pas distraire les collègues autour d'eux, ils (les développeurs et pas seulement) commencent à chercher une conversation libre. En migrant d'une pièce à l'autre (dans notre bureau, les salles de réunion sont disposées les unes après les autres), les collègues «vérifient soigneusement» laquelle des salles est actuellement libre. En conséquence, ils distraient les collègues à l'intérieur. Ces gars-là ont toujours été et seront toujours, même si le tournage est terminé dans la charte de l'entreprise pour l'interruption du rallye. Qui a compris, il comprendra.
  2. Et voici un autre cas. Vous venez de quitter la salle à manger et vous vous dirigez vers vous, mais ici votre collègue (ou manager) d'un autre service vous intercepte. Il veut vous dire quelque chose d'urgence, et pour cela, vous avez besoin d'une salle de réunion. Selon la réglementation, vous devez d'abord réserver une chambre (à partir de votre téléphone ou ordinateur) et ensuite l'occuper. C'est bien si vous avez un téléphone mobile avec Outlook mobile. Et sinon? Retournez à l'ordinateur, puis à nouveau pour retourner dans la salle de réunion? Pour forcer chaque employé à mettre Outlook Express sur le téléphone et à s'assurer que tout le monde porte le téléphone avec lui? Ce ne sont pas nos méthodes.

C'est pourquoi il y a 2,5 ans chacune des salles de réunion était équipée de sa propre tablette:



Pour ce projet, mon collègue a développé la première version de l'application: Meeting Room Little Helper ( ici vous pouvez en lire plus ). MRLH autorisé à réserver une réservation, annuler et renouveler une réservation, a montré l'état des conversations restantes. Reconnaître l'identité d'un employé (en utilisant le service cloud de l'API Microsoft Face et nos analyseurs internes) est devenu une «astuce» innovante. L'application s'est avérée solide et a servi fidèlement l'entreprise pendant 2,5 ans.

Mais le temps a passé ... De nouvelles idées sont apparues. Je voulais quelque chose de nouveau et nous avons donc décidé de réécrire l'application.

Mandat


Comme cela arrive souvent - mais malheureusement pas toujours - le développement a commencé avec la préparation des spécifications techniques. Tout d'abord, nous avons appelé les gars qui utilisent le plus souvent les tablettes pour faire des réservations. Il se trouve que la plupart du temps, ils étaient dépendants des RH et des managers qui avaient auparavant utilisé exclusivement Outlook. De leur part, nous avons reçu les commentaires suivants (à partir des exigences, il est immédiatement clair ce que les RH ont demandé et ce que les gestionnaires ont demandé):

  • vous devez ajouter la possibilité de réserver n'importe quelle salle de réunion à partir de n'importe quelle tablette (auparavant, chaque tablette vous permettait de réserver uniquement votre chambre);
  • ce serait cool de regarder le calendrier des rassemblements pour une réunion d'une journée (idéalement, pour n'importe quel jour);
  • l'ensemble du cycle de développement doit être effectué en peu de temps (pendant 6-7 semaines).

Tout est clair avec les souhaits du client, mais qu'en est-il des exigences techniques et de l'avenir? Ajoutez quelques exigences pour le projet de la guilde des développeurs:

  • Le système devrait fonctionner à la fois avec les tablettes existantes et avec les nouvelles;
  • évolutivité du système - à partir de 50 conversations et plus (cela devrait suffire avec une marge pour la plupart des clients si le système commence à se répliquer);
  • maintenir la fonctionnalité précédente (la première version de l'application utilisait l'API Java pour communiquer avec les services Outlook, et nous avions prévu de la remplacer par une API Microsoft Graph spécialisée, il était donc important de ne pas perdre de fonctionnalité);
  • minimisation de la consommation d'énergie (les tablettes sont alimentées par une batterie externe, car le centre d'affaires ne permettait pas de percer ses murs pour poser nos fils);
  • nouveau design UX / UI, reflétant ergonomiquement toutes les innovations.

Total de 8 points. Les exigences sont assez justes. De plus, nous stipulons les règles générales de développement:

  • utiliser uniquement des technologies avancées (cela permettra à l'équipe de se développer en tant que spécialiste et de ne pas stagner au même endroit, tout en simplifiant le soutien au projet dans un avenir prévisible);
  • suivre les meilleures pratiques, mais ne les prenez pas aveuglément pour acquises, la règle principale de tout professionnel (et d'un développeur qui y aspire) est de tout évaluer de manière critique;
  • Écrire du code propre et bien rangé (c'est peut-être le plus difficile lorsque vous essayez de combiner innovation et temps de développement serré).

Un début a été fait. C'est, comme toujours, enthousiaste! Voyons ce qui se passe ensuite.

La conception


Conception d'application développée par le concepteur UX:




Ceci est l'écran principal. Il sera affiché la plupart du temps. Toutes les informations nécessaires se trouvent ici de manière ergonomique:

  • le nom de la chambre et son numéro;
  • état actuel;
  • délai jusqu'à la prochaine réunion (ou jusqu'à sa fin);
  • les statuts des pièces restantes en bas de l'écran.

Remarque: le cadran n'affiche que 12 heures, car le système est configuré selon les besoins de l'entreprise (les tablettes Arcadia fonctionnent de 8 h à 20 h, s'allument et s'éteignent automatiquement)




Pour réserver une chambre, il suffit d'appeler la fenêtre de réservation et d'indiquer la durée du rallye. Les étapes de réservation des chambres restantes sont similaires, elles ne commencent qu'en cliquant sur l'icône de la chambre.



Si vous souhaitez planifier une réunion à une heure spécifique, passez à l'onglet suivant, dans la liste des réunions qui se tiendront aujourd'hui dans la salle de réunion, et cliquez sur Temps libre. De plus, tout est comme dans le premier cas.

L'arbre de transition complet devrait ressembler à ceci:




Essayons de l'implémenter avec compétence.

Pile technologique


Les techniques de développement évoluent assez rapidement et changent. Pendant encore 2 ans, Java était le langage de développement Android officiel. Tout le monde a écrit en Java et utilisé la liaison de données. Maintenant, il me semble, nous nous dirigeons vers la programmation réactive et Kotlin. Java est un excellent langage, mais il présente quelques imperfections par rapport à ce que Kotlin et AndroidX ont à offrir. Kotlin et AndroidX peuvent réduire au minimum l'utilisation de la liaison de données, voire l'exclure complètement. Ci-dessous, je vais essayer d'expliquer mon point de vue.

Kotlin


Je pense que de nombreux développeurs Android sont déjà passés à Kotlin, et je suis donc d'accord avec moi qu'écrire un nouveau projet Android en 2019 dans une langue autre que Kotlin, c'est comme combattre la mer. Bien sûr, vous pouvez discuter, mais qu'en est-il de Flutter et Dart? Qu'en est-il de C ++, C # et même de Cordova? A quoi je répondrai: le choix vous appartient toujours.

En 480 avant JC le roi perse Xerxès a ordonné à ses soldats de traverser la mer comme punition pour avoir détruit une partie de son armée lors d'une tempête, et cinq siècles plus tard, l'empereur romain Caligula a déclaré la guerre à Poséidon. Une question de goût. Pour 9 sur 10, Kotlin est bon, mais pour 10, il peut être mauvais. Tout dépend de vous, de vos désirs et aspirations.

Kotlin est mon choix. La langue est simple et belle. Écrire dessus est facile et agréable, et surtout, il n'est pas nécessaire d'en écrire trop: classe de données, objet, setter et getter optionnels, expressions lambda simples et fonctions d'extension. Ce n'est qu'une infime partie de ce que cette langue a à offrir. Si vous n'êtes pas encore passé à Kotlin, n'hésitez pas! Dans la section avec la pratique, je vais démontrer certains des avantages de la langue (ce n'est pas une offre publicitaire).

Model-View-ViewModel


MVVM est actuellement l'architecture d'application recommandée par Google. Pendant le développement, nous respecterons ce modèle particulier, mais nous ne le respecterons pas pleinement, car MVVM recommande d'utiliser la liaison de données, mais nous le refusons.

Avantages de MVVM

  • Différenciation de la logique métier et de l'interface utilisateur. Dans l'implémentation correcte de MVVM, il ne devrait pas y avoir un seul androïde d'importation dans le ViewModel, à l'exception des objets LiveData des packages AndroidX ou Jetpack. Une utilisation appropriée laisse automatiquement tout le travail de l'interface utilisateur à l'intérieur des fragments et des activités. N'est-ce pas génial?
  • Le niveau d'encapsulation est pompé. Il sera plus facile de travailler en équipe: vous pouvez désormais travailler tous ensemble sur un même écran sans interférer les uns avec les autres. Alors qu'un développeur travaille avec l'écran, un autre peut créer un ViewModel et un troisième peut écrire des requêtes dans le référentiel.
  • MVVM a un effet positif sur l'écriture des tests unitaires. Cet article découle du précédent. Si toutes les classes et méthodes sont encapsulées de travailler avec l'interface utilisateur, elles peuvent facilement être testées.
  • Une solution naturelle avec rotation d'écran. Peu importe à quel point cela peut sembler étrange, mais cette fonctionnalité est acquise automatiquement, avec la transition vers MVVM (car les données sont stockées dans le ViewModel). Si vous cochez des applications assez populaires (VK, Telegram, Sberbank-Online et Aviasales), il s'avère que exactement la moitié d'entre elles ne peuvent pas faire pivoter l'écran. Ce qui me surprend et me méprise en tant qu'utilisateur de ces applications.

Pourquoi MVVM est-il dangereux?

  • Fuite de mémoire. Cette erreur dangereuse se produit si vous enfreignez les lois d'utilisation de LiveData et d'observateur. Nous examinerons cette erreur en détail dans la section pratique.
  • Sprawling ViewModel. Si vous essayez d'intégrer toute la logique métier dans le ViewModel, vous obtiendrez un code illisible. Le moyen de sortir de cette situation peut être de diviser le ViewModel en une hiérarchie ou d'utiliser des présentateurs. C'est exactement ce que j'ai fait.

Règles de travail avec MVVM

Commençons par le plus de bévues et passons aux moins de bévues:

  • le corps de la demande ne doit pas être dans ViewModel (uniquement dans le référentiel);
  • Les objets LiveData sont définis dans le ViewModel, ils ne se jettent pas à l'intérieur du référentiel, car les demandes dans le référentiel sont traitées à l'aide de Rx-Java (ou coroutines);
  • toutes les fonctions de traitement doivent être déplacées vers des classes et des fichiers tiers ("Presenters"), afin de ne pas encombrer le ViewModel et de ne pas distraire de l'essence.

Livedata


LiveData est une classe de détenteurs de données observables. Contrairement à un observable normal, LiveData est sensible au cycle de vie, ce qui signifie qu'il respecte le cycle de vie des autres composants de l'application, tels que les activités, les fragments ou les services. Cette prise de conscience garantit que LiveData ne met à jour que les observateurs de composants d'application qui sont dans un état de cycle de vie actif.
Source: developer.android.com/topic/libraries/architecture/livedata

Une conclusion simple peut être tirée de la définition: LiveData est un outil de programmation réactive fiable. Nous l'utiliserons pour mettre à jour la partie UI sans liaison de données. Pourquoi

La structure des fichiers XML ne permet pas une distribution concise des données obtenues à partir de <data> ... </data>. Si tout est clair avec de petits fichiers, qu'en est-il des gros fichiers? Que faire avec des écrans complexes, plusieurs inclure et passer plusieurs champs? Utilisez des modèles partout? Obtenez des fixations de champ rigides? Et si le champ doit être formaté, appeler des méthodes à partir de packages Java? Cela rend le code désespérément et complètement spaghetti. Pas du tout ce que MVVM avait promis.

Le rejet de la liaison de données rendra les modifications de la partie d'interface utilisateur transparentes. Toutes les mises à jour se feront directement à l'intérieur de l'observateur. Parce que Puisque le code Kolin est concis et clair, nous n’aurons pas de problèmes avec un observateur gonflé. L'écriture et la maintenance du code deviendront plus faciles. Les fichiers XML seront utilisés uniquement pour la conception - aucune propriété à l'intérieur.

La liaison de données est un outil puissant. Il est idéal pour résoudre certains problèmes, et il s'harmonise bien avec Java, mais avec Kotlin ... Avec Kotlin, dans la plupart des cas, la liaison de données est juste rudimentaire. La liaison de données ne fait que compliquer le code et ne confère aucun avantage concurrentiel.

En Java, vous aviez le choix: soit utiliser la liaison de données, soit écrire beaucoup de code laid. Dans Kotlin, vous pouvez accéder directement aux éléments de vue, en contournant findViewById (), ainsi que sa propriété. Par exemple:

// Instead of TextView textView = findViewById<TextView>(R.id.textView) textView.text = "Hello, world!" textView.visibility = View.VISIBLE 

Une question logique se pose: pourquoi s'embêter avec des modèles de jardinage à l'intérieur de fichiers XML, invoquer des méthodes Java dans des fichiers XML, surcharger la logique de la partie XML si tout cela peut être évité?

Coroutines au lieu de Thread () et Rx-Java


Les coroutines sont incroyablement légères et faciles à utiliser. Ils sont idéaux pour la plupart des tâches asynchrones simples: traitement des résultats de requête, mise à jour de l'interface utilisateur, etc.

Les coroutines peuvent remplacer efficacement Thread () et Rx-Java dans les cas où de hautes performances ne sont pas requises, car ils paient pour la légèreté avec rapidité. Rx-Java, sans aucun doute, est plus fonctionnel, mais pour les tâches simples, tous ses actifs ne sont pas nécessaires.

Microsoft et le reste


Pour travailler avec les services Outlook, l'API Microsoft Graph sera utilisée. Avec les autorisations appropriées, vous pouvez obtenir à travers lui toutes les informations nécessaires sur les employés, les salles et les événements (réunions). Pour la reconnaissance faciale, le service cloud de l'API Microsoft Face sera utilisé.

En regardant un peu plus loin, je dirai que pour résoudre le problème d'évolutivité, le stockage cloud Firebase a été utilisé. Ceci sera discuté ci-dessous.

L'architecture


Problèmes d'évolutivité


Il est assez difficile de rendre le système entièrement ou partiellement évolutif. Cela est particulièrement difficile à faire si la première version de l'application n'était pas évolutive et que la seconde devrait le devenir. L'application v1 a envoyé des demandes à toutes les chambres à la fois. Chacune des tablettes envoyait régulièrement des demandes au serveur pour mettre à jour toutes les données. Dans le même temps, les appareils ne se synchronisent pas entre eux, car le projet n'a tout simplement pas son propre serveur.

Bien sûr, si nous suivons le même chemin et envoyons N demandes depuis chacune des N tablettes, alors à un moment donné, nous renverserons l'API Microsoft Graph ou verrons notre système se figer.

Il serait logique d'utiliser une solution client-serveur dans laquelle le serveur interroge le graphique, accumule des données et, sur demande, fournit des informations aux tablettes, mais ici nous sommes confrontés à la réalité. L'équipe du projet est composée de 2 personnes (développeur et designer Android). Ils doivent respecter le délai de 7 semaines et le backend n'est pas fourni, car la mise à l'échelle est une exigence du développeur. Mais cela ne signifie pas que l'idée doit être abandonnée?

La seule bonne solution dans cette situation sera probablement l'utilisation du stockage cloud. Firebase remplacera le serveur et servira de tampon. Il s'avère ensuite ce qui suit: chaque tablette interroge uniquement son adresse à partir de l'API Microsoft Graph et, si nécessaire, synchronise les données dans le stockage cloud, d'où elles peuvent être lues par d'autres appareils.

L'avantage de cette implémentation sera une réponse rapide, car Firebase fonctionne en mode temps réel. Nous réduirons le nombre de demandes envoyées au serveur N fois, ce qui signifie que l'appareil fonctionnera sur batterie un peu plus longtemps. D'un point de vue financier, le projet n'a pas augmenté de prix, car Pour ce projet, la version gratuite de Firebase suffit avec de multiples réserves: 1 Go de stockage, 10 mille autorisations par mois et 100 connexions à la fois. Les inconvénients pourraient inclure la dépendance à l'égard d'un cadre tiers, mais Firebase nous inspire confiance, car Il s'agit d'un produit stable maintenu et développé par Google.

L'idée générale du nouveau système était la suivante: N tablettes et une plateforme cloud pour la synchronisation des données en temps réel. Commençons par concevoir l'application elle-même.

LiveData dans le référentiel


Il semblerait que j'ai récemment établi les règles de bonne forme et violé immédiatement l'une d'entre elles. Contrairement à l'utilisation recommandée de LiveData dans le ViewModel, dans ce projet, les objets LiveData sont initialisés dans le référentiel et tous les référentiels sont déclarés singleton. Pourquoi

Une solution similaire est associée au mode d'application. Les tablettes sont ouvertes de 8h à 20h. Pendant tout ce temps, seul l'assistant de salle de réunion a été lancé sur eux. Par conséquent, de nombreux objets peuvent et doivent durer longtemps (c'est pourquoi tous les référentiels sont conçus comme singleton).

Au cours du travail, le contenu de l'interface utilisateur est régulièrement changé, ce qui entraîne à son tour la création et la recréation d'objets ViewModel. Il s'avère que si vous utilisez LiveData à l'intérieur du ViewModel, alors pour chaque fragment créé son propre ViewModel sera créé avec un ensemble d'objets LiveData spécifiés. Si 2 fragments similaires sont affichés simultanément à l'écran, avec différents ViewModel et un Base-ViewModel commun, alors lors de l'initialisation, il y aura une duplication des objets LiveData du Base-ViewModel. À l'avenir, ces doublons occuperont de l'espace mémoire jusqu'à ce qu'ils soient détruits par le "garbage collector". Parce que Si nous avons déjà un référentiel sous la forme d'un singleton et que nous voulons minimiser le coût de recréation des écrans, il serait judicieux de transférer des objets LiveData à l'intérieur d'un référentiel singleton, facilitant ainsi les objets ViewModel et accélérant l'application.

Bien sûr, cela ne signifie pas que vous devez transférer tous les LiveData du ViewModel vers le référentiel, mais vous devriez aborder ce problème de manière plus réfléchie et faire votre choix consciemment. L'inconvénient de cette approche est l'augmentation du nombre d'objets à longue durée de vie, car tous les référentiels sont définis comme singleton et chacun d'entre eux stocke des objets LiveData. Mais dans un cas spécifique, Meeting Room Helper n'est pas un inconvénient, car l'application s'exécute sans interruption toute la journée, sans basculer le contexte vers d'autres applications.

Architecture résultante




  • Toutes les demandes sont exécutées dans des référentiels. Tous les référentiels (dans Meeting Room Helper il y en a 11) sont conçus comme singleton. Ils sont divisés par type d'objets retournés et cachés derrière les façades.
  • La logique métier réside dans le ViewModel. Grâce à l'utilisation de "Presenters", la taille totale de tous les ViewModel (il y en a 6 dans le projet) s'est avérée être inférieure à 120 lignes.
  • L'activité et le fragment ne sont impliqués que dans la modification de la partie UI, en utilisant Observer et LiveData renvoyés par le ViewModel.
  • Les fonctions de traitement et de génération de données sont stockées dans "présentateur". Utilisé activement les fonctions d'autorisation de Kotlin pour le traitement des données.

La logique d'arrière-plan a été déplacée vers Intent-Service:

  • Event-Update-Service. Service responsable de la synchronisation des données de la salle actuelle dans Firebase et Graph API.
  • Service de reconnaissance des utilisateurs. Fonctionne uniquement sur la tablette principale. Responsable de l'ajout de nouveaux employés au système. Vérifie une liste de personnes déjà formées avec une liste d'Active Directory. Si de nouvelles personnes apparaissent, le service les ajoute à l'API Face et recycle le réseau de neurones. Une fois l'opération terminée, il est désactivé. Il démarre au démarrage de l'application.
  • Le service de notification en ligne informe les autres tablettes que cette tablette fonctionne, c'est-à-dire La batterie externe n'est pas épuisée. Cela fonctionne via Firebase.

Le résultat a été une architecture plutôt flexible et correcte du point de vue de la répartition des responsabilités qui répond à toutes les exigences du développement moderne. Si à l'avenir nous abandonnons l'API Microsoft Graph, Firebase ou tout autre module, ils peuvent facilement être remplacés par de nouveaux sans interférer avec le reste de l'application. La présence d'un vaste système de «présentateurs» a permis de prendre toutes les fonctions de traitement des données au-delà du cœur. En conséquence, l'architecture est devenue limpide, ce qui est un gros plus. Le problème d'un ViewModel envahi a complètement disparu.

Ci-dessous, je vais donner un exemple du bundle couramment utilisé dans une application développée.

Pratique. Regarder les mises à jour


Selon l'état de la salle de réunion, le cadran affiche l'une des conditions suivantes:




De plus, des arcs temporaires de rallyes sont situés le long du contour du cadran, et le centre décompte jusqu'à la fin de la réunion ou jusqu'au début du prochain rallye. Tout cela est fait par la bibliothèque de canevas que nous avons développée. Si la grille des réunions a changé, nous devons mettre à jour les données dans la bibliothèque.

Étant donné que LiveData est annoncé dans les référentiels, il est plus logique de commencer par eux.

Dépôts


FirebaseRoomRepository - une classe responsable de l'envoi et du traitement des demandes dans Firebase liées au modèle de salle.

 // 1. object FirebaseRoomRepository { private val database = FirebaseFactory.database val rooms: MutableList<Room> = ArrayList() // 2. var currentRoom: MutableLiveData<Room?> = MutableLiveData() val onlineStatus: MediatorLiveData<HashMap<String, Boolean>> = MediatorLiveData() var otherRooms: MutableLiveData<List<Room>> = MutableLiveData() var ownRoom: MutableLiveData<Room> = MutableLiveData() // 3. private val roomsListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { updateRooms(dataSnapshot) } override fun onCancelled(error: DatabaseError) {} } init { // 4. database.getReference(ROOMS_CURRENT_STATES) .addValueEventListener(roomsListener) } // 5. private fun updateRooms(dataSnapshot: DataSnapshot) { rooms.updateRooms(dataSnapshot) otherRooms.updateOtherRooms(rooms) ownRoom.updateOwnRoom(rooms) currentRoom.updateCurrentRoom(rooms, ownRoom) } } 

Pour le démontrer, le code d'initialisation de la base de données de l'écouteur a été légèrement simplifié (la fonction de reconnexion a été supprimée). Jetons un coup d'œil aux points de ce qui se passe ici:

  1. le référentiel est conçu comme un singleton (dans Kotlin, il suffit de remplacer le mot-clé class par object);
  2. initialisation des objets LiveData;
  3. ValueEventListener est déclaré comme une variable afin d'éviter de recréer une classe anonyme en cas de reconnexion (rappelez-vous, j'ai simplifié l'initialisation en supprimant la reconnexion en cas de déconnexion);
  4. initialisation de ValueEventListener (si les données dans Firebase changent, l'écouteur exécutera et mettra immédiatement à jour les données dans les objets LiveData);
  5. Mises à jour des objets LiveData.

Les fonctions elles-mêmes sont déplacées vers un fichier FirebaseRoomRepositoryPresenter distinct et décorées en tant que fonctions d'extension.

 fun MutableLiveData<List<Room>>.updateOtherRooms(rooms: MutableList<Room>) { this.postValue(rooms.filter { !it.isOwnRoom() }) } 

Exemple de fonction d'extension de FirebaseRoomRepositoryPresenter

Aussi pour une compréhension générale de l'image, je donnerai une liste de l'objet Chambre.

 // 1. data class Room(var number: String = "", var nickName: String = "", var email: String? = null, var imgSmall: String? = null, var imgOffline: String? = null, var imgFree: String? = null, var imgWait: String? = null, var imgBusy: String? = null, var events: List<Event.Short> = emptyList()) // 2. 

  1. Classe de données. Ce modificateur génère et remplace automatiquement les méthodes toString (), HashCode () et equal (). Plus besoin de les redéfinir vous-même.
  2. La liste des événements de l'objet Salle. C'est cette liste qui est nécessaire pour mettre à jour les données dans la bibliothèque de numérotation.

Toutes les classes de référentiels sont cachées derrière la classe de façade.

 object Repository { // 1. private val firebaseRoomRepository = FirebaseRoomRepository // ......... /** * Rooms queries */ fun getOtherRooms() = firebaseRoomRepository.otherRooms fun getOwnRoom() = firebaseRoomRepository.ownRoom fun getAllRooms() = firebaseRoomRepository.rooms // 2. fun getCurrentRoom() = firebaseRoomRepository.currentRoom //   // ....... } 

  1. Ci-dessus, vous pouvez voir une liste de toutes les classes de référentiel utilisées et des façades de deuxième niveau. Cela simplifie la compréhension générale du code et montre une liste de toutes les classes de référentiel connectées.
  2. Liste des méthodes qui renvoient des références aux objets LiveData du FirebaseRoomRepository. Les setters et les getters de Kotlin sont facultatifs, vous n'avez donc pas besoin de les écrire inutilement.

Une telle organisation vous permet de placer confortablement de 20 à 30 demandes dans un référentiel racine. Si votre application a plus de demandes, vous devrez diviser la façade racine en 2 ou plus.

ViewModel


BaseViewModel est le ViewModel de base dont tous les ViewModels sont hérités. Il comprend un seul objet currentRoom, utilisé universellement.

 // 1. open class BaseViewModel : ViewModel() { // 2. fun getCurrentRoom() = Repository.getCurrentRoom() } 

  1. Le marqueur ouvert signifie que vous pouvez hériter de la classe. Par défaut dans Kotlin, toutes les classes et méthodes sont finales, c'est-à-dire les classes ne peuvent pas être héritées et les méthodes ne peuvent pas être redéfinies. Il s'agit de se protéger contre les modifications de version incompatibles accidentelles. Je vais vous donner un exemple.

    Vous développez une nouvelle version de la bibliothèque. À un moment donné, pour une raison ou une autre, vous décidez de renommer la classe ou de modifier la signature d'une méthode. En le modifiant, vous avez accidentellement créé une incompatibilité de version. Oups ... Si vous saviez probablement que la méthode pouvait être annulée par quelqu'un et que la classe était héritée, vous auriez probablement été plus précis et vous vous seriez à peine tiré une balle dans le pied. Pour ce faire, dans Kotlin, par défaut, tout est déclaré comme final, et pour l'annulation il y a un modificateur «ouvert».
  2. La méthode getCurrentRoom () renvoie un lien vers l'objet LiveData de la salle actuelle du référentiel, qui, à son tour, est extrait du FirebaseRoomRepository. Lorsque cette méthode est appelée, l'objet Room renvoie toutes les informations sur la salle, y compris une liste d'événements.

Afin de convertir les données d'un format à un autre, nous utiliserons la transformation. Pour ce faire, créez un MainFragmentViewModel et héritez -le de BaseViewModel .

MainFragmentViewModel est une classe dérivée de BaseViewModel. Ce ViewModel est utilisé uniquement dans MainFragment.

 // 1. class MainFragmentViewModel: BaseViewModel () { // 2. var currentRoomEvents = Transformations.switchMap(getCurrentRoom()) { val events: MutableLiveData<List<Event.Short>> = MutableLiveData() // some business logic events.postValue(it?.eventsList) events } // 3. val currentRoomEvents2 = MediatorLiveData<List<Event.Short>>().apply { addSource(getCurrentRoom()) { room -> // some business logic postValue(room?.eventsList) } } } 

  1. Notez l'absence du modificateur open. Cela signifie que personne n'hérite de la classe.
  2. currentRoomEvents - un objet obtenu à l'aide de la transformation. Dès que l'objet de la pièce actuelle change, la transformation est effectuée et l'objet currentRoomEvents est mis à jour.
  3. MediatorLiveData. Le résultat est identique à la transformation (montré pour référence).

La première option est utilisée pour convertir les données d'un type à un autre, ce dont nous avions besoin, et la deuxième option est nécessaire pour exécuter une logique métier. Cependant, la conversion des données ne se produit pas. N'oubliez pas que l'importation Android dans ViewModel n'est pas valide. Par conséquent, je lance des demandes supplémentaires à partir d'ici ou redémarre les services si nécessaire.

Avis important! Pour que la transformation ou le médiateur fonctionne, quelqu'un doit y être abonné à partir d'un fragment ou d'une activité. Sinon, le code ne sera pas exécuté, car personne ne s'attend à un résultat (ce sont des objets observateurs).

Mainfragment


La dernière étape de la conversion des données en résultat. MainFragment comprend une bibliothèque de numérotation et un View-Pager en bas de l'écran.

 class MainFragment : BaseFragment() { // 1. private lateinit var viewModel: MainFragmentViewModel // 2. private val currentRoomObserver = Observer<List<Event.Short>> { clockView.updateArcs(it) } override fun onAttach(context: Context?) { super.onAttach(context) // 3. viewModel = ViewModelProviders.of(this).get(MainFragmentViewModel::class.java) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_main, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) // 4. viewModel.currentRoomEvents.observe(viewLifecycleOwner, currentRoomObserver) } } 

  1. Initialisation de MainFragmentViewModel. Le modificateur lateinit indique que nous nous engageons à initialiser cet objet plus tard, avant de l'utiliser. Kotlin essaie de protéger le programmeur d'une écriture de code incorrecte, nous devons donc soit dire tout de suite que l'objet peut être nul, soit mettre Lateinit. Dans ce cas, le ViewModel doit être initialisé par l'objet.
  2. Observateur-auditeur pour mettre à jour le cadran.
  3. Initialisation du ViewModel. Veuillez noter que cela se produit immédiatement après que le fragment s'est attaché à l'activité.
  4. Une fois l'activité créée, nous nous abonnons aux modifications apportées à l'objet currentRoomEvents. Veuillez noter que je ne suis pas abonné au cycle de vie du fragment (this), mais à l'objet viewLifecycleOwner. Le fait est que dans la bibliothèque de support 28.0.0 et AndroidX 1.0.0, un bogue a été détecté lorsque l'observateur était "désabonné". Pour résoudre ce problème, un correctif sous la forme de viewLifecycleOwner a été publié et Google recommande de s'y abonner. Cela résout le problème de l'observateur de zombies lorsque le fragment est mort et que l'observateur continue de fonctionner. Si vous l'utilisez toujours, assurez-vous de le remplacer par viewLifecycleOwner.

Ainsi, je veux démontrer la simplicité et la beauté de MVVM et LiveData sans utiliser de liaison de données. Veuillez noter que dans ce projet, je viole la règle généralement acceptée en plaçant LiveData dans le référentiel en raison des spécificités du projet. Cependant, si nous les déplacions vers le ViewModel, l'image globale resterait inchangée.

Cerise sur le gâteau, j'ai préparé pour vous une courte vidéo avec une démonstration (les noms sont barbouillés conformément aux exigences de sécurité, je m'excuse):




Résumé


À la suite du travail de l'application au cours du premier mois, certains bogues ont été révélés dans l'affichage des rallyes croisés (Outlook vous permet de créer plusieurs événements en même temps, contrairement à notre système). Maintenant, le système fonctionne depuis 3 mois. Les erreurs ou pannes ne sont pas observées.

PS Merci jericho_code pour le commentaire. Dans Kotlin, vous pouvez et devez initialiser List <> dans le modèle à l'aide de emptyList (), puis aucun objet supplémentaire n'est créé.
 var events: List<Event.Short> = emptyList() //      EmptyList var events: List<Event.Short> = ArrayList() //    

Source: https://habr.com/ru/post/fr457960/


All Articles