MVVM + RxJava: erros comuns
MVVM + RxJava é uma ótima fórmula para uma arquitetura de aplicativo. Na atualização, reconhecemos isso e o usamos em nosso aplicativo, tornando-o escalonável e sustentável. Apesar disso, tivemos que aprender algumas lições da maneira mais difícil.
Nesta postagem do blog, estarei compartilhando dois desses aprendizados usando os cenários reais específicos que os inspiraram. Como resultado, vou me aprofundar nos detalhes do código. Agora, se você estiver com pouco tempo, aqui estão as duas opções:
- Exponha estados em vez de eventos.
- Tudo deve passar pelo modelo de visualização.
Se você está pronto para aprender mais sobre “como e por quê”, vamos lá!
Expor estados e não eventos
O principal recurso do Upday é mostrar as notícias ao usuário de uma maneira fácil de ler e fluida. Com esta especificação o ViewPager parecia uma boa escolha, poderíamos apresentar as novidades em forma de cartões um após o outro. Mais especificamente, precisamos implementar os seguintes comportamentos:
- Role para uma determinada posição.
- Substitua/atualize os elementos no ViewPager.
- Substitua/atualize os elementos no ViewPager E role para uma determinada posição.
Lendo esses requisitos, parece muito natural ter um fluxo Rx com a posição e um fluxo Rx diferente com o conjunto de cartas. Tudo o que precisamos fazer é expor esses dois fluxos no modelo de visualização para que o Fragment possa se inscrever neles e emitir os eventos recebidos para o adaptador e / ou ViewPager.
Portanto, este é o plano: fazer o processamento da posição e do conjunto de dados separadamente em threads de fundo dentro do modelo de visualização, expor esses fluxos e inscrevê-los no Fragment, mudar para o thread principal apenas para receber os eventos e modificar as visualizações . Em teoria, isso deve funcionar, estamos separando a lógica de negócios da lógica relacionada ao framework muito bem, estamos tornando o Fragment muito burro para que possamos testar a unidade da maior parte da lógica por meio do modelo de exibição. Também estamos fazendo todo o processamento em threads de fundo, apenas usando a thread principal quando for necessário. O que poderia dar errado?
public class NewsFragment extends Fragment { private NewsViewModel mViewModel; private ViewPagerAdapter mAdapter; private ViewPager mViewPager; private final CompositeSubscription mSubscription = new CompositeSubscription(); // Set up fragment (onCreate, etc.) @Override public void onResume() { super.onResume(); mSubscription.add(subscribeToPositionChanges()); mSubscription.add(subscribeToCardChanges()); } private Subscription subscribeToPositionChanges() { return mViewModel.getPositionStream() .observeOn(AndroidSchedulers.mainThread()) .subscribe(mViewPager::setCurrentItem, this::handleError); } private Subscription subscribeToCardChanges() { return mViewModel.getSetOfCardsStream() .observeOn(AndroidSchedulers.mainThread()) .subscribe(mAdapter::update, this::handleError); } @Override public void onPause() { mSubscription.clear(); super.onPause(); } }
Fizemos exatamente isso e começamos a receber relatórios de bug indicando um estado final errado no ViewPager. Esses tipos de bugs costumam ter a temida característica de não serem 100% reproduzíveis. Tivemos que enfrentar a realidade brutal: tínhamos condições de corrida, mas por quê? Temos uma arquitetura bacana usando MVVM, tudo é testado na unidade e usamos RxJava para enviar eventos.
A melhor maneira de explicar o que estava acontecendo é com um exemplo. Então, imagine que inicialmente temos um conjunto de dados com 5 itens no ViewPager e a posição real é 3.
Agora, o usuário executa uma ação e o estado final esperado é ter 9 elementos no ViewPager centralizados na posição 7. Todos os eventos RxJava disparam após a ação do usuário, não temos nenhum controle sobre quando as coisas acontecem, ingenuamente, nós apenas sente-se e torça para que tudo esteja configurado corretamente e, de alguma forma, tudo dê certo no final. Mas isso não acontece. Na verdade, em alguns casos, o fluxo de posição emite um 7 antes que o fluxo do conjunto de dados emita os 9 elementos. O evento de posição é capturado pelo fragmento que diz ao ViewPager para mover para a posição 7, mas ele tem apenas 5 elementos, como pode mover para a posição 7? Não pode, então simplesmente ignora o comando. Não falha, não permite que você saiba de forma alguma.
Logo em seguida, ocorre o evento do conjunto de dados, mas já é tarde demais, embora o adaptador vá substituir o conjunto de dados, o ViewPager não ficará centralizado na posição correta.
Claro, nem sempre é o caso, às vezes o evento do conjunto de dados virá primeiro e tudo funcionará conforme o esperado. Isso se deve à natureza assíncrona de nossa arquitetura. O modelo de visualização está fazendo o processamento dos dados em threads de fundo, portanto, quando a ação do usuário vier, a posição e o conjunto de dados serão processados sem garantia de qual terminará primeiro.
Afinal, não foi uma boa ideia expor dois fluxos paralelos com eventos. O que nós devemos fazer então? A resposta é simples, exponha um fluxo por visualização que emite estados em vez de eventos. A posição e o conjunto de dados devem ser agrupados para que o ViewPager nunca receba um sem o outro. Isso é verdadeiro para qualquer exibição com estado intra-dependente. Você nunca exporia dois fluxos separados para um TextView, um que define o texto e outro que emite a posição da letra que deve ser destacada em negrito, mas por algum motivo é muito mais fácil cometer esse erro com um ViewPager ou listas.
Tudo passa pelo modelo de visualização
Às vezes, o upday recebe notícias de última hora na forma de notificações push para que o usuário saiba imediatamente que algo importante aconteceu no mundo. O comportamento esperado quando o usuário toca na notificação é abrir o upday e mostrar o cartão das últimas notícias. Praticamente, isso significa abrir o fragmento de notícias principais e centralizar seu ViewPager na posição das notícias de última hora. Normalmente, esse tipo de notícia é colocado na primeira posição, então, para simplificar, vamos assumir que tudo o que precisamos fazer é definir a posição do ViewPager como 0.
Temos um mecanismo para capturar as ações do usuário nas notificações push que as transformam em um stream Rx, então por que não assinar diretamente no Fragment? A operação aqui é trivial: quando o fluxo emite um evento, o ViewPager deve apenas rolar para a posição 0, não há lógica ou transformação intermediária que precise ser testada na unidade.
breakingNewsStream().observeOn(AndroidSchedulers.mainThread()) .subscribe(event -> mViewPager.setCurrentItem(0), this::handleError);
Em primeiro lugar, isso já está quebrando a regra anterior de estados em vez de eventos, uma vez que define apenas a posição, mas mesmo que tivéssemos um fluxo de estados do ViewPager, ainda estaria errado. A razão é que o modelo de exibição não está ciente do que acabou de acontecer e poderíamos ter outras coisas dependendo do que o modelo de exibição diz que é o estado atual. Portanto, a próxima etapa natural aqui seria notificar o modelo de visualização sobre o que acabou de acontecer.
breakingNewsStream().observeOn(AndroidSchedulers.mainThread()) .subscribe(event -> { mViewPager.setCurrentItem(0); mViewModel.notifyCurrentPosition(0); }, this::handleError);
Neste ponto, podemos pensar que fizemos um ótimo trabalho, resolvemos o problema. O modelo de visualização agora sabe o que está acontecendo e podemos lidar com essa mudança de estado adequadamente. Mas, novamente, a realidade é bem diferente e, ao fazer isso, apenas atiramos no próprio pé. Se alguma vez adicionarmos outros efeitos com base no que o modelo de visualização pensa que é o estado do ViewPager, então criamos novamente uma condição de corrida. Há uma janela de tempo em que o modelo de visualização foi “notificado”, mas o estado ainda não foi processado devido à natureza assíncrona dos eventos.
O problema aqui é que não seguimos o padrão natural de nossa arquitetura MVVM. O modelo de visualização transforma os dados em algo fácil de usar nas Visualizações ou em qualquer outro consumidor, isso significa que a visualização deve saber sobre as mudanças sempre após o modelo de visualização. Neste exemplo, a View sabe sobre as mudanças antes do modelo da vista, tornando o último não confiável.
Então, basicamente, não importa o quão trivial ou fácil uma operação seja, tudo deve passar pelo modelo de visualização, desta forma outras coisas que são baseadas no estado do modelo de visualização podem acontecer de forma confiável.