Vues ou comment je n'aime pas les passe-partout depuis l'enfance

Bonjour, Habr! Dans cet article, je veux partager l'expérience de la création de mon propre mécanisme pour automatiser l'affichage de différents types de vues: ContentView, LoadingView, NoInternetView, EmptyContentView, ErrorView.





Ce fut un long chemin. Le chemin des essais et des erreurs, l'énumération des méthodes et des options, les nuits blanches et l'expérience inestimable que je veux partager et entendre des critiques, dont je tiendrai certainement compte.


Je dirai tout de suite que j'envisagerai de travailler sur RxJava, car pour les coroutines je n'ai pas fait un tel mécanisme - mes mains n'ont pas atteint. Et pour d'autres outils similaires (Loaders, AsyncTask, etc.), cela n'a aucun sens d'utiliser mon mécanisme, car le plus souvent c'est RxJava ou coroutines qui est utilisé.


Vues d'action


Un de mes collègues a dit qu'il était impossible de normaliser le comportement de la vue, mais j'ai quand même essayé de le faire. Et l'a fait.


L'écran d'application standard, dont les données sont extraites du serveur, doit au moins traiter 5 états:


  • Affichage des données
  • Chargement
  • Erreur - toute erreur non décrite ci-dessous
  • Le manque d'Internet est une erreur mondiale
  • Écran vide - demande passée, mais aucune donnée
  • Un autre état est que les données ont été chargées à partir du cache, mais la demande de mise à jour est retournée avec une erreur, c'est-à-dire, montrant des données obsolètes (mieux que rien) - La bibliothèque ne prend pas en charge cela.

En conséquence, pour chacun de ces États, il devrait y avoir sa propre vision.


J'appelle ces View - ActionViews , car ils répondent à une sorte d'action. En fait, si vous pouvez déterminer exactement à quel moment votre vue doit être affichée et quand la masquer, il peut également s'agir d'un ActionView.


Il existe une (ou peut-être pas une) méthode standard pour travailler avec une telle vue.


Dans les méthodes qui contiennent l'utilisation de RxJava, vous devez ajouter des arguments d'entrée pour tous les types d'ActionViews et ajouter de la logique à ces appels pour afficher et masquer ActionViews, comme cela se fait ici:


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

Mais cette méthode contient une énorme quantité de passe-partout, et par défaut, nous ne l'aimons pas. Et j'ai donc commencé à travailler sur la réduction du code de routine.


Niveau supérieur


La première étape de la mise à niveau de la façon standard de travailler avec ActionViews a été de réduire le passe-partout en plaçant la logique dans les classes utilitaires. Le code ci-dessous n'a pas été inventé par moi. Je suis plagiaire et j'ai espionné un collègue sensé. Merci Arutar


Maintenant, notre code ressemble à ceci:


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

Le code que nous voyons ci-dessus, bien que dépourvu de code passe-partout, ne provoque toujours pas un tel enchantement enchanteur. C'est déjà devenu beaucoup mieux, mais il reste le problème de passer des liens vers ActionViews dans chaque méthode où il y a du travail avec Rx. Et il peut y avoir un nombre infini de telles méthodes dans un projet. Écrivez également ces compositions en permanence. Buueee. Qui en a besoin? Seulement des gens travailleurs, têtus et pas paresseux. Je ne suis pas comme ça. Je suis un fan de la paresse et un fan de l'écriture de code beau et pratique, donc une décision importante a été prise - simplifier le code par tous les moyens.


Point de rupture


Après de nombreuses réécritures du mécanisme, je suis arrivé à cette option:


 public void getSomeData() { execute(() -> mApi.getProjects(), new BaseSubscriber<>(response -> { /*   */ })); } 

J'ai réécrit mon mécanisme environ 10 à 15 fois, et à chaque fois, il était très différent de la version précédente. Je ne vous montrerai pas toutes les versions, concentrons-nous sur les deux dernières. Le premier que vous venez de voir.


D'accord, c'est joli? Je dirais même très jolie. J'ai essayé de telles décisions. Et absolument tous nos ActionViews fonctionneront correctement au moment dont nous avons besoin. J'ai pu y parvenir en écrivant une énorme quantité de code pas le plus beau. Les classes qui permettent un tel mécanisme contiennent beaucoup de logique complexe, et je ne l'aimais pas. En un mot - ma chérie, qui est un monstre sous le capot.





À l'avenir, un tel code sera de plus en plus difficile à maintenir, et il comportait lui-même des inconvénients et des problèmes assez graves qui étaient critiques:


  • Que se passe-t-il si vous devez afficher plusieurs vues de chargement à l'écran? Comment les séparer? Comment comprendre quelle LoadingView doit être affichée quand?
  • Violation du concept de Rx - tout devrait être dans un seul flux (flux). Ce n'est pas le cas ici.
  • La complexité de la personnalisation. Le comportement et la logique décrits sont très difficiles à modifier pour l'utilisateur final et, par conséquent, il est difficile d'ajouter de nouveaux comportements.
  • Vous devez utiliser une vue personnalisée pour que le mécanisme fonctionne. Cela est nécessaire pour que le mécanisme comprenne quel ActionView appartient à quel type. Par exemple, si vous souhaitez utiliser ProgressBar, il doit contenir des implémentations LoadingView.
  • L'identifiant de notre ActionView doit correspondre à ceux spécifiés dans les classes de base afin de se débarrasser du passe-partout. Ce n'est pas très pratique, bien que vous puissiez accepter cela.
  • La réflexion Oui, elle était là, et à cause d'elle, le mécanisme nécessitait clairement une optimisation.

Bien sûr, j'avais des solutions à ces problèmes, mais toutes ces solutions ont donné lieu à d'autres problèmes. J'ai essayé de me débarrasser du plus critique possible, et en conséquence, seules les exigences nécessaires pour utiliser la bibliothèque sont restées.


Au revoir Java!


Après un certain temps, j'étais assis à la maison, barboté J'étais en train de jouer et j'ai soudain réalisé que je devais essayer Kotlin et maximiser les extensions, les valeurs par défaut, les lambdas et les délégués.


Au début, il n'avait pas beaucoup l'air. Mais maintenant, il est dépourvu de presque toutes les lacunes, qui, en principe, peuvent l'être.


Voici notre code précédent, mais dans la version finale:


 fun getSomeData() { api.getProjects() .withActionViews(view) .execute(onComplete = { /*   */ }) } 

Grâce aux extensions, j'ai pu faire tout le travail dans un seul thread sans violer le concept de base de la programmation réactive. J'ai également laissé l'opportunité de personnaliser le comportement. Si vous souhaitez modifier l'action au début ou à la fin du show, vous pouvez simplement passer la fonction à la méthode et tout fonctionnera:


 fun getSomeData() { api.getProjects() .withActionViews( view, doOnLoadStart = { /* */ }, doOnLoadEnd = { /* */ }) .execute(onComplete = { /*   */ }) } 

Des modifications de comportement sont également disponibles pour d'autres ActionViews. Si vous souhaitez utiliser un comportement standard, mais que vous n'avez pas d'ActionViews par défaut, vous pouvez simplement spécifier quelle vue doit remplacer notre ActionView:


 fun getSomeData(projectLoadingView: LoadingView) { mApi.getPosts(1, 1) .withActionViews( view, loadingView = projectLoadingView ) .execute(onComplete = { /*   */ }) } 

Je vous ai montré la crème de ce mécanisme, mais il a aussi son propre prix.
Tout d'abord, vous devrez créer des vues personnalisées pour que cela fonctionne:


 class SwipeRefreshLayout : android.support.v4.widget.SwipeRefreshLayout, LoadingView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) } 

Il n'est peut-être même pas nécessaire de le faire. En ce moment, je recueille des commentaires et accepte des suggestions pour améliorer ce mécanisme. La principale raison pour laquelle nous devons utiliser CustomViews est d'hériter d'une interface qui indique à quel type d'ActionView il appartient. C'est pour la sécurité, car vous pourriez accidentellement faire une erreur en spécifiant le type de vue dans la méthode withActionsViews.


Voici à quoi ressemble la méthode withActionsViews:


 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) } ) { /**/ } 

Cela semble effrayant, mais pratique et rapide! Comme vous pouvez le voir, dans les paramètres d'entrée, il accepte loadingView: LoadingView? .. Cela nous assure contre les erreurs avec le type ActionView.


Par conséquent, pour que le mécanisme fonctionne, vous devez suivre quelques étapes simples:


  • Ajoutez à notre présentation nos ActionViews, qui sont personnalisées. J'en ai déjà fait certains, et vous pouvez simplement les utiliser.
  • Implémentez l'interface HasActionsView et remplacez les variables par défaut responsables des ActionViews dans le code:
     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) } 
  • Ou héritez d'une classe dans laquelle nos ActionViews sont déjà remplacées. Dans ce cas, vous devrez utiliser l'identifiant strictement spécifié dans votre mise en page:


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

  • Profitez du travail sans passe-partout!

Si vous utiliserez les extensions Kotlin, n'oubliez pas que vous pouvez renommer l'importation en un nom qui vous convient:


 import kotlinx.android.synthetic.main.fr_gifts.contentView as recyclerView 

Et ensuite?


Quand j'ai commencé à travailler sur ce mécanisme, je ne pensais pas à la nature d'une bibliothèque. Mais il s'est avéré que je voulais partager ma création, et maintenant la chose la plus douce m'attend - publier la bibliothèque, collecter les problèmes, recevoir des commentaires, ajouter / améliorer des fonctionnalités et corriger les bugs.


Pendant que j'écrivais un article ...


J'ai réussi à tout arranger sous forme de bibliothèques:



La bibliothèque et le mécanisme lui-même ne prétendent pas être un incontournable dans votre projet. Je voulais juste partager mon idée, écouter les critiques, les commentaires et améliorer mon mécanisme pour qu'il devienne plus pratique, utilisé et pratique. Peut-être pouvez-vous améliorer un tel mécanisme que moi. Je serai seulement content. J'espère sincèrement que mon article vous a inspiré pour créer quelque chose de vous-même, peut-être même similaire et plus concis.


Si vous avez des suggestions et des recommandations pour améliorer la fonctionnalité et le fonctionnement du mécanisme lui-même, je serai heureux de les écouter. Bienvenue dans les commentaires et, au cas où, dans mon télégramme: @tanchuev


PS J'ai pris beaucoup de plaisir à créer quelque chose d'utile de mes propres mains. Peut-être que ActionViews ne sera pas en demande, mais l'expérience et le buzz qui en découlent n'iront nulle part.


PPS Pour qu'ActionViews se transforme en une bibliothèque utilisée à part entière, vous devez collecter des commentaires et, éventuellement, affiner la fonctionnalité ou changer fondamentalement l'approche elle-même si tout va vraiment mal.


PPPS Si vous êtes intéressé par mon travail, nous pouvons en discuter personnellement le 28 septembre à Moscou lors de la Conférence internationale MBLT DEV 2018 pour les développeurs mobiles. Soit dit en passant, les billets pour les lève-tôt sont déjà épuisés!

Source: https://habr.com/ru/post/fr414773/


All Articles