ActionViews或我从小就不喜欢样板

哈Ha! 在本文中,我想分享创建用于自动显示各种视图类型的机制的经验:ContentView,LoadingView,NoInternetView,EmptyContentView,ErrorView。





这已经很长了。 我想分享和听到批评的反复试验,方法和选择的列举,不眠之夜和宝贵的经验,我一定会考虑到这些。


我将立即说我将考虑使用RxJava,因为对于协程,我没有使用这种机制-我的双手没有伸开。 对于其他类似工具(加载程序,AsyncTask等),使用我的机制没有任何意义,因为最经常使用的是RxJava或协程。


动作视图


我的一位同事说,不可能对View的行为进行标准化,但我仍在努力做到这一点。 做到了。


标准应用程序屏幕(其数据来自服务器)至少应处理5个状态:


  • 数据显示
  • 载入中
  • 错误-以下未描述的任何错误
  • 缺乏互联网是全球性的错误
  • 黑屏-请求已通过,但无数据
  • 另一个状态是数据是从缓存中加载的,但是更新请求返回了错误,即显示过时的数据(总比没有好) -该库不支持此功能。

因此,对于每个这样的状态,应该有自己的视图。


我称它们为View- ActionViews ,因为它们响应某种动作。 实际上,如果您可以确定要在什么位置显示视图以及何时隐藏视图,那么它也可以是ActionView。


使用这种视图有一种(或可能不是一种)标准方法。


在包含使用RxJava的方法中,您需要为所有类型的ActionView添加输入参数,并向这些调用添加一些逻辑以显示和隐藏ActionView,如此处所示:


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

但是此方法包含大量样板,默认情况下我们不喜欢它。 因此,我开始着手减少例程代码。


升级


升级使用ActionView的标准方法的第一步是通过将逻辑放入实用程序类来减少样板。 以下代码不是我发明的。 我是一个抄袭者,并在一个明智的同事身上进行监视。 感谢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)); } 

我们上面看到的代码虽然没有样板代码,但仍然不会引起这种迷人的愉悦。 它已经变得更好,但是仍然存在在Rx可以使用的每种方法中将链接传递到ActionView的问题。 一个项目中可以有无数种这样的方法。 也要不断地写这些组成。 布埃 谁需要这个? 只有勤奋,固执而不懒惰的人。 我不是那样的。 我是懒惰的爱好者,也是喜欢编写美观且方便的代码的爱好者,因此做出了一个重要的决定-通过任何方式简化代码。


突破点


在对该机制进行了无数次重写之后,我来到了这个选项:


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

我重写了大约10到15次的机制,每次都与以前的版本有很大不同。 我不会向您展示所有版本,让我们集中讨论最后两个版本。 您刚刚看到的第一个。


同意,看起来很漂亮吗? 我什至会说很漂亮。 我为这样的决定而努力。 绝对所有的ActionView都可以在需要时正常工作。 我能够通过编写大量并非最漂亮的代码来实现这一目标。 允许这种机制的类包含很多复杂的逻辑,我不喜欢它。 一句话-亲爱的,这是一个引擎盖下的怪物。





将来,这样的代码将越来越难维护,它本身包含了非常严重的不利条件和至关重要的问题:


  • 如果需要在屏幕上显示多个LoadingViews,该怎么办? 如何分开? 如何理解何时应显示哪个LoadingView?
  • 违反Rx的概念-一切都应该在一个流(流)中。 这里不是这种情况。
  • 定制的复杂性。 对于最终用户而言,所描述的行为和逻辑很难更改,因此,很难添加新的行为。
  • 您必须使用自定义视图才能使该机制起作用。 这是必需的,以便该机制了解哪个ActionView属于哪种类型。 例如,如果要使用ProgressBar,则它必须包含工具LoadingView。
  • 我们的ActionView的ID必须与基类中指定的ID匹配,才能摆脱样板。 尽管您可以对此感到满意,但这不是很方便。
  • 倒影 是的,她在这里,由于她的原因,该机制显然需要优化。

当然,我对这些问题有解决方案,但是所有这些解决方案都带来了其他问题。 我试图尽可能地摆脱最关键的问题,因此,仅保留了使用该库的必要要求。


再见Java!


过了一段时间,我坐在家里, 涉足 我无所事事,突然间我意识到我必须尝试Kotlin并最大化扩展名,默认值,lambda和委托。


起初他看上去并不多。 但是现在它几乎没有原则上可能存在的所有缺点。


这是我们以前的代码,但在最终版本中:


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

多亏了扩展,我才能够在一个线程中完成所有工作,而不会违反反应式编程的基本概念。 我还留下了自定义行为的机会。 如果您想在节目开始或结束时更改操作,只需将函数传递给方法即可,一切都会正常进行:


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

行为更改也可用于其他ActionView。 如果要使用标准行为,但没有默认的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类型。 这是出于安全性考虑,因为在withActionsViews方法中指定View类型时,您可能会偶然出错。


这是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类型的错误。


因此,为了使该机制起作用,您需要采取一些简单的步骤:


  • 将自定义的ActionView添加到我们的布局中。 我已经制作了其中一些,您可以使用它们。
  • 实现HasActionsView接口,并在代码中覆盖负责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) } 
  • 或者从已经重写我们的ActionView的类继承。 在这种情况下,您将必须在布局中使用严格指定的ID:


     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扩展,请不要忘记可以将导入重命名为方便的名称:


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

接下来是什么?


当我开始研究这种机制时,我没有想到图书馆会变成什么样。 但是碰巧我想分享自己的创作,现在最甜蜜的事情在等待我-发布库,收集问题,接收反馈,添加/改进功能以及修复错误。


当我写一篇文章时...


我设法以库的形式安排了一切:



库和机制本身并不声称是您项目中必须具备的。 我只是想分享我的想法,听取批评,评论并改善我的机制,使其变得更加方便,实用和实用。 也许您可以使这种机制比我更好。 我只会很高兴。 我衷心希望我的文章启发您创建自己的东西,也许甚至相似并且更简洁。


如果您有任何改进机制本身的功能和操作的建议,我将很高兴听取他们的意见。 欢迎评论,以防万一我的电报: @tanchuev


PS:我用自己的双手创造了一些有用的东西,对此我感到非常高兴。 也许不需要ActionViews,但是经验和嗡嗡声将无处可寻。


PPS为了使ActionView成为完整的二手库,您需要收集反馈,并可能会改进一切功能,或者在一切都变得很糟糕的情况下从根本上改变方法本身。


PPPS如果您对我的工作感兴趣,那么我们可以9月28日在莫斯科举行的MBLT DEV 2018国际移动开发者大会上亲自讨论它。 顺便说一句,早鸟票已经用完了!

Source: https://habr.com/ru/post/zh-CN414773/


All Articles