Olá Habr! Neste artigo, quero compartilhar a experiência de criar meu próprio mecanismo para automatizar a exibição de vários tipos de exibição: ContentView, LoadingView, NoInternetView, EmptyContentView, ErrorView.

Foi um longo caminho. O caminho da tentativa e erro, enumeração de métodos e opções, noites sem dormir e experiência inestimável que quero compartilhar e ouvir críticas, as quais definitivamente irei levar em consideração.
Eu direi imediatamente que considerarei trabalhar no RxJava, pois para as corotinas não fiz esse mecanismo - minhas mãos não alcançaram. E para outras ferramentas semelhantes (Loaders, AsyncTask, etc.), não faz sentido usar meu mecanismo, pois na maioria das vezes são RxJava ou coroutines que são usados.
Actionviews
Um colega meu disse que era impossível padronizar o comportamento do View, mas eu ainda tentei fazê-lo. E fez isso.
A tela padrão do aplicativo, cujos dados são retirados do servidor, deve processar pelo menos 5 estados:
- Exibição de dados
- A carregar
- Erro - qualquer erro não descrito abaixo
- Falta de internet é um erro global
- Tela em branco - solicitação aprovada, mas nenhum dado
Outro estado é que os dados foram carregados do cache, mas a solicitação de atualização retornou com um erro, ou seja, mostrando dados desatualizados (melhor que nada) - A biblioteca não suporta isso.
Consequentemente, para cada um desses estados deve haver sua própria visão.
Eu os chamo de View - ActionViews , porque eles respondem a algum tipo de ação. De fato, se você pode determinar exatamente em que ponto sua exibição deve ser exibida e quando ocultar, também pode ser um ActionView.
Existe uma (ou talvez não) uma maneira padrão de trabalhar com essa Visualização.
Nos métodos que contêm o trabalho com o RxJava, você precisa adicionar argumentos de entrada para todos os tipos de ActionViews e adicionar alguma lógica a essas chamadas para mostrar e ocultar os ActionViews, como é feito aqui:
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()); } ); }
Mas esse método contém uma quantidade enorme de clichê, e por padrão não gostamos. E então comecei a trabalhar na redução do código de rotina.
Subir de nível
A primeira etapa na atualização da maneira padrão de trabalhar com o ActionViews foi reduzir o padrão, colocando a lógica nas classes de utilitário. O código abaixo não foi inventado por mim. Sou plágio e espiei isso em um colega sensato. Obrigado Arutar
Agora, nosso código fica assim:
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)); }
O código que vemos acima, embora desprovido de um código padrão, ainda não causa um prazer tão encantador. Já ficou muito melhor, mas continua a haver o problema de passar links para o ActionViews em todos os métodos em que há trabalho com o Rx. E pode haver um número infinito desses métodos em um projeto. Escreva também estas composições constantemente. Buueee. Quem precisa disso? Apenas pessoas trabalhadoras, teimosas e não preguiçosas. Eu não sou assim. Sou fã da preguiça e sou fã de escrever códigos bonitos e convenientes, por isso foi tomada uma decisão importante - simplificar o código de qualquer maneira.
Ponto de avanço
Após inúmeras reescritas do mecanismo, cheguei a esta opção:
public void getSomeData() { execute(() -> mApi.getProjects(), new BaseSubscriber<>(response -> { })); }
Reescrevi meu mecanismo cerca de 10 a 15 vezes e, a cada vez, era muito diferente da versão anterior. Não vou mostrar todas as versões, vamos nos concentrar nas duas finais. O primeiro que você acabou de ver.
Concordo, parece bonito? Eu diria mesmo muito bonita. Eu lutei por essas decisões. E absolutamente todas as nossas ActionViews funcionarão corretamente no momento em que precisamos. Consegui fazer isso escrevendo uma quantidade enorme de códigos não tão bonitos. As classes que permitem esse mecanismo contêm muita lógica complexa, e eu não gostei. Em uma palavra - querida, que é um monstro sob o capô.

Esse código no futuro será cada vez mais difícil de manter, e ele próprio contém desvantagens e problemas bastante sérios, críticos:
- O que acontece se você precisar exibir vários LoadingViews na tela? Como separá-los? Como entender qual LoadingView deve ser exibido quando?
- Violação do conceito de Rx - tudo deve estar em um fluxo (fluxo). Este não é o caso aqui.
- A complexidade da personalização. O comportamento e a lógica descritos são muito difíceis de mudar para o usuário final e, portanto, é difícil adicionar novos comportamentos.
- Você deve usar o modo de exibição personalizado para que o mecanismo funcione. Isso é necessário para que o mecanismo entenda qual ActionView pertence a qual tipo. Por exemplo, se você quiser usar o ProgressBar, ele deverá conter os implementos LoadingView.
- O ID do nosso ActionView deve corresponder ao especificado nas classes base para se livrar do padrão. Isso não é muito conveniente, embora você possa aceitar isso.
- Reflexão Sim, ela estava aqui e, por causa dela, o mecanismo claramente exigia otimização.
Obviamente, eu tinha soluções para esses problemas, mas todas essas soluções deram origem a outros problemas. Tentei me livrar do mais crítico possível e, como resultado, apenas os requisitos necessários para o uso da biblioteca permaneceram.
Adeus Java!
Depois de algum tempo, eu estava sentado em casa, envolvido em Eu estava brincando e de repente percebi que tinha que experimentar o Kotlin e maximizar extensões, valores padrão, lambdas e delegados.
No começo, ele não parecia muito. Mas agora ele está privado de quase todas as deficiências que, em princípio, podem ser.
Aqui está o nosso código anterior, mas na versão final:
fun getSomeData() { api.getProjects() .withActionViews(view) .execute(onComplete = { }) }
Graças ao Extensions, pude fazer todo o trabalho em um thread sem violar o conceito básico de programação reativa. Também deixei a oportunidade de personalizar o comportamento. Se você deseja alterar a ação no início ou no final do programa, basta passar a função para o método, e tudo funcionará:
fun getSomeData() { api.getProjects() .withActionViews( view, doOnLoadStart = { }, doOnLoadEnd = { }) .execute(onComplete = { }) }
Alterações comportamentais também estão disponíveis para outros ActionViews. Se você deseja usar o comportamento padrão, mas não possui ActionViews padrão, pode simplesmente especificar qual View deve substituir nosso ActionView:
fun getSomeData(projectLoadingView: LoadingView) { mApi.getPosts(1, 1) .withActionViews( view, loadingView = projectLoadingView ) .execute(onComplete = { }) }
Eu mostrei a você mesmo esse mecanismo, mas também tem seu próprio preço.
Primeiro, você precisará criar CustomViews para que isso funcione:
class SwipeRefreshLayout : android.support.v4.widget.SwipeRefreshLayout, LoadingView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) }
Pode nem ser necessário fazê-lo. No momento, estou coletando comentários e aceitando sugestões para melhorar esse mecanismo. O principal motivo pelo qual precisamos usar o CustomViews é herdar de uma interface que informa a qual tipo de ActionView ele pertence. Isso é por segurança, pois você pode cometer um erro acidental ao especificar o tipo de exibição no método withActionsViews.
É assim que o método withActionsViews se parece:
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 assustador, mas conveniente e rápido! Como você pode ver, nos parâmetros de entrada ele aceita loadingView: LoadingView? .. Isso protege contra erros com o tipo ActionView.
Portanto, para que o mecanismo funcione, você precisa executar algumas etapas simples:
- Adicione ao nosso layout nossos ActionViews, que são personalizados. Eu já fiz alguns deles, e você pode apenas usá-los.
- Implemente a interface HasActionsView e substitua as variáveis padrão responsáveis por ActionViews no 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) }
Ou herdar de uma classe na qual nossos ActionViews já foram substituídos. Nesse caso, você precisará usar o ID estritamente especificado em seu 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) } }
- Aproveite o trabalho sem um clichê!
Se você usar as extensões Kotlin, não esqueça que pode renomear a importação para um nome conveniente:
import kotlinx.android.synthetic.main.fr_gifts.contentView as recyclerView
O que vem a seguir?
Quando comecei a trabalhar nesse mecanismo, não pensei no que seria uma biblioteca. Mas aconteceu que eu queria compartilhar minha criação, e agora o mais legal está me esperando - publicar a biblioteca, coletar problemas, receber feedback, adicionar / melhorar a funcionalidade e corrigir bugs.
Enquanto escrevia um artigo ...
Eu consegui organizar tudo na forma de bibliotecas:
A biblioteca e o próprio mecanismo não afirmam ser obrigatórios em seu projeto. Eu só queria compartilhar minha ideia, ouvir críticas, comentários e melhorar meu mecanismo para que ele se torne mais conveniente, usado e prático. Talvez você possa fazer esse mecanismo melhor do que eu. Eu ficarei feliz apenas. Espero sinceramente que meu artigo tenha inspirado você a criar algo próprio, talvez até semelhante e mais conciso.
Se você tiver alguma sugestão e recomendação para melhorar a funcionalidade e a operação do próprio mecanismo, ficarei feliz em ouvi-las. Bem-vindo aos comentários e, apenas por precaução, ao meu telegrama: @tanchuev
PS: Gostei muito de criar algo útil com minhas próprias mãos. Talvez o ActionViews não seja uma demanda, mas a experiência e o zumbido disso não serão levados a lugar algum.
PPS Para que o ActionViews se transforme em uma biblioteca usada completa, você precisa coletar feedback e, possivelmente, refinar a funcionalidade ou alterar fundamentalmente a abordagem em si, se tudo der errado.
PPPS Se você estiver interessado no meu trabalho, poderemos discuti-lo pessoalmente em 28 de setembro em Moscou na MBLT DEV 2018 International Mobile Developers Conference . A propósito, os ingressos para madrugadores já estão acabando!