Hallo Habr! In diesem Artikel möchte ich die Erfahrung beim Erstellen eines eigenen Mechanismus zur Automatisierung der Anzeige verschiedener Ansichtstypen teilen: ContentView, LoadingView, NoInternetView, EmptyContentView, ErrorView.

Es war ein langer Weg. Der Weg des Versuchs und Irrtums, die Aufzählung von Methoden und Optionen, schlaflose Nächte und unschätzbare Erfahrungen, die ich teilen und Kritik hören möchte, die ich auf jeden Fall berücksichtigen werde.
Ich werde sofort sagen, dass ich überlegen werde, an RxJava zu arbeiten, da ich bei Coroutinen keinen solchen Mechanismus angewendet habe - meine Hände haben nicht erreicht. Und für andere ähnliche Tools (Loader, AsyncTask usw.) macht es keinen Sinn, meinen Mechanismus zu verwenden, da meistens RxJava oder Coroutinen verwendet werden.
Aktionsansichten
Ein Kollege von mir sagte, es sei unmöglich, das Verhalten von View zu standardisieren, aber ich habe es trotzdem versucht. Und hat es getan.
Der Standardanwendungsbildschirm, dessen Daten vom Server übernommen werden, sollte mindestens 5 Zustände verarbeiten:
- Datenanzeige
- Laden
- Fehler - Jeder Fehler, der unten nicht beschrieben wird
- Der Mangel an Internet ist ein globaler Fehler
- Leerer Bildschirm - Anfrage bestanden, aber keine Daten
Ein anderer Zustand ist, dass die Daten aus dem Cache geladen wurden, die Aktualisierungsanforderung jedoch mit einem Fehler zurückgegeben wurde, dh veraltete Daten anzeigt (besser als nichts). - Die Bibliothek unterstützt dies nicht.
Dementsprechend sollte es für jeden solchen Zustand eine eigene Ansicht geben.
Ich nenne diese View - ActionViews , weil sie auf eine Aktion reagieren. Wenn Sie genau festlegen können, an welcher Stelle Ihre Ansicht angezeigt werden soll und wann sie ausgeblendet werden soll, kann es sich auch um eine ActionView handeln.
Es gibt eine (oder vielleicht auch keine) Standardmethode, um mit einer solchen Ansicht zu arbeiten.
In Methoden, die Arbeit mit RxJava enthalten, müssen Sie Eingabeargumente für alle Arten von ActionViews hinzufügen und diesen Aufrufen eine Logik hinzufügen, um ActionViews anzuzeigen und auszublenden, wie hier beschrieben:
public void getSomeData(LoadingView loadingView, ErrorView errorView, NoInternetView noInternetView, EmptyContentView emptyContentView) { mApi.getProjects() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe(disposable -> { loadingView.show(); noInternetView.hide(); emptyContentView.hide(); }) .doFinally(loadingView::hide) .flatMap(projectResponse -> { }) .subscribe( response -> {}, throwable -> { if (ApiUtils.NETWORK_EXCEPTIONS .contains(throwable.getClass())) noInternetView.show(); else errorView.show(throwable.getMessage()); } ); }
Diese Methode enthält jedoch eine große Menge an Boilerplate, und standardmäßig gefällt sie uns nicht. Und so fing ich an, den Routinecode zu reduzieren.
Level auf
Der erste Schritt bei der Aktualisierung der Standardmethode für die Arbeit mit ActionViews bestand darin, die Boilerplate zu reduzieren, indem Logik in Dienstprogrammklassen eingefügt wurde. Der folgende Code wurde nicht von mir erfunden. Ich bin ein Plagiat und habe einen vernünftigen Kollegen ausspioniert. Danke Arutar
Jetzt sieht unser Code so aus:
public void getSomeData(LoadingView loadingView, ErrorView errorView, NoInternetView noInternetView, EmptyContentView emptyContentView) { mApi.getProjects() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .compose(RxUtil::loading(loadingView)) .compose(RxUtil::emptyContent(emptyContentView)) .compose(RxUtil::noInternet(errorView, noInternetView)) .subscribe(response -> { }, RxUtil::error(errorView)); }
Der Code, den wir oben sehen, ist zwar ohne Boilerplate-Code, verursacht aber immer noch keine so bezaubernde Freude. Es ist bereits viel besser geworden, aber es bleibt das Problem, Links zu ActionViews in jeder Methode zu übergeben, in der mit Rx gearbeitet wird. Und es kann unendlich viele solcher Methoden in einem Projekt geben. Schreiben Sie auch diese komponieren ständig. Buueee. Wer braucht das? Nur fleißige, störrische und nicht faule Leute. Ich bin nicht so Ich bin ein Fan von Faulheit und ein Fan von schönem und praktischem Code, daher wurde eine wichtige Entscheidung getroffen - den Code mit allen Mitteln zu vereinfachen.
Durchbruchspunkt
Nach zahlreichen Änderungen des Mechanismus kam ich zu folgender Option:
public void getSomeData() { execute(() -> mApi.getProjects(), new BaseSubscriber<>(response -> { })); }
Ich habe meinen Mechanismus ungefähr 10-15 Mal umgeschrieben und jedes Mal war er ganz anders als die vorherige Version. Ich werde Ihnen nicht alle Versionen zeigen, konzentrieren wir uns auf die beiden letzten. Das erste was du gerade gesehen hast.
Stimmen Sie zu, es sieht hübsch aus? Ich würde sogar sehr hübsch sagen. Ich habe mich um solche Entscheidungen bemüht. Und absolut alle unsere ActionViews funktionieren zum gewünschten Zeitpunkt korrekt. Ich konnte dies erreichen, indem ich eine große Menge nicht des schönsten Codes schrieb. Klassen, die einen solchen Mechanismus erlauben, enthalten eine Menge komplexer Logik, und ich mochte es nicht. Mit einem Wort - Süße, die ein Monster unter der Haube ist.

Ein solcher Code wird in Zukunft immer schwieriger zu warten sein und selbst schwerwiegende Nachteile und Probleme enthalten, die kritisch waren:
- Was passiert, wenn Sie mehrere LoadingViews auf dem Bildschirm anzeigen müssen? Wie trenne ich sie? Wie kann man verstehen, welche LoadingView wann angezeigt werden soll?
- Verletzung des Rx-Konzepts - alles sollte in einem Stream (Stream) sein. Dies ist hier nicht der Fall.
- Die Komplexität der Anpassung. Das beschriebene Verhalten und die beschriebene Logik sind für den Endbenutzer sehr schwer zu ändern, und dementsprechend ist es schwierig, neue Verhaltensweisen hinzuzufügen.
- Sie müssen die benutzerdefinierte Ansicht verwenden, damit der Mechanismus funktioniert. Dies ist erforderlich, damit der Mechanismus versteht, welche ActionView zu welchem Typ gehört. Wenn Sie beispielsweise die ProgressBar verwenden möchten, muss sie Implementierungen von LoadingView enthalten.
- Die ID für unser ActionView muss mit den in den Basisklassen angegebenen übereinstimmen, um die Boilerplate zu entfernen. Dies ist nicht sehr praktisch, obwohl Sie sich damit abfinden können.
- Reflexion Ja, sie war hier und wegen ihr musste der Mechanismus eindeutig optimiert werden.
Natürlich hatte ich Lösungen für diese Probleme, aber all diese Lösungen führten zu anderen Problemen. Ich habe versucht, das Kritischste so weit wie möglich loszuwerden, und infolgedessen blieben nur die notwendigen Anforderungen für die Nutzung der Bibliothek übrig.
Auf Wiedersehen Java!
Nach einiger Zeit saß ich zu Hause, versuchte es Ich habe herumgespielt und plötzlich wurde mir klar, dass ich Kotlin ausprobieren und Erweiterungen, Standardwerte, Lambdas und Delegaten maximieren musste.
Zuerst sah er nicht sehr aus. Aber jetzt werden ihm fast alle Mängel vorenthalten, die im Prinzip sein können.
Hier ist unser vorheriger Code, aber in der endgültigen Version:
fun getSomeData() { api.getProjects() .withActionViews(view) .execute(onComplete = { }) }
Dank Extensions konnte ich die gesamte Arbeit in einem Thread erledigen, ohne das Grundkonzept der reaktiven Programmierung zu verletzen. Ich habe auch die Möglichkeit gelassen, das Verhalten anzupassen. Wenn Sie die Aktion zu Beginn oder am Ende der Show ändern möchten, können Sie die Funktion einfach an die Methode übergeben und alles funktioniert:
fun getSomeData() { api.getProjects() .withActionViews( view, doOnLoadStart = { }, doOnLoadEnd = { }) .execute(onComplete = { }) }
Verhaltensänderungen sind auch für andere ActionViews verfügbar. Wenn Sie das Standardverhalten verwenden möchten, aber keine Standard-ActionViews haben, können Sie einfach angeben, welche View unsere ActionView ersetzen soll:
fun getSomeData(projectLoadingView: LoadingView) { mApi.getPosts(1, 1) .withActionViews( view, loadingView = projectLoadingView ) .execute(onComplete = { }) }
Ich habe Ihnen die Creme dieses Mechanismus gezeigt, aber er hat auch seinen eigenen Preis.
Zunächst müssen Sie benutzerdefinierte Ansichten erstellen, damit dies funktioniert:
class SwipeRefreshLayout : android.support.v4.widget.SwipeRefreshLayout, LoadingView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) }
Möglicherweise ist dies nicht einmal erforderlich. Im Moment sammle ich Feedback und akzeptiere Vorschläge zur Verbesserung dieses Mechanismus. Der Hauptgrund für die Verwendung von CustomViews besteht darin, von einer Schnittstelle zu erben, die angibt, zu welcher Art von ActionView sie gehört. Dies dient der Sicherheit, da Sie möglicherweise versehentlich einen Fehler machen, wenn Sie den Ansichtstyp in der withActionsViews-Methode angeben.
So sieht die withActionsViews-Methode aus:
fun <T> Observable<T>.withActionViews( view: ActionsView, contentView: View = view.contentActionView, loadingView: LoadingView? = view.loadingActionView, noInternetView: NoInternetView? = view.noInternetActionView, emptyContentView: EmptyContentView? = view.emptyContentActionView, errorView: ErrorView = view.errorActionView, doOnLoadStart: () -> Unit = { doOnLoadSubscribe(contentView, loadingView) }, doOnLoadEnd: () -> Unit = { doOnLoadComplete(contentView, loadingView) }, doOnStartNoInternet: () -> Unit = { doOnNoInternetSubscribe(contentView, noInternetView) }, doOnNoInternet: (Throwable) -> Unit = { doOnNoInternet(contentView, errorView, noInternetView) }, doOnStartEmptyContent: () -> Unit = { doOnEmptyContentSubscribe(contentView, emptyContentView) }, doOnEmptyContent: () -> Unit = { doOnEmptyContent(contentView, errorView, emptyContentView) }, doOnError: (Throwable) -> Unit = { doOnError(errorView, it) } ) { }
Es sieht beängstigend aus, aber bequem und schnell! Wie Sie sehen können, akzeptiert es in den Eingabeparametern loadView: LoadingView? .. Dies versichert uns gegen Fehler mit dem ActionView-Typ.
Dementsprechend müssen Sie einige einfache Schritte ausführen, damit der Mechanismus funktioniert:
- Fügen Sie unserem Layout unsere benutzerdefinierten ActionViews hinzu. Ich habe bereits einige davon gemacht, und Sie können sie einfach verwenden.
- Implementieren Sie die HasActionsView-Schnittstelle und überschreiben Sie die Standardvariablen, die für ActionViews im Code verantwortlich sind:
override var contentActionView: View by mutableLazy { recyclerView } override var loadingActionView: LoadingView? by mutableLazy { swipeRefreshLayout } override var noInternetActionView: NoInternetView? by mutableLazy { noInternetView } override var emptyContentActionView: EmptyContentView? by mutableLazy { emptyContentView } override var errorActionView: ErrorView by mutableLazy { ToastView(baseActivity) }
Oder erben Sie von einer Klasse, in der unsere ActionViews bereits überschrieben wurden. In diesem Fall müssen Sie die genau angegebene ID in Ihrem Layout verwenden:
abstract class ActionsFragment : Fragment(), HasActionsView { override var contentActionView: View by mutableLazy { findViewById<View>(R.id.contentView) } override var loadingActionView: LoadingView? by mutableLazy { findViewByIdNullable<View>(R.id.loadingView) as LoadingView? } override var noInternetActionView: NoInternetView? by mutableLazy { findViewByIdNullable<View>(R.id.noInternetView) as NoInternetView? } override var emptyContentActionView: EmptyContentView? by mutableLazy { findViewByIdNullable<View>(R.id.emptyContentView) as EmptyContentView? } override var errorActionView: ErrorView by mutableLazy { ToastView(baseActivity) } }
- Genießen Sie die Arbeit ohne Heizplatte!
Wenn Sie Kotlin-Erweiterungen verwenden, vergessen Sie nicht, dass Sie den Import in einen für Sie geeigneten Namen umbenennen können:
import kotlinx.android.synthetic.main.fr_gifts.contentView as recyclerView
Was weiter?
Als ich anfing, an diesem Mechanismus zu arbeiten, dachte ich nicht darüber nach, woraus eine Bibliothek entstehen würde. Aber so kam es, dass ich meine Kreation teilen wollte, und jetzt wartet das Süßeste auf mich - die Bibliothek veröffentlichen, Probleme sammeln, Feedback erhalten, Funktionen hinzufügen / verbessern und Fehler beheben.
Während ich einen Artikel schrieb ...
Ich habe es geschafft, alles in Form von Bibliotheken zu ordnen:
Die Bibliothek und der Mechanismus selbst erheben keinen Anspruch darauf, ein Muss in Ihrem Projekt zu sein. Ich wollte nur meine Idee teilen, Kritik und Kommentare anhören und meinen Mechanismus verbessern, damit er bequemer, gebrauchter und praktischer wird. Vielleicht können Sie einen solchen Mechanismus besser machen als ich. Ich werde nur froh sein. Ich hoffe aufrichtig, dass mein Artikel Sie dazu inspiriert hat, etwas Eigenes zu kreieren, vielleicht sogar ähnliches und prägnanteres.
Wenn Sie Vorschläge und Empfehlungen zur Verbesserung der Funktionalität und Funktionsweise des Mechanismus selbst haben, höre ich ihnen gerne zu. Willkommen zu den Kommentaren und für alle Fälle zu meinem Telegramm: @tanchuev
PS Ich habe mich sehr darüber gefreut, dass ich mit meinen eigenen Händen etwas Nützliches geschaffen habe. Vielleicht sind ActionViews nicht gefragt, aber Erfahrung und Begeisterung dafür werden nirgendwo hingehen.
PPS Damit ActionViews zu einer vollwertigen gebrauchten Bibliothek wird, müssen Sie Feedback sammeln und möglicherweise die Funktionalität verfeinern oder den Ansatz selbst grundlegend ändern, wenn alles wirklich schlecht läuft.
PPPS Wenn Sie an meiner Arbeit interessiert sind, können wir sie am 28. September in Moskau auf der MBLT DEV 2018 International Mobile Developers Conference persönlich diskutieren. Frühbuchertickets gehen übrigens schon aus!