Architecture et conception d'applications Android (mon expérience)

Habr, bonjour!

Aujourd'hui, je veux parler de l'architecture que je suis dans mes applications Android. Je prends l'architecture propre comme base et j'utilise les composants d'architecture Android (ViewModel, LiveData, LiveEvent) + Kotlin Coroutines comme outils. Ci-joint un exemple de code fictif disponible sur GitHub .

Avertissement


Je veux partager mon expérience de développement, je ne prétends en aucun cas que ma solution est la seule vraie et sans défaut. L'architecture de l'application est une sorte de modèle que nous choisissons pour résoudre un problème particulier, et pour le modèle sélectionné, son adéquation à l'application à une tâche spécifique est importante.

Problème: pourquoi avons-nous besoin d'une architecture?


La plupart des projets auxquels j'ai participé ont le même problème: la logique d'application est placée à l'intérieur de l'environnement Android, ce qui conduit à une grande quantité de code à l'intérieur de Fragment and Activity. Ainsi, le code est entouré de dépendances qui ne sont pas du tout nécessaires, le test unitaire devient presque impossible, ainsi que la réutilisation. Les fragments deviennent des objets divins au fil du temps, même de petits changements conduisent à des erreurs, soutenir un projet devient cher et émotionnellement cher.

Il y a des projets qui n'ont pas d'architecture du tout (tout est clair ici, il n'y a pas de questions pour eux), il y a des projets qui revendiquent l'architecture, mais exactement les mêmes problèmes y apparaissent quand même. Il est désormais à la mode d'utiliser l'architecture propre dans Android. J'ai souvent vu que Clean Architecture se limitait à créer des référentiels et des scripts qui invoquaient ces référentiels et ne faisaient rien d'autre. Pire encore: ces scripts renvoient des modèles à partir de référentiels appelés. Et dans une telle architecture, cela n'a aucun sens. Et parce que Étant donné que les scripts appellent simplement les référentiels nécessaires, la logique repose souvent sur le ViewModel ou, pire encore, s'installe en fragments et en activités. Tout cela se transforme alors en un gâchis, impossible à tester automatiquement.

Le but de l'architecture et du design


Le but de l'architecture est de séparer notre logique métier des détails. Par détails, je veux dire, par exemple, les API externes (lorsque nous développons un client pour un service REST), Android - un environnement (UI, services), etc. Au fond, j'utilise une architecture propre, mais avec mes hypothèses d'implémentation.

Le but de la conception est de relier l'interface utilisateur, l'API, la logique métier, les modèles afin que tout cela se prête à des tests automatiques, soit couplé de manière lâche et s'étende facilement. Dans la conception, j'utilise des composants d'architecture Android.

Pour moi, l'architecture doit répondre aux critères suivants:

  1. L'interface utilisateur est aussi simple que possible et n'a que trois fonctions:
  2. Présentez les données à l'utilisateur. Les données sont prêtes à être affichées. C'est la fonction principale de l'interface utilisateur. Voici des widgets, des animations, des fragments, etc.
  3. . ViewModel LiveData.
  4. . framework, . .
  5. - . .


Le schéma de l'architecture est illustré dans la figure ci-dessous:

image

Nous nous déplaçons de bas en haut en couches, et la couche qui est en dessous ne sait rien de la couche au-dessus. Et la couche supérieure se réfère uniquement à une couche inférieure d'un niveau. Ceux. La couche API ne peut pas faire référence à un domaine.

Une couche de domaine contient une entité commerciale avec sa propre logique. Il existe généralement des entités qui existent sans application. Par exemple, pour une banque, il peut y avoir des entités de prêt avec une logique complexe pour le calcul des intérêts, etc.

La couche logique d'application contient des scripts pour l'application elle-même. C'est ici que toutes les connexions de l'application sont déterminées, son essence se construit.

La couche API, Android n'est qu'une implémentation spécifique de notre application dans l'environnement Android. Idéalement, cette couche peut être modifiée en n'importe quoi.

, , — . . 2- . , . . TDD , . Android, API ..

Android-.

image

Ainsi, la couche logique est la clé, c'est l'application. Seule une couche logique peut faire référence à un domaine et interagir avec lui. De plus, la couche logique contient des interfaces qui permettent à la logique d'interagir avec les détails de l'application (api, android, etc.). C'est ce qu'on appelle le principe de l'inversion de dépendance, qui permet à la logique de ne pas dépendre des détails, mais vice versa. La couche logique contient les scénarios d'utilisation de l'application (cas d'utilisation), qui opèrent sur différentes données, interagissent avec le domaine, les référentiels, etc. En développement, j'aime penser dans les scripts. Pour chaque action ou événement utilisateur du système, un certain script est lancé avec des paramètres d'entrée et de sortie, ainsi qu'une seule méthode - pour exécuter le script.

Quelqu'un introduit un concept supplémentaire d'interaction, qui peut combiner plusieurs scénarios d'utilisation et liquider une logique supplémentaire. Mais je ne fais pas ça, je crois que chaque script peut se développer ou inclure n'importe quel autre script, cela ne nécessite pas d'interacteur. Si vous regardez les schémas UML, alors vous pouvez voir la connexion de l'inclusion et de l'extension.

Le schéma général de la demande est le suivant:

image

  1. Un environnement android est créé (activité, fragments, etc.).
  2. Un ViewModel est créé (un ou plusieurs).
  3. ViewModel crée les scripts nécessaires qui peuvent être exécutés à partir de ce ViewModel. Les scénarios sont mieux injectés avec DI.
  4. L'utilisateur valide une action.
  5. Chaque composant de l'interface utilisateur est associé à une commande qu'il peut exécuter.
  6. Un script est exécuté avec les paramètres nécessaires, par exemple, Login.execute (login, mot de passe).
  7. DI , . ( api, ). . , , , REST JSON . , , . , . , . - . , . , , . , , .
  8. . ViewModel, UI. LiveData (.9 10).

Ceux. le rôle clé que nous avons est la logique et son modèle de données. Nous avons vu une double conversion: la première est la transformation du référentiel en modèle de données de scénario et la seconde est la conversion lorsque le script donne les données à l'environnement à la suite de son travail. Habituellement, le résultat du script est donné au viewModel pour être affiché dans l'interface utilisateur. Le script doit renvoyer ces données avec lesquelles le viewModel et l'interface utilisateur ne font rien d'autre.

Commandes L'

interface utilisateur démarre l'exécution du script à l'aide d'une commande. Dans mes projets, j'utilise ma propre implémentation de commandes, elles ne font pas partie de composants architecturaux ou d'autre chose. En général, leur implémentation est simple, comme une compréhension plus profonde de l'idée, vous pouvez voir l'implémentation des commandes dans reactiveui.netpour c #. Malheureusement, je ne peux pas disposer mon code de travail, seulement une implémentation simplifiée pour un exemple.

La tâche principale de la commande est d'exécuter un script, de lui passer les paramètres d'entrée et, après exécution, de retourner le résultat de la commande (données ou message d'erreur). Normalement, toutes les commandes sont exécutées de manière asynchrone. De plus, l'équipe encapsule la méthode de calcul d'arrière-plan. J'utilise des coroutines, mais elles sont faciles à remplacer par RX, et vous n'avez qu'à le faire dans le tas abstrait de commande + cas d'utilisation. En bonus, une équipe peut signaler son statut: si elle est exécutée maintenant ou non et si elle peut être exécutée en principe. Les équipes résolvent facilement certains problèmes, par exemple le problème du double appel (lorsque l'utilisateur a cliqué plusieurs fois sur le bouton pendant l'exécution de l'opération) ou les problèmes de visibilité et d'annulation.

Exemple


Mettre en œuvre la fonctionnalité: connectez-vous à l'application en utilisant le login et le mot de passe.
La fenêtre devrait contenir les champs de connexion et de mot de passe et le bouton «Connexion». La logique du travail est la suivante:

  1. Le bouton «Connexion» doit être inactif si le nom d'utilisateur et le mot de passe contiennent moins de 4 caractères.
  2. Le bouton «Connexion» doit être inactif pendant le processus de connexion.
  3. Pendant la procédure de connexion, un indicateur (chargeur) doit être affiché.
  4. Si la connexion réussit, un message de bienvenue doit s'afficher.
  5. Si la connexion et / ou le mot de passe sont incorrects, un message d'erreur devrait apparaître au-dessus du champ de connexion.
  6. Si un message d'erreur s'affiche à l'écran, toute entrée d'un caractère dans les champs de connexion ou de mot de passe supprimera ce message jusqu'à la prochaine tentative.

Vous pouvez résoudre ce problème de différentes manières, par exemple, en mettant tout dans MainActivity.
Mais je suis toujours la mise en œuvre de mes deux règles principales:

  1. La logique métier est indépendante des détails.
  2. L'interface utilisateur est aussi simple que possible. Il ne s'occupe que de sa tâche (il présente les données qui lui ont été transférées et diffuse également les commandes de l'utilisateur).

Voici à quoi ressemble l'application:

image

MainActivity ressemble à ceci:

class MainActivity : AppCompatActivity() {

   private val vm: MainViewModel by viewModel()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       bindLoginView()
       bindProgressBar()

       observeAuthorization()
       observeRefreshView()
   }

   private fun bindProgressBar() {
       progressBar.bindVisibleWithCommandIsExecuting(this, vm.loginCommand)
   }

   private fun bindLoginView() {
       loginEdit.bindAfterTextChangedWithCommand(vm.loginValidityCommand)
       passwordEdit.bindAfterTextChangedWithCommand(vm.passwordValidityCommand)

       loginButton.bindCommand(this, vm.loginCommand) {
           LoginParameters(loginEdit.text.toString(), passwordEdit.text.toString())
       }
   }

   private fun observeAuthorization() {
       vm.authorizationSuccessLive.observe(this, Observer {
           showAuthorizeSuccessMsg(it?.data)
       })
       vm.authorizationErrorLive.observe(this, Observer {
           showAuthorizeErrorMsg()
       })
   }

   private fun observeRefreshView() {
       vm.refreshLoginViewLive.observe(this, Observer {
           hideAuthorizeErrorMsg()
       })
   }

   private fun showAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = false
   }

   private fun hideAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = true
   }

   private fun showAuthorizeSuccessMsg(name : String?) {
       val msg = getString( R.string.success_login, name)
       Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
   }
}


L'activité est assez simple, la règle d'interface utilisateur est exécutée. J'ai écrit quelques extensions simples, telles que bindVisibleWithCommandIsExecuting, pour associer des commandes avec des éléments d'interface utilisateur et non du code en double.

Le code de cet exemple avec commentaires est disponible sur GitHub , si vous êtes intéressé, vous pouvez télécharger et vous familiariser.

C'est tout, merci d'avoir regardé!

All Articles