Actionviews or like i don't like boilerplate since childhood

    Hi, Habr! In this article I want to share the experience of creating my own mechanism for automating the display of various View types: ContentView, LoadingView, NoInternetView, EmptyContentView, ErrorView.





    It has been a long journey. The path of trial and error, enumeration of methods and options, sleepless nights and invaluable experience that I want to share, and hear the criticism, which I will definitely take into account.


    I will say right away that I will consider the work on RxJava, since for coroutines I did not make such a mechanism - my hands did not reach. And for other similar tools (Loaders, AsyncTask, and so on), it makes no sense to use my mechanism, since RxJava or coroutines are most often used.


    ActionViews


    One of my colleagues said that it is impossible to standardize the behavior of the View, but I still tried to do it. And did.


    The standard screen of the application, the data of which is taken from the server, should minimally process 5 states:


    • Data display
    • Loading
    • Error - any error that is not described below.
    • Lack of Internet - global error
    • Blank screen - the request has passed, but there is no data
    • Another state - the data is loaded from the cache, but the update request returned with an error, that is, the display of outdated data (better than nothing) - The library does not support this.

    Accordingly, for each such state there must be its own View.


    I call such View - ActionViews , because they react to some actions. In fact, if you can determine exactly at what point your View should be shown, and when to hide, it can also be an ActionView.


    There is one (and maybe not one) standard way to work with such a View.


    In the methods that contain the work with RxJava, you need to add input arguments for all types of ActionViews and add to these calls some logic for defining showing and hiding ActionViews, as done here:


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

    But this method contains a huge amount of boilerplate, and by default we do not love it. And so I began work on reducing the routine code.


    Level up


    The first step in upgrading the standard way to work with ActionViews was to reduce the boilerplate by putting logic into the utility classes. I did not invent the code below. I - a plagiarist and spied it at one sensible colleague. Thank you, Arutar !


    Now our code looks like this:


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

    The code that we see above, although deprived of a boilerplate-code, but still does not cause such enchanting delight. It has already become much better, but there remains the problem of passing references to ActionViews in each method where there is work with Rx. And there can be an infinite number of such methods in the project. Even these compose constantly write. Buueeee Who needs it? Only hardworking, stubborn and not lazy people. I'm not like that. I am a fan of laziness and a fan of writing beautiful and convenient code, so an important decision was made to simplify the code by any means.


    Breakout point


    After numerous rewriting of the mechanism, I came to this variant of work:


    publicvoidgetSomeData(){
      execute(() -> mApi.getProjects(),
            new BaseSubscriber<>(response -> {
               /*логика обработки успешного ответа*/
            }));
    }

    I rewrote my mechanism about 10-15 times, and each time it was very different from the previous version. I will not show you all versions, let's focus on the final two. The first you saw just now.


    Agree, looks pretty? I would even say very nice. I sought such solutions. And absolutely all of our ActionViews will work correctly at the right time. I was able to achieve this by writing a huge amount of not the most beautiful code. The classes that allow the use of such a mechanism contain a lot of complex logics, and I did not like it. In a word - sweetie, which is a monster under the hood.





    Such a code in the future will be harder and harder to maintain, and it itself contained quite serious drawbacks and problems that were critical:


    • What happens if you need to display multiple LoadingViews on the screen? How to separate them? How to understand what LoadingView should be displayed when?
    • Violation of the concept of Rx - everything should be in one stream (stream). Here it is not.
    • The complexity of customization. The behavior and logic that is described is very difficult to change for the end user and, accordingly, it is difficult to add new behaviors.
    • You must use a custom View for the mechanism to work. This is necessary so that the mechanism understands which ActionView belongs to which type. For example, if you want to use the ProgressBar, then it must contain implements LoadingView.
    • The id for our ActionView should match those in the base classes to get rid of the boilerplate. This is not very convenient, although you can put up with it.
    • Reflection. Yes, she was here, and because of her the mechanism clearly demanded optimization.

    Of course, I had solutions to these problems, but all these solutions gave rise to other problems. I tried to get rid of the most critical ones as much as possible, and as a result only the necessary requirements for using the library remained.


    Goodbye, java!


    After some time I was at home dabbled in I was dizzy and suddenly I suddenly realized that I had to try Kotlin and, to the maximum, use extensions, default values, lambdas and delegates.


    At first he looked very not very. But now he is deprived of almost all the flaws that, in principle, can be.


    This is what our previous code looks like, but in the final version:


    fungetSomeData() {
       api.getProjects()
           .withActionViews(view)
           .execute(onComplete = { /*логика обработки успешного ответа*/ })
    }

    Thanks to Extensions, I was able to do all the work in one thread, without violating the basic concept of reactive programming. I also left the opportunity to customize the behavior. If you want to change the action at the start or at the end of the download display, you can simply transfer the function to the method, and everything will work:


    fungetSomeData() {
        api.getProjects()
            .withActionViews(
                view,
                doOnLoadStart = { /*ваше поведение*/ },
                doOnLoadEnd = { /*ваше поведение*/ })
            .execute(onComplete = { /*логика обработки успешного ответа*/ })
    }

    Also change behavior is available for other ActionViews. If you want to use the standard behavior, but you do not have default ActionView, you can simply specify which View should replace our ActionView:


    fungetSomeData(projectLoadingView: LoadingView) {
       mApi.getPosts(1, 1)
           .withActionViews(
               view,
               loadingView = projectLoadingView
           )
           .execute(onComplete = { /*логика обработки успешного ответа*/ })
    }

    I showed you the cream of this mechanism, but it has its own price.
    First, you will need to create CustomViews for this to work:


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

    Maybe it doesn't even need to be done. At the moment I collect feedback and accept suggestions for improving this mechanism. The main reason that we need to use CustomViews is inheritance from an interface that indicates which type of ActionView it belongs to. This is necessary for security, as you may accidentally make a mistake when specifying the type of View in the withActionsViews method.


    This is the withActionsViews method itself:


    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) }
    ) {
       /*реализация*/
    }

    It looks scary, but it's convenient and fast! As you can see, in the input parameters it takes loadingView: LoadingView? .. This insures us against an error with type ActionView.


    Accordingly, in order for the mechanism to work, you need to take a few simple steps:


    • Add our ActionViews to the layout, which are custom. Some of them I've already done, and you can just use them.
    • Implement the HasActionsView interface and in the code redefine the default variables that are responsible for ActionViews:
      overridevar contentActionView: View by mutableLazy { recyclerView }
      overridevar loadingActionView: LoadingView? by mutableLazy { swipeRefreshLayout }
      overridevar noInternetActionView: NoInternetView? by mutableLazy { noInternetView }
      overridevar emptyContentActionView: EmptyContentView? by mutableLazy { emptyContentView }
      overridevar errorActionView: ErrorView by mutableLazy { ToastView(baseActivity) }
    • Or inherit from a class in which our ActionViews are already redefined. In this case, you have to use strictly defined id in your layout:


      abstractclassActionsFragment : Fragment(), HasActionsView {
      overridevar contentActionView: View by mutableLazy { findViewById<View>(R.id.contentView) }
      overridevar loadingActionView: LoadingView? by mutableLazy { findViewByIdNullable<View>(R.id.loadingView) as LoadingView? }
      overridevar noInternetActionView: NoInternetView? by mutableLazy { findViewByIdNullable<View>(R.id.noInternetView) as NoInternetView? }
      overridevar emptyContentActionView: EmptyContentView? by mutableLazy { findViewByIdNullable<View>(R.id.emptyContentView) as EmptyContentView? }
      overridevar errorActionView: ErrorView by mutableLazy { ToastView(baseActivity) }
      }

    • Enjoy work without boilerplate!

    If you use Kotlin Extensions, then do not forget about the fact that you can rename the import to a name convenient for you:


    importkotlinx.android.synthetic.main.fr_gifts.contentViewasrecyclerView

    What's next?


    When I started working on this mechanism, I did not think that this would result in a library. But it turned out that I wanted to share my creation, and now the sweetest is waiting for me - publishing the library, collecting issues, getting feedback, adding / improving functionality and fixing bugs.


    While I was writing an article ...


    I managed to arrange everything in the form of libraries:



    The library and the mechanism itself do not claim to be a must have in your project. I just wanted to share my idea, listen to criticism, comments and improve my mechanism to make it more convenient, usable and practical. Perhaps you can make a similar mechanism better than me. I would be glad. I sincerely hope that my article inspired you to create something of your own, perhaps even something similar and more concise.


    If you have any suggestions and recommendations for improving the functionality and operation of the mechanism itself, I will be glad to hear them out. Welcome in the comments and, just in case, my Telegram: @tanchuev


    PS I got great pleasure from the fact that I created something useful with my own hands. Perhaps ActionViews will not be in demand, but the experience and the buzz from it will not go anywhere.


    PPS In order for ActionViews to become a full-fledged library, you need to collect feedback and, possibly, modify the functionality or fundamentally change the approach itself, if everything is really bad.


    PPPS If you are interested in my work, then we can discuss it personally on September 28 in Moscow at the International Conference of Mobile Developers MBLT DEV 2018 . By the way, early bird tickets are already running out!


    Also popular now: