- PVSM.RU - https://www.pvsm.ru -
Привет! В данной статье я хочу поделиться опытом создания своего механизма для автоматизации показа различных View типа: ContentView, LoadingView, NoInternetView, EmptyContentView, ErrorView.
Это был долгий путь. Путь проб и ошибок, перебор методов и вариантов, бессонные ночи и бесценный опыт, которым я хочу поделиться, и услышать критику, которую я обязательно приму во внимание.
Скажу сразу, что буду рассматривать работу на RxJava, так как для coroutines я не делал подобного механизма — не дошли руки. А для других подобных инструментов (Loaders, AsyncTask и так далее) нет смысла использовать мой механизм, так как чаще всего применяется именно RxJava или coroutines.
Один мой коллега сказал, что невозможно шаблонизировать поведение 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());
}
);
}
Но такой способ содержит огромное количество бойлерплейта, а по умолчанию мы его не любим. И поэтому я начал работу над сокращением рутинного кода.
Первым этапом модернизации стандартного способа для работы с ActionViews стало сокращение бойлерплейта путем вынесения логики в утильные классы. Код ниже придумал не я. Я — плагиатор и подсмотрел это у одного толкового коллеги. Спасибо, Arutar [1]!
Теперь наш код выглядит так:
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 будут работать корректно в нужное нам время. Достичь этого я смог с помощью написания огромного количества не самого красивого кода. В классах, которые позволяют использовать такой механизм, содержится очень много сложных логик, и мне это не нравилось. Одним словом — конфетка, которая под капотом является монстром.
Такой код в будущем будет всё тяжелее и тяжелее поддерживать, да и сам он содержал достаточно серьёзные минусы и проблемы, которые были критичными:
Разумеется, у меня были решения этих проблем, но все эти решения порождали другие проблемы. Я постарался максимально избавиться от самых критичных, и в итоге остались только необходимые требования к использованию библиотеки.
Спустя какое-то время я сидел дома, маялся дурью и вдруг я внезапно осознал — надо попробовать 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.
Соответственно, чтобы механизм заработал, нужно сделать несколько простых шагов:
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) }
}
Если будете использовать Kotlin Extensions, то не забывайте про то, что можно переименовать импорт в удобное для вас название:
import kotlinx.android.synthetic.main.fr_gifts.contentView as recyclerView
Когда я начинал работу над этим механизмом, я не думал о том, что из этого получится библиотека. Но так вышло, что я захотел поделиться своим творением, и теперь меня ждёт самое сладкое — публикация библиотеки, сбор issues, получение фидбека, добавление/улучшение функциональности и исправление багов.
Успел оформить всё в виде библиотек:
Библиотека и сам механизм не претендуют на звание must have в вашем проекте. Я лишь хотел поделиться своей идеей, выслушать критику, комментарии и улучшить свой механизм, чтобы он стал более удобным, используемым и практичным. Возможно, вы сможете сделать подобный механизм лучше, чем я. Буду только рад. Я искренне надеюсь, что моя статья вдохновила вас на создание чего-то своего, возможно, даже подобного и более лаконичного.
Если у вас есть пожелания и рекомендации по улучшению функциональности и работы самого механизма, буду рад выслушать их. Welcome в комментарии и, на всякий случай, мой Telegram: @tanchuev [4]
P.S. Я получил огромное удовольствие от того, что я создал что-то полезное своими руками. Возможно, ActionViews не будет пользоваться спросом, но опыт и кайф от этого никуда не денутся.
P.P.S. Чтобы ActionViews превратился в полноценную используемую библиотеку, нужно собрать отзывы и, возможно, доработать функциональность или в корне изменить сам подход, если всё будет совсем плохо.
P.P.P.S. Если вы заинтересовались моей наработкой, то мы можем обсудить её лично 28 сентября в Москве на Международной конференции мобильных разработчиков MBLT DEV 2018 [5]. Кстати, early bird билеты уже заканчиваются!
Автор: tanchuev
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/283612
Ссылки в тексте:
[1] Arutar: https://habr.com/users/arutar/
[2] ActionViews-ViewModel: https://github.com/tanchuev/ActionViews-ViewModel
[3] ActionViews-MVP: https://github.com/tanchuev/ActionViews-MVP
[4] @tanchuev: https://t.me/tanchuev
[5] MBLT DEV 2018: https://mbltdev.ru/ru?utm_source=habr_marat&utm_medium=ActionViews
[6] Источник: https://habr.com/post/414773/?utm_campaign=414773
Нажмите здесь для печати.