- Marcio Granzotto Rodrigues
- 4 anni fa
- Categorie:Opinione, Tecnico
- Tag:Android, Architettura, sviluppo, iOS, kotiln, Mobile, VIPER
Partendo come sviluppatore Android e poi lavorando anche con iOS, ho avuto contatti con diverse architetture di progetti – alcune buone e alcune cattive.
Ho usato felicemente l’architettura MVP per Android fino a quando ho incontrato – e lavorato otto mesi con – l’architettura VIPER in un progetto iOS. Quando sono tornato ad Android, ho deciso di adattare e implementare VIPER su di esso, nonostante alcuni altri sviluppatori suggerissero che non avrebbe avuto senso usare un’architettura iOS su Android. Data la differenza fondamentale tra i framework di Android e iOS, avevo alcune domande su quanto sarebbe stato utile VIPER per Android. Sarebbe fattibile e ne varrebbe la pena? Cominciamo dalle basi.
Che cos’è VIPER?
VIPER è un’architettura pulita usata principalmente nello sviluppo di app per iOS. Aiuta a mantenere il codice pulito e organizzato, evitando la situazione Massive-View-Controller.
VIPER sta per View Interactor Presenter Entity Router, che sono classi che hanno una responsabilità ben definita, seguendo il Single Responsibility Principle. Potete leggere di più su questo eccellente articolo.
Architetture Android
Ci sono già alcune architetture molto buone per Android. Le più famose sono Model-View-ViewModel (MVVM) e Model-View-Presenter (MVP).
MVVM ha molto senso se la si usa insieme al data binding, e siccome non mi piace molto l’idea del data binding, ho sempre usato MVP per i progetti a cui ho lavorato. Tuttavia, quando i progetti crescono, il presentatore può diventare una classe enorme con un sacco di metodi, rendendolo difficile da mantenere e capire. Questo accade perché è responsabile di un sacco di cose: deve gestire gli eventi UI, la logica UI, la logica di business, la rete e le query al database. Questo viola il Principio di Singola Responsabilità, qualcosa che VIPER può risolvere.
Provvediamo!
Con questi problemi in mente, ho iniziato un nuovo progetto Android e ho deciso di usare MVP + Interactor (o VIPE, se volete). Questo mi ha permesso di spostare alcune responsabilità dal presentatore all’Interactor. Lasciando al presentatore la gestione degli eventi UI e la preparazione dei dati che provengono dall’Interactor per essere visualizzati sulla View. Quindi, l’Interactor è responsabile solo della logica di business e del recupero dei dati dai DB o dalle API.
Inoltre, ho iniziato ad usare le interfacce per collegare i moduli insieme. In questo modo, non possono accedere a metodi diversi da quelli dichiarati nell’interfaccia. Questo protegge la struttura e aiuta a definire una chiara responsabilità per ogni modulo, evitando errori di sviluppo come mettere la logica nel posto sbagliato. Ecco come appaiono le interfacce:
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) } }
E ecco un po’ di codice per illustrare le classi che implementano quelle interfacce (è in Kotlin, ma Java dovrebbe essere lo stesso).
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!") }) } }
Il codice completo è disponibile su questo Gist.
Si può vedere che i moduli sono creati e collegati insieme all’avvio. Quando l’attività viene creata, inizializza il Presenter, passando se stesso come View nel costruttore. Il Presenter inizializza poi l’Interactor passando se stesso come InteractorOutput
.
In un progetto VIPER iOS questo sarebbe gestito dal Router, creando il UIViewController
, o prendendolo da uno Storyboard, e poi collegando tutti i moduli insieme. Ma su Android non creiamo noi stessi le Attività: dobbiamo usare gli Intenti, e non abbiamo accesso all’Attività appena creata da quella precedente. Questo aiuta a prevenire le perdite di memoria, ma può essere un dolore se si vogliono solo passare dati al nuovo modulo. Non possiamo anche mettere il Presenter sugli extra dell’Intent perché dovrebbe essere Parcelable
o Serializable
. Non è semplicemente fattibile.
Ecco perché in questo progetto ho omesso il Router. Ma è il caso ideale?
VIPE + Router
L’implementazione precedente di VIPE ha risolto la maggior parte dei problemi di MVP, dividendo le responsabilità del Presenter con l’Interactor.
Tuttavia, la View non è passiva come la View di iOS VIPER. Deve gestire tutte le normali responsabilità della View più il routing verso altri moduli. Questa NON dovrebbe essere la sua responsabilità e possiamo fare meglio. Inserisci il Router.
Ecco le differenze tra “VIPE” e 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) } }
Codice completo disponibile qui.
Ora abbiamo spostato la logica di routing della vista al Router. Ha solo bisogno di un’istanza dell’attività per poter chiamare il metodo startActivity
. Ancora non cabla tutto insieme come il VIPER iOS, ma almeno rispetta il principio di responsabilità unica.
Conclusione
Avendo sviluppato un progetto con MVP + Interactor e aiutando un collega a sviluppare un progetto VIPER Android completo, posso dire che l’architettura funziona su Android e ne vale la pena. Le classi diventano più piccole e più mantenibili. Guida anche il processo di sviluppo, perché l’architettura rende chiaro dove il codice dovrebbe essere scritto.
Qui a Cheesecake Labs stiamo progettando di usare VIPER sulla maggior parte dei nuovi progetti, in modo da poter avere una migliore manutenibilità e un codice più chiaro. Inoltre, rende più facile saltare da un progetto iOS a un progetto Android e viceversa. Naturalmente questo è un adattamento in evoluzione, quindi nulla è scolpito nella pietra. Apprezziamo volentieri qualche feedback al riguardo!