- Marcio Granzotto Rodrigues
- vor 4 Jahren
- Kategorien:Meinung, Technisch
- Tags:Android, Architektur, Entwicklung, iOS, kotiln, Mobile, VIPER
Als ich als Android-Entwickler begann und später auch mit iOS arbeitete, hatte ich Kontakt mit verschiedenen Projektarchitekturen – einige gut, andere schlecht.
Ich war mit der MVP-Architektur für Android sehr zufrieden, bis ich in einem iOS-Projekt die VIPER-Architektur kennenlernte und acht Monate damit arbeitete. Als ich zu Android zurückkehrte, beschloss ich, VIPER anzupassen und zu implementieren, obwohl einige andere Entwickler meinten, dass es keinen Sinn machen würde, eine iOS-Architektur für Android zu verwenden. In Anbetracht des grundlegenden Unterschieds zwischen den Frameworks von Android und iOS stellte sich mir die Frage, wie nützlich VIPER für Android sein würde. Wäre es machbar und den Aufwand wert? Beginnen wir mit den Grundlagen.
Was ist VIPER?
VIPER ist eine saubere Architektur, die hauptsächlich in der iOS-App-Entwicklung verwendet wird. Sie hilft dabei, den Code sauber und organisiert zu halten und die Massive-View-Controller-Situation zu vermeiden.
VIPER steht für View Interactor Presenter Entity Router, das sind Klassen, die eine klar definierte Verantwortung haben, die dem Single Responsibility Principle folgt. Sie können mehr darüber in diesem ausgezeichneten Artikel lesen.
Android Architekturen
Es gibt bereits einige sehr gute Architekturen für Android. Die bekanntesten sind Model-View-ViewModel (MVVM) und Model-View-Presenter (MVP).
MVVM macht viel Sinn, wenn man es zusammen mit Datenbindung verwendet, und da ich die Idee der Datenbindung nicht sehr mag, habe ich für die Projekte, an denen ich gearbeitet habe, immer MVP verwendet. Wenn die Projekte jedoch wachsen, kann der Presenter zu einer riesigen Klasse mit vielen Methoden werden, was es schwer macht, ihn zu pflegen und zu verstehen. Das liegt daran, dass er für eine Menge Dinge verantwortlich ist: Er muss UI-Ereignisse, UI-Logik, Geschäftslogik, Netzwerke und Datenbankabfragen verwalten. Das verstößt gegen das Prinzip der einzigen Verantwortung, was VIPER beheben kann.
Let’s fix it!
Mit diesen Problemen im Hinterkopf begann ich ein neues Android-Projekt und entschied mich für MVP + Interactor (oder VIPE, wenn Sie so wollen). Dadurch konnte ich einige Aufgaben vom Presenter auf den Interactor verlagern. Der Presenter kümmert sich um die UI-Ereignisse und bereitet die Daten vor, die vom Interactor kommen und in der Ansicht angezeigt werden sollen. Der Interactor ist dann nur noch für die Geschäftslogik und das Abrufen von Daten aus DBs oder APIs verantwortlich.
Auch habe ich begonnen, Schnittstellen für die Verknüpfung der Module untereinander zu verwenden. Auf diese Weise können sie nur auf die Methoden zugreifen, die in der Schnittstelle deklariert sind. Dies schützt die Struktur und hilft dabei, eine klare Verantwortung für jedes Modul zu definieren, wodurch Entwicklerfehler wie das Ablegen der Logik an der falschen Stelle vermieden werden. So sehen die Schnittstellen aus:
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) } }
Und hier ist etwas Code zur Veranschaulichung der Klassen, die diese Schnittstellen implementieren (in Kotlin, aber in Java sollte es genauso sein).
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!") }) } }
Der vollständige Code ist auf dieser Gist verfügbar.
Sie können sehen, dass die Module beim Start erstellt und miteinander verbunden werden. Wenn die Activity erstellt wird, initialisiert sie den Presenter und übergibt sich selbst als View im Konstruktor. Der Presenter initialisiert dann den Interactor und übergibt sich selbst als InteractorOutput
.
In einem iOS-VIPER-Projekt würde dies vom Router gehandhabt werden, der das UIViewController
erstellt oder es von einem Storyboard erhält und dann alle Module miteinander verdrahtet. Aber auf Android erstellen wir die Activities nicht selbst: wir müssen Intents verwenden, und wir haben keinen Zugriff auf die neu erstellte Activity aus der vorherigen Activity. Das hilft dabei, Speicherlecks zu vermeiden, kann aber ein Problem sein, wenn man nur Daten an das neue Modul übergeben möchte. Wir können den Presenter auch nicht auf die Extras des Intents setzen, weil er Parcelable
oder Serializable
sein müsste. Ist einfach nicht machbar.
Deshalb habe ich bei diesem Projekt den Router weggelassen. Aber ist das der Idealfall?
VIPE + Router
Die obige Implementierung von VIPE hat die meisten Probleme des MVP gelöst, indem sie die Verantwortlichkeiten des Presenters und des Interactors aufteilt.
Der View ist jedoch nicht so passiv wie der View des iOS VIPER. Sie muss alle regulären View-Verantwortlichkeiten und die Weiterleitung an andere Module übernehmen. Das sollte NICHT seine Aufgabe sein und wir können es besser machen. Geben Sie den Router ein.
Hier sind die Unterschiede zwischen „VIPE“ und 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) } }
Der vollständige Code ist hier verfügbar.
Jetzt haben wir die View-Routing-Logik zum Router verschoben. Er braucht nur eine Instanz der Activity, damit er die startActivity
Methode aufrufen kann. Es ist immer noch nicht alles miteinander verdrahtet wie beim iOS VIPER, aber zumindest respektiert es das Single Responsibility Principle.
Fazit
Nachdem ich ein Projekt mit MVP + Interactor entwickelt habe und einem Kollegen geholfen habe, ein komplettes VIPER Android Projekt zu entwickeln, kann ich mit Sicherheit sagen, dass die Architektur auf Android funktioniert und es sich lohnt. Die Klassen werden kleiner und sind besser wartbar. Es leitet auch den Entwicklungsprozess, weil die Architektur klar macht, wo der Code geschrieben werden sollte.
Hier bei Cheesecake Labs planen wir, VIPER für die meisten neuen Projekte zu verwenden, damit wir eine bessere Wartbarkeit und klareren Code haben. Außerdem ist es einfacher, von einem iOS-Projekt zu einem Android-Projekt zu wechseln und umgekehrt. Natürlich handelt es sich hierbei um eine sich entwickelnde Anpassung, so dass nichts in Stein gemeißelt ist. Wir freuen uns über Feedback dazu!