- Marcio Granzotto Rodrigues
- 4 ans
- Catégories:Opinion, Technique
- Tags :Android, Architecture, développement, iOS, kotiln, Mobile, VIPER
En commençant comme développeur Android et plus tard en travaillant aussi avec iOS, j’ai été en contact avec plusieurs architectures de projets différents – certaines bonnes et certaines mauvaises.
J’utilisais avec bonheur l’architecture MVP pour Android jusqu’à ce que je rencontre – et travaille huit mois avec – l’architecture VIPER dans un projet iOS. Lorsque je suis revenu à Android, j’ai décidé d’adapter et d’implémenter VIPER dessus, malgré le fait que certains autres devs suggéraient que cela n’aurait pas de sens d’utiliser une architecture iOS sur Android. Étant donné la différence fondamentale entre les frameworks d’Android et d’iOS, je me suis posé des questions sur l’utilité de VIPER pour Android. Serait-il possible de le faire et cela en vaudrait-il la peine ? Commençons par les bases.
Qu’est-ce que VIPER?
VIPER est une architecture propre principalement utilisée dans le développement d’applications iOS. Elle aide à garder le code propre et organisé, en évitant la situation de Massive-View-Controller.
VIPER signifie View Interactor Presenter Entity Router, qui sont des classes qui ont une responsabilité bien définie, suivant le principe de responsabilité unique. Vous pouvez en savoir plus à ce sujet sur cet excellent article.
Architectures Android
Il existe déjà de très bonnes architectures pour Android. Les plus connues étant Model-View-ViewModel (MVVM) et Model-View-Presenter (MVP).
MVVM a beaucoup de sens si vous l’utilisez parallèlement à la liaison de données, et comme je n’aime pas beaucoup l’idée de liaison de données, j’ai toujours utilisé MVP pour les projets sur lesquels j’ai travaillé. Cependant, à mesure que les projets se développent, le présentateur peut devenir une classe énorme avec beaucoup de méthodes, ce qui le rend difficile à maintenir et à comprendre. Cela se produit parce qu’elle est responsable de beaucoup de choses : elle doit gérer les événements de l’interface utilisateur, la logique de l’interface utilisateur, la logique commerciale, la mise en réseau et les requêtes de base de données. Cela viole le principe de responsabilité unique, quelque chose que VIPER peut réparer.
Réparons-le !
Avec ces problèmes à l’esprit, j’ai commencé un nouveau projet Android et j’ai décidé d’utiliser MVP + Interactor (ou VIPE, si vous voulez). Cela m’a permis de déplacer certaines responsabilités du présentateur vers l’Interactor. Laissant au présentateur le soin de gérer les événements de l’interface utilisateur et de préparer les données provenant de l’Interacteur à être affichées dans la vue. Ensuite, l’Interactor n’est responsable que de la logique métier et de la récupération des données à partir des BDD ou des API.
Aussi, j’ai commencé à utiliser des interfaces pour relier les modules entre eux. De cette façon, ils ne peuvent pas accéder à des méthodes autres que celles déclarées sur l’interface. Cela protège la structure et aide à définir une responsabilité claire pour chaque module, évitant les erreurs de développeur comme mettre la logique au mauvais endroit. Voici à quoi ressemblent les interfaces :
class LoginContracts { interface View { fun goToHomeScreen(user: User) fun showError(message: String) } interface Presenter { fun onDestroy() fun onLoginButtonPressed(username: String, password: String) } interface Interactor { fun login(username: String, password: String) } interface InteractorOutput { fun onLoginSuccess(user: User) fun onLoginError(message: String) } }
Et voici un peu de code pour illustrer les classes qui implémentent ces interfaces (c’est en Kotlin, mais Java devrait être le même).
class LoginActivity: BaseActivity, LoginContracts.View { var presenter: LoginContracts.Presenter? = LoginPresenter(this) override fun onCreate() { //... loginButton.setOnClickListener { onLoginButtonClicked() } } override fun onDestroy() { presenter?.onDestroy() presenter = null super.onDestroy() } private fun onLoginButtonClicked() { presenter?.onLoginButtonClicked(usernameEditText.text, passwordEditText.text) } fun goToHomeScreen(user: User) { val intent = Intent(view, HomeActivity::class.java) intent.putExtra(Constants.IntentExtras.USER, user) startActivity(intent) } fun showError(message: String) { //shows the error on a dialog } } class LoginPresenter(var view: LoginContracts.View?): LoginContracts.Presenter, LoginContracts.InteractorOutput { var interactor: LoginContracts.Interactor? = LoginInteractor(this) fun onDestroy() { view = null interactor = null } fun onLoginButtonPressed(username: String, password: String) { interactor?.login(username, password) } fun onLoginSuccess(user: User) { view?.goToNextScreen(user) } fun onLoginError(message: String) { view?.showError(message) } } class LoginInteractor(var output: LoginContracts.InteractorOutput?): LoginContracts.Interactor { fun login(username: String, password: String) { LoginApiManager.login(username, password) ?.subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe({ //does something with the user, like saving it or the token output?.onLoginSuccess(it) }, { output?.onLoginError(it.message ?: "Error!") }) } }
Le code complet est disponible sur ce Gist.
Vous pouvez voir que les modules sont créés et liés ensemble au démarrage. Lorsque l’activité est créée, elle initialise le Presenter, en se passant comme la vue sur le constructeur. Le Presenter initialise ensuite l’Interactor en se passant comme le InteractorOutput
.
Sur un projet VIPER iOS, ceci serait géré par le Router, créant le UIViewController
, ou l’obtenant d’un Storyboard, puis câblant tous les modules ensemble. Mais sur Android, nous ne créons pas les activités nous-mêmes : nous devons utiliser les Intents, et nous n’avons pas accès à l’activité nouvellement créée à partir de l’activité précédente. Cela permet d’éviter les fuites de mémoire, mais cela peut s’avérer pénible si l’on veut simplement transmettre des données au nouveau module. Nous ne pouvons pas non plus mettre le présentateur sur les extras de l’Intent car il faudrait qu’il soit Parcelable
ou Serializable
. C’est juste pas faisable.
C’est pourquoi sur ce projet j’ai omis le Routeur. Mais est-ce le cas idéal ?
VIPE + Routeur
L’implémentation ci-dessus de VIPE a résolu la plupart des problèmes du MVP, en partageant les responsabilités du Présentateur avec l’Interacteur.
Cependant, la Vue n’est pas aussi passive que la Vue du VIPER iOS. Elle doit gérer toutes les responsabilités régulières de la vue, plus le routage vers d’autres modules. Cela ne devrait PAS être sa responsabilité et nous pouvons faire mieux. Entrez le routeur.
Voici les différences entre « VIPE » et VIPER:
class LoginContracts { interface View { fun showError(message: String) //fun goToHomeScreen(user: User) //This is no longer a part of the View's responsibilities } interface Router { fun goToHomeScreen(user: User) // Now the router handles it } } class LoginPresenter(var view: LoginContracts.View?): LoginContracts.Presenter, LoginContracts.InteractorOutput { //now the presenter has a instance of the Router and passes the Activity to it on the constructor var router: LoginContracts.Router? = LoginRouter(view as? Activity) //... fun onLoginSuccess(user: User) { router?.goToNextScreen(user) } //... } class LoginRouter(var activity: Activity?): LoginContracts.Router { fun goToHomeScreen(user: User) { val intent = Intent(view, HomeActivity::class.java) intent.putExtra(Constants.IntentExtras.USER, user) activity?.startActivity(intent) } }
Code complet disponible ici.
Maintenant, nous avons déplacé la logique de routage de la vue vers le routeur. Il a seulement besoin d’une instance de l’activité pour pouvoir appeler la méthode startActivity
. Il ne câble toujours pas tout ensemble comme le VIPER iOS, mais au moins il respecte le principe de responsabilité unique.
Conclusion
Ayant développé un projet avec MVP + Interactor et en aidant un collègue à développer un projet complet VIPER Android, je peux dire sans risque que l’architecture fonctionne sur Android et que cela en vaut la peine. Les classes deviennent plus petites et plus faciles à maintenir. Cela guide également le processus de développement, car l’architecture indique clairement où le code doit être écrit.
Ici, à Cheesecake Labs, nous prévoyons d’utiliser VIPER sur la plupart des nouveaux projets, afin d’avoir une meilleure maintenabilité et un code plus clair. En outre, il est plus facile de passer d’un projet iOS à un projet Android et vice-versa. Bien sûr, il s’agit d’une adaptation évolutive, donc rien n’est gravé dans la pierre. Nous apprécions volontiers vos commentaires à ce sujet!