- Marcio Granzotto Rodrigues
- 4 jaar geleden
- Categorieën:Opinie, Technisch
- Tags:Android, Architectuur, ontwikkeling, iOS, kotiln, Mobiel, VIPER
Toen ik begon als Android ontwikkelaar en later ook met iOS werkte, kwam ik in aanraking met verschillende architecturen van projecten – sommige goed en sommige slecht.
Ik gebruikte met plezier de MVP-architectuur voor Android totdat ik de VIPER-architectuur tegenkwam – en er acht maanden mee werkte – in een iOS-project. Toen ik terugkwam op Android, heb ik besloten om VIPER aan te passen en te implementeren op Android, ondanks dat sommige andere ontwikkelaars suggereerden dat het geen zin zou hebben om een iOS-architectuur op Android te gebruiken. Gezien het fundamentele verschil tussen de frameworks van Android en iOS, had ik wat vragen over hoe nuttig VIPER zou zijn voor Android. Zou het doenbaar zijn en de moeite waard? Laten we beginnen met de basis.
Wat is VIPER?
VIPER is een schone architectuur voornamelijk gebruikt in iOS app ontwikkeling. Het helpt om de code schoon en georganiseerd te houden, waardoor de Massive-View-Controller situatie wordt vermeden.
VIPER staat voor View Interactor Presenter Entity Router, dat zijn klassen die een goed gedefinieerde verantwoordelijkheid hebben, volgens het Single Responsibility Principle. Je kunt er meer over lezen in dit uitstekende artikel.
Android architecturen
Er zijn al een aantal zeer goede architecturen voor Android. De bekendste zijn Model-View-ViewModel (MVVM) en Model-View-Presenter (MVP).
MVVM heeft veel zin als je het gebruikt naast data binding, en omdat ik niet veel op heb met het idee van data binding, heb ik altijd MVP gebruikt voor de projecten waar ik aan heb gewerkt. Maar naarmate projecten groeien, kan de presenter een enorme klasse worden met veel methodes, waardoor hij moeilijk te onderhouden en te begrijpen wordt. Dat gebeurt omdat het verantwoordelijk is voor een heleboel dingen: het moet UI Events, UI logica, business logica, netwerken en database queries afhandelen. Dat is in strijd met het Single Responsibility Principle, iets dat VIPER kan verhelpen.
Lets fix it!
Met die problemen in het achterhoofd, begon ik aan een nieuw Android project en besloot MVP + Interactor (of VIPE, zo je wilt) te gebruiken. Dat stelde me in staat om wat verantwoordelijkheid van de presenter naar de Interactor te verplaatsen. De presenter blijft verantwoordelijk voor het afhandelen van UI events en het voorbereiden van de data die van de Interactor komt om op de View te worden getoond. De Interactor is dan alleen verantwoordelijk voor de business logica en het ophalen van data uit DBs of APIs.
Ook ben ik interfaces gaan gebruiken om de modules aan elkaar te koppelen. Op die manier kunnen ze geen toegang krijgen tot andere methoden dan die welke in de interface zijn gedeclareerd. Dit beschermt de structuur en helpt bij het definiëren van een duidelijke verantwoordelijkheid voor elke module, waardoor fouten van ontwikkelaars zoals het plaatsen van de logica op de verkeerde plaats vermeden worden. Hier is hoe de interfaces eruit zien:
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) } }
En hier is wat code om de klassen te illustreren die deze interfaces implementeren (het is in Kotlin, maar Java zou hetzelfde moeten zijn).
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!") }) } }
De volledige code is beschikbaar op deze Gist.
Je kunt zien dat de modules worden gemaakt en aan elkaar worden gekoppeld bij het opstarten. Wanneer de Activiteit is gemaakt, initialiseert het de Presenter, waarbij het zichzelf als View doorgeeft in de constructor. De Presenter initialiseert vervolgens de Interactor en geeft zichzelf door als InteractorOutput
.
Op een iOS VIPER project zou dit worden afgehandeld door de Router, die de UIViewController
maakt, of deze van een Storyboard krijgt, en dan alle modules aan elkaar koppelt. Maar op Android maken we de Activiteiten niet zelf aan: we moeten Intents gebruiken, en we hebben geen toegang tot de nieuw aangemaakte Activiteit van de vorige. Dit helpt om geheugenlekken te voorkomen, maar het kan lastig zijn als je alleen gegevens wilt doorgeven aan de nieuwe module. We kunnen ook de Presenter niet op de extra’s van de Intent zetten, omdat die dan Parcelable
of Serializable
zou moeten zijn. Dat is gewoon niet te doen.
Daarom heb ik bij dit project de Router weggelaten. Maar is dat het ideale geval?
VIPE + Router
De bovenstaande implementatie van VIPE loste de meeste problemen van MVP op, door de verantwoordelijkheden van de Presenter en de Interactor te splitsen.
De View is echter niet zo passief als de iOS VIPER’s View. Het moet alle normale View-verantwoordelijkheden afhandelen plus de routing naar andere modules. Dit zou NIET zijn verantwoordelijkheid moeten zijn en we kunnen het beter doen. Enter de Router.
Hier zijn de verschillen tussen “VIPE” en 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) } }
Volledige code hier beschikbaar.
Nu hebben we de view routing logica verplaatst naar de Router. Het heeft alleen een instantie van de activiteit nodig, zodat het de startActivity
methode kan aanroepen. Het bedraadt nog steeds niet alles aan elkaar zoals de iOS VIPER, maar het respecteert tenminste het Single Responsibility Principle.
Conclusie
Het ontwikkelen van een project met MVP + Interactor en door een collega te helpen een volledig VIPER Android project te ontwikkelen, kan ik gerust zeggen dat de architectuur wel werkt op Android en het is de moeite waard. De klassen worden kleiner en beter onderhoudbaar. Het begeleidt ook het ontwikkelproces, omdat de architectuur duidelijk maakt waar de code geschreven moet worden.
Hier op Cheesecake Labs zijn we van plan om VIPER te gebruiken op de meeste nieuwe projecten, zodat we een betere onderhoudbaarheid en duidelijkere code kunnen hebben. Het maakt het ook makkelijker om van een iOS project naar een Android project te gaan en vice-versa. Natuurlijk is dit een evoluerende aanpassing, dus niets hier is in steen gehouwen. We stellen feedback hierover zeer op prijs!