ActionViews o cómo no me gusta el repetitivo desde la infancia

Hola Habr! En este artículo quiero compartir la experiencia de crear mi propio mecanismo para automatizar la visualización de varios tipos de Vista: ContentView, LoadingView, NoInternetView, EmptyContentView, ErrorView.





Ha sido un largo camino. El camino de la prueba y el error, la enumeración de métodos y opciones, noches de insomnio y una experiencia invaluable que quiero compartir y escuchar críticas, que definitivamente tendré en cuenta.


Diré de inmediato que consideraré trabajar en RxJava, ya que para las corutinas no hice tal mecanismo, mis manos no llegaron. Y para otras herramientas similares (Loaders, AsyncTask, etc.), no tiene sentido usar mi mecanismo, ya que la mayoría de las veces se usa RxJava o corutinas.


Vistas de acción


Un colega mío dijo que era imposible estandarizar el comportamiento de la Vista, pero aún así intenté hacerlo. Y lo hizo


La pantalla de aplicación estándar, cuyos datos se toman del servidor, debe procesar al menos 5 estados:


  • Visualización de datos
  • Cargando
  • Error: cualquier error que no se describe a continuación
  • La falta de internet es un error global
  • Pantalla en blanco: solicitud aprobada, pero sin datos
  • Otro estado es que los datos se cargaron desde la memoria caché, pero la solicitud de actualización regresó con un error, es decir, mostrando datos obsoletos (mejor que nada) - La biblioteca no es compatible con esto.

En consecuencia, para cada estado de este tipo debe haber su propia Vista.


Yo llamo a estos View - ActionViews , porque responden a algún tipo de acción. De hecho, si puede determinar exactamente en qué punto se debe mostrar su Vista y cuándo ocultarla, entonces también puede ser una Vista de Acción.


Hay una (o quizás no una) forma estándar de trabajar con dicha Vista.


En los métodos que contienen trabajo con RxJava, debe agregar argumentos de entrada para todos los tipos de ActionViews y agregar algo de lógica a estas llamadas para mostrar y ocultar ActionViews, como se hace aquí:


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

Pero este método contiene una gran cantidad de repetitivo, y por defecto no nos gusta. Y así comencé a trabajar para reducir el código de rutina.


Subir de nivel


El primer paso para actualizar la forma estándar de trabajar con ActionViews fue reducir el estándar poniendo la lógica en las clases de utilidad. El siguiente código no fue inventado por mí. Soy plagio y lo espié a un colega sensato. Gracias, Arutar


Ahora nuestro código se ve así:


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

El código que vemos arriba, aunque carece de un código repetitivo, todavía no causa un deleite tan encantador. Ya ha mejorado mucho, pero sigue existiendo el problema de pasar enlaces a ActionViews en todos los métodos en los que hay trabajo con Rx. Y puede haber un número infinito de tales métodos en un proyecto. También escriba estos componen constantemente. Buueee ¿Quién necesita esto? Solo gente trabajadora, terca y no perezosa. No soy asi Soy un fanático de la pereza y un fanático de escribir código hermoso y conveniente, por lo que se tomó una decisión importante: simplificar el código de cualquier manera.


Punto de ruptura


Después de numerosas reescrituras del mecanismo, llegué a esta opción:


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

Reescribí mi mecanismo entre 10 y 15 veces, y cada vez era muy diferente de la versión anterior. No te mostraré todas las versiones, centrémonos en las dos últimas. El primero que acabas de ver.


De acuerdo, se ve bonito? Incluso diría que muy bonita. Me esforcé por esas decisiones. Y absolutamente todos nuestros ActionViews funcionarán correctamente en el momento que lo necesitemos. Pude lograr esto escribiendo una gran cantidad de no el código más hermoso. Las clases que permiten tal mecanismo contienen mucha lógica compleja, y no me gustó. En una palabra: cariño, que es un monstruo bajo el capó.





Tal código en el futuro será cada vez más difícil de mantener, y en sí contenía desventajas y problemas bastante serios que fueron críticos:


  • ¿Qué sucede si necesita mostrar múltiples LoadingViews en la pantalla? ¿Cómo separarlos? ¿Cómo entender qué LoadingView se debe mostrar cuando?
  • Violación del concepto de Rx: todo debe estar en una secuencia (secuencia). Este no es el caso aquí.
  • La complejidad de la personalización. El comportamiento y la lógica que se describen son muy difíciles de cambiar para el usuario final y, en consecuencia, es difícil agregar nuevos comportamientos.
  • Debe usar Vista personalizada para que el mecanismo funcione. Esto es necesario para que el mecanismo entienda qué ActionView pertenece a qué tipo. Por ejemplo, si desea utilizar ProgressBar, debe contener implementos LoadingView.
  • El id para nuestro ActionView debe coincidir con los especificados en las clases base para deshacerse de la repetitiva. Esto no es muy conveniente, aunque puede aceptarlo.
  • Reflexion Sí, ella estaba aquí, y gracias a ella, el mecanismo claramente requería optimización.

Por supuesto, tenía soluciones a estos problemas, pero todas estas soluciones dieron lugar a otros problemas. Traté de deshacerme de lo más crítico posible y, como resultado, solo quedaron los requisitos necesarios para usar la biblioteca.


¡Adiós Java!


Después de un tiempo, estaba sentado en casa incursionado en Estaba bromeando y de repente me di cuenta de que tenía que probar Kotlin y maximizar las extensiones, valores predeterminados, lambdas y delegados.


Al principio no se veía mucho. Pero ahora está privado de casi todas las deficiencias, que, en principio, pueden ser.


Aquí está nuestro código anterior, pero en la versión final:


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

Gracias a Extensiones, pude hacer todo el trabajo en un hilo sin violar el concepto básico de programación reactiva. También dejé la oportunidad de personalizar el comportamiento. Si desea cambiar la acción al principio o al final del programa, simplemente puede pasar la función al método, y todo funcionará:


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

Los cambios de comportamiento también están disponibles para otras ActionViews. Si desea utilizar un comportamiento estándar, pero no tiene ActionViews predeterminadas, simplemente puede especificar qué Vista debe reemplazar nuestra ActionView:


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

Te mostré la crema de este mecanismo, pero también tiene su propio precio.
Primero, necesitará crear CustomViews para que esto funcione:


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

Puede que ni siquiera sea necesario hacerlo. En este momento, estoy recopilando comentarios y aceptando sugerencias para mejorar este mecanismo. La razón principal por la que necesitamos usar CustomViews es heredar de una interfaz que indica a qué tipo de ActionView pertenece. Esto es por seguridad, ya que puede cometer un error accidentalmente al especificar el tipo de Vista en el método withActionsViews.


Así es como se ve el método 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) } ) { /**/ } 

¡Parece aterrador, pero conveniente y rápido! Como puede ver, en los parámetros de entrada acepta loadingView: LoadingView? .. Esto nos asegura contra errores con el tipo ActionView.


En consecuencia, para que el mecanismo funcione, debe seguir algunos pasos simples:


  • Agregue a nuestro diseño nuestras ActionViews, que son personalizadas. Ya hice algunos de ellos, y puedes usarlos.
  • Implemente la interfaz HasActionsView y anule las variables predeterminadas responsables de ActionViews en el código:
     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) } 
  • O herede de una clase en la que nuestras ActionViews ya están anuladas. En este caso, tendrá que usar la identificación estrictamente especificada en su diseño:


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

  • ¡Disfruta del trabajo sin repeticiones!

Si va a utilizar las extensiones de Kotlin, no olvide que puede cambiar el nombre de la importación a un nombre conveniente para usted:


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

Que sigue


Cuando comencé a trabajar en este mecanismo, no pensé de qué vendría una biblioteca. Pero sucedió que quería compartir mi creación, y ahora lo más dulce me está esperando: publicar la biblioteca, recopilar problemas, recibir comentarios, agregar / mejorar la funcionalidad y corregir errores.


Mientras escribía un artículo ...


Logré organizar todo en forma de bibliotecas:



La biblioteca y el mecanismo en sí no pretenden ser imprescindibles en su proyecto. Solo quería compartir mi idea, escuchar críticas, comentarios y mejorar mi mecanismo para que sea más conveniente, usado y práctico. Quizás puedas hacer un mecanismo así mejor que yo. Solo me alegraré. Espero sinceramente que mi artículo lo haya inspirado a crear algo propio, tal vez incluso similar y más conciso.


Si tiene alguna sugerencia y recomendación para mejorar la funcionalidad y el funcionamiento del mecanismo en sí, me complacerá escucharla. Bienvenido a los comentarios y, por si acaso, mi Telegram: @tanchuev


PD: Me complació mucho el hecho de que creé algo útil con mis propias manos. Quizás ActionViews no tendrá demanda, pero la experiencia y el zumbido de esto no irán a ninguna parte.


PPS Para que ActionViews se convierta en una biblioteca usada completa, debe recopilar comentarios y, posiblemente, refinar la funcionalidad o cambiar fundamentalmente el enfoque en sí mismo si todo sale realmente mal.


PPPS Si está interesado en mi trabajo, podemos discutirlo personalmente el 28 de septiembre en Moscú en la Conferencia Internacional de Desarrolladores Móviles MBLT DEV 2018 . Por cierto, ¡las entradas anticipadas ya se están agotando!

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


All Articles