Привет, Хабр! В данной статье я хочу поделиться опытом создания своего механизма для автоматизации показа различных View типа: ContentView, LoadingView, NoInternetView, EmptyContentView, ErrorView.





Это был долгий путь. Путь проб и ошибок, перебор методов и вариантов, бессонные ночи и бесценный опыт, которым я хочу поделиться, и услышать критику, которую я обязательно приму во внимание.


Скажу сразу, что буду рассматривать работу на RxJava, так как для coroutines я не делал подобного механизма — не дошли руки. А для других подобных инструментов (Loaders, AsyncTask и так далее) нет смысла использовать мой механизм, так как чаще всего применяется именно RxJava или coroutines.


ActionViews


Один мой коллега сказал, что невозможно шаблонизировать поведение View, но я всё-таки попытался это сделать. И сделал.


Стандартный экран приложения, данные которого берутся с сервера, минимально должен обрабатывать 5 состояний:


  • Показ данных
  • Загрузка
  • Ошибка — любая ошибка, которая не описана ниже
  • Отсутствие интернета — глобальная ошибка
  • Пустой экран — запрос прошёл, но данных нет
  • Еще один стейт — данные подгружены из кеша, но запрос на обновление вернулся с ошибкой, то есть показ устаревших данных (лучше, чем ничего) — Библиотека это не поддерживает.

Соответственно, для каждого такого состояния должна быть своя View.


Я называю такие View — ActionViews, потому что они реагируют на какие-то действия. По факту, если вы можете точно определить, в какой момент ваша View должна показываться, а когда скрываться, то она тоже может быть ActionView.


Существует один (а может, и не один) стандартный способ для того, чтобы с такими View работать.


В методы, которые содержат работу с RxJava, нужно добавить входные аргументы для всех типов ActionViews и добавить в эти вызовы некоторую логику определения показа и скрытия ActionViews, как это сделано тут:


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

Но такой способ содержит огромное количество бойлерплейта, а по умолчанию мы его не любим. И поэтому я начал работу над сокращением рутинного кода.


Level Up


Первым этапом модернизации стандартного способа для работы с ActionViews стало сокращение бойлерплейта путем вынесения логики в утильные классы. Код ниже придумал не я. Я — плагиатор и подсмотрел это у одного толкового коллеги. Спасибо, Arutar!


Теперь наш код выглядит так:


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

Код, который мы видим выше, хоть и лишён boilerplate-кода, но всё равно не вызывает такого фееричного восторга. Стало уже намного лучше, но осталась проблема передачи ссылок на ActionViews в каждый метод, где есть работа с Rx. А таких методов в проекте может быть бесконечное количество. Ещё и эти compose постоянно писать. Бууэээ. Кому это надо? Только трудолюбивым, упорным и не ленивым людям. Я не такой. Я поклонник лени и фанат написания красивого и удобного кода, поэтому было принято важное решение — любыми способами упростить код.


Точка прорыва


Спустя многочисленные переписывания механизма я пришёл вот к такому варианту работы:


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

Я переписывал свой механизм около 10-15 раз, и каждый раз он очень сильно отличался от предыдущего варианта. Я не стану вам показывать все версии, давайте сосредоточимся на двух финальных. Первый вы увидели только что.


Согласитесь, выглядит симпатично? Я бы даже сказал, очень симпатично. Я стремился к таким решениям. И абсолютно все наши ActionViews будут работать корректно в нужное нам время. Достичь этого я смог с помощью написания огромного количества не самого красивого кода. В классах, которые позволяют использовать такой механизм, содержится очень много сложных логик, и мне это не нравилось. Одним словом — конфетка, которая под капотом является монстром.





Такой код в будущем будет всё тяжелее и тяжелее поддерживать, да и сам он содержал достаточно серьёзные минусы и проблемы, которые были критичными:


  • Что будет, если на экране нужно отображать несколько LoadingView? Как разделять их? Как понять, какая LoadingView когда должна отображаться?
  • Нарушение концепции Rx — всё должно быть в одном потоке (stream). Здесь это не так.
  • Сложность кастомизации. Поведение и логики, которые описаны, очень сложно изменить конечному пользователю и, соответственно, сложно добавлять новые поведения.
  • Вы должны использовать кастомные View для работы механизма. Это нужно для того, чтобы механизм понимал, какая ActionView какому типу принадлежит. Например, если вы захотите использовать ProgressBar, то он обязательно должен содержать implements LoadingView.
  • id для наших ActionView должны совпадать с теми, что указаны в базовых классах, чтобы избавиться от boilerplate. Это не очень удобно, хоть и с этим можно смириться.
  • Рефлексия. Да, она тут была, и из-за неё механизм явно требовал оптимизации.

Разумеется, у меня были решения этих проблем, но все эти решения порождали другие проблемы. Я постарался максимально избавиться от самых критичных, и в итоге остались только необходимые требования к использованию библиотеки.


До свидания, Java!


Спустя какое-то время я сидел дома, баловался маялся дурью и вдруг я внезапно осознал — надо попробовать Kotlin и по-максимуму заюзать экстеншены, дефолтные значения, лямбды и делегаты.


Сперва он выглядел очень не очень. Но теперь он лишён практически всех недостатков, которые, в принципе, могут быть.


Вот так выглядит наш предыдущий код, но уже в финальном варианте:


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

Благодаря Extensions я смог сделать всю работу в одном потоке, не нарушая основной концепции реактивного программирования. Также я оставил возможность кастомизировать поведение. Если вы захотите изменить действие на старте или окончании показа загрузки, вы просто можете передать функцию в метод, и всё будет работать:


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

Также изменение поведения доступно и для других ActionViews. Если вы захотите использовать стандартное поведение, но при этом у вас не дефолтные ActionView, то можно просто указать, какая View должна заменить нашу ActionView:


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

Я показал вам самые сливки этого механизма, но и у него есть своя цена.
Во-первых, вам нужно будет создавать CustomViews для того, чтобы это работало:


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

   constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
}

Может быть, это даже не потребуется делать. На данный момент я собираю отзывы и принимаю предложения по улучшению данного механизма. Основная причина того, что нам необходимо использовать CustomViews — наследование от интерфейса, который указывает, какому типу ActionView она принадлежит. Это нужно для безопасности, так как вы можете случайно ошибиться при указании типа View в методе withActionsViews.


Так выглядит сам метод 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) }
) {
   /*реализация*/
}

Выглядит страшновато, но зато удобно и быстро! Как видите, во входных параметрах он принимает loadingView: LoadingView?.. Это страхует нас от ошибки с типом ActionView.


Соответственно, чтобы механизм заработал, нужно сделать несколько простых шагов:


  • Добавить в layout наши ActionView, которые являются кастомными. Некоторые из них я уже сделал, и вы можете просто их использовать.
  • Реализовать интерфейс HasActionsView и в коде переопределить default-переменные, которые отвечают за ActionViews:
    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) }
  • Или унаследоваться от класса, в котором уже переопределены наши ActionViews. В этом случае придётся использовать строго заданные id в ваших layout:


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

  • Наслаждаться работой без boilerplate!

Если будете использовать Kotlin Extensions, то не забывайте про то, что можно переименовать импорт в удобное для вас название:


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

Что дальше?


Когда я начинал работу над этим механизмом, я не думал о том, что из этого получится библиотека. Но так вышло, что я захотел поделиться своим творением, и теперь меня ждёт самое сладкое — публикация библиотеки, сбор issues, получение фидбека, добавление/улучшение функциональности и исправление багов.


Пока я писал статью...


Успел оформить всё в виде библиотек:



Библиотека и сам механизм не претендуют на звание must have в вашем проекте. Я лишь хотел поделиться своей идеей, выслушать критику, комментарии и улучшить свой механизм, чтобы он стал более удобным, используемым и практичным. Возможно, вы сможете сделать подобный механизм лучше, чем я. Буду только рад. Я искренне надеюсь, что моя статья вдохновила вас на создание чего-то своего, возможно, даже подобного и более лаконичного.


Если у вас есть пожелания и рекомендации по улучшению функциональности и работы самого механизма, буду рад выслушать их. Welcome в комментарии и, на всякий случай, мой Telegram: @tanchuev


P.S. Я получил огромное удовольствие от того, что я создал что-то полезное своими руками. Возможно, ActionViews не будет пользоваться спросом, но опыт и кайф от этого никуда не денутся.


P.P.S. Чтобы ActionViews превратился в полноценную используемую библиотеку, нужно собрать отзывы и, возможно, доработать функциональность или в корне изменить сам подход, если всё будет совсем плохо.


P.P.P.S. Если вы заинтересовались моей наработкой, то мы можем обсудить её лично 28 сентября в Москве на Международной конференции мобильных разработчиков MBLT DEV 2018. Кстати, early bird билеты уже заканчиваются!

Комментарии (9)


  1. Scrobot
    21.06.2018 16:05
    +1

    Я безусловно плюсанул, спасибо за статью! Но мне показалось на первый взгляд, что это попытка спрятать бойлер-плейт под другой бойлер-плейт.
    Зачем выносить все в набор ActionView, когда лучше это сделать ввиде сборного паттерна, прикрутив к твоей концепции Компоновщик и строитель какой-нить, и пусть человек сам бахает нужные ему ActionView. А также, на самом деле, при кейсе, когда нужно в зависимости от внешнего стейта обновить ActionView, при вашей реализации мне показалось на первый взгляд, что реактивность может пропасть. Этим ActionView нужно наблюдаемое состояние, чего я не увидел(мож проглядел). В любом случае, я поизучаю исходники повнимательнее и выскажу свой более подробный фидбэк) Может даже пул-реквест кину)


    1. tanchuev Автор
      21.06.2018 21:01

      Спасибо за отзыв) По поводу билдера я подумаю, просто он в концепцию Rx не очень вписывается, но это можно придумать как его вкрутить) По поводу что спрятал бойлер плейт не совсем так, базовые логики то написаны, они чаще всего(90% случаев) и используются, поэтому я стремился к какой-то шаблонизации, чтобы бойлерплейта было как можно меньше)


  1. Scrobot
    21.06.2018 16:07
    +1

    Кстати, ты предусмотрел не все кейсы ActionView) А что насчет LocationDisabled?) Тоже довольно распрастранненый) Да и в целом, всегда может быть что-то такое, чего ты не видишь. Поэтому, как я и сказал, нужен общий механизм


    1. tanchuev Автор
      21.06.2018 21:03

      Да, этот стейт надо будет добавить) Занесу в список, спасибо) И подумаю над тем как добавлять новые ActionViews


  1. eskander_service
    22.06.2018 10:54

    А почему на java не реализовали? Неужели котлин прям все проблемы решает?


    1. Scrobot
      23.06.2018 13:36

      Тут я за автора могу сказать так: с google i/o 2k17 kotlin стал оффициально поддерживаться гуглом в качестве языка разработки. Поэтому это полное право теперь авторов библиотек делать их на котлине. Да и Java андроидовская сильно ущербная. На Java 9/10 писать можно, но это в основном всякие бэк-сервисы и прочее, а андроидовский форк 6ой версии, ИМХО, не особо хорош.
      Котлин не серебрянная пуля, но во многом куда удобнее. Имеет свои плюсы и свои минусы. Как впрочем и любой другой язык)


      1. eskander_service
        23.06.2018 13:52

        Это я понимаю, но так как его продвигают он только отталкивает. К тому же я сомневаюсь что дядя гугл просто так это сделал) Впрочем время покажет. Да, и поддержку java никто не отменял), я уже почти 10 лет пишу под Android и не собираюсь менять шило на мыло, слишком много написано и поддерживать на НЕ языке я не собираюсь)
        Представьте Деннис Ритчи и Бьёрн Страуструп так впаривают свои ТВОРЕНИЯ и при этом хают всех кто старается работать на том на чём привык? Я встречал на собеседованиях много джунов, которых учили только котлину, как думаете, это перспективные разработчики ?)
        Поживём увидим конечно, я не против нового, но против любого навязывания, разработчик должен сам ставить приоритеты и выбирать более эффективные пути реализации. Пока я вижу только Очень навязчивую рекламу, как будто салом на базаре торгуют.


        1. Scrobot
          23.06.2018 13:59

          Ну, вы возможно забыли упомянуть, что у Java и Kotlin 100% обратная совместимость. Тут я проблем не вижу. Вы даже сами можете взять эту библиотеку и использовать ее в Java коде. Поэтому проблем нет.
          А вот касаемо вопроса «кто в чем привык». Есть один немаловажный момент. Не стоит относится к 1 языку как всеобъемному инструменту. Языков лучше знать несколько. Рекомендую ознакомится с ним в книге Роберта Мартина(Дядюшка боб, думаю имя должно быть знакомо) «Идеальный программист» ))


          1. eskander_service
            23.06.2018 14:11
            +1

            Согласен, не один а несколько, я когда учился, начинали с Assembler, C/C++ и только потом .Net, Java. Но нужно ещё понимать что Android не только для адептов котлина, и дело не в совместимости, дело в его навязчивом продвижении.
            Нельзя так жёстко давить, нельзя плодить тех кто не придерживается высказанному вами выше правилу «Языков лучше знать несколько», ведь приходят со «знанием» только котлина и внушением что кто пишет не на одном а на нескольких языках — НЕ ПРОГРАММИСТ ) Это реальный пример из опыта проведения собеседования в этом году))) Вот насмотришься такого и больше отвращения к котлину)
            Ну и я до сих пор, кроме хвальбы и слов, не увидел его преимуществ, вижу копию SWIFT (кстате многие из iosников переползает на дроиды именно под котлин, но это больше комедия, считают сходство языка — одинаковая платформа)))
            Ну да ладно, как и говорил — время покажет, много таких языков появлялось с шиком и пропадало тихо, а C/C++, Java пока остаются и развиваются, и так будет ещё очень долго и не только в десктопах и на бэке)