• Marcio Granzotto Rodrigues
  • 4 vuotta sitten
  • Kategoriat: Mielipide, Tekninen
  • Avainsanat:Android, arkkitehtuuri, kehitys, iOS, kotiln, Mobile, VIPER

Aloittaessani Android-kehittäjänä ja myöhemmin työskennellessäni myös iOS:n parissa, olen ollut tekemisissä useiden eri projektien arkkitehtuurien kanssa – joidenkin hyvien ja joidenkin huonojen.

Käytin tyytyväisenä MVP-arkkitehtuuria Androidissa, kunnes tapasin – ja työskentelin kahdeksan kuukautta sen kanssa – VIPER-arkkitehtuurin iOS-projektissa. Kun palasin Androidiin, päätin mukauttaa ja toteuttaa VIPERin siihen, vaikka jotkut muut kehittäjät ehdottivat, ettei iOS-arkkitehtuurin käyttämisessä Androidissa olisi järkeä. Koska Androidin ja iOS:n kehysten välillä on perustavanlaatuinen ero, minulla oli joitakin kysymyksiä siitä, kuinka hyödyllinen VIPER olisi Androidissa. Olisiko se toteutettavissa ja vaivan arvoista? Aloitetaan perusasioista.

Mikä on VIPER?

VIPER on puhdas arkkitehtuuri, jota käytetään pääasiassa iOS-sovelluskehityksessä. Se auttaa pitämään koodin puhtaana ja organisoituna, jolloin vältetään Massive-View-Controller-tilanne.

VIPER on lyhenne sanoista View Interactor Presenter Entity Router, jotka ovat luokkia, joilla on tarkkaan määritelty vastuu ja jotka noudattavat Single Responsibility Principle -periaatetta. Voit lukea siitä lisää tästä erinomaisesta artikkelista.

Android-arkkitehtuurit

Androidille on jo olemassa erittäin hyviä arkkitehtuureja. Tunnetuimpia ovat Model-View-ViewModel (MVVM) ja Model-View-Presenter (MVP).

MVVM:ssä on paljon järkeä, jos sitä käytetään datan sitomisen rinnalla, ja koska en pidä juurikaan ajatuksesta datan sitomisesta, olen aina käyttänyt MVP:tä projekteissa, joissa olen työskennellyt. Projektien kasvaessa esittelijästä voi kuitenkin tulla valtava luokka, jossa on paljon metodeja, jolloin sitä on vaikea ylläpitää ja ymmärtää. Näin tapahtuu, koska se on vastuussa monista asioista: sen on käsiteltävä UI-tapahtumia, UI-logiikkaa, liiketoimintalogiikkaa, verkottumista ja tietokantakyselyjä. Tämä rikkoo yhden vastuun periaatetta, minkä VIPER voi korjata.

Korjataan se!

Näiden ongelmien kanssa aloitin uuden Android-projektin ja päätin käyttää MVP + Interactoria (tai VIPE:tä, jos haluat). Näin pystyin siirtämään osan vastuusta esittelijältä Interactorille. Esittelijälle jäi UI-tapahtumien käsittely ja Interactorista tulevien tietojen valmistelu näkymässä näytettäväksi. Tällöin Interactor vastaa vain liiketoimintalogiikasta ja tietojen hakemisesta tietokannoista tai API:ista.

Aloin myös käyttää rajapintoja moduulien yhdistämiseen toisiinsa. Näin ne eivät voi käyttää muita metodeja kuin niitä, jotka on ilmoitettu rajapinnassa. Tämä suojaa rakennetta ja auttaa määrittelemään selkeän vastuun kullekin moduulille, jolloin vältetään kehittäjien virheet, kuten logiikan laittaminen väärään paikkaan. Näin rajapinnat näyttävät:

 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) } } 

Ja tässä on koodia havainnollistamaan luokkia, jotka toteuttavat nämä rajapinnat (se on Kotlin-kielellä, mutta Javassa pitäisi olla sama).

 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!") }) } } 

Koko koodi on saatavilla tässä Gistissä.

Voit nähdä, että moduulit luodaan ja linkitetään toisiinsa käynnistyksen yhteydessä. Kun Activity luodaan, se alustaa Presenterin ja välittää itsensä View:ksi konstruktorissa. Presenter alustaa sitten Interactorin välittäen itsensä InteractorOutput.

IOS-VIPER-projektissa tämän hoitaisi Router, joka luo UIViewController tai saa sen Storyboardista ja kytkee sitten kaikki moduulit yhteen. Mutta Androidissa emme luo aktiviteetteja itse: meidän on käytettävä Intenttejä, eikä meillä ole pääsyä äskettäin luotuun aktiviteettiin edellisestä aktiviteetista. Tämä auttaa estämään muistivuodot, mutta se voi olla hankalaa, jos haluat vain siirtää dataa uuteen moduuliin. Emme myöskään voi laittaa Presenteriä Intentin ekstroihin, koska sen pitäisi olla Parcelable tai Serializable. Ei vain ole toteutettavissa.

Sen takia tässä projektissa olen jättänyt Routerin pois. Mutta onko se ideaalitapaus?

VIPE + Router

Yllä oleva VIPE:n toteutus ratkaisi suurimman osan MVP:n ongelmista jakamalla Presenterin ja Interactorin vastuut.

Näkymä ei kuitenkaan ole yhtä passiivinen kuin iOS:n VIPER:n View. Sen on hoidettava kaikki tavalliset View-vastuualueet sekä reititys muihin moduuleihin. Tämän EI pitäisi olla sen vastuulla ja voimme tehdä paremmin. Enter the Router.

Tässä on eroja ”VIPE:n” ja VIPER:n välillä:

 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) } } 

Täydellinen koodi löytyy täältä.

Nyt siirsimme näkymän reitityslogiikan Routeriin. Se tarvitsee vain Activityn instanssin, jotta se voi kutsua startActivity-metodia. Se ei edelleenkään johdota kaikkea yhteen kuten iOS:n VIPER, mutta ainakin se kunnioittaa yhden vastuun periaatetta.

Johtopäätös

Kehitettyäni projektin MVP + Interactorilla ja autettuani työtoveriani kehittämään täydellisen VIPER-Android-projektin, voin varmuudella sanoa, että arkkitehtuuri tosiaankin toimii Androidissa ja se on sen arvoinen. Luokista tulee pienempiä ja helpommin ylläpidettäviä. Se myös ohjaa kehitysprosessia, koska arkkitehtuuri tekee selväksi, mihin koodi pitäisi kirjoittaa.

Täällä Cheesecake Labsissa aiomme käyttää VIPERiä useimmissa uusissa projekteissa, jotta saamme paremman ylläpidettävyyden ja selkeämmän koodin. Lisäksi se helpottaa siirtymistä iOS-projektista Android-projektiin ja päinvastoin. Kyseessä on tietysti kehittyvä mukautus, joten mikään ei ole kiveen hakattua. Otamme mielellämme vastaan palautetta siitä!

Articles

Vastaa

Sähköpostiosoitettasi ei julkaista.