- Marcio Granzotto Rodrigues
- před 4 lety
- Rubriky:Názory, Technické
- Štítky:Názory, Technické
- Štítky:Názory, Technické
- Štítky:Názory:
Když jsem začínal jako vývojář Androidu a později jsem pracoval i s iOS, přišel jsem do styku s několika různými architekturami projektů – některými dobrými a některými špatnými.
Spokojeně jsem používal architekturu MVP pro Android, dokud jsem se nesetkal – a nepracoval osm měsíců s – architekturou VIPER v projektu iOS. Když jsem se vrátil k Androidu, rozhodl jsem se přizpůsobit a implementovat na něm VIPER, přestože někteří jiní vývojáři naznačovali, že by nemělo smysl používat architekturu iOS na Androidu. Vzhledem k zásadnímu rozdílu mezi frameworky pro Android a iOS jsem měl několik otázek, nakolik by byl VIPER pro Android užitečný. Bylo by to proveditelné a stálo by to za námahu? Začněme od základů.
Co je VIPER?
VIPER je čistá architektura používaná především při vývoji aplikací pro iOS. Pomáhá udržovat kód čistý a organizovaný a vyhnout se situaci Massive-View-Controller.
VIPER je zkratka pro View Interactor Presenter Entity Router, což jsou třídy, které mají přesně definovanou odpovědnost podle principu jedné odpovědnosti. Více si o tom můžete přečíst v tomto vynikajícím článku.
Android architektury
Pro Android již existuje několik velmi dobrých architektur. Mezi ty známější patří Model-View-ViewModel (MVVM) a Model-View-Presenter (MVP).
MVVM dává velký smysl, pokud jej používáte spolu s datovou vazbou, a protože se mi myšlenka datové vazby příliš nelíbí, vždy jsem pro projekty, na kterých jsem pracoval, používal MVP. S růstem projektů se však z prezentéru může stát obrovská třída se spoustou metod, což ztěžuje její údržbu a pochopení. K tomu dochází proto, že je zodpovědná za spoustu věcí: musí zpracovávat události uživatelského rozhraní, logiku uživatelského rozhraní, obchodní logiku, síťování a dotazy do databáze. To porušuje zásadu jediné odpovědnosti, což může VIPER napravit.
Napravme to!“
S těmito problémy jsem začal nový projekt pro Android a rozhodl jsem se použít MVP + Interactor (nebo VIPE, chcete-li). To mi umožnilo přesunout část odpovědnosti z prezentéru na Interactor. Ponechat prezentéru obsluhu událostí uživatelského rozhraní a přípravu dat, která přicházejí z Interaktoru, k zobrazení ve View. Interaktor je pak zodpovědný pouze za obchodní logiku a načítání dat z DB nebo API.
Také jsem začal používat rozhraní pro propojení modulů. Tímto způsobem nemohou přistupovat k jiným metodám než k těm, které jsou deklarovány na rozhraní. To chrání strukturu a pomáhá definovat jasnou odpovědnost za každý modul, čímž se vyhnete chybám vývojářů, jako je umístění logiky na nesprávné místo. Tady je, jak rozhraní vypadají:
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) } }
A tady je kód pro ilustraci tříd, které tato rozhraní implementují (je v Kotlinu, ale v Javě by to mělo být stejné).
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!") }) } }
Plný kód je k dispozici na tomto Gistu.
Můžete vidět, že moduly se vytvářejí a propojují při spuštění. Při vytváření Aktivity se inicializuje Presenter a v konstruktoru se předá jako View. Presenter pak inicializuje Interaktor, přičemž sám sebe předá jako InteractorOutput
.
V projektu iOS VIPER by se o to postaral Router, který by vytvořil UIViewController
nebo jej získal ze Storyboardu a pak by všechny moduly propojil dohromady. V systému Android však aktivity sami nevytváříme: musíme používat Intenty a k nově vytvořené aktivitě nemáme přístup z předchozí aktivity. To pomáhá zabránit únikům paměti, ale může to být nepříjemné, pokud chceme novému modulu pouze předat data. Nemůžeme také umístit Presenter na přídavek Intentu, protože by musel být Parcelable
nebo Serializable
. Je prostě neproveditelný.
Proto jsem na tomto projektu vynechal Router. Ale je to ideální případ?“
VIPE + Router
Výše uvedená implementace VIPE vyřešila většinu problémů MVP, rozdělila zodpovědnost Presentera s Interaktorem.
Pohled však není tak pasivní jako pohled VIPER v iOS. Musí zvládnout všechny běžné odpovědnosti View plus směrování na další moduly. To by NEMĚLO být jeho zodpovědností a můžeme to udělat lépe. Zadejte Router.
Tady jsou rozdíly mezi „VIPE“ a 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) } }
Plný kód je k dispozici zde.
Nyní jsme logiku směrování zobrazení přesunuli do Routeru. Ten potřebuje pouze instanci aktivity, aby mohl volat metodu startActivity
. Stále to nezapojuje všechno dohromady jako VIPER pro iOS, ale alespoň to respektuje princip jedné odpovědnosti.
Závěr
Po vývoji projektu s MVP + Interaktorem a tím, že jsem pomáhal spolupracovníkovi vyvinout plnohodnotný projekt VIPER pro Android, mohu s jistotou říct, že tato architektura na Androidu funguje a stojí za to. Třídy se zmenšují a jsou lépe udržovatelné. Také usměrňuje proces vývoje, protože díky architektuře je jasné, kam se má kód psát.
V Cheesecake Labs plánujeme používat VIPER u většiny nových projektů, abychom měli lepší udržovatelnost a přehlednější kód. Také to usnadňuje přechod z projektu pro iOS na projekt pro Android a naopak. Samozřejmě se jedná o vyvíjející se adaptaci, takže nic zde není vytesáno do kamene. Budeme rádi, když nám k tomu dáte nějakou zpětnou vazbu!