
Nos últimos dois anos, os desenvolvedores do Android no Badoo percorreram um caminho longo e espinhoso do MVP para uma abordagem completamente diferente da arquitetura de aplicativos.
ANublo e
eu queremos compartilhar uma tradução de um artigo do nosso colega
Zsolt Kocsi , descrevendo os problemas que encontramos e sua solução.
Este é o primeiro de vários artigos dedicados ao desenvolvimento da arquitetura MVI moderna no Kotlin.
Vamos começar do começo: problemas de estado
A todo momento, o aplicativo tem um determinado estado que determina seu comportamento e o que o usuário vê. Se você se concentrar apenas em algumas classes, esse estado incluirá todos os valores das variáveis - de simples sinalizadores a objetos individuais. Cada uma dessas variáveis tem vida própria e é controlada por diferentes partes do código. Você pode determinar o estado atual do aplicativo apenas verificando todos eles um por um.
Trabalhando no código, criamos um modelo existente do trabalho do sistema em nossas cabeças. Implementamos facilmente casos ideais quando tudo corre conforme o planejado, mas não conseguimos calcular todos os possíveis problemas e condições do aplicativo. E mais cedo ou mais tarde, uma das condições que não imaginamos nos ultrapassará e encontraremos um bug.
Inicialmente, o código é escrito de acordo com nossas idéias sobre como o sistema deve funcionar. Mas, no futuro, passando pelos
cinco estágios da depuração , é necessário refazer tudo dolorosamente, mudando simultaneamente o modelo do sistema já criado que se desenvolveu na minha cabeça. Resta esperar que, mais cedo ou mais tarde, possamos entender o que deu errado e o bug será corrigido.
Mas isso está longe de sempre ter sorte. Quanto mais complexo o sistema, maior a probabilidade de encontrar algum estado imprevisto, cuja depuração será um sonho por muito tempo em pesadelos.
No Badoo, todos os aplicativos são substancialmente assíncronos - não apenas devido à ampla funcionalidade disponível para o usuário através da interface do usuário, mas também pela possibilidade de envio de dados unidirecionais pelo servidor. O estado e o comportamento do aplicativo são influenciados muito - desde a alteração do status do pagamento até novas correspondências e solicitações de verificação.
Como resultado, em nosso módulo de bate-papo, encontramos vários bugs estranhos e difíceis de reproduzir que estragavam muito sangue para todos. Às vezes, os testadores conseguiam anotá-las, mas elas não eram repetidas no dispositivo do desenvolvedor. Devido ao código assíncrono, a repetição completa de uma cadeia de eventos era extremamente improvável. E como o aplicativo não travou, nem tínhamos um rastreamento de pilha que mostrasse por onde iniciar a pesquisa.
A Arquitetura Limpa também não conseguiu nos ajudar. Mesmo depois de reescrevermos o módulo de bate-papo, os testes A / B revelaram discrepâncias pequenas, porém significativas, no número de mensagens dos usuários usando os módulos novos e antigos. Decidimos que isso se devia à difícil reprodutibilidade dos bugs e ao estado da corrida. A discrepância persistiu após a verificação de todos os outros fatores. Os interesses da empresa sofreram, foi difícil para os desenvolvedores manterem o código.
Você não pode liberar um novo componente se ele funcionar pior que o existente, mas também não pode liberá-lo - já que ele foi atualizado, houve um motivo. Portanto, você precisa entender por que, em um sistema que parece completamente normal e não falha, o número de mensagens diminui.
Por onde começar a pesquisa?
Spoiler: isso não é culpa da Arquitetura Limpa - como sempre, o fator humano é o culpado. No final, é claro, corrigimos esses bugs, mas gastamos muito tempo e esforço nisso. Então pensamos: existe uma maneira mais fácil de evitar esses problemas?
A luz no fim do túnel ...
Termos modernos como Model-View-Intent e "unidirectional data flow" são familiares para nós. Se esse não for o seu caso, aconselho a pesquisá-los no Google - há muitos artigos sobre esses tópicos na Internet. Os desenvolvedores do Android recomendam especialmente
o material de oito peças de Hannes Dorfman .
Começamos a brincar com essas idéias retiradas do desenvolvimento da Web no início de 2017. Abordagens como Flux e Redux acabaram sendo muito úteis - elas nos ajudaram a lidar com muitos problemas.
Antes de tudo, é muito útil conter todos os elementos de estado (variáveis que afetam a interface do usuário e disparam várias ações) em um objeto -
Estado . Quando tudo é armazenado em um só lugar, a imagem geral fica melhor visível. Por exemplo, se você deseja imaginar o carregamento de dados usando essa abordagem, precisará dos campos de
carga útil e
isLoading . Observando-os, você verá quando os dados são recebidos (
carga útil ) e se a animação (
isLoading ) é mostrada ao usuário.
Além disso, se nos afastarmos da execução de código paralelo com retornos de chamada e expressarmos alterações no estado do aplicativo como uma série de transações, obteremos um único ponto de entrada. Apresentamos o
Redutor , que veio até nós da programação funcional. Ele pega o estado atual e os dados em outras ações (
Intent ) e cria um novo estado a partir deles:
Reducer = (State, Intent) -> State
Continuando o exemplo anterior com o carregamento de dados, obtemos as seguintes ações:
- IniciadoCarregando
- FinishedWithSuccess
Em seguida, você pode criar o Redutor com as seguintes regras:
- No caso de StartedLoading, crie um novo objeto State copiando o antigo e defina o valor isLoading como true.
- No caso de FinishedWithSuccess, crie um novo objeto State , copiando o antigo, no qual o valor isLoading será definido como false e o valor da carga útil será
corresponde ao upload.
Se enviarmos a série de
estados resultante para o log, veremos o seguinte:
- State ( carga = nula, isLoading = false) - o estado inicial.
- Estado ( carga = nulo, isLoading = true) - após StartedLoading.
- Estado ( carga = dados, isLoading = false) - após FinishedWithSuccess.
Ao conectar esses estados à interface do usuário, você verá todas as etapas do processo: primeiro uma tela em branco, depois uma tela de carregamento e, finalmente, os dados necessários.
Essa abordagem tem muitas vantagens.
- Primeiramente, ao alterar centralmente o estado usando uma série de transações, não permitimos o estado da corrida e muitos bugs irritantes invisíveis.
- Em segundo lugar, depois de estudar uma série de transações, podemos entender o que aconteceu, por que aconteceu e como isso afetou o estado do aplicativo. Além disso, com o Redutor, é muito mais fácil imaginar todas as alterações de estado antes do primeiro lançamento do aplicativo no dispositivo.
- Finalmente, somos capazes de criar uma interface simples. Como todos os estados são armazenados em um único local (Store), que leva em consideração intenções (Intents), faz alterações usando o Redutor e demonstra uma cadeia de estados, você pode colocar toda a lógica de negócios na Store e usar a interface para ativar intenções e exibir estados.
Ou não?
... talvez o trem correndo em sua direção
Redutor por si só claramente não é suficiente. E as tarefas assíncronas com resultados diferentes? Como responder a push do servidor? E o lançamento de tarefas adicionais (por exemplo, limpar o cache ou carregar dados do banco de dados local) após uma alteração de estado? Acontece que, ou não incluímos toda essa lógica no Reducer (ou seja, boa parte da lógica de negócios não será abordada e terá que ser cuidada por quem decide usar nosso componente) ou forçamos o Reducer a fazer tudo de uma vez.
Requisitos de estrutura MVI
Obviamente, gostaríamos de incluir toda a lógica comercial de um recurso individual em um componente independente, com o qual desenvolvedores de outras equipes poderiam trabalhar facilmente, simplesmente criando uma instância e assinando seu estado.
Além disso:
- Ele deve interagir facilmente com outros componentes do sistema;
- em sua estrutura interna, deve haver uma clara separação de tarefas;
- todas as partes internas do componente devem ser completamente determinísticas;
- a implementação básica de tal componente deve ser simples e complicada somente se forem necessários elementos adicionais.
Não mudamos imediatamente do Reducer para a solução que usamos hoje. Cada equipe enfrentou problemas usando abordagens diferentes, e desenvolver uma solução universal que seria adequada a todos parecia improvável.
E, no entanto, o estado atual das coisas agrada a todos. Temos o prazer de apresentar o MVICore! O código fonte da biblioteca está aberto e disponível no
GitHub .
O que é bom MVICore
- Uma maneira fácil de implementar recursos de negócios de programação reativa com um fluxo de dados unidirecional.
- Escalonamento: a implementação básica inclui apenas o Redutor e, em casos mais complexos, você pode usar componentes adicionais.
- Uma solução para trabalhar com eventos que você não deseja incluir no estado ( problema do SingleLiveEvent ).
- Uma API simples para vincular recursos (e outros componentes reativos do seu sistema) à interface do usuário e entre si com suporte para o ciclo de vida do Android (e não apenas).
- Suporte de middleware (veja abaixo) para cada componente do sistema.
- Registrador pronto e a capacidade de viajar no tempo para depuração de cada componente.
Breve introdução ao recurso
Como as instruções passo a passo já foram publicadas no GitHub, vou omitir exemplos detalhados e focar nos principais componentes da estrutura.
Recurso - o elemento central da estrutura que contém toda a lógica de negócios do componente. O recurso é definido por três parâmetros:
recurso de interface <Desejo, Estado, Notícias>O desejo corresponde à Intenção da Model-View-Intent - essas são as mudanças que queremos ver no modelo (como o termo Intent tem seu próprio significado no ambiente dos desenvolvedores do Android, tivemos que encontrar um nome diferente). O desejo é o ponto de entrada para o recurso.
Estado é, como você já entendeu, o estado do componente. O Estado não é imutável: não podemos mudar seus valores internos, mas podemos criar novos Estados. Esta é a saída: toda vez que criamos um novo estado, passamos para o fluxo Rx.
Notícias - um componente para processar sinais que não devem estar no Estado; As notícias são usadas uma vez durante a criação (
problema do SingleLiveEvent ). Usar o News é opcional (você pode usar o Nothing do Kotlin na assinatura do recurso).
Também em Feature deve estar presente
Redutor .
O recurso pode conter os seguintes componentes:
- Ator - executa tarefas assíncronas e / ou modificações de estado condicional com base no estado atual (por exemplo, validação de formulário). O ator vincula o desejo a um número de efeito específico e o passa para o redutor (na ausência de ator, o redutor recebe o desejo diretamente).
- NewsPublisher - Chamado quando o Wish se torna qualquer Efeito que produz o resultado como um novo Estado. Com base nesses dados, ele decide se deve criar Notícias.
- PostProcessor - também chamado após a criação de um novo Estado e também sabe qual efeito levou à sua criação. Ele lança certas ações adicionais (ações). Ação - esses são “desejos internos” (por exemplo, limpando o cache) que não podem ser iniciados do lado de fora. Eles são executados no ator, o que leva a uma nova cadeia de efeitos e estados.
- O Bootstrapper é um componente que pode executar ações por conta própria. Sua principal função é inicializar o Feature e / ou correlacionar fontes externas com o Action. Essas fontes externas podem ser Notícias de outro Recurso ou dados do servidor que devem modificar o Estado sem a intervenção do usuário.
O diagrama pode parecer simples:

ou inclua todos os componentes adicionais acima:

O próprio recurso, contendo toda a lógica de negócios e pronto para uso, não parece nada mais fácil:

O que mais?
O recurso, a pedra angular da estrutura, funciona em um nível conceitual. Mas a biblioteca tem muito mais a oferecer.
- Como todos os componentes do Feature são determinísticos (com exceção do Actor, que não é completamente determinístico porque interage com fontes de dados externas, mas mesmo assim, o ramo que ele executa é determinado pelos dados de entrada e não pelas condições externas), cada um deles pode ser envolvido no Middleware. Ao mesmo tempo, a biblioteca já contém soluções prontas para registro e depuração de viagens no tempo .
- O middleware é aplicável não apenas ao Feature, mas também a outros objetos que implementam a interface Consumer <T>, o que a torna uma ferramenta de depuração indispensável.
- Ao usar um depurador para depuração enquanto se move na direção oposta, você pode implementar o módulo DebugDrawer .
- A biblioteca inclui um plug-in IDEA que pode ser usado para adicionar modelos para as implementações mais comuns do Feature, o que economiza muito tempo.
- Existem classes auxiliares para oferecer suporte ao Android, mas a biblioteca em si não está vinculada ao Android.
- Existe uma solução pronta para vincular componentes à interface do usuário e entre si por meio de uma API elementar (isso será discutido no próximo artigo).
Esperamos que você experimente nossa
biblioteca e seu uso traga a você tanta alegria quanto nós - sua criação!
Nos dias 24 e 25 de novembro, você pode tentar e se juntar a nós! Realizaremos um evento de contratação móvel: em um dia será possível percorrer todas as etapas da seleção e receber uma oferta. Meus colegas das equipes iOS e Android se comunicarão com os candidatos em Moscou. Se você é de outra cidade, o Badoo incorre em custos de viagem. Para receber um convite, faça o teste de triagem no link . Boa sorte