Palestras da Kaspersky Mobile # 1. Multi-modularidade

No final de fevereiro, lançamos um novo formato para reuniões de desenvolvedores Android do Kaspersky Mobile Talks . A principal diferença dos comícios comuns é que, em vez de centenas de ouvintes e belas apresentações, desenvolvedores "experientes" se reuniram em vários tópicos diferentes para discutir apenas um tópico: como eles implementam a multi-modularidade em seus aplicativos, quais problemas enfrentam e como resolvê-los.



Conteúdo


  1. Antecedentes
  2. Mediadores no HeadHunter. Alexander Blinov
  3. Módulos de Domínio Tinkoff Vladimir Kokhanov, Alexander Zhukov
  4. Análise de impacto no Avito. Mikhail Yudin, Evgeny Krivobokov
  5. Como em Tinkoff, eles reduziram o tempo de montagem para o PR de quarenta minutos para quatro. Vladimir Kokhanov
  6. Links úteis


Antes de prosseguir para o conteúdo imediato da reunião no escritório da Kaspersky Lab, lembremos de onde veio o mod para dividir o aplicativo em módulos (daqui em diante, o módulo é entendido como um módulo Gradle, não um Dagger, a menos que seja indicado de outra forma).


O tópico da multi-modularidade está na mente da comunidade Android há anos. Um dos fundamentos pode ser considerado um relatório de Denis Neklyudov no ano passado "Mobius" em São Petersburgo. Ele propôs dividir o aplicativo monolítico, que há muito deixou de ser um thin client, em módulos para aumentar a velocidade de construção.
Link para o relatório: Apresentação , Vídeo


Houve um relatório de Vladimir Tagakov, do Yandex.Maps, sobre a vinculação de módulos usando o Dagger. Assim, eles resolvem o problema de alocar um único componente de cartões para reutilização em muitos outros aplicativos Yandex.
Link para o relatório: Apresentação , Vídeo


A Kaspersky Lab também não se afastou da tendência: em setembro, Evgeni Matsyuk escreveu um artigo sobre como conectar módulos usando o Dagger e, ao mesmo tempo, construiu uma arquitetura de múltiplos módulos horizontalmente, sem esquecer de seguir verticalmente os princípios da arquitetura limpa.
Link para o artigo


E no inverno Mobius houve dois relatórios ao mesmo tempo. Primeiro, Alexander Blinov falou sobre a multi-modularidade no aplicativo HeadHunter usando o Toothpick como DI, e logo após ele Artem Zinnatulin falou sobre a dor de mais de 800 módulos no Lyft. Sasha começou a falar sobre multi-modularidade, como uma maneira de melhorar a arquitetura do aplicativo, e não apenas acelerar a montagem.
Relatório Blinov: Apresentação , Vídeo
Relatório Zinnatulin: Vídeo


Por que comecei o artigo com uma retrospectiva? Em primeiro lugar, ele ajudará você a estudar melhor o tópico se estiver lendo sobre multi-modularidade pela primeira vez. Em segundo lugar, o primeiro discurso em nossa reunião começou com uma mini-apresentação de Alexey Kalaida, da empresa Stream, que mostrou como eles dividiram seu aplicativo em módulos com base no artigo de Zhenya (e alguns pontos me pareciam semelhantes à abordagem de Vladimir).


O principal recurso dessa abordagem foi a ligação à interface do usuário: cada módulo é conectado como uma tela separada - um fragmento para o qual as dependências são transferidas do módulo de aplicativo principal, incluindo o FragmentManager. Primeiro, os colegas tentaram implementar a multi-modularidade por meio de injetores proxy, que Zhenya propôs no artigo. Mas essa abordagem parecia esmagadora: havia problemas quando um recurso dependia de outro, que por sua vez dependia do terceiro - tivemos que escrever um injetor proxy para cada módulo de recurso. A abordagem baseada nos componentes da interface do usuário permite que você não escreva nenhum injetor, permitindo dependências no nível de dependência dos fragmentos de destino.


As principais limitações desta implementação: um recurso deve ser um fragmento (ou outra visualização); a presença de fragmentos aninhados, o que leva a um grande clichê. Se um recurso implementa outros recursos, ele deve ser adicionado ao mapa de dependência, que o Dagger verifica ao compilá-lo. Quando existem muitos desses recursos, surgem dificuldades no momento de vincular o gráfico de dependência.



Após o relatório de Alexey, Alexander Blinov tomou a palavra. Na sua opinião, a implementação vinculada à interface do usuário seria adequada para contêineres DI em Flutter. Em seguida, a discussão mudou para uma discussão com vários módulos no HeadHunter. O objetivo de sua divisão em módulos era a possibilidade de isolamento arquitetônico de recursos e aumentar a velocidade de montagem.


Antes de dividir em módulos, é importante se preparar. Primeiro, você pode criar um gráfico de dependência - por exemplo, usando essa ferramenta . Isso ajudará a identificar componentes com um número mínimo de dependências e a se livrar de desnecessárias (chop). Somente depois disso, os componentes menos conectados podem ser selecionados nos módulos.


Alexander lembrou os principais pontos sobre os quais falou com mais detalhes na Mobius. Uma das tarefas complexas que a arquitetura deve levar em consideração é a reutilização de um módulo de vários locais no aplicativo. No exemplo com o aplicativo hh, este é um módulo de currículo, que deve estar acessível tanto ao módulo da lista de vagas (VacanciesList), quando o usuário acessa o currículo que ele enviou para essa vaga quanto ao módulo de resposta negativa (Negociação). Para maior clareza, redesenhei a imagem que Sasha mostrava em um flipchart.



Cada módulo contém duas entidades principais: Dependências - as dependências que esse módulo precisa e API - os métodos que o módulo fornece para outros módulos. A comunicação entre os módulos é realizada por mediadores, que são uma estrutura plana no principal módulo de aplicativo. Cada recurso tem uma escolha. Os próprios mediadores estão incluídos em um determinado MediatorManager no módulo de aplicativo do projeto. No código, parece algo como isto:


object MediatorManager { val chatMediator: ChatMediator by lazy { ChatMediator() } val someMediator: ... } class TechSupportMediator { fun provideComponent(): SuppportComponent { val deps = object : SuppportComponentDependencies { override fun getInternalChat{ MediatorManager.rootMediator.api.openInternalChat() } } } } class SuppportComponent(val dependencies) { val api: SupportComponentApi = ... init { SupportDI.keeper.installComponent(this) } } interface SuppportComponentDependencies { fun getSmth() fun close() { scopeHolder.destroyCoordinator < -ref count } } 

Alexander prometeu publicar em breve um plug-in para a criação de módulos no Android Studio, que é usado para se livrar de copiar e colar em sua empresa, bem como um exemplo de projeto de múltiplos módulos de console.


Mais alguns fatos sobre os resultados atuais da separação de módulos de aplicativos hh:


  • ~ 83 módulos de recursos.
  • Para realizar um teste A / B, os recursos podem ser substituídos inteiramente pelo módulo de recursos no nível do mediador.
  • O gráfico do Gradle Scan mostra que, após a compilação dos módulos em paralelo, ocorre um processo bastante demorado de dexing do aplicativo (neste caso, dois: para candidatos e empregadores):


Intervenções de Alexander e Vladimir de Tinkoff:
O esquema de sua arquitetura multi-módulo é assim:


Os módulos são divididos em duas categorias: módulos de recurso e módulos de domínio.
Os módulos de recursos contêm lógica de negócios e recursos da interface do usuário. Eles dependem de módulos de domínio, mas não podem depender um do outro.


Os módulos de domínio contêm código para trabalhar com fontes de dados, ou seja, alguns modelos, DAO (para trabalhar com o banco de dados), API (para trabalhar com a rede) e repositórios (combinam o trabalho da API e DAO). Os módulos de domínio, diferentemente dos módulos de recursos, podem depender um do outro.


A conexão entre o domínio e os módulos de recursos ocorre inteiramente dentro dos módulos de recursos (ou seja, na terminologia de hh, as dependências de API e dependências dos módulos de domínio são completamente resolvidas nos módulos de recursos que os utilizam, sem o uso de entidades adicionais, como mediadores).


Isto foi seguido por uma série de perguntas, que colocarei quase inalteradas aqui no formato de perguntas e respostas:


- Como é feita a autorização? Como você o arrasta para os módulos de recursos?
- Os recursos conosco não dependem de autorização, porque quase todas as ações do aplicativo ocorrem na zona autorizada.

- Como rastrear e limpar componentes não utilizados?
- Temos uma entidade como InjectorRefCount (implementada através do WeakHashMap) que, ao excluir a última atividade (ou fragmento) usando esse componente, a exclui.

- Como medir uma verificação "limpa" e criar tempo? Se os caches estiverem ativados, uma verificação bastante suja será obtida.
- Você pode desativar o Gradle Cache (org.gradle.caching em gradle.properties).

- Como executar testes de unidade de todos os módulos no modo de depuração? Se você executar apenas o teste de classificação, os testes de todos os tipos e buildType serão puxados.
(Esta pergunta provocou a discussão de muitos participantes da reunião.)
- Você pode tentar executar o testDebug.
- Os módulos para os quais não há configuração de depuração não serão apertados. Começa muito ou pouco.
- Você pode escrever uma tarefa Gradle, que substituirá testDebug para esses módulos, ou fará uma configuração de depuração falsa no build.gradle do módulo.
- Você pode implementar essa abordagem da seguinte maneira:

 withAndroidPlugin(project) { _, applicationExtension -> applicationExtension.testVariants.all { testVariant -> val testVariantSuffix = testVariant.testedVariant.name.capitalize() } } val task = project.tasks.register < SomeTask > ( "doSomeTask", SomeTask::class.java ) { task.dependsOn("${project.path}:taskName$testVariantSuffix") } 



A próxima apresentação improvisada foi feita por Evgeny Krivobokov e Mikhail Yudin, de Avito.
Eles usaram o mapa mental para visualizar sua história.


Agora, o projeto da empresa possui mais de 300 módulos, com 97% da base de código escrita em Kotlin. O principal objetivo da decomposição em módulos era acelerar a montagem do projeto. A divisão em módulos ocorreu gradualmente, com as partes menos dependentes do código sendo alocadas aos módulos. Para isso, foi desenvolvida uma ferramenta para marcar as dependências dos códigos-fonte no gráfico para análise de impacto ( relatório sobre análise de impacto no Avito ).


Usando esta ferramenta, você pode marcar um módulo de recurso como final, para que outros módulos não possam depender dele. Essa propriedade será verificada durante a análise de impacto e fornece uma designação de dependências e acordos explícitos com as equipes responsáveis ​​pelo módulo. Com base no gráfico construído, a distribuição das alterações também é verificada para executar testes de unidade para o código afetado.


A empresa usa um repositório mono, mas apenas para fontes Android. O código de outras plataformas vive separadamente.


Gradle é usado para construir o projeto (embora os colegas já estejam pensando em um colecionador como Buck ou Bazel, mais adequado para projetos com vários módulos). Eles já experimentaram o Kotlin DSL e retornaram ao Groovy nos scripts Gradle, porque é inconveniente oferecer suporte a diferentes versões do Kotlin no Gradle e no projeto - a lógica geral é colocada nos plug-ins.


O Gradle pode paralelizar tarefas, armazenar em cache e não remontar dependências binárias se a ABI não tiver sido alterada, o que garante a montagem mais rápida de um projeto com vários módulos. Para um cache mais eficiente, Mainfraimer e várias soluções auto-escritas são usadas:


  • Ao alternar de ramificação para ramificação, o Git pode deixar pastas vazias que interrompem o cache ( problema Gradle nº 2463 ). Portanto, eles são excluídos manualmente usando o gancho Git.
  • Se você não controlar o ambiente nas máquinas dos desenvolvedores, versões diferentes do Android SDK e outros parâmetros poderão degradar o cache. Durante a construção do projeto, o script compara os parâmetros do ambiente com os esperados: se as versões ou parâmetros incorretos estiverem instalados, a construção será interrompida.
  • O Analytics está ativando / desativando parâmetros e o ambiente. Isso é para monitorar e ajudar os desenvolvedores.
  • Erros de compilação também são enviados para a análise. Problemas conhecidos e populares são inseridos em uma página especial com uma solução.

Tudo isso ajuda a obter 15% de perda de cache no IC e 60-80% localmente.


As seguintes dicas do Gradle também podem ser úteis se um grande número de módulos aparecer no seu projeto:


  • Desabilitar os módulos por meio de sinalizadores IDE é inconveniente; esses sinalizadores podem ser redefinidos. Portanto, os módulos são desativados por meio de settings.gradle.
  • No estúdio 3.3.1, há uma caixa de seleção "Ignorar geração de código-fonte na sincronização Gradle se um projeto tiver mais de 1 módulos". Por padrão, está desativado, é melhor ativá-lo.
  • As dependências são registradas no buildSrc para reutilização em todos os módulos. Outra opção é o DSL de plug - ins , mas você não pode colocar o aplicativo do plug-in em um arquivo separado.


Nossa reunião terminou com Vladimir, de Tinkoff, com o título clickbait do relatório, “Como reduzir a montagem no PR de 40 minutos para quatro” . Na verdade, estávamos falando sobre a distribuição de partidas de plugues graduados: compilações de apk, testes e analisadores estáticos.


Inicialmente, os sujeitos de cada solicitação pull executavam uma análise estática, diretamente na montagem e nos testes. Esse processo levou 40 minutos, dos quais apenas Lint e SonarQube levaram 25 e caíram apenas 7% dos lançamentos.


Assim, foi decidido colocar o lançamento em um Job separado, que é executado em uma programação a cada duas horas e, em caso de erro, envia uma mensagem ao Slack.


A situação oposta estava usando o Detect. Falhava quase constantemente, e é por isso que foi submetido a uma verificação preliminar prévia.


Portanto, apenas os testes de montagem e unidade do apk permaneceram na verificação da solicitação de recebimento. Os testes compilam as fontes antes da execução, mas não coletam recursos. Como a mesclagem de recursos quase sempre teve êxito, o próprio assembly apk também foi abandonado.


Como resultado, apenas o lançamento dos testes de unidade permaneceu na solicitação pull, o que nos permitiu atingir os 4 minutos indicados. A compilação apk é realizada com a solicitação de extração por fusão no dev.



Apesar de a reunião ter durado quase quatro horas, não conseguimos discutir o problema de organizar a navegação em um projeto com vários módulos. Talvez este seja o tópico para as próximas conversas do Kaspersky Mobile. Além disso, os participantes realmente gostaram do formato. Diga-nos sobre o que você gostaria de falar na pesquisa ou nos comentários.


E, finalmente, links úteis do mesmo chat:


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


All Articles