- Marcio Granzotto Rodrigues
- 4 år siden
- Kategorier:Udtalelser, Tekniske
- Tags:Android, Arkitektur, udvikling, iOS, kotiln, Mobil, VIPER
Da jeg startede som Android-udvikler og senere også arbejdede med iOS, har jeg haft kontakt med flere forskellige projekters arkitekturer – nogle gode og nogle dårlige.
Jeg var glad for at bruge MVP-arkitekturen til Android, indtil jeg mødte – og arbejdede otte måneder med – VIPER-arkitekturen i et iOS-projekt. Da jeg kom tilbage til Android, besluttede jeg mig for at tilpasse og implementere VIPER på det, på trods af at nogle andre udviklere foreslog, at det ikke ville give mening at bruge en iOS-arkitektur på Android. I betragtning af den grundlæggende forskel mellem Android- og iOS-rammer, havde jeg nogle spørgsmål om, hvor nyttig VIPER ville være til Android. Ville det kunne lade sig gøre og være indsatsen værd? Lad os starte med det grundlæggende.
Hvad er VIPER?
VIPER er en ren arkitektur, der primært anvendes i iOS-appudvikling. Den hjælper med at holde koden ren og organiseret, så man undgår Massive-View-Controller-situationen.
VIPER står for View Interactor Presenter Entity Router, som er klasser, der har et veldefineret ansvar, og som følger Single Responsibility-princippet. Du kan læse mere om det på denne glimrende artikel.
Android-arkitekturer
Der findes allerede nogle meget gode arkitekturer til Android. De mere kendte er Model-View-ViewModel (MVVM) og Model-View-Presenter (MVP).
MVVM giver meget mening, hvis du bruger det sammen med databinding, og da jeg ikke bryder mig så meget om tanken om databinding, har jeg altid brugt MVP til de projekter, jeg har arbejdet på. Men efterhånden som projekterne vokser, kan præsenteren blive en stor klasse med mange metoder, hvilket gør den svær at vedligeholde og forstå. Det sker, fordi den er ansvarlig for en masse ting: den skal håndtere UI-hændelser, UI-logik, forretningslogik, netværks- og databaseforespørgsler. Det er i strid med Single Responsibility Principle, noget som VIPER kan løse.
Lad os løse det!
Med disse problemer i tankerne startede jeg et nyt Android-projekt og besluttede mig for at bruge MVP + Interactor (eller VIPE, om du vil). Det gjorde det muligt for mig at flytte noget ansvar fra præsentatoren til Interactor. Overlader præsenteren med UI-hændelseshåndtering og forberedelse af de data, der kommer fra Interactor, til at blive vist på View’en. Så er Interactor kun ansvarlig for forretningslogikken og hentning af data fra DB’er eller API’er.
Også begyndte jeg at bruge interfaces til at forbinde modulerne sammen. På den måde kan de ikke få adgang til andre metoder end dem, der er deklareret på grænsefladen. Dette beskytter strukturen og hjælper med at definere et klart ansvar for hvert modul, så man undgår udviklerfejl som f.eks. at placere logikken det forkerte sted. Sådan ser grænsefladerne ud:
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) } }
Og her er noget kode til illustration af de klasser, der implementerer disse grænseflader (det er i Kotlin, men Java burde være det samme).
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 fulde kode er tilgængelig på denne Gist.
Du kan se, at modulerne oprettes og linkes sammen ved opstart. Når aktiviteten oprettes, initialiserer den Presenter, idet den overdrager sig selv som View i konstruktøren. Presenter initialiserer derefter Interactor og overdrager sig selv som InteractorOutput
.
På et iOS VIPER-projekt ville dette blive håndteret af Routeren, der opretter UIViewController
, eller henter den fra et Storyboard, og derefter kobler alle modulerne sammen. Men på Android opretter vi ikke selv aktiviteterne: Vi er nødt til at bruge Intents, og vi har ikke adgang til den nyoprettede aktivitet fra den foregående aktivitet. Dette er med til at forhindre hukommelseslækager, men det kan være besværligt, hvis man blot ønsker at videregive data til det nye modul. Vi kan heller ikke sætte Presenter på Intent’ens ekstramateriale, fordi det ville skulle være Parcelable
eller Serializable
. Kan bare ikke lade sig gøre.
Det er derfor, at jeg på dette projekt har udeladt Routeren. Men er det det ideelle tilfælde?
VIPE + Router
Overstående implementering af VIPE løste de fleste af MVP’s problemer, idet den delte ansvaret for Presenter og Interactor.
View er dog ikke så passiv som iOS VIPER’s View. Den skal håndtere alle de almindelige View-ansvarligheder plus routing til andre moduler. Dette bør IKKE være dens ansvar, og vi kan gøre det bedre. Indtast Router.
Her er forskellene mellem “VIPE” og 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) } }
Fuld kode er tilgængelig her.
Nu har vi flyttet view-routing-logikken til Routeren. Den har kun brug for en instans af aktiviteten, så den kan kaldestartActivity
-metoden. Den kobler stadig ikke alt sammen som iOS VIPER, men i det mindste respekterer den Single Responsibility Principle.
Konklusion
Efter at have udviklet et projekt med MVP + Interactor og ved at hjælpe en kollega med at udvikle et fuldt VIPER Android-projekt, kan jeg roligt sige, at arkitekturen virker på Android, og at det er det hele værd. Klasserne bliver mindre og mere vedligeholdelsesvenlige. Det guider også udviklingsprocessen, fordi arkitekturen gør det klart, hvor koden skal skrives.
Her på Cheesecake Labs planlægger vi at bruge VIPER på de fleste af de nye projekter, så vi kan få bedre vedligeholdbarhed og klarere kode. Det gør det også nemmere at springe fra et iOS-projekt til et Android-projekt og omvendt. Selvfølgelig er dette en tilpasning under udvikling, så intet her er mejslet i sten. Vi sætter gerne pris på noget feedback om det!