- Marcio Granzotto Rodrigues
- 4 år sedan
- Kategorier:Yttrande, Teknisk
- Taggar:Android, Arkitektur, utveckling, iOS, kotiln, Mobil, VIPER
Som Android-utvecklare och senare även iOS-utvecklare har jag haft kontakt med flera olika projekts arkitekturer – en del bra och en del dåliga.
Jag använde gärna MVP-arkitekturen för Android tills jag träffade – och arbetade åtta månader med – VIPER-arkitekturen i ett iOS-projekt. När jag kom tillbaka till Android bestämde jag mig för att anpassa och implementera VIPER på det, trots att vissa andra utvecklare föreslog att det inte skulle vara meningsfullt att använda en iOS-arkitektur på Android. Med tanke på den grundläggande skillnaden mellan Android- och iOS-ramverk hade jag några frågor om hur användbar VIPER skulle vara för Android. Skulle det vara genomförbart och värt ansträngningen? Låt oss börja med grunderna.
Vad är VIPER?
VIPER är en ren arkitektur som främst används inom iOS-apputveckling. Den hjälper till att hålla koden ren och organiserad och undvika Massive-View-Controller-situationen.
VIPER står för View Interactor Presenter Entity Router, vilket är klasser som har ett väldefinierat ansvar, enligt Single Responsibility Principle. Du kan läsa mer om det i denna utmärkta artikel.
Android-arkitekturer
Det finns redan några mycket bra arkitekturer för Android. De mer kända är Model-View-ViewModel (MVVM) och Model-View-Presenter (MVP).
MVVM är mycket meningsfull om du använder den tillsammans med databindning, och eftersom jag inte gillar tanken på databindning så mycket har jag alltid använt MVP för de projekt jag har arbetat med. När projekten växer kan dock presentatören bli en enorm klass med många metoder, vilket gör den svår att underhålla och förstå. Det händer eftersom den är ansvarig för många saker: den måste hantera UI-händelser, UI-logik, affärslogik, nätverk och databasfrågor. Detta bryter mot principen om ett enda ansvar, något som VIPER kan åtgärda.
Let’s fix it!
Med dessa problem i åtanke startade jag ett nytt Android-projekt och bestämde mig för att använda MVP + Interactor (eller VIPE, om du så vill). Det gjorde det möjligt för mig att flytta en del ansvar från presentatören till interaktören. Lämna presentatören med UI-händelsehantering och förbereda data som kommer från Interactor för att visas på View. Då är Interactor bara ansvarig för affärslogiken och för att hämta data från databaser eller API:er.
Det började jag också använda gränssnitt för att koppla ihop modulerna. På så sätt kan de inte få tillgång till andra metoder än de som deklarerats i gränssnittet. Detta skyddar strukturen och hjälper till att definiera ett tydligt ansvar för varje modul, vilket undviker misstag av utvecklare som att placera logiken på fel ställe. Så här ser gränssnitten ut:
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) } }
Och här är lite kod för att illustrera de klasser som implementerar dessa gränssnitt (det är i Kotlin, men i Java borde det vara likadant).
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!") }) } }
Den fullständiga koden finns på den här Gist.
Du kan se att modulerna skapas och länkas ihop vid uppstart. När aktiviteten skapas initialiserar den Presenter och överlämnar sig själv som View i konstruktören. Presenteraren initialiserar sedan Interactor och överlämnar sig själv som InteractorOutput
.
I ett iOS VIPER-projekt skulle detta hanteras av Router, som skapar UIViewController
, eller hämtar den från en Storyboard, och sedan kopplar ihop alla moduler. Men på Android skapar vi inte aktiviteterna själva: vi måste använda Intents, och vi har inte tillgång till den nyskapade aktiviteten från den föregående aktiviteten. Detta hjälper till att förhindra minnesläckage, men det kan vara besvärligt om du bara vill skicka data till den nya modulen. Vi kan inte heller lägga Presenter på Intents extras eftersom det skulle behöva vara Parcelable
eller Serializable
. Är helt enkelt inte görbart.
Det är därför jag i det här projektet har utelämnat Router. Men är det idealfallet?
VIPE + Router
Ovanstående implementering av VIPE löste de flesta av MVP:s problem, genom att dela upp presentatörens och interaktörens ansvarsområden.
Vyn är dock inte lika passiv som iOS VIPER:s View. Den måste hantera alla vanliga ansvarsområden för View plus dirigering till andra moduler. Detta bör INTE vara dess ansvar och vi kan göra det bättre. Inför Router.
Här är skillnaderna mellan ”VIPE” och 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) } }
Full kod finns här.
Nu har vi flyttat logiken för vyns routing till Router. Den behöver bara en instans av aktiviteten så att den kan anropastartActivity
-metoden. Den kopplar fortfarande inte ihop allting som iOS VIPER, men den respekterar åtminstone Single Responsibility Principle.
Slutsats
Efter att ha utvecklat ett projekt med MVP + Interactor och genom att hjälpa en kollega att utveckla ett komplett VIPER Android-projekt kan jag lugnt säga att arkitekturen fungerar på Android och att det är värt det. Klasserna blir mindre och mer underhållbara. Det vägleder också utvecklingsprocessen, eftersom arkitekturen gör det tydligt var koden ska skrivas.
Här på Cheesecake Labs planerar vi att använda VIPER på de flesta nya projekt, så att vi kan få bättre underhållbarhet och tydligare kod. Dessutom blir det lättare att hoppa från ett iOS-projekt till ett Android-projekt och vice versa. Naturligtvis är detta en anpassning under utveckling, så ingenting här är hugget i sten. Vi uppskattar gärna lite feedback om det!