Dicas para começar a modularizar seu antigo aplicativo Android
Trabalhamos com um projeto legado cujo desenvolvimento começou há mais de 4 anos por outra empresa com outros padrões, práticas e experiências.
Neste projeto trabalhamos com sprints de 2 semanas, sempre entregando novas funcionalidades e corrigindo bugs, com escopos muito fechados onde é difícil adicionar tarefas técnicas. Adicionamos práticas recomendadas comuns, como testes e arquitetura limpa, e também adicionamos novos elementos no ambiente de desenvolvimento Android, como Room ou Jetpack Compose.
Agora começamos a enfrentar uma necessidade: começar a modularizar o aplicativo Android para obter todas as suas vantagens.
Por que modularizar seu aplicativo Android?
Existem vários benefícios em ter seu aplicativo modularizado:
- Construções mais rápidas. O Gradle acelera o tempo de compilação do seu projeto, permitindo que você faça tarefas em paralelo e reutilizando todo o código já compilado sem modificá-lo. Quando você inicia um novo projeto pode não apreciar o tempo de compilação, mas quando seu projeto cresce é comum ter tempos de compilação em torno de 5 minutos. Para ajustar pequenos detalhes da visualização, você deve compilar seu projeto entre 3 e 5 vezes, acumulando tempo morto.
- Tende a simplificar o desenvolvimento. Trabalhando em pequenos módulos focados em uma única funcionalidade, fica mais fácil corrigir, manter e melhorar o código. Pelo simples fato de você limitar o tamanho do código, revisar ou adicionar testes para iniciar uma refatoração se torna mais confortável.
- Reutilize módulos entre aplicativos. Talvez um de seus módulos possa facilmente se tornar uma biblioteca e você possa abri-lo para a comunidade de código aberto com todos os benefícios sem afetar todo o seu aplicativo, tanto em termos de segurança quanto de integração.
- Isso torna mais fácil adicionar mais pessoas à equipe. Ao mergulhar seu código em módulos menores, você pode atribuir pessoas ou equipes a eles diretamente sem afetar o restante do aplicativo. Por exemplo, se você tem um recurso de bate-papo em seu aplicativo Android e precisa de ajuda para cumprir um prazo com uma funcionalidade, apenas ter que entender esse módulo sem ter que entender o resto do aplicativo torna muito mais rápido receber ou dar ajuda.
- Automação de teste aprimorada. Ao poder testar módulos sem ter que fazer todo o fluxo, os testes serão mais rápidos e os erros das etapas anteriores não serão propagados. Você pode criar regras específicas em seu CI para acelerar seu tempo de compilação, por exemplo, ativar apenas testes de interface do usuário, geralmente os mais lentos, ao mesclar com seu branch de desenvolvimento principal.
Como podemos começar a modularizar um aplicativo Android sem interromper o desenvolvimento ou afetar nossa capacidade de entrega?
Ao enfrentar um desafio tão grande, tendemos a ficar sobrecarregados, mas o conselho mais importante é ter paciência. No final, encontraremos o primeiro passo do caminho e aos poucos iremos adicionando módulos até que tenhamos ele completamente modularizado.
Neste artigo, não estamos procurando uma receita perfeita sobre como modularizar seu aplicativo. Cada projeto é diferente e abordá-lo com passos rigorosos pode ser mais um problema do que uma solução. Por exemplo, se nos dissessem que em nosso projeto começaremos com a parte de rede, seria caótico e falharíamos na tentativa de ver que temos algumas dependências na análise e exibição de erros. Apenas para criar esses dois módulos que usarão a rede, podemos perder algumas semanas para testar tudo e garantir que não quebramos nada. E voltando ao contexto, não temos essas duas semanas para focar em tarefas técnicas. Devemos nos adaptar à nossa situação e procurar maneiras de adicionar pequenos módulos para poder abordar a rede e fazê-lo nos finais típicos de sprint com algum espaço para tarefas externas.
- Analise suas dependências. Procure uma pequena funcionalidade que tenha poucas dependências ou melhor ainda, nenhuma. No nosso caso, foi toda a parte de programação funcional e todas as extensões kotlin que criamos ao longo dos anos. Criamos o módulo, movemos todo o código para lá, testes incluídos e compilamos. Eureca! Funcionou! Fácil? Bom, tivemos que alterar todas as importações do projeto, mais de 100 arquivos modificados, verificar se não havíamos quebrado nenhum teste e se todas as funcionalidades do app ainda estavam intactas, mas tínhamos nosso primeiro módulo.
- Teste antes de começar. Uma boa prática que deveríamos ter em nossos projetos seria ter uma boa base de testes, mas às vezes chegamos a um projeto e não é assim. Portanto, se encontrarmos essa pequena funcionalidade sem dependências, antes de movê-la para o novo módulo, criaremos alguns testes que envolvem e migram tudo para o novo módulo, e esperamos alterar as importações. Conseguimos fazer o primeiro módulo porque já tínhamos uma boa base de testes que cobriam essa parte.
- Usando Gradle para compor dependências. Uma das desvantagens que podemos encontrar ao modularizar nosso projeto pode ser o gerenciamento de dependências e versões. Muitas vezes é o erro típico de compilação de conflitos de versão da biblioteca Android X. Para fazer isso, podemos aproveitar o Gradle para compor nossos arquivos build.gradle. Em nosso projeto temos um arquivo dependencies.gradle onde primeiro definimos as versões de nossas dependências e depois as próprias dependências.
versões = [ "koin_version" : "3.1.4", ]libs = [ "koin" : "io.insert-koin:koin-android:$versions.koin_version", "koinCore" : "io.insert-koin:koin-core:$versions.koin_version", "koinJavaCompat" : " io.insert-koin:koin-android-compat:$versions.koin_version", "koinCompose" : "io.insert-koin:koin-androidx-compose:$versions.koin_version", ]
Como você pode ver, ao usar koin temos várias dependências que compartilham a versão, e se tivéssemos que atualizá-lo para resolver algum erro, basta vir aqui e não procurar em todos os arquivos Gradle que temos em cada módulo do projeto.
Mas, se tivermos muitos módulos, teremos muitos arquivos build.gradle e podemos aproveitar a composição para economizar muito código repetido. Se definirmos um arquivo com a configuração básica de todos os módulos do Android, a única coisa que teremos que adicionar em cada arquivo é uma linha de código e as dependências particulares daquele.
Android_lib.gradle aplicar plug-in: 'com.android.library' aplicar plug-in: 'org.jetbrains.kotlin.android' aplicar plug-in: 'kotlin-kapt' aplicar de: "$rootDir/buildsystem/dependencies.gradle"android { compileSdkVersion configs.compileSdk defaultConfig { targetSdkVersion configs.targetSdk minSdkVersion configs.minSdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } buildTypes { depurar { depurável = true } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } sourceSets { main. java.srcDirs += 'src/main/kotlin' test.java.srcDirs += 'src/test/kotlin' androidTest.java.srcDirs += 'src/androidTest/kotlin' } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion ' 1.0.1' } }
Com esta definição podemos compor nossas dependências nos novos módulos.
módulo.gradle plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' }aplique de: "$rootDir/buildsystem/android_lib.gradle"android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion '1.0.1' } }dependências { implementação libs.core implementação libs.appcompat implementação libs.material //Implementação COMPOSE Implementação de libs.composeActivities Implementação de libs.composeMaterial Implementação de libs.composeConstraintLayout Implementação de libs.composeTooling Implementação de libs.composeRuntime libs.composeViewModel // Implementação da sala libs.roomRuntime implementação libs.roomKtx kapt libs.roomCompiler // Implementação Koin Implementação libs.koin Implementação libs.koinJavaCompat Implementação libs.koinCompose libs.koinCore //TEST testImplementation testLibs.junit testImplementation testLibs.coroutinesTest }
Podemos até aproveitar o gradle para compor ainda mais dependências e não precisar adicionar ou atualizar muitos arquivos ao adicionar um novo compartilhado por vários módulos.
- Use o Leiame como base de conhecimento.Dentro de nossos repositórios o arquivo Readme.md normalmente contém passos para compilar o projeto, é o mais comum em projetos fechados. A documentação mais elaborada costuma ser encontrada em soluções externas como Confluence, Notion, etc. Uma opção interessante é ter em cada módulo um arquivo Leiame que contenha algumas informações básicas. Essas informações podem ser uma descrição da funcionalidade do módulo, suas dependências de outros módulos (com seu link relativo para facilitar a navegabilidade), dependências externas relevantes, possíveis melhorias a serem feitas, etc. apenas dando a eles acesso ao repositório, eles podem navegar pela documentação simples sem ter que navegar pelo código que é muito mais confuso e intimidador. Uma consideração é que o Android Studio, no momento em que escrevo isso, não tem um bom suporte MD. Uma boa alternativa é o Visual Studio Code, onde você pode visualizar como seu arquivo ficaria no repositório.